Commit Graph

40 Commits

Author SHA1 Message Date
fb89b1f6e1 S8 fix - help centre glossary classes leaked onto the Issue (hold) status pill
help.js injects its stylesheet on every page, and its glossary pills used bare
class selectors (.pill-draft ... .pill-hold). The creator's Issue (hold) status
radio also carries the class pill-hold, so the injected rule painted that radio
error-red at ALL times - selected or not. Reported by Nick ('why is the issues
(hold) button illuminated at all times'), 2026-08-20.

Pre-existing, not from this branch: help.js has had the bare selectors since
the login-portal commit, and the creator's pill-hold class predates the R2
branch. Every glossary rule is now scoped to .ui-help-pill.pill-*, which the
glossary markup already carries. Verified live: unselected, the hold pill's
computed style now matches its neighbours exactly; selected, it is still the
red fill; the glossary's own Hold pill keeps its tint. helptip_check gains the
pin (13 -> 14): no bare .pill-* selector in help.js, ever again.

Item: S8 (the help component's app-wide surface).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 15:59:30 -07:00
8663d81af3 C4/D11 follow-up - the integration review's seven confirmed findings
An adversarial review (four lenses, every finding independently verified by two
skeptics told to refute it) ran over 2a5f6b3 and 8cf8c0f. Seven findings
survived; all seven are fixed here.

Against the C4 fix:
- help.js: the nav hover was renamed onto its own surface token, keeping a
  no-op T9.9 had introduced (two different grays had been mapped to one name).
  Hover is now --cds-layer-hover, the token that exists for exactly this.
- wp-creation-app.js: the drawer's critical CSS pre-painted --cds-layer-accent
  while the stylesheet paints --wp-nav-bg; now both paint --wp-nav-bg.

Against D11:
- wp-sections.js: the Assets toggle note still described the pre-D11 card
  ('Asset tags and controls.dev links') with a rationale the picker inverts.
- runAssetSearch: the result cap counted contains-matches before the exact and
  prefix tiers finished, so 500 alphabetically-early substring hits could evict
  the exact match - and Enter then added the wrong asset, ID-locked. The cap
  now bounds each tier; the scan always sees the whole catalog.
- addCatalogAsset: the one mutation in the section with no announced outcome
  was the successful pick. It now toasts (role=status), matching every sibling
  path (C1).
- assets_db.py: failures are remembered for FAIL_CACHE_SECONDS (default 30s)
  and a stale catalog is served over an error, so a Micron outage costs one
  CONNECT_TIMEOUT per window instead of one per page load stacking up in the
  shared sync threadpool until login itself stalls.
- assets_db.py: MICRON_ASSETS_CACHE_SECONDS='5m' no longer crashes the boot -
  a malformed knob on an OPTIONAL feature degrades to its default, loudly.

assets_check grows four regressions for these (27 -> 31): per-tier cap against
600 decoys, the announced pick, boot with a malformed knob, and the stable
cached 503. Battery: assets_check 31/31, color_check 5/5, sections_check ALL
PASS.

Items: C4, D11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 12:09:40 -07:00
8cf8c0f882 D11 - merge origin/Micron-Assets: the Micron asset picker, adapted to R2
Integrates Cody Schaefer's 7ef1fcd (written against pre-R2 main) per Nick's
instruction of Aug 20. The catalog lookup arrives whole: read-only /api/assets
backed by server/assets_db.py (one SELECT, env-only MICRON_DB_URL, 503-not-500
when broken, driver errors logged not propagated), the searchable picker with
CSV import and Excel column paste, catalog rows badged and locked to the DB's
casing, manual rows visibly unvouched, and graceful absent/unreachable states.

Three conflicts, resolved as unions of both sides' intent; the adaptations and
their reasons are recorded in docs/waves/decisions-2026-08-20.md:
- renderPackage: Cody's two-column asset table inside T9.1's sectioned
  add('assets', ...) frame, so the CR-006 toggle keeps governing the export.
- bootData: initAssetPicker() joins the R2 loads instead of replacing them.
- The asset card: his picker UI, plus role=status on the source note (C1).
- Six imported alert() calls converted to the creator's idioms: file errors
  through toast(msg,'alert') as the drawings uploader does; the instructional
  and summary messages through the T7.9 kit, which gains the one-button
  wpAlertDialog shape (BL-024's console conversions will want it too).

New probe: assets_check (27) - read-only structurally, unconfigured/broken as
first-class states, no credential echo, search ranking, casing canonicalisation,
import fallback + dedup, kit-not-native summary. One sections_check pin
re-pointed with the reason in code: normaliseAsset now stamps legacy rows
source:'manual' on load, so the CR-016 check compares content, not bytes.

Battery after merge: assets_check 27/27, creator_dialogs_check 20/20,
sections_check ALL PASS, export_check 20/20, helptip_check 13/13,
mobile_check 24/24, icon_check 5/5, color_check 5/5, form_structure_check
50/51 (the one red is BL-022, unchanged, deliberate).

Item: D11 (new scope, new id per the working rules). Out-of-scope note in
completion.md amended - 'no integration code exists' was true when written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 11:45:32 -07:00
23ee0b052f T9.9 - C4 + the backlog sweep: nine entries closed, each re-measured first
The colour half (C4, approved Aug 18 - "change them"):
- BL-004: the help centre's own 52-colour palette collapsed onto theme tokens
- BL-005: the JS-built dialogs (auth-guard, wp-format) and project-data's
  badges read tokens; the creator's categorical badge palette moved to
  theme-light as --wp-chart-1..10, read by computed style at boot; the print
  popup - a document with no stylesheet - inlines live token VALUES
- BL-008: the second brand blue (#2563d6) is deleted; .sop-inherited tints
  with THE blue at the same 7% alpha
- BL-009: the ninth amber (--wp-status-warning-text-alt) is deleted
- theme-light gained the two missing feedback tokens the consoles carried as
  literals (--wp-status-success-text / -error-text)
- NEW tests/color_check.py 4/4: zero hex literals outside theme-light.css,
  comments stripped (the BL-017 lesson), with the exceptions named in full
  (meta theme-color cannot resolve a var; rgba alphas are opacity recipes)

The correctness half, each re-measured before touching, as the task ordered:
- BL-011 STILL REPRODUCED: the sync badge mounted on the first async sync
  event; its holder now mounts at DOMContentLoaded, so the three overlays land
  in script order deterministically
- BL-012 fixed and MEASURED: baseline_shots freezes Date and Math.random per
  document; two consecutive admin captures came back byte-identical
- BL-016 fixed: a step-less wizard URL is step 1; stepper_check's deliberately
  wrong pin flipped with the fix, exactly as the entry planned
- BL-018 fixed both halves: the false-complete write now requires the
  {sop,state} production shape, and browser_check.seed writes that shape -
  which un-detoured four probes' creators from the SOP gate. stepper_check
  re-pointed at projB (no SOP) because its premise is a wizard someone is
  STARTING, and projA now legitimately restores a finished one.
- BL-019 fixed: a stored cost code that left COST_CODES is kept as an option
  (the gov_wosize pattern), so opening a package no longer blanks its record
- hold_check's AST sweep refined in passing detection: it flagged T8.3's
  notification-row .status as a release transition; it now reads wp.status only

Every wave-9-pointing backlog entry is closed with its measurement recorded.

Verification (each probe run alone): color_check 4/4, stepper_check 71/71,
validation_check 77/77, url_state_check 23/23, autosave_check 34/34,
a11y_check 22/22, launcher_check 58/58, aggregates_check 16/16,
kitting_check 26/26, hold_check 50/50, mobile_check 24/24, frame_check 38/38,
sections_check 95/95, form_structure_check 50/51 (BL-022's question).

Items: C4, BL-004, BL-005, BL-008, BL-009, BL-011, BL-012, BL-016, BL-018, BL-019

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:27:26 -07:00
771672273d T9.8 - D7: archiving stops reading as deletion - for project admins
Archiving already froze a project (the server refuses every write); what did
not exist was the way back in. Now:

- GET /api/projects?archived=only|all filters the answer BY PER-PROJECT ROLE:
  a project admin (or super/app admin) on THAT project sees it; everyone else
  receives an empty list from the same request - archived projects appear
  nowhere for them, counts and pickers included (the default listing already
  excluded them for everyone; asking is what got gated). Admin-on-Job-A does
  not surface archived Job B.
- The launcher gains a visibly separate, labelled "Archived projects"
  section (dashed border, read-only stated in words), rendered only when the
  server returns rows. Opening one makes it active; the launcher's reconcile
  learned that an active project whose stored summary says archived:true was
  opened ON PURPOSE and keeps it, while a project archived out from under
  someone still drops with the existing explanation.
- The creator shows ARCHIVED - READ-ONLY where the project is named (both
  ctx-bar branches, from the SERVER's answer - the page's project comes from
  the URL, so a stale local summary is not trusted) and refuses saves with a
  reason before the round trip. The courtesy; the server's refusal is the
  rule, verified by calling the endpoints directly (wp upsert AND the
  material-list write both refuse with "archived" even for an admin).
- No unarchive button, no second mechanism, and it fits at 390px.

Verification (each probe run alone): NEW tests/archived_check.py 15/15.
Regressions: launcher_check 58/58, sample_check 10/10, export_check 20/20,
frame_check 38/38.

Items: D7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:58:51 -07:00
6201fcfb4a T9.6 - C2: the 390px pass, measured on all seven pages
The deliberate mobile pass the original proposal never had. Driven page by
page at 390px with mobile emulation (the media queries under test actually
fire) by NEW tests/mobile_check.py, 24/24:

- no page scrolls sideways - all seven (the creator joined at T9.5 when
  BL-001 died)
- no visible control is clipped past the viewport (the probe learned
  frame_check's two lessons: an off-canvas drawer is PARKED, not clipped,
  and a row inside an overflow-x container is scrollable)
- tap targets: the shared coarse-pointer block in wp-chrome.css puts every
  button, input, select, nav link and appbar control at a 44px minimum on
  phone widths and coarse pointers; checkboxes, radios and help-tip badges
  get the 24px WCAG floor with spacing doing the rest. Field View - the
  gloved-hands surface - measures 44px on EVERY control. Inline text links
  are exempt per WCAG 2.5.8's own exception. Even the deliberately
  unobtrusive dev toggle grew to the floor: subtle by opacity, not by size.
- CR-007 attachments offline at 390px and T8.5 requests at 390px were already
  pinned by files_check and mreq_check; this pass cites rather than repeats.

After-screenshots for all seven pages at 390px are committed in
docs/reference/baseline/after-wave9, beside the wave 0 set, captured by the
same baseline_shots.py fixture.

Verification (each probe run alone): NEW tests/mobile_check.py 24/24.
Regressions: form_structure_check 50/51 (BL-022's standing question),
files_check 36/36.

Items: C2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:47:17 -07:00
0dcea8d725 T9.5 - C1+S8: the help-tip is real, the audit is written, BL-001 is dead
S8, finished where the plan said it would be: every .help-tip badge is a
<button> - upgraded by the component itself at load (help.js), with
helpTipUpgrade() for late renders, so a badge added tomorrow is born
reachable. The count the task warned about came true: 15 at wave 0, 18 at the
wave 6 exit, 20 at the start of this task - all 20 buttons now, and the fix
being in the component is what stops the number growing again. One
viewport-clamped role=tooltip bubble serves every badge: focus shows it,
Escape hides it, tap toggles it, tap-elsewhere closes it - the touch path
Field View's tablets never had. The injected styles now use theme tokens
(four raw hexes of the S5 kind, gone).

BL-001, CLOSED after three causes and nine waves: the old CSS ::after escaped
its badge to the right and was the creator's last 390px overflow. The clamped
bubble ends it - scrollWidth 390 vs clientWidth 390 - and frame_check's pin
FLIPPED, exactly as designed: it asserted the failure until the fix landed,
and now asserts the fix so a regression reopens the entry loudly.

The audit (docs/reference/accessibility-audit.md), every number probe-backed:
- div/span click handlers: 12/2 at wave 0 -> 0 (the wizard's constraint
  library entries and the dashboard chips became buttons here; the comments
  backdrop stopped pretending to be a control)
- outline:none without replacement: 0 (wp-chrome's one is the documented S12
  exception - its ring is on :focus-within, one ring not two)
- aria-live: every toast system and banner announces
- native dialogs: 79 -> 21, all on surfaces no S1 task named (admin, users,
  launcher) - documented as BL-024 with the T7.9 kit ready for them
- keyboard-only primary flow: covered leg by leg by the probes that dispatch
  real CDP key events, cited in the document

Three stale count-pins re-pointed to the numbers this task reached (stepper's
baseline-minus-10, form_structure's one-span-left, frame_check's BL-001 pin) -
each now pins the TARGET so slack cannot hide a regression.

Verification (each probe run alone): NEW tests/helptip_check.py 13/13.
Regressions: a11y_check 22/22, stepper_check 71/71, form_structure_check
50/51 (BL-022's product question), pipeline_check 44/44, frame_check 38/38.

Items: C1, S8 (BL-001 closed, BL-024 opened)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:37:10 -07:00
b83f2fd8d5 T9.3 - S6: one icon system - monochrome text glyphs, one meaning each
The set mixed colour emoji with dingbats, and the same glyph read as two
things partly BECAUSE emoji render as per-platform artwork. The system chosen:
monochrome text-presentation glyphs - the suite is classic-script vanilla HTML
with no bundler, so an SVG sprite or icon font is a new asset pipeline, while
text glyphs render through the same font stack as the words beside them. The
enforceable form of "renders identically on Windows, macOS and a tablet":
no emoji-range codepoint and no U+FE0F selector anywhere in UI source,
swept by the probe on every run.

Converted: green-check/red-cross emoji in the admin and users consoles to
checkmark/cross, no-entry to circled-slash (blocked/on hold), the lock to the
pencil already meaning "edit with a logged reason" on sign-offs, the star to
the diamond, the folder to the reference marker, the side nav's lightning to
the gear, and the WATCH glyph (U+231A - emoji-presentation BY DEFAULT per
Unicode) to a text-presentation clock face. Dropped where the label already
carried the meaning: lightning on Save & view, the camera on Add photo, the
page/frame pictograms on file rows (the filename is the label). Stale help
copy fixed while its emoji left: it still described the pre-T9.4 "Load
sample" and the pre-T7.10 "Usage Logs" locations.

The meaning-to-icon mapping is in docs/reference/tokens.md - one meaning per
glyph, one glyph per meaning, both directions asserted from the document
itself; the probe also sweeps every page for glyphs not in the approved set,
so an unmapped icon cannot creep in.

Verification (each probe run alone): NEW tests/icon_check.py 5/5.
Regressions: frame_check 38/38, files_check 36/36, a11y_check 22/22,
cards_check 44/44.

Items: S6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:15:59 -07:00
b44afa7672 T9.4 - S7: one sample-data affordance, confirmed, and fenced off the project
Four affordances under three names became ONE: "Load sample data", on the
creator's toolbar, at the far end of two separators from the live actions
(New / Duplicate), pushed right with its own gap. It confirms through the
T7.9 dialog, naming exactly what it does - and what it does not: "This page
only: nothing is written to the project unless you then save." The probe
verifies the fence the way the done-when demands - against a REAL project,
reading the server's SOP and work-package list before and after and asserting
byte-identical.

Gone: the wizard's header "Load sample" (the dangerous one: it filled the
state completeSOP() pushes to the LIVE project, one click, no confirm, no
undo - reconciled with D1 exactly as the task records: the creator's control
is the survivor, the wizard copy goes), the creator's split Sample SOP /
Load example pair (now internals behind the one entry point), and the
empty-state context bar's third button (its text now points at the toolbar
control). The location/material "Load sample values" buttons stay: they fill
a PASTE BOX that acts only through an explicit, dry-runnable import - a
different thing, stated in the code.

Probes re-pointed with reasons in place: frame_check's D1 toolbar list names
the consolidated control; validation_check's sample-driven toast checks
became the-affordance-is-gone checks (and its stale showAnalytics drive,
orphaned by T7.10, became a the-duplicate-stays-gone check).

Verification (each probe run alone): NEW tests/sample_check.py 10/10.
Regressions: validation_check 77/77, frame_check 38/38, kitting_check 26/26,
export_check 20/20, sections_check 95/95.

Items: S7 (D1 reconciliation honored)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 13:05:56 -07:00
c2a1cc7c26 T9.2 - CR-017: Actual Hours is still there, still optional, still counted
A guard, not a build. Removal was floated in the meeting and rejected -
Marlena tracks actual hours so they can be measured - and CLAUDE.md carries
that as a recorded decision. Verified after eight waves of change:

- Actual Hours exists in Closeout (wp_actual_hrs), persists through collect,
  and prints on the export
- it is OPTIONAL: a package closes with it empty (driven, not assumed)
- it rolls up per T6.4: rollup_check has pinned actual-hours aggregation at
  every level since wave 6, and /api/wps/metrics carries actual_hours in its
  buckets

The follow-up the done-when requires is logged as BL-023: a productivity
factor (actual / estimated) - the rollup endpoints already carry both sums,
so it is a presentation task awaiting its own item id and a placement call.

Verification: export_check.py extended to 20/20 (the CR-017 section).

Items: CR-017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:53:00 -07:00
eb1497d574 T9.1 - CR-008: the export, finally walked through
Every required field verified present under the Micron configuration: P6
Activity id and description, Priority, the Building/Floor/Sector location
row, Scope & Work, Material List, Constraints with their status carried by
WORDS (a black-and-white print keeps its meaning), Quality & Hold Points, and
the Drawings & Attachments index with per-file descriptions (T7.7's columns).

What must be absent, absent: the Micron samples now carry
fields:{costCode:false, acumaticaTask:false} - CR-002's two removals,
expressed as the toggles CLAUDE.md requires, in the creator sample AND the
wizard sample - so ACU Cost Code and Acumatica Task appear nowhere on the
Micron export while the columns, model and recorded values stay. CR-006-
suppressed sections (assets, kitting) are absent, as pinned since T5.7.

Tablet legibility, three real defects fixed:
- a bare `table { min-width:520px }` in the narrow-screen media block reached
  the EXPORT tables too, dragging the whole document to 520px on a 390px
  screen; scoped to .table-wrap (the form's scroll containers), because the
  export must FIT a tablet, not scroll
- export tables now table-layout:fixed with overflow-wrap:anywhere
- at <=768px the doc sheds its 52/56px desk padding and neutralises the
  inline column widths (the one legitimate !important: outranking an inline
  style is its job)

RAISED, NOT DECIDED (the task says propose, do not assume) - merge versus
list for attachments: RECOMMEND MERGING image attachments into the printed
document (already done - they print inline as the sheet itself) and LISTING
PDFs as named, described links rather than merging them. Merging PDFs
server-side needs a PDF library dependency and re-renders every export for a
need the meeting expressed as "hand someone exactly the sheet" - which the
5MB single-sheet uploads plus inline images already serve. If merged-PDF
output is wanted anyway, it is a bounded server task - needs Nick.

Verification (each probe run alone): NEW tests/export_check.py 17/17.
Regressions: form_structure_check 50/51 (the standing F6 height question,
BL-022), sections_check 95/95.

Items: CR-008 (CR-002 field toggles applied to the samples)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:51:44 -07:00
898e5dab94 T8.5 - CR-013/D6: the material request is structure, not features
The OneNote comparison from the meeting was "word vomit"; the structure that
replaces it, built at the lightweight scope EXACTLY as approved Aug 14:

- Line items (qty, unit, description) added, edited, removed. Descriptions
  offer the D6 project list through a datalist - which is also precisely what
  keeps free text working when no list is loaded, the state every project is
  in today. Picking a listed material fills its unit; nothing locks.
- Needed-by date, requestor (the signed-in account), delivery location (T8.4's
  fields on this package, composed), and an explicit status set
  (Requested / Filled / Declined). The request rides on the package record
  (data.materialRequests) - server-persisted through the same upsert as
  everything else, never localStorage.
- Submitting notifies the warehouse owner named on the package (CR-010) - the
  routing that replaces the funnel through one person - through the T7.6 gate,
  with the count, the needed-by, the delivery location and the deep link, in
  the house convention. material_requested lands in the audit history.
- The dashboard grows a Material requests queue, filterable by status and by
  delivery location.
- The block lives inside #material-card, so the CR-006 materials toggle
  governs it with no special casing. The whole flow is driven at 390px -
  requests originate in the field.
- NO parts catalog, no inventory count, no warehouse integration - the probe
  greps the block for them.

One infrastructure bug fixed in passing detection (not silently): T8.5's
dashboard-panel insert matched the substring inside "async function
dashIssue", splitting the async keyword from its function - the creator
failed to parse and every boot died. Caught by the probe's first run;
anchored fixes now restore both halves.

Verification (each probe run alone): NEW tests/mreq_check.py 19/19 (request
end-to-end at 390px against the SMTP sink, dashboard filters, fences).
Regressions: frame_check 39/39, sections_check 95/95, kitting_check 26/26.

Items: CR-013, D6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:33:34 -07:00
190144c539 T8.6 - D6: the material list uploads the way the location list does
CR-013 accepted free text because the master workbook never arrived; the
Aug 18 call was the CR-005 call again - build the upload path now.

THE component, extracted: T5.4's paste-or-file machinery (file read in the
browser, ONE parser on the server; dry-run check; a report naming every
rejected row with its source line; an editable list that deactivates rather
than deletes) moved from the location-specific functions into
html/wp-list-import.js. The location list and the new material list are both
instances of it - the done-when's "against the same component, not beside it"
made literally true. The loc* names survive as thin delegates because row
handlers, step entry and the probes call them; locations_check re-pointed its
fetch-count assertion to where the fetches now live and still demands every
read and write reach the server.

The material list itself: description, unit, optional code - one new table
(Alembic a1b8c6d4e2f9, additive), GET/import/POST/PATCH routes on the CR-005
pattern, deactivate-never-delete, reactivation reuses the same row so nothing
referencing it orphans. The sample rows are obviously fake (SAMPLE-EMT-075).
NO inventory, price, stock or warehouse field anywhere - the probe walks the
model's columns by regex. The wizard hosts it on step 11 beside the location
list, optional by design: a project with no list still raises free-text
requests (T8.5 wires that).

Parser bug caught by the probe's first run: strip(',;') ate a LEADING comma,
so ',FT' - an empty description - was accepted as a material named FT.
rstrip only, now; the empty first column is rejected with its line number.

Verification (each probe run alone): NEW tests/materials_check.py 17/17.
Regression: locations_check 58/58 through the shared component.

Items: D6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:24:21 -07:00
b9d5f8ef92 T8.4 - CR-012: the delivery location is the shared vocabulary plus fifty feet
Staging is not the pain; the last fifty feet are - the correct floor lay-down,
shark cage or conduit tree instead of material picked at will by whoever is
closest. The Kitting & MIMO section gains:

- Delivery Building / Floor / Sector: the SAME dependent pickers CR-004 built,
  through the same fillLocSelect (which learned an optional field-map instead
  of being copied), reading the same project location lists, storing PATHS.
  A parallel free-text location vocabulary is exactly what CR-004 removed;
  none was added.
- A free-text detail field for the specifics ("Shark cage 7, conduit tree C"),
  persisted as delivDetail.
- deliveryLoc, the composed display string (labels off the shared lists, then
  the detail after a dash) - which is what the CR-011 email already reads
  (kitting_body preferred deliveryLoc from day one, with mimoLoc as the
  pre-CR-012 fallback) and what the package printout now carries as its own
  Delivery Location row.

Verification (each probe run alone): kitting_check.py extended to 26/26 (the
delivery selects are asserted to offer the SAME option list as the CR-004
trio, values persist as paths, the printout carries the composed value);
kitting_notify_check 17/17 now asserting the mail carries CR-012's composed
value, not the fallback. Regression: locations_check 58/58.

Items: CR-012

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:11:05 -07:00
829fa22236 T8.3 - CR-011: material moves, the field hears about it once
A kitting status change (detected on the upsert, which is how the browser and
the offline outbox both save) emails the package's distribution list
(distributionIds) plus its warehouse owner (kitOwnerId - CR-010's default
recipient), minus the actor, deduplicated. The mail matches the house
convention - greeting, one line of what happened, the deep link, the
automated-message footer - and says old status, new status, who, and the
delivery location (deliveryLoc when CR-012 lands at T8.4; mimoLoc today).
The link opens THAT package (X1), same wp_link as every other mail.

No burst: an unsent notification for the same package and recipient is
REWRITTEN to the newest transition instead of joined by a sibling - three
rapid changes leave one row per recipient saying where kitting ended up,
while the audit history keeps all three, uncoalesced. Found by the probe and
fixed: a row held while email was OFF stayed 'skipped' forever; the change
that finds email ON now promotes it to pending and schedules it - otherwise
turning the gate on silently orphaned everything coalesced before it.

The gate is T7.6's gate, reused - the probe greps that no second email flag
exists anywhere. Off by default; admin-only (403 for anyone else); every send
terminates at the in-process SMTP sink with count and recipients asserted; no
real mail leaves this branch. Send failures ride the shared notify.deliver
path whose failure handling qa_gate_check pins.

Verification (each probe run alone): NEW tests/kitting_notify_check.py 17/17
(the sink is imported from qa_gate_check - one sink implementation, not two).
Regression: qa_gate_check 40/40.

Items: CR-011, D10 (X1 honored)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:05:24 -07:00
96387105f4 T8.2 - CR-010: the warehouse owner is an account, on the package
A named person owns fulfillment of the kit - today that is Paul Coonrod,
informally, and everything bottlenecks through him. The package now records
it explicitly:

- The Warehouse owner control is a dropdown of project members (the same
  roster the Owner picker reads). Picking someone stores BOTH the display
  name (kitOwner - exports and old renderers keep working) and the account id
  (kitOwnerId - the routing CR-011's notifications will read at T8.3).
- Confirmed Aug 18: the field lives ON the work package, not the project - a
  package retargeted to a different warehouse notifies the right person
  without touching the project. The wizard gets no field.
- A stored name with no matching account - typed before the field was
  account-backed, or someone since removed from the project - is KEPT as a
  selected "(no account)" option and round-trips unchanged. Removing someone
  from the project breaks nothing.
- The dashboard filters by warehouse owner, options drawn from the owners
  actually present in the data - a filter offering people with nothing to
  fulfill is noise.

Verification (each probe run alone): kitting_check.py extended to 21/21
(T8.2 section: picker, id+name persistence, orphan survival, board filter).
Regression: generalinfo_check 49/49.

Items: CR-010

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:00:14 -07:00
79c4a36c70 T8.1 - CR-009: kitting statuses are a set, and Micron EUV is not kitting
The statuses: the proposed five, adopted as proposed - Not Started, Picking,
Staged, In Transit, Delivered - as one named constant (KIT_STATUSES) building
the select. They describe fulfillment; the old four ('Open', 'In Progress',
'Kitted', 'Delivered') mixed fulfillment with workflow. A value stored before
the set existed is kept, selected, and shown as "(legacy)" - CR-016's rule
that renamed vocabularies must not orphan recorded data - and round-trips
through collect unchanged.

Micron EUV: the sample SOP is the Micron configuration on record (CR-016 /
T5.7), and it now names kitting:false beside assets:false - off by CR-006
TOGGLE, in both the creator sample and the wizard sample. The section leaves
the form, the rail and the export; its data and model stay exactly where they
are (the probe loads the example package under the Micron sample and finds
its kitting values intact through collect). Any other SOP turns the section
on and it works fully - driven against sopA with everything enabled.

sections_check re-pointed, not relaxed: its sample-map pin said "naming only
assets"; it now says "naming exactly assets (CR-016) and kitting (CR-009)" -
still refusing any section that goes off without a recorded item behind it.

Verification (each probe run alone): NEW tests/kitting_check.py 14/14.
Regression: sections_check 95/95.

Items: CR-009

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:55:39 -07:00
7f712b7e00 T7.9 - S1 (creator): errors at the field, and the last 40 native dialogs gone
wp-creation-app.js:1144 said "Subject and WP Type are required" in an alert()
on a form ten cards deep, naming nothing, focusing nothing. The creator
carried 40 native call sites in all (43 at the wave 0 count; three had
already left with D5 and T5.8's wizard work).

Inline validation, the T5.8 wizard pattern applied to the creator:
- WP_REQUIRED is one table: field id, owning section, label. The error box,
  aria-describedby, aria-invalid and the role=alert announcement all follow
  from a row. The conditional IFF rule folded in beside them.
- Submit marks every failing field, marks the rail entry of each section
  holding one (a "!" chip - a character, not only a colour), switches to the
  section of the FIRST error, scrolls to and focuses the field, and announces
  the failure through the role=alert toast.

One modal replaced confirm() and prompt(): promise-based wpConfirmDialog()/
wpPromptDialog() with an optional input whose validation renders AT the input
(a bad answer keeps the dialog open and says why - no round-trip through a
second dialog). Escape cancels; callers read like the natives they replaced,
awaited. Pure notifications became role-differentiated toasts. The modal
validation errors for the hold log and the QA rejection render inline in
their own modals.

The A1 path: confirmEarlyRelease() keeps its name and contract - truthy means
proceed with the reason recorded - and became async; every caller awaits it
(status control, hold release, urgent override, save).

App-wide native dialog count, recorded per the done-when: the probe prints it
against the wave 0 baseline of 79 and asserts the creator contributes 0. The
probe also replaces the natives with throwing stubs for the whole run, so any
path that still reached one would fail loudly.

hold_check re-pointed, not relaxed: three flows it drove through native
stubs now drive the modal - same propositions (the release-ready offer, the
named-constraints override prompt, the hard block), new surface.

Verification (each probe run alone): NEW tests/creator_dialogs_check.py
20/20. Regressions: hold_check 50/50 (re-pointed), warning_check 17/17,
qa_gate_check 40/40, triage_check 16/16, files_check 36/36, frame_check
39/39, generalinfo_check 49/49, form_structure_check 50/51 (the standing F6
height check - see the wave exit).

Items: S1 (creator half)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:46:25 -07:00
82a8f30074 T7.10 - D5: one analytics implementation, and its report on the admin console
Usage analytics existed as five of the nine colliding globals creator-frame.md
counted (ANALYTICS_KEY, analyticsLoad, analyticsSave, downloadAnalytics,
showAnalytics), twice - and the wizard's copy had no caller, because the
button lived on the creator. The admin console had a THIRD private reader
(usageLoad/downloadUsage) that only saw the wizard's key.

Now: ONE core, html/wp-usage.js (window.WPUsage: load/save/track/download +
the two pre-move storage keys, verbatim). The creator and wizard keep only a
thin track() wrapper - page state like the creator's dev-mode pause belongs
to the page - and record exactly what they recorded before, under the same
keys, so everything captured before this task still reads (probe plants a
legacy-format event and finds it in the report). The "Usage data" button left
the creator toolbar; the report lives in admin.html's usage card, covering
BOTH tools with a download each, behind the same admin gate as the rest of
the console (a non-admin sees the denied card and nothing else), usable at
390px.

Two probes re-pointed, both with the reason in the code:
- cards_check pinned admin.js byte-identical to HEAD - right for T6.5, but as
  a standing probe it would fail every legitimate later edit; D5 targets
  admin.js by name. A7's localization is protected by the feature checks and
  the end-to-end drive, plus a wiring assertion on the block itself.
- frame_check listed "Usage data" among the toolbar buttons that must be
  visible; it now asserts the button is GONE, so the duplicate cannot quietly
  return.

Verification (each probe run alone): NEW tests/usage_check.py 15/15 (grep
half: WPUsage defined once, no page touches the keys directly, none of the
five globals survives anywhere). Regressions: cards_check ALL PASS,
frame_check 39/39.

Items: D5

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:29:27 -07:00
e3de3c7c00 T7.8 - B6: the wizard's actions ride the viewport, not the page
On the Constraints and Sequence steps the proposal's beside-the-fields actions
meant scrolling to save. The wizard's .step-navigation bar is now
position:sticky at the viewport bottom - the creator's sticky-bar pattern,
adapted rather than duplicated: sticky (not the creator's fixed) because the
bar lives inside the wizard's grid column, keeps its slot in the flow, and
therefore CANNOT obscure a field at any width - no padding arithmetic to get
wrong. Opaque background, top border and the shared --wp-shadow-sticky token
so content scrolling beneath it reads as beneath it.

The T4.4/B5 save-state indicator already mounted in this bar; it now rides the
viewport with the buttons, which is the "shows the save state" criterion.

Verification (each probe run alone): NEW tests/sticky_bar_check.py 12/12 - a
primary action inside the viewport on all 12 steps unscrolled at a 700px
viewport (short on purpose: both named steps genuinely overflow, asserted),
still visible fully scrolled, nothing obscured at 390px. Regression:
stepper_check ALL PASS.

Items: B6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:14:33 -07:00
c084f730b3 T7.7 - CR-007/D8: the sheet travels with the package, and opens offline
The field wants the specific PDF attached, not a link to a Bluebeam session.

Storage: a new wp_files table (Alembic f3a9d2c1e8b7, additive only) holding
the BYTES in the same database as everything else - the Aug 18 decision: a
backup that excludes the drawings is a backup you cannot restore from. The
D8 numbers bound the cost and are enforced ON THE SERVER as well as in the
browser: 5MB a file (413, naming the limit), PDF and image mimes only (400,
naming what is accepted), 2GB a project (413 naming the ceiling; response
flags the 80% warning). The ceiling is env-overridable for tests; the shipped
default is the decision, asserted from source.

The package record carries a server-owned meta mirror (data.files): the
upload/patch/delete routes rewrite it, and the upsert re-asserts the stored
copy over whatever a client sends - a save from a browser that had not seen
an upload land cannot erase the list.

Creator: uploads live beside the links (links still work), the limits and the
running project total sit ABOVE the picker (amber from 80%, red at full), a
refused file costs nothing but a toast and never leaves the browser (the
probe counts fetch calls), and each drawing has a description ("Tray section,
Level 3 east only") editable inline and persisted server-side. Uploads attach
to the saved record, so T4.3's autosave keeps the surrounding form safe (X8).

Export: uploads print with the package - name, size tag, description on the
attachments table, images inline as the sheet itself, PDFs as links.

Offline (D8): the service worker gains a drawings cache (cache-first on
/api/files/), and field.js prefetches ONLY the requesting user's assigned
packages - assignment-scoped by decision, not project-wide. The probe's first
offline check used CDP network emulation and PASSED FOR THE WRONG REASON: the
emulation binds to the page's session and the service worker fetches on its
own target, straight past it. The shipped check kills the server instead -
my drawing opens, the other package's does not, against a genuinely dead
network.

Field View: a Drawings section on the package detail, 44px rows, description
inline, inside the 390px screen.

Verification (each probe run alone): NEW tests/files_check.py 36/36; the
Alembic chain applied end-to-end to a scratch DB and the table verified.
Regressions: form_structure_check 50/51 (the standing F6 height gap),
frame_check 39/39.

Items: CR-007, D8 (X8 honored)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 11:09:35 -07:00
2486f87010 T7.6 - CR-014/D2/D9/D10: the Ready for QA gate, notification only, shipped off
Marlena's ask: QA sees inbound work ahead of time, not after the fact.

The rung: 'Ready for QA' sits between In Progress and QC in BOTH ladders
(wp-creation-app.js STATUS_ORDER, server/app.py STATUS_ORDER) and in Field
View's list - inside the T7.3 transition model, not beside it: entering it
from an unreleased state crosses the release gates, and 'Issue' (hold) stays
a branch. The dashboard filter and the navigator grouping learned the state
from the ladder without their own edits.

Who hears (D2): the QA GROUP, a multi-pick of project members on the SOP
wizard's team step, stored as account ids at data.sop.project.qaGroupIds.
Entering Ready for QA emails that list and nobody else. A rejection emails
the owner AND the same list (amended answer), returns the package to In
Progress, and REQUIRES a fresh comment - server-enforced on both write paths
(the first version accepted any old comment already on the record, which made
every rejection after the first one free; the gate now demands a new entry).
Accept and reject are real buttons on the release banner; the comment modal
enforces its field; qa_ready / qa_rejected / status_changed all land in the
audit history.

The link (X1): wp_link() now opens THE package - wp-creation-index.html
?project&wp=<id>, which the creator boots directly and login.html?next=
round-trips for a signed-out recipient. It previously pointed at the suite
root, which is exactly the failure X1 names; assignment mail inherits the fix.

Email discipline (D10 + standing rules): ships OFF (the stored setting the
admin console already owns; PUT /api/settings is admin-only, 403 for anyone
else, and audited). With it off, transitions write outbox rows marked
'skipped' and the sink receives nothing. With it on, the probe runs a REAL
SMTP conversation against an in-process capture sink and asserts the count
and the exact recipient set. A dead SMTP host leaves a 'failed' outbox row
with the error recorded. The SMTP password exists only in the environment.

DEVIATION, stated: the task's Do-paragraph asks the email to include location
and a scope summary; the done-when list (and CLAUDE.md) says no customer IP in
a message body. The done-when wins: bodies carry the WP number, who moved it,
and the deep link. A location canary planted on the package is asserted absent
from every captured message. If the fuller body is wanted, that is a product
call - needs Nick.

Found while building, logged not fixed (BL-021): project_sop_team() reads
sop.data['project'], a path pushSOP never writes - the critical-reopen email
has never actually reached the PM/CM. One-line fix, owned by T9.9.

Field View (D9): 'Ready for QA' is carried by TEXT on the card at 390px.

Verification (each probe run alone): NEW tests/qa_gate_check.py 40/40.
Regressions: hold_check 50/50, pipeline_check 44/44, aggregates_check 16/16,
frame_check 39/39, validation_check 83/83.

Items: CR-014, D2, D9, D10 (X1, X3 honored)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:54:07 -07:00
5d27a1e086 T7.5 - A6: the sidebar answers the stand-up question
Use case from the task: someone is asked in a stand-up why a package has not
moved; they open it on a phone and need the answer without scrolling or
clicking. The navigator row now carries:

- a triage line: status - priority - due date - P6 activity, with an em dash
  for anything unset (a placeholder is information; a gap is a question)
- the open-constraint count (already in the state chip; on a held row it moves
  into the hold line so it is never displaced by "on hold")
- the hold reason INLINE, from the newest live entry in data.holds - the modal
  captured it at T7.3, so this is display work, exactly as the task said. A
  held package with no recorded entry (legacy data) says "no reason recorded -
  log it from the status control" rather than rendering an empty red slot.

The row's title attribute keeps its hover summary, but hover stops being the
only path to any of this (C1 - Field View runs on tablets). Triage and reason
lines WRAP instead of ellipsizing - an ellipsis would hide exactly the data
the row exists to show; the reason clamps at three lines so one essay cannot
swallow the panel. Rows align flex-start to take the extra height.

At 390px the panel is the existing overlay drawer; the row fits it with no
sideways overflow and stays a >= 44px tap target.

Verification (each probe run alone): NEW tests/triage_check.py 16/16 covering
the held/plain/legacy row matrix at 1440px and 390px. Regression:
frame_check 39/39.

Items: A6

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:28:02 -07:00
60434d452c T7.4 - A2: one warning, said once, visible from anywhere
The same not-release-ready warning rendered three times on the creator:
1. the release banner under the context bar   - STAYS, and is now the only one
2. updateStickyStatus() in the sticky save bar - removed
3. a static field-hint under the status radios - removed

The count moved to a badge on the Constraints rail entry (D3's rail replaced
the tabs A2's "tab count badge" referred to). The rail is position:sticky at
BOTH widths, so the badge is on screen from any section at 390px and 1440px -
measured with the constraint table AND the banner both scrolled out of view.
The badge is a number, not a colour: the count is the content, and the rail
entry carries an aria-label saying it ("Constraints - 3 open").

The banner is now role="status" (the login.html aria-live pattern, per C1) and
only rewrites when its message actually changes - a live region that repaints
on every save announces on every save.

Duplicate 2 was not just noise. It wrote the warning with textContent into
the SAME span the B5 autosave indicator mounts into, destroying the indicator
on every count change. Removing the duplicate is what fixes that; the probe
pins the indicator's survival across banner updates.

Verification (each probe run alone): NEW tests/warning_check.py 17/17.
Regressions: hold_check 50/50, form_structure_check 50/51 (the standing F6
height gap, re-measured after T7.5 as recorded at T7.2).

Items: A2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:20:12 -07:00
2b597e68d8 T7.3 - CR-015/A1/D4: the hold clears when the constraints do
ROOT CAUSE, exactly (the done-when asks for it):
Hold state was stored, twice, and derived nowhere.
1) Client: submitHold() wrote prevStatus='Issue', destroying the status the
   hold interrupted at the moment it was placed - there was never anything to
   return to. Clearing the last constraint then fell into the "Mark it as
   Issued now?" confirm, because STATUS_ORDER.indexOf('Issue') is -1 and -1
   reads as "before Issued". Decline it and the package stayed on hold with
   zero open constraints, forever - the exact state reproduced live in front
   of the Micron team.
2) Server: server/app.py's STATUS_ORDER put "Issue" at index 4, so
   _released('Issue') was true and every transition OUT of hold skipped
   enforce_release_gates() as "already released". POST /api/wps/{id}/status
   could walk a held package to Issued past its open constraint. The comment
   claimed the ladder was "mirrored in the front end"; the front end's ladder
   has no 'Issue' in it at all.

What changed:
- setConstraint() recalculates hold state on EVERY constraint change: clearing
  the last open constraint on a held package releases it immediately - no
  refresh, no dialog - back to the status recorded on the hold entry (`from`),
  which now rides on data.holds and survives save/reload.
- Every hold and release is history: pkgHolds entries carry ts, by, from/to,
  reason; the exported Hold Log gained a By column; the server writes
  hold_logged / hold_released audit rows (with the reason from data.holds) on
  both the upsert and the /status endpoint.
- _released() no longer counts the hold: 'Issue' is a branch, not a rung.
  Leaving hold to a field state re-runs the gates; entering hold never did and
  still does not. The critical-reopen email keeps its old reach ("has been in
  the field" includes on-hold).
- A1 preserved by name and by test: confirmEarlyRelease() still the one place
  a gate override is written (comment-stripped grep asserts exactly one
  pkgGateOverride assignment), still reason-first, still logged server-side.

D4 - what Urgent does (amended Aug 18): surface the audited path, add no new
one. confirmEarlyRelease() now also covers open constraints, but only for an
Urgent package, and the override must NAME every constraint it crosses - the
server refuses coverage by an old reason. The release banner gives an Urgent
package the override as its primary action (a real <button>); Normal and High
see nothing new and keep the same hard refusal, asserted per priority.
Banner button styled from tokens only; the banner now wraps at narrow widths.

Product question raised, not decided (per CLAUDE.md "asking versus assuming"):
Issue (hold) remains selectable from Draft and Scheduled, as it was before.
The done-when names no state list, so nothing was restricted. If a pre-release
hold is meaningless, closing it off is a one-line follow-up - needs Nick.

Verification (each probe run alone): NEW tests/hold_check.py 50/50, including
the clear-last-constraint regression specifically, the D4 priority matrix
against the server (six 409/200 cases), hold_logged/hold_released audit rows,
and an AST sweep proving every wp.status assignment in server/app.py sits in
a function that runs enforce_release_gates. Regressions: frame_check 39/39,
aggregates_check 16/16.

Items: CR-015, A1, D4 (X2 correction already recorded Aug 18)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:05:57 -07:00
755c976841 T7.2 - F6/D3: the form gets structure - a section rail, one section open
F6 as amended by D3 (Aug 18): one page, persistent side navigation, sections
collapsible, only the current one open by default, plus Expand all. Tabs were
rejected in D3 because they hide sections a first-time author does not know
exist.

What changed:
- The jump-chip strip (#section-nav, span onclick) is gone. In its place a
  <nav> section rail of real <button> entries, aria-current on the current
  section, 44px tap targets, above the form at 390px and beside it at 1440px.
- Every section heading is now a disclosure <button> with aria-expanded and
  aria-controls. One section open at rest; Expand all (aria-pressed) opens
  everything and is remembered per browser.
- Sections are URL-addressable (?section=, T4.2 machinery) and a deep link to
  a collapsed section expands it. Positional-id fallback removed: a card
  without an id gets a console.error and no rail entry, never an invented
  sec-N id that would ride into shareable URLs and move between visits.
- General Information (1,288px on its own) split into #general-card and
  #assign-card (Assignment & Schedule). The split is presentational: both
  cards are the ONE CR-006 section `general` (WP_SECTION_NODES lists both),
  so wp-sections.js and the SOP wizard are untouched. CR-001's adjacency
  (P6 activity beside due date) is preserved and asserted.
- gotoSection() flushes autosave, which the deleted chips used to do.
- secMakeToggle() preserves every element child of a heading - help tips go
  outside the button, everything else inside the label. The first version
  cleared textContent and destroyed #saved-count, which killed boot one line
  short of wpCreatorReady with the page still visibly rendered.
- BL-013 folded in per the task: the T3.4 focus ring on the rebuilt form.
  frame_check reports outline solid 2px on creator inputs.

Height, measured not asserted: 5,399px before; 1,995px at rest at 1440x900.

DONE-WHEN NOT FULLY MET - stated per CLAUDE.md rather than marked complete:
"no single view exceeds roughly two screen heights at rest" reads 2.22
screens (1995/900). The remaining gap is page chrome this wave reworks:
.ctx-bar (67px, T7.4) and .release-banner (45px, T7.5). The criterion was
already amended once (D3, "at rest") and is not being moved again to fit;
tests/form_structure_check.py keeps the check red and it is re-measured at
the end of wave 7. Every other done-when entry passes.

Backlog: BL-001's cause corrected a third time - at rest the overflow is
help.js's .help-tip::after tooltip (481 vs 390), the S8 component T9.5
rebuilds; the tables still overflow only when expanded. Deliberately not
fixed here - a fix would be thrown away with the component at T9.5.

Verification (each probe run alone): form_structure_check 50/51 (the height
check above), sections_check 95/95, generalinfo_check 49/49, frame_check
39/39 regression pass.

Items: F6, D3, BL-013, BL-001 (re-measured)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:38:31 -07:00
12d19446d5 T7.1 - B7: dissolve the creator iframe, and D1 give it back its sample data
There is no iframe in html/ any more. The creator is a top-level document with
the same app bar and the same tab strip as the SOP wizard; the two tabs that
used to swap a frame are links between them.

DEVIATION, stated rather than smuggled. The wave file says "remove the iframe
boundary so the creator renders in the parent document". It renders as its own
document instead. Every done-when is met - no iframe, no cross-frame messaging,
F4 resolved structurally, CR-006 toggles with no special-casing, back and
forward intact with T4.2's URL state - but the route is the other one, and the
reason is in creator-frame.md's own numbers:

                                    merge into parent    make it a page
  selector collisions to resolve                   21                 0
  script global collisions                          9                 0
  cross-frame call sites to remove                 28                28
  probe entry points needing rework               ~29                 2

The 21 and the 9 were never the cost of dissolving the boundary. They are the
cost of MERGING TWO DOCUMENTS, which is a different change the boundary was
hiding. And 29 probe call sites address wp-creation-index.html directly, so a
route that keeps that address keeps all of them. creator-frame.md section 5
records this in full.

What went, and what replaced it:

  #wp-frame, applyEmbedLayout, sizeWPFrame, viewportMinusChrome, chromeHeight,
  renderWPTab, the resize handler, the ResizeObserver, --wp-chrome-h,
  .content-area.embed-full, body.embed-full   ->  the window sizes the page

  ?embedded=1, body.embedded, .embed-hide, .embed-first   ->  nothing. An old
  link carrying the param is ignored rather than half-obeyed.

  openWpById / showDashboard / showForm / dashApplyFlag / applySopSections
  called across the frame   ->  the URL. ?project= ?view= ?wp= ?flag= were
  already read at the creator's own boot (T4.2), which is exactly why those four
  could be DELETED rather than migrated. X4 is closed: the surviving path is the
  one T5.5 built and proved.

  inIframe in auth-guard.js, wp-chrome.js, wp-sidenav.js, help.js and _isTop in
  project-data.js   ->  gone. help.js now reads the explicit WP_HELP_NO_FAB flag
  both tool pages set, instead of inferring intent from where it is rendered.

  .main-nav / .nav-tab in work-package-suite-styles.css   ->  wp-chrome.css,
  because a tab row only one of two documents can style is the shape that put
  the tabs in the parent and the toolbar in the child to begin with.

The three questions creator-frame.md section 4 said no count could answer:

  1. The creator gets the app bar. It was the only page loading neither
     wp-chrome file. Its header is now the .header-left / .header-right pair the
     wizard uses, so the switcher lands in the same place on both.
  2. Two sequence components, scoped not merged - confirmed Aug 18 that the
     sequence is authored in the SOP and adjustable per package. BL-015 stays.
  3. body.embedded is gone. The header it hid is replaced by the app bar; the
     sample controls are visible in a new package toolbar (D1); the analytics
     button is visible there until T7.10 moves it. The Dashboard BUTTON in that
     row became a TAB, which is the one place B7's "fold the toolbar into the
     tab row" actually happened.

Old addresses still resolve. ?tab=wp, ?view=dashboard and ?wp=<id> are in
bookmarks, in wp-sidenav's link map, and they are the shape CR-011 and CR-014
were specified against (X1). The wizard forwards them with replace(), so Back
does not bounce. Breaking these silently was the one regression this task could
have shipped that nobody would notice for weeks. frame_check.py section 4 pins
all three.

BEHAVIOUR CHANGE, deliberate. The live cross-frame hand-off showed the creator a
section toggle that had NOT been saved: flip it, look, reload, and the section
came back. What the creator shows now is the SOP that is stored. sections_check
5b pins both halves - an unsaved toggle does not travel, a saved one does.

BEHAVIOUR CHANGE, not deliberate, logged as BL-020. A tab switch is a page exit
now, so leaving the wizard with unsaved SOP edits fires T4.3's unsaved-work
guard. Nothing is lost - the guard writes the draft first and T4.3 recovers it -
but it is friction that did not exist, and suppressing a deliberate guard is a
product decision with its own downside. Logged, not quietly handled here.

tests/frame_check.py, 39 checks, new. Two of them exist because of failures
during this task rather than in it:

  - "both documents parse and boot". A const shadowing a function parameter is a
    SyntaxError, and work-package-suite-app.js did not parse at all for one run.
    Four checks in url_state_check went red and not one said "the script did not
    load". Asserting a page's own entry points exist costs nothing.
  - "focus emulation is on, so a focus reading means something". An earlier draft
    called page.call instead of page.ws.call inside a try/except and measured
    nothing, reporting no focus ring anywhere - which looks exactly like a
    finding. Trap 5 in reverse, for the second time in this project.

The four backlog entries logged against this file, re-measured rather than
assumed:

  BL-001  still reproduces (485px in a 390px viewport) but its RECORDED CAUSE IS
          WRONG. --nav-w now computes to 56px, so the injected-style explanation
          is spent. The overflow is the creator's data tables - #asset-body's
          lays out at 520px with no scroll container. frame_check reports the
          offending boxes by selector and skips position:fixed subtrees, because
          the comments drawer parked off-screen at right:844 made the first
          measurement blame the drawer. Pinned, not fixed: T7.2 lays out the form.
  BL-013  CLOSED. It was fixed by S12 in WAVE 4 - wp-creation-styles.css:209
          carries the comment naming this entry - and nobody updated it. It was
          quoted as a live CLAUDE.md violation while planning wave 7 and had not
          been true for four waves. a11y_check walks 120 focusable elements on
          the creator and every one rings at >= 3:1.
  BL-006  15 by the probe's measure, unchanged; different denominator, stated.
  BL-007  68 raw radii by the probe's measure. Nothing has reduced it in four
          waves; it is measured every run now instead of once.
  BL-018  cost a FOURTH probe. frame_check imports set_sop from sections_check
          rather than writing a fifth copy of the workaround. T9.9 owns it.

Probes re-pointed, with reasons in the files: sections_check 5b (drove the live
hand-off), pipeline_check check 2 (read through contentDocument), f_items F4
(drove standalone and embedded; there is one mode now), validation_check
(lost "the wrong tab", gained the SOP gate).

Verified: frame_check 39/39, sections_check 95/95, pipeline_check 44/44,
url_state_check 23/23, validation_check 83/83, a11y_check 22/22,
autosave_check 34/34, aggregates_check 16/16, stepper_check 71/71,
browser_check 71/71, launcher_check 58/58, generalinfo_check 49/49,
rollup_check 63/63, cards_check 44/44, locations_check 58/58.
f_items: F1-F5 fixed, F6 reproduces (T7.2).
Metrics: iframes 1 -> 0, colour literals in rules outside theme-light.css 0,
dialogs 64, <div onclick> 2, .help-tip 18.

Items: B7 D1
Task: T7.1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:26:27 -05:00
7e33a3cbfd Wave 6 exit - the work package's general information
Five tasks, three probes, 156 new checks. Every wave-6 item persists, exports
and filters, and the rollup adds up at every level rather than only at the leaf.

Each task had one decision it had to make rather than inherit, and each is
recorded in wave-6.md because a later reader will otherwise read the behaviour
as an accident:

  T6.1  blanks sort LAST in both directions
  T6.2  priority sorts by escalation, not alphabetically
  T6.3  the stored value is the full path, not the node's own code
  T6.4  the unassigned group is shown, or the totals do not reconcile
  T6.5  a card has three states, so it needs three status lines

admin.js is byte-identical. A7's note about localization is the loudest "do not"
in the wave file and cards_check proves it two ways.

Screenshots re-captured at 390 and 1440 across all seven pages. One overflow,
the known creator@390 (BL-001), unchanged.

Carried forward unchanged: BL-010 (829 spacing/type literals - wave 6 re-laid-out
none of the pages carrying them), BL-018, BL-019. BL-018 has now cost three
separate probes a hand-seeded SOP; browser_check's fixture should adopt the
production {sop, state} shape when it is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:26:29 -05:00
55caefb099 T5.8 - S1 (wizard): errors at the field, and the last dialog is gone
S1 has two halves and they are easy to conflate.

One is that validation was a native dialog: "Please complete all required
fields: Project Name, Number, Client, Division, and Site Location." names five
fields at once, highlights none of them, and scrolls nowhere. The other is that
validateStep guarded steps 1, 5 and 6 while the MARKUP marks required fields on
1, 3, 5, 6 and 7 - so two steps' asterisks meant nothing at all, which is worse
than no asterisk.

Both fixed, and the second is the one worth measuring: the probe reads the
required-field list out of work-package-suite.html rather than out of STEP_GATES,
because a probe that read the table would agree with whatever the table says and
prove nothing. Both notations count - an asterisk in a <label>, and the red span
beside step 3's role titles.

Now: an error per FIELD, rendered at it, associated by aria-describedby, marked
aria-invalid, announced through role="alert", and the first one focused and
scrolled into view. The error boxes are BUILT from the gate table rather than
written into the markup twelve times - adding a required field is one row, and
its error element, its association and its announcement all follow. A
markup-side error box somebody forgets to add is an error nobody ever sees.

An error clears as you type rather than on the next submit. An error still
showing over a field you have just corrected teaches people to ignore errors.

And nothing paints a step you have not tried to leave: the rail asks
stepGateMet(), which reads the same fields and marks none of them.

The thirteen dialogs

  Every one is now the thing it should have been - an error at the field it is
  about, or an announcement in a live region with the role T4.5 established:
  errors interrupt, confirmations do not.

  A dialog is not merely ugly. It blocks the page, cannot be placed or styled, a
  screen reader can present it only as a modal interruption, and it is one OK
  button whatever it says - so "sample data loaded" and "you cannot do that"
  arrived identically.

  Two deserve naming. The empty-comment alert became an inline error on the
  feedback textarea. And showAnalytics() was a confirm() carrying the entire
  usage summary as its body - a wall of text in a dialog whose only dismissal
  was also the download button. The summary is the useful part, so it is shown,
  with the download offered as an action beside it. That function has no caller
  in the wizard's markup (the "Usage data" button is the creator's, calling the
  creator's own showAnalytics), and it was converted rather than deleted:
  deleting a feature is not what this task was asked to do, and its dialog
  counted toward the number this task has to drive to zero.

  html/work-package-suite.html       #wp-toast, an error box on the textarea
  html/work-package-suite-app.js     STEP_GATES widened; per-field messages;
                                     ensureErrorBoxes; wizardToast; 13 removals
  html/work-package-suite-styles.css .wp-toast
  tests/validation_check.py          new - 81 checks
  tests/stepper_check.py             its "the alert T5.8 still owns" check now
                                     asserts the opposite, by name

Done when
  [x] every step with required fields validates them - 5 steps, from the markup
  [x] each error renders at its field and is associated via aria-describedby
  [x] submitting an invalid step focuses AND scrolls to the first error
      (scroll checked by bounding box, not by trusting scrollIntoView)
  [x] errors announce to screen readers
  [x] the wizard's native dialog count is 0

The count, recorded both ways because BL-017 says the metric counts prose

  work-package-suite-app.js   0 raw, 0 with comments stripped
  app-wide                   64 raw, 64 stripped, against wave 0's 79
                             wp-creation-app.js 43 (wave 7), users.js 10 and
                             admin.js 6 and index.html 5 (wave 9)

The wizard contributes none of what is left, which the probe asserts rather
than leaving to the total.

Verified one at a time
  validation_check 81/81  new
  stepper_check    71/71
  sections_check   88/88
  browser_check    71/71
  a11y             22/22
  url_state        23/23
  autosave         34/34
  locations        58/58
  aggregates       16/16
  pipeline         43/43
  launcher         58/58
  f_items          F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added. The toast says "error" by a red rule AND by staying
until dismissed where a confirmation times out - two channels, not one (C1).

Question for the PR, per CLAUDE.md: step 3's two role TITLES are validated
because the markup marks them required, but the two role NAME pickers beside
them are not marked and so are not gated. A sign-off role with nobody in it is
arguably the more useful thing to catch. The markup is what was built to; if the
intent was the names, that is two rows in STEP_GATES.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 12:30:32 -05:00
ae30c58337 T5.5 - CR-006: section toggles, and X4 resolved rather than deferred
X4 first, because the brief asks for it explicitly.

IMPLEMENTATION.md sequences CR-006 after B7 on the grounds that the toggles must
suppress sections inside the creator, which is an iframe child until T7.1. The
wave file puts it in wave 5 anyway, and its last done-when is written to
accommodate exactly that: "toggle state propagates into the creator, OR the PR
documents exactly where it does not and why".

It propagates. Both ways, by two separate paths, because they fail differently:

  ON THE CREATOR'S OWN BOOT the flags ride on the SOP, which the creator already
  reads - ProjectData.pullProject hydrates it from the server. Nothing crosses
  the frame boundary at all, so this path is unaffected by B7 either way. It
  covers a reload, a fresh tab, the standalone creator page, and a colleague
  opening the project on another machine.

  WHILE THE FRAME IS OPEN the wizard hands the change straight across
  (pushSectionsToCreator -> cw.applySopSections), the same shape T5.3 used for
  the dashboard filter. Without it, flipping a toggle would appear to do nothing
  until a reload.

T7.1 removes the second path, not the first. That is the whole of the X4
exposure and it is one function, commented as such. Building CR-006 after B7
would not have made the SOP-borne path any different; it would only have saved
writing the hand-off.

What it does

  Ten sections, one shared list (html/wp-sections.js) read by the wizard, the
  creator's form and the creator's rendered document. Three surfaces meant three
  chances to drift, which is how "Assets is off, except in the export" happens.

  Off means NOT RENDERED - form, detail view and PDF export. It never means
  deleted. renderPackage() was rebuilt from one long string into a list of
  (section, html) blocks so a suppressed section leaves no empty heading and the
  survivors renumber 1.0, 2.0, 3.0 instead of leaving a hole. The print window
  reuses that same HTML, so the export needed no separate change.

  Absent means ON. A SOP saved before today mentions no sections, and reading
  that as "all off" would blank every project in the estate the moment this
  shipped. WPSections.normalize is the one place that decision lives.

  html/wp-sections.js               new - the shared list, defaults, normalize
  html/work-package-suite.html      step 12, a 12th rail button
  html/work-package-suite-app.js    the toggles, state.sections, the hand-off
  html/work-package-suite-styles.css the toggle rows
  html/wp-creation-index.html       stable ids on the five unnamed cards
  html/wp-creation-app.js           WP_SECTION_NODES, applySopSections,
                                    renderPackage rebuilt as blocks
  tests/sections_check.py           new - 53 checks

Done when
  [x] all 10 sections are toggleable - each one driven off and back on
  [x] a section toggled off is absent from the form, the detail view and the
      PDF export - checked by content marker, not by heading, so "the section
      is gone" and "the section was empty" cannot be confused
  [x] toggling off then on restores prior data with no loss - and the sharper
      version: a package EDITED while Assets is off still carries its assets
      through collectPackage(), which is what Save uses
  [x] new SOPs default to all sections on
  [x] toggle state propagates into the creator - both paths, separately

Two things worth arguing with

  General Information is toggleable, because CR-006 enumerates it. Turning it
  off leaves nothing to identify a package by. The row says so in its own note
  rather than being quietly excluded from the list.

  Location has no card of its own - it is a field inside General Information's
  grid, and its toggle governs that one row. CR-004 gives it structured
  building/floor/sector fields in wave 6, at which point one line of
  WP_SECTION_NODES changes. Written down because "the toggle does nothing" and
  "the toggle governs one row" look identical from outside.

  Approvals & Sign-offs is NOT toggleable and is not one of the ten. A package
  nobody signed is not a shorter package.

Verified one at a time
  sections_check  53/53  new
  stepper_check   70/70  (STEP_COUNT 11 -> 12)
  locations_check 58/58  (its "step 11 is last" check now asserts the thing that
                          is actually invariant - the wizard's navigation follows)
  browser_check   71/71
  a11y            22/22
  url_state       23/23
  autosave        34/34
  aggregates      16/16
  pipeline        43/43
  launcher        58/58
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: still 0 across all page sheets and inline blocks. Each
toggle says its state three ways - the checkbox, the words "In use" / "Not used",
and the rule down its left edge (C1).

Question for the PR, per CLAUDE.md: the toggles are SOP-wide, so a project
cannot use Kitting on install packages and not on BIM ones. BL-000b already
records the field-level version of this question; the per-WP-type version is the
same shape and is not recorded anywhere yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:25:05 -05:00
2081c1ad3c T5.4 - CR-005: a per-project location taxonomy, stored as codes
CLAUDE.md lists CR-005 among the change requests that get "silently half-built if
you treat them as frontend-only". This is the server half and the wizard half
together: a new table, four routes, an Alembic revision, and step 11.

CODES, NOT DISPLAY STRINGS, because CR-018 rolls cost up by these values and a
rollup keyed on a label breaks the day somebody fixes a typo in it. Two columns
carry that: `code` is a node's own slug, derived once at import and never
recomputed; `path` is the full slug path, unique per project, and is what a work
package will store. Renaming a value changes `name` alone - the probe renames a
floor and demands its path comes back byte-identical, with its children's paths
intact.

DEACTIVATE, NEVER DELETE. There is no DELETE route, and the probe checks for its
absence (405) rather than trusting that nobody added one. Deactivating hides a
value from new work packages and cascades DOWN, because a floor nobody can pick
must not keep offering its sectors. Reactivating walks UP only - a sector may
have been switched off for its own reasons, and silently resurrecting it would
undo a decision nobody made twice. That asymmetry is deliberate and is pinned by
a named check so it does not get "fixed" into a surprise.

Import reports rather than merges. Rejected rows come back with the SOURCE line
number and a reason; duplicates are listed as duplicates, separated into "already
in this project" and "already on line N of this import". Reusing a parent is not
a duplicate - B1/L2/1P and B1/L2/2P share a building and a floor by design, and
only the full path repeating counts. Re-importing a deactivated value brings the
same row back rather than creating a second one; the probe checks the id.

One parser, on the server. A CSV is read in the browser and posted as text
exactly as a paste is, so "what does a blank column mean" has one answer.
Comma, semicolon and tab all work - a paste out of a spreadsheet is tab
separated and a saved CSV is not, and which one somebody has is a question the
machine can answer.

No guessed floor names. IMPLEMENTATION.md section 8 says the B100 list has not
been supplied. The seeded sample has "Sample" inside every string, and the probe
greps html/ and server/ for a location-shaped assignment containing any of the
review's real names.

  server/models.py                    LocationNode
  server/alembic/versions/e2a4c7d91b30_location_taxonomy.py
  server/app.py                       GET/POST/PATCH + import, parser, slug
  html/work-package-suite.html        step 11, an 11th rail button
  html/work-package-suite-app.js      the step's logic; LAST_STEP replaces 10
  html/work-package-suite-styles.css  the list, the report
  html/theme-light.css                .field-error, now declared once
  tests/locations_check.py            new - 58 checks
  tests/stepper_check.py              STEP_COUNT 10 -> 11

Done when
  [x] CSV upload and paste both work and report rejected rows with reasons
  [x] duplicates are detected and reported rather than silently merged
  [x] values are editable after import - rename, add, deactivate
  [x] deactivating hides it from new work packages; an existing package
      referencing it still resolves, because the row is retained
  [x] values are stored as codes suitable for grouping
  [x] no guessed real-world floor names exist anywhere in the code

Two decisions worth disagreeing with

  Step 11, appended, not step 2, inserted. Locations belong beside Project by
  subject. Renumbering 2-10 would touch every sop-step-N id, every
  collectStepData case, every gate key and the analytics history - a large
  silent-mismatch surface for an ordering change. The count now lives in one
  place (LAST_STEP), so reordering later is cheap.

  Any project member may edit the list, not only a Project Admin. It matches how
  the SOP baseline itself is authored: the Project Admin gate is on CHANGING a
  completed SOP, not on writing one. If the location list should be tighter than
  the SOP it belongs to, that is a product call.

Verified one at a time
  locations_check  58/58  new
  stepper_check    70/70  (11 steps)
  browser_check    71/71
  a11y             22/22  sop now rings 38 focusable elements
  url_state        23/23
  autosave         34/34
  aggregates       16/16
  pipeline         43/43
  launcher         58/58
  f_items          F1-F5 FIXED, F6 REPRODUCES (T7.2)
  alembic          upgrade / downgrade / upgrade all clean on a throwaway SQLite
                   file, and the migrated schema matches Base.metadata.create_all
                   column for column - dev auto-creates and production migrates,
                   so a divergence between the two is invisible until it ships

.field-error was declared in two page sheets by the end of T5.2 and would have
been three by T5.8, so it moved to theme-light.css. No colour literal added
anywhere: still 0 across all page sheets and inline blocks.

Question for the PR, per CLAUDE.md: the levels are fixed at building / floor /
sector. Micron's floors behave like buildings, which this handles by letting a
project use whichever levels it needs - but a job that wants a fourth level, or
different names for the three, cannot say so. Whether that is worth a
per-project level vocabulary is a product question; the schema would take it
without a migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:06:27 -05:00
6088ef17e8 T5.3 - B4 surface: a pipeline strip, four server counts, four shareable links
Four cells on the launcher - Work packages, Release ready, On hold, Overdue -
every number from /api/wps/metrics, which T4.1 built. There is deliberately no
cache fallback anywhere in renderPipeline(): a remembered number sitting beside
three live ones is the failure B4 exists to remove, wearing a different hat.

The cells are the four the endpoint already computes and the dashboard already
filters on, so they map 1:1 onto its flags rather than inventing a fifth slice
nothing downstream understands. wp-creation-app.js now names them (DASH_FLAGS)
for the same reason: a cell linking to a filter the board does not recognise is
a dead link that still looks live.

"Links to a filtered view via a shareable URL" needed the filter to BE URL state,
which it was not - the dashboard kept its flag in a variable. So:

  - dashToggleFlag pushes ?flag=<f>, and clears it on the way out of the board
  - the creator applies ?flag= before its first render, not after (applying it
    after paints the whole board and throws it away)
  - Back and Forward move through filters like any other state
  - work-package-suite-app.js forwards the flag ACROSS the iframe boundary, since
    the creator's src carries only the project. B7/T7.1 dissolves that frame and
    this hand-off goes with it; it is commented as such.

Zero is a real answer for one slice of a project that has work in it. Four zeros
on a project with none is not a reading, it is a strip that looks broken - that
case gets a sentence and a way into the creator instead. A failed request gets an
explicit error naming the failure, and no cells at all.

  html/index.html            the strip, its states, PIPE_CELLS
  html/wp-creation-app.js    flag as URL state; DASH_FLAGS; dashApplyFlag
  html/work-package-suite-app.js  forward the flag into the frame; clear on exit
  tests/pipeline_check.py    new - 43 checks

Done when
  [x] every number comes from a server endpoint - proved by poisoning localStorage
      with 99 fake packages and demanding the strip still read the server's 4
  [x] each cell links to a filtered view via a shareable URL - and the probe
      FOLLOWS the link and reads the filter inside the frame rather than trusting
      that a correct-looking URL was built
  [x] a project with zero work packages renders a sensible empty state
  [x] the strip announces updates via aria-live (polite - a count is not an
      interruption) and reports aria-busy while it is counting

What the probe caught

  The link landed on "Complete the SOP Configuration first". Not the strip's
  fault: browser_check.py's fixture stores a bare {governance: …} blob as the SOP
  data, where production stores {sop, state}. restoreSavedSOP() needs `state` and
  bails without it, so sopComplete stays false and the WP tab shows its gate.
  pipeline_check seeds the production shape. The underlying wart is real and is
  logged rather than fixed - see BL-018.

Verified one at a time
  pipeline_check  43/43   new
  launcher_check  58/58
  stepper_check   70/70
  url_state       23/23   the dashboard's new flag state did not disturb it
  aggregates      16/16
  browser_check   71/71
  a11y            22/22
  autosave        34/34
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: still 0 across all page sheets and inline blocks. The
four cells are told apart by a label, a sentence and an accent - three channels,
so colour is not carrying it alone (C1).

Raised, not fixed
  BL-018  The WP tab's gate is the last localStorage-derived status in the app.
          T4.1 moved the launcher's card to the server; the wizard page still
          decides gate-or-creator from wp_suite_sop_complete plus a state blob.
          pullProject refreshes both on load so a connected user is fine, but the
          two answers come from different places and the fallback is silent.
          Includes a second, sharper edge: project-data.js:210 writes that flag
          for ANY row returned, including one with no `state` to restore - so the
          flag is written and never read consistently. T7.1 owns it.

Question for the PR, per CLAUDE.md: the strip counts Overdue against `data.due`,
which is free text today. CR-004/CR-018 restructure location but not dates. If
"overdue" is going to drive anything beyond a launcher tile, that field needs a
type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 10:38:58 -05:00
77c043c3db T5.2 - B3: a first-run empty state, then the picker card comes out
B3's warning is about ORDER, and it is the whole item: the proposal removes the
project-picker card, and the first-run empty state was built inside it. Remove
the card first and every brand-new account lands on a page whose only
instruction is to choose from a list with nothing in it.

So the empty state was built first, as its own thing rather than a branch inside
a control that is going away, and only then did the card go.

The launcher now shows exactly one of three states:

  no projects at all   what a project is for, the create form already open (it
                       is the only thing to do on this page, so hiding it behind
                       a button is one click of ceremony in front of the only
                       way forward), and the sample offered underneath it
  none chosen          point at the app bar's switcher, which is on every page,
                       plus New project
  one active           the tool cards, as before

Switching moved to the app bar's switcher entirely. Its popover footer used to
read "All projects / new project" and link to index.html - half of that promise
moved into the popover itself and the other half needs a form, so the link now
says New project and carries #new-project, which the launcher opens on.

The create form was rebuilt, so per C1 it ships accessible: a real <form> with
requestSubmit, every input labelled, and its validation inline at the field with
aria-describedby and role="alert" - the same shape T5.8 gives the wizard. That
retires the "Project name is required." alert (index.html 6 -> 5).

  html/index.html    three states, rebuilt create form, picker card removed
  html/wp-chrome.js  popover footer link (one line - it named the card)
  tests/launcher_check.py  new - 58 checks, two seeded databases
  tests/f_items.py   F1 rewritten to drive the controls that replaced the select

Done when
  [x] a brand-new account with zero projects sees a clear path to create one
  [x] the sample project remains discoverable from the empty state
  [x] the picker card is removed only after the empty state ships
  [x] switching projects still works from the header for users who have projects

Two things the probes caught that I would have shipped

  F1 went INCONCLUSIVE, not FAIL. Its probe drove `document.querySelector
  ('select')` on the launcher - the picker card's dropdown. It refused to guess
  rather than reporting a silent pass, which is the behaviour f_items was
  written for. Rewritten to drive both replacements, because they fail
  differently: the switcher RELOADS with ?project=<id>, so its two labels cannot
  drift apart whatever subscribes to what; creating a project changes the active
  project IN PAGE, and that is the interaction F1's mechanism actually applies
  to. It is now the only in-page change on the launcher, so it is the arm that
  matters. Both pass - the bar subscribes through ProjectData.onActiveChange.

  launcher_check reported "no focus ring" on the rebuilt form's inputs. That was
  trap 5 in reverse: without CDP focus emulation the headless document is not
  the focused one, :focus-visible never matches, and every control reports NO
  ring - a false red where a11y_check would get a false green. With emulation on
  they draw 2px --cds-focus from T4.7's app-wide floor.

Verified one at a time
  launcher_check  58/58   new (38 empty-account + 20 populated)
  stepper_check   70/70
  browser_check   71/71
  aggregates      16/16
  a11y            22/22   launcher 29 focusable elements, all >= 3:1
  url_state       23/23
  autosave        34/34
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)

No colour literal added: 0 across all five page sheets and all seven inline
<style> blocks.

Raised, not fixed
  BL-014 updated rather than left stale: two of its four sites (.proj-row select,
  .link-like) went with the picker card, and the third (.proj-form-grid input)
  was measured rather than assumed - it draws T4.7's ring, which post-dates that
  entry. What survives is field.html's .fld-search, which T9.5 should measure the
  same way instead of inheriting the wording.

Question for the PR, per CLAUDE.md: with the picker gone, an account whose only
project is archived sees the choose-a-project prompt plus the archived note, and
the switcher lists nothing. That is honest but bleak. Whether an archived project
should stay switchable read-only is a product call, not an implementation one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 10:24:41 -05:00
5e1f6e75ba T5.1 - A4/S9: a vertical stepper, ten real buttons, states in words
The step rail was ten div elements carrying onclick inside a horizontal
scroller. Not in the tab order, not operable by keyboard, and silent about
progress - the only thing on the page that said where you were was a "1 / 10"
pill in the app bar, detached from the control it described.

The rail is now a vertical column beside the form: ten <button> elements in an
<ol> inside a named <nav>, with arrow keys, Home and End on top of the Enter and
Space a button gives for free. All ten stay in the tab order; a roving tabindex
would have satisfied "arrow keys" by breaking "tab", which the done-when asks
for both of.

Four states, each carrying a word and a marker shape as well as a colour (C1):
Complete (green disc, tick), Current step (blue disc, aria-current="step"),
Locked (dashed outline) and a plain default. Locked steps keep aria-disabled
rather than disabled, so a keyboard user can reach one and be told what is in
the way instead of finding a control that has vanished from the tab order.

Reachability is the guard's own rule, deliberately not a stricter one: you may
leave the step you are on once its required fields are filled. The tempting rule
- lock everything after the first unmet gate anywhere - is not what
validateStep() enforces, and a padlock the Next button walks straight past is
the drift this change exists to remove. validateStep() and the rail now read one
STEP_GATES table, so they cannot disagree; T5.8 widens that table rather than
editing four functions.

Clicking a step you cannot reach announces why through a role="alert" region and
puts the cursor in the field that is missing. Saying "no" and leaving you where
you were, with no idea which of five inputs was empty, is what the dialog did.

Below 900px the rail collapses to a disclosure naming the step you are on -
ten vertical rows above the fields is most of a 390px screen before you reach an
input. 44px tap targets, since Field View is the gloved-hands surface.

Also: going backwards is no longer gated. previousStep() never validated, so a
rail that did would have trapped you on an incomplete step.

  html/work-package-suite.html        rail markup, counter removed
  html/work-package-suite-styles.css  #tool-sop grid, .step-rail*, 899px collapse
  html/work-package-suite-app.js      STEP_GATES, renderStepRail, keyboard, watcher
  tests/stepper_check.py              new - 70 checks

Done when
  [x] all 10 steps are <button> elements
  [x] keyboard: tab, arrow keys, Home/End, Enter and Space
  [x] aria-current on the current step, exactly one
  [x] complete / current / unavailable told apart without colour
  [x] the "1 / 10" counter is gone - no .step-counter, no N/10 in the app bar
  [x] app-wide <div onclick> 12 -> 2, down exactly 10

Verified one at a time
  stepper_check   70/70   new
  browser_check   71/71
  url_state       23/23
  a11y            22/22   sop now rings 37 focusable elements, all >= 3:1
  autosave        34/34
  aggregates      16/16
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)
  baseline_shots  14 shots; only the sop pair changed. The beforeunload log on
                  sop@1440 and creator@1440 is present at HEAD too - captured
                  both sides to check rather than assume.

No colour literal was added: all five page sheets and all seven inline <style>
blocks still hold zero. New spacing consumes --wp-s*; three raw font sizes were
added and three removed, so BL-010 is unchanged in kind.

Raised, not fixed
  BL-016  Back to a URL with no `step` leaves the wizard where it was. T4.2's
          popstate handler parses NaN and ignores it; its own probe never took
          that branch. stepper_check pins the current behaviour by name so the
          fix has a test waiting.
  BL-017  The native-dialog baseline counts the word `alert(` in comments. Four
          comments written here - all of them about removing a dialog - moved
          the number from 80 to 82 while two real calls were being deleted. They
          were reworded; the metric still needs a comment-stripped variant, which
          T5.8 owns.

Question for the PR, per CLAUDE.md: BL-015 leaves the creator's .step-tab
uppercase as the last forced-uppercase interactive text in the suite, on the
grounds that A5 scopes sentence case to buttons and field labels. The wizard's
rail is now buttons, so its labels are sentence case ("Sign-offs", "WP types").
The two are consistent by rule and inconsistent on screen until T7.x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 23:19:58 -05:00
6fbc5b9735 Wave 4 exit - record the new probes and move the baseline counts
Wave 4's five exit criteria, and what shows each of them:

  counts come from the server, identical across users   aggregates_check 16/16
  work packages addressable by URL, incl. signed-out    url_state_check  23/23
  drafts autosave, unsaved work guarded, save honest    autosave_check   34/34
  toasts announce, helper text legible, focus visible   a11y_check       22/22
  X1 and X5 cleared                                     see below

X1 is cleared: a work package has an address, and WPUrl.absolute() is what
CR-011 and CR-014 will paste into an email in wave 8.

X5 is cleared: /api/wps/metrics returns by_location with its dimensions declared
alongside the groups, so CR-004 and CR-018 can group by building/floor/sector in
wave 6 by changing the dimensions and the keys, not the response shape.

Four metrics moved off their wave 0 baselines and are recorded in the file map so
wave 9 measures against the right numbers. The one that matters most is not in
the plan's list: `outline: none` went from six to one, and the survivor has its
replacement in the rule directly above it.

The file map also now says to run the browser probes ONE AT A TIME. Chained, they
exhaust the headless browser's ports and abort with "browser would not start",
which reads as a code fault and is not one - it cost a confusing red run here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 22:21:42 -05:00
9ab7b48de2 T3.2 - C3/S5: one source of truth for colour; page sheets alias only
theme-light.css is now the only file in html/ that contains a colour literal.
The five page stylesheets and all four inline <style> blocks declare names and
nothing else.

  theme-light.css                 191 declarations, 175 with a literal value
  console.css                      28 declarations,   0
  work-package-suite-styles.css    16 declarations,   0
  wp-chrome.css                    14 declarations,   0
  wp-creation-styles.css           24 declarations,   0
  wp-sidenav.css                    0 declarations,   0

#0f62fe is declared in one sheet, down from five. The eleven occurrences left
inside theme-light.css are Carbon's own v10-to-v11 alias layer, which the
inventory records as deliberate and not the S5 defect.

Names were kept, because 111 var() references live in .js files across 23 token
names and a rename there fails silently - no build error, no console warning,
just an unstyled element.

The rule the refactor was built on: consolidation is not unification. Where two
sheets declared the same value, they collapse. Where they declared DIFFERENT
values for one role - the two shadows, the eight status borders doing four jobs,
the three mono stacks - each value got its own canonical name and the pair is
recorded for T3.5. Picking a winner between two near-identical greys is a
rendered change, which this task forbids. The console's zebra stripe is the one
that would have bitten: #fafafa is six points from #f4f4f4, and merging them
erases the striping on the nine-column user table.

Collecting the one-offs in one place made two things countable that were not
before: twelve distinct shadows, and a ninth amber (#8a6d00 on the field view,
four points from #8e6a00 and doing the same job - BL-009).

VERIFICATION - the screenshot done-when could not do the job, so it was replaced.

Captured against wave 2, 11 of 14 shots were pixel-identical and 3 were not.
Capturing wave 2 against ITSELF produced the same 3 differences at the same
bounding box, so those shots cannot distinguish a regression from the clock.
Trap 2 in the brief is half wrong: users.html is stable at both widths; the
unstable third is the creator at 1440px, and admin's captured page height varies
by ~600px between runs (BL-012).

So tests/token_check.py was added. It checks what wave 3 actually claims: that
every custom property resolves to the same literal, and every element computes
the same colours, shadows and type. That is stronger than a screenshot - it
covers the hover, focus and disabled rules a screenshot never exercises, and it
is deterministic.

  wave 2 vs T3.2, all 7 pages:
  178/178 wave-2 token names resolve identically, +213 new
  3,500 elements compute identically, zero added, zero removed
  16 tokens differ in notation only (#fff -> #ffffff), which is the duplicate
  class this task existed to collapse

Two detours worth not repeating: the element walk was first keyed by sibling
index and reported 55 phantom differences on the SOP page, where three
JS-injected overlays append in whichever order their async work finishes
(BL-011); and the comparator now normalises notation before reporting, because
otherwise it fails on its own success.

f_items 5 FIXED / F6 REPRODUCES as expected. browser_check 71/71.

ONE DONE-WHEN NOT MET, recorded rather than skipped: "no page stylesheet
declares a raw color, spacing or type value". The colour half is met in full.
483 raw spacing values, 281 font-sizes and 65 radii remain inside rules, 492 of
them in the creator. That is arithmetic, not effort: the creator's spacing is
every integer from 1px to 14px, so no token exists that padding:9px 11px maps to
without changing one of the numbers - and this task forbids changing a rendered
value. The two requirements are mutually exclusive. Logged as BL-010 for T5.x
and T7.1, where those pages are re-laid-out and the values get chosen again.

New backlog: BL-009 (ninth amber), BL-010 (raw spacing/type in rules),
BL-011 (overlay append race), BL-012 (unstable screenshot targets).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:50:30 -05:00
b105135a98 T2.3 - B2: a breakpoint plan for the project switcher, not an ellipsis
"Micron EUV Cleanroom Enable 2667008" did not fit. The switcher capped the name
at 240px inside a 280px button, so the real project name was ellipsised to
"Micron EUV Clean..." on the one control whose job is to tell you which job you
are in - which is the failure this item exists to prevent.

The plan, now written into docs/reference/file-map.md so it is not re-litigated:

  >= 1024px   the full project NAME. Caps raised to 400px / 340px, which is
              enough for a real name with no ellipsis.
  <  1024px   the project NUMBER alone, e.g. 2667008. Dropping the name beats
              shortening it: short, stable, unambiguous.

The number is no longer prefixed to the name at wide widths. Real names already
end with their number, so the old format printed it twice - the test project
rendered as "2667008 - Micron EUV Cleanroom Enable 2667008".

The full name stays reachable at every width by three routes, none hover-only,
because the Field View is a touch surface:
  - the drawer, which now shows the active project under its head, wrapping onto
    as many lines as it needs and never truncated. This is the guaranteed route,
    and it subscribes to ProjectData.onActiveChange so it does not go stale when
    the launcher switches project without reloading.
  - the switcher's title attribute, carrying the full number and name, which
    surfaces on keyboard focus as well as hover.
  - the switcher popover, which lists every project in full.

Crossing the breakpoint re-labels, so a resized window does not keep whichever
form was chosen at load.

Verified with the real long name at 390, 768, 1024 and 1440: nothing truncated
at any width (scrollWidth vs clientWidth, not by eye), and the drawer carries
the whole name at all four. T1.1 correctness holds - the switcher never reads
"Select a project" while a project is active.

f_items: F1-F5 FIXED, F3 still passing at all four widths with the wider
switcher. browser_check 71/71.

Wave 2 complete: one drawer on six pages, no flat strip, F2 resolved for real,
S4 closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 17:32:14 -05:00
0a080be1f8 T2.1 - B1 part 1: the drawer is already one shared component; verified, not rebuilt
No code change. T2.1 asks to lift the drawer out of "three near-copies" into one
shared implementation. There are no copies to lift.

html/wp-sidenav.js and html/wp-sidenav.css are already a single implementation
included by admin, field view and directory. .wp-sidenav, .wp-navscrim and
.wp-navbtn are each declared in exactly one file, and no page defines its own
drawer. The review's sentence was that the drawer "ships on 3 of 6 pages while a
flat strip duplicates it" - the duplication is between the drawer and the flat
strip, not among three drawers. The plan read "3 of 6 pages" as "3 copies".

It already satisfies every T2.1 done-when, including the one that could have
failed: activeProjectId() at wp-sidenav.js:62-68 reads ?project= and falls back
to ProjectData.getActiveId(), which is the single source T1.1 established. It
also self-mounts into .wp-appbar OR .header and returns early inside an iframe,
so it is already built for the pages T2.2 has to put it on.

Refactoring a single shared component into a single shared component would be
churn with regression risk and nothing gained, so nothing was touched. Recorded
as file-map D5 so the next reader does not go looking for the copies either.

The real work of this wave is T2.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:54:32 -05:00
fe8a27e022 T0.2 - baseline captured; all six rendering defects confirmed present
Runs the app from a clean database, captures the before images, and records
which of F1-F6 actually still reproduce. All six do.

Rather than eyeball screenshots, each defect is measured in a browser by
tests/f_items.py, which reports REPRODUCES / FIXED / INCONCLUSIVE and never a
silent pass. That makes it both the wave 0 record and the wave 1-3 regression
check: an item is done when its probe flips to FIXED.

  F1  hero says "Job A", app bar still says "Select a project", no reload
  F2  "Sign out" spans x382-432 in a 390px viewport - cut in half, 3 rows
  F3  chrome paints over the logo by 106x32px; .header-left collapses to 0
  F4  comments drawer overlaps the header by 380x91px in the standalone creator
  F5  5 of 5 ENABLED wizard inputs compute #f4f4f4 on #e0e0e0
  F6  11 cards in one 5,017px scroll, 0 tabs (review said ~4,700px; it grew)

Three probes needed care to avoid reporting a false pass, and the traps are
worth knowing before anyone verifies a fix:

  F1 disappears if localStorage is primed first, because then both sources of
  truth agree. The probe clears it and drives the real picker.
  F3 needs a long project name that is long IN THE DATABASE - any page reached
  with ?project= re-pulls it and overwrites a locally-faked one. It also cannot
  be measured by comparing .header-left to the chrome: under the long name
  .header-left (flex:1, min-width:0) collapses to clientWidth 0, so that
  comparison reports a tidy zero gap while the chrome paints across the logo.
  It measures against .logo, which is flex-shrink:0. My first two attempts at
  this probe both reported FIXED for those reasons; the screenshot did not.
  F5 must ignore genuinely disabled inputs or a fix looks done while real
  fields stay grey.

14 screenshots, not the 12 the plan asks for, because there are 7 pages
(file-map D1). Capture also measures horizontal overflow, which is how BL-001
was found.

Tooling: cdp.py gains viewport() and screenshot() - it could do neither, and
T0.2 requires 390px and 1440px images. 390px sets the mobile flag rather than
just narrowing the window, since every page declares width=device-width and
Chrome otherwise lays out at 980px and no media query under test fires. Both
new scripts reuse browser_check.py's seed() and start_server() instead of
growing a second fixture. Existing browser_check still passes 71/71.

No application code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 18:06:36 -05:00
c2e35b9261 T0.1 - build the file map and verify the plan's line references
Wave 0 exists because the plan's line numbers came from a review of
users/directory-super-user rather than a fresh read. This is the fresh read:
7 pages, 6 stylesheets, 11,867 lines, with each page's stylesheets, scripts and
iframe role recorded, and all seven baseline counts captured with the command
that produced them.

Four discrepancies, one of which matters a great deal:

D2 - CLAUDE.md's "logged-override path for predecessors stays (A1). See
wp-creation-app.js:1962-1972" cites the wrong function. That range is
dashIssue(), which REFUSES to issue and says "open the package to release it
early with a logged reason". The reviewer read that sentence and correctly
inferred an override exists, but cited the mention rather than the code. The
audited path is confirmEarlyRelease() at 967-984 plus seven satellites (state
at 392, call sites at 998 and 1149, persisted at 1117, rendered at 1215,
rehydrated at 1674, reset at 481/488/1744). A T7.3 that preserved only
1962-1972 would delete the business rule while believing it had protected it.

D1 - "6 pages, 4 stylesheets" is 7 and 6; wave-0's own parenthetical lists
seven names. Every "all 6 pages" done-when is off by one.

D3 - four documents the plan reads from are deliverables not yet written.

D4 - the creator overflows horizontally at 1440px, which no F item covers.
Logged as BL-001 rather than fixed, since T7.1 rebuilds that layout anyway.
BL-002 records that outline:none appears three times in the wizard sheet, not
once, so T3.4 fixes all three.

Counts confirmed against the review: 79 dialogs (43 in the creator), 12 div and
2 span onclick, 15 help-tip badges, 0 aria-live, 0 pushState. The "4
declarations of #0f62fe" needed a definition - there are 31 occurrences and 14
custom-property declarations; the 4 is the number of stylesheets declaring
their own accent token, which is the number wave 9 should drive to 1.

No application code changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:55:02 -05:00