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>
25 KiB
File map — html/ and the references the plan depends on
Task: T0.1 · Produced: August 14, 2026 · Branch: feat/wp-suite-r2-implementation
Wave 0 exists because the item IDs and line numbers in this plan came from a UX review of
branch users/directory-super-user, not from a fresh read. This document is the fresh read.
Everything below was verified against the working tree, not quoted from the review.
Read the Discrepancies section before starting any wave. Four things the
plan asserts are wrong, and one of them (A1) guards a code path CLAUDE.md says must not change.
1. Pages
html/ holds 7 pages, 6 stylesheets, 11,867 lines. The plan says "6 pages, 4 stylesheets,
roughly 11,900 lines" — the line count is right, the other two are not. See D1.
| Page | Called in this plan | Lines | Stylesheets (load order) | Scripts (load order) | Iframe |
|---|---|---|---|---|---|
html/login.html |
login | 163 | theme-light.css |
login.js |
neither |
html/index.html |
launcher | 703 | theme-light.css, wp-chrome.css |
auth-guard, wp-format, feedback-config, project-data, help, (inline 418–700), wp-chrome |
neither |
html/work-package-suite.html |
SOP wizard | 437 | theme-light.css, wp-chrome.css, work-package-suite-styles.css |
auth-guard, wp-format, feedback-config, project-data, help, work-package-suite-app, wp-chrome |
hosts |
The SOP wizard has 11 steps, not 10, since
T5.4.CR-005's location list was appended as step 11 rather than inserted beside Project, where it belongs by subject: renumbering 2–10 would touch everysop-step-Nid, everycollectStepDatacase, every gate key and the analytics history, for an ordering change. The count lives in one place —LAST_STEPinwork-package-suite-app.js— and reordering is cheap once nothing depends on the numbers.tests/stepper_check.pynames itSTEP_COUNTfor the same reason. |html/wp-creation-index.html| creator | 401 |theme-light.css,wp-creation-styles.css|auth-guard,wp-format,feedback-config,project-data,help,wp-creation-app| child | |html/admin.html| admin | 219 |theme-light.css,wp-chrome.css,console.css,wp-sidenav.css|auth-guard,wp-format,console-util,admin,wp-chrome,wp-sidenav| neither | |html/users.html| directory/users | 106 |theme-light.css,wp-chrome.css,console.css,wp-sidenav.css|auth-guard,wp-format,console-util,users,wp-chrome,wp-sidenav| neither | |html/field.html| field view | 93 |theme-light.css,wp-chrome.css,wp-sidenav.css|auth-guard,wp-format,project-data,help,field,wp-chrome,wp-sidenav| neither |
No script anywhere in html/ uses defer, async, or type="module". Every one is a
render-blocking classic script. auth-guard.js and wp-format.js load in <head>; the rest
load at the end of <body>.
The iframe boundary (matters for B7 / T7.1)
work-package-suite.html:383 declares <iframe id="wp-frame"> with no src.
work-package-suite-app.js:581 sets it at runtime to
wp-creation-index.html?embedded=1&project=<id>.
The creator is the only page that loads neither wp-chrome.css nor wp-chrome.js — that is
why it has no app bar of its own and why it looks like part of the wizard. T7.1 has to give it
that chrome back, or deliberately not.
Three scripts branch on window.top !== window.self and will change behaviour when the frame
goes away:
| Script | Line | Framed behaviour |
|---|---|---|
auth-guard.js |
15 | redirects window.top to login.html |
wp-chrome.js |
17 | returns early, renders no chrome |
help.js |
— | suppresses the Help FAB in the child |
Pages that are not what the plan implies
field.htmlis 93 lines. It is a shell;field.js(178 lines) builds it. A task that says "edit the field view page" almost always meansfield.js.index.htmlcarries ~296 lines of inline<style>(lines 16–312) — the bulk of the launcher's CSS is not in any stylesheet.T3.xtoken work has to reach inside it.admin.htmlandusers.htmlare near-identical shells overconsole.css+console-util.js.
2. Stylesheets
| File | Lines | Purpose | Consumed by |
|---|---|---|---|
theme-light.css |
249 | Carbon light (g10) token set, base type, shared dark appbar | all 7 pages (always first) |
console.css |
191 | dense admin tables and toolbars | admin, users |
work-package-suite-styles.css |
610 | SOP wizard page sheet | SOP wizard |
wp-chrome.css |
237 | injected chrome: project switcher, global search | launcher, SOP wizard, admin, users, field |
wp-creation-styles.css |
884 | creator page sheet | creator |
wp-sidenav.css |
90 | off-canvas nav drawer | admin, users, field |
No @import exists in any stylesheet. Composition is entirely <link> order.
theme-light.css is linked first on all 7 pages.
The four parallel token systems (S5, consolidated in wave 3)
| Sheet | Prefix | Root tokens |
|---|---|---|
theme-light.css |
--cds-* (2–117) and --wp-appbar-* (169–176) |
114 + 6 |
console.css |
unprefixed (12–20) | 28 |
work-package-suite-styles.css |
unprefixed (1–18) | 16 |
wp-creation-styles.css |
unprefixed (9–31) | 21 |
wp-chrome.css |
--wpc-*, scoped to .wp-chrome, not :root (18–36) |
0 at root |
wp-sidenav.css |
none — every colour is a hardcoded hex | 0 |
The three unprefixed sheets are the collision risk. They are mutually exclusive per page today (console on admin+users, wizard sheet on the wizard, creator sheet on the creator), so nothing currently breaks — wave 3 must not assume that stays true once chrome is unified.
Redeclared across sheets with the same value: --bg, --border, --border-strong,
--text, --accent, --red, --shadow, --text-dim.
Redeclared with different values — these are the real defects:
--shadow-lg—0 4px 16px rgba(0,0,0,0.16)(wizard) vs a different blur in the creator--mono—console.cssdropsui-monospaceandSegoe UI Monofrom the stack--surface—#fff(console) vs#ffffff(creator); same colour, two notations--appbar: #161616(wizard) duplicates--wp-appbar-bg: #161616(theme) under another name
Set at runtime on documentElement, declared in no stylesheet:
--wp-chrome-h (work-package-suite-app.js:528), --rail-top (wp-creation-app.js).
Declared on body rather than :root: --nav-w (wp-creation-styles.css:673,674,805).
Only cross-file dependency: work-package-suite-styles.css consumes --cds-hover-primary,
which only theme-light.css declares.
3. Verified references
Every reference the plan relies on, checked against the working tree.
| Reference | Claim | Status | Actually |
|---|---|---|---|
wp-creation-app.js:1144 |
alert() "Subject and WP Type are required" |
CONFIRMED | exact |
wp-creation-app.js:1962-1972 |
logged-override path for predecessors | MOVED — see D2 | that range is dashIssue(), which refuses. Real path: 967–984 |
work-package-suite-app.js:326 |
only beforeunload, analytics dwell |
CONFIRMED | exact; sole beforeunload in all of html/ |
work-package-suite-styles.css:322-328 |
outline:none + pale 3px glow |
CONFIRMED | block runs 321–328, outline:none on 325 |
work-package-suite-styles.css:336 |
comment about .field-hint unstyled |
CONFIRMED | comment 336–338, rule on 339 |
console.css:85-87 |
contrast fix not propagated | CONFIRMED | comment 85–87, fix on 88 |
admin.js:484-517 |
language and time localization | CONFIRMED | exact — "Localization defaults" block |
server/seed_demo.py |
seeds without authenticating | CONFIRMED | zero auth code in 178 lines |
Notes that change what a later task should do:
work-package-suite-styles.css—outline: noneappears three times, not once: 325 (the cited block), 347 (.user-pick:focus, same pale glow), and 501 (.seq-step input.seq-label:focus, no replacement glow at all).A3/F5should fix all three.console.css:85-87— the comment measures#8d8d8don white at 3.3:1. Wave 4'sT4.6says "about 2.9:1". They disagree;T4.6should re-measure rather than quote either. Both other sheets still bind helper text to the rejected#8d8d8d(work-package-suite-styles.css:10,wp-creation-styles.css:17), and the creator applies it at 10px (wp-creation-styles.css:374), worse than the 12px the comment measures.seed_demo.pyfails loudly but confusingly:call()swallows theHTTPErrorand returns the error body, so line 101'sproj["id"]raisesKeyErrorinstead of reporting a 401. Only/api/healthis unauthenticated, so the health check passes and it dies immediately after.
4. Baseline counts
Recorded so wave 9 can prove they went down. Run from html/ unless stated.
| # | Metric | Review | Actual | Command |
|---|---|---|---|---|
| 1 | native dialogs app-wide | 79 | 79 ✓ | grep -ohE '\b(alert|confirm|prompt)\(' *.js *.html | wc -l |
| 2 | …of those, in the creator | 43 | 43 ✓ | same, over wp-creation-app.js |
| 3 | <div onclick> |
12 | 12 ✓ | grep -oE '<div[^>]*onclick' *.html *.js | wc -l |
| 4 | <span onclick> |
2 | 2 ✓ | grep -oE '<span[^>]*onclick' *.html *.js | wc -l |
| 5 | #0f62fe accent systems |
4 | 4 ✓ | see below |
| 6 | .help-tip badges |
15 | 15 ✓ | grep -oE 'class="help-tip"' *.html | wc -l |
| 7 | aria-live regions |
0 | 0 ✓ | grep -ohE 'aria-live' *.html *.js | wc -l |
| 8 | pushState calls |
0 | 0 ✓ | grep -ohE 'pushState' *.html *.js | wc -l |
Dialogs by file: wp-creation-app.js 43 · work-package-suite-app.js 14 · users.js 10 ·
admin.js 6 · index.html 6.
<div onclick> by file: work-package-suite.html 10 · wp-creation-index.html 1 ·
work-package-suite-app.js 1. Both <span onclick> are in wp-creation-app.js.
Metric 5 needs its definition stated, or wave 9 will measure a different thing.
#0f62fe appears 31 times across html/, and 14 of those are custom-property
declarations. The review's "4" is the number of stylesheets that declare their own accent
token — the four parallel systems:
grep -nE '^\s*--[a-zA-Z0-9-]+\s*:\s*#0f62fe' *.css # 14 declarations, in 4 sheets
| Sheet | Token |
|---|---|
theme-light.css |
--cds-interactive-01 (+ 6 Carbon aliases) |
work-package-suite-styles.css:2 |
--primary |
wp-chrome.css:25 |
--wpc-accent |
wp-creation-styles.css:18 |
--accent |
The wave 9 target is 1. Track the sheet count, not the raw occurrence count.
Metric 6: 15 in page markup (work-package-suite.html 3, wp-creation-index.html 12).
A 16th lives in help.js:261 inside the help centre's own copy, demonstrating the component —
it is not a page badge. None carries tabindex, and help.js:26 styles only :hover/:focus,
so a <span> with no tabindex is keyboard-unreachable. That is the C1 defect, and it
confirms "unreachable" literally.
Metric 7: login.html:99-100 uses role="alert" and role="status", which are implicit
live regions. The count of the literal aria-live attribute is 0, matching the review. CLAUDE.md
points at these two lines as the pattern to copy — they are real and correct.
Metric 8: pushState is 0. There is one history.replaceState at login.js:196,
cleaning the URL after sign-in. It is not routing, so X1's premise holds: no work package has
an addressable URL.
5. Running it
uvicorn server.app:app # against a throwaway SQLite database
python server/smoketest.py # API; needs WP_SMOKE_USER / WP_SMOKE_PASSWORD
python tests/browser_check.py # pages boot and render, self-contained
python tests/baseline_shots.py # screenshots, self-contained
python tests/f_items.py # does each of F1-F6 still reproduce?
python tests/token_check.py --out a.json # every resolved token + computed style
python tests/token_check.py --compare a.json b.json
tests/token_check.py was added by T3.2, because a screenshot cannot prove a token refactor:
three of the fourteen shots are not stable capture-to-capture (see backlog.md BL-012), and no
screenshot exercises a hover, focus or disabled rule, which is where half the tokens live. It
snapshots every custom property's resolved value and every element's computed colours, shadows
and type, on all 7 pages, and diffs two snapshots. Use it for any task that claims to change
styling without changing appearance.
Wave 4 added three more, each written because its task's done-when could not be checked by anything that already existed:
python tests/aggregates_check.py # B4 — do the counts come from the server? 16 checks
python tests/url_state_check.py # S3 — does the app's state have an address? 23 checks
python tests/autosave_check.py # S2/B5 — does unsaved work survive? 34 checks
python tests/a11y_check.py # S10/S11/S12 — announce, legible, focus 22 checks
Wave 5 added more, for the same reason:
python tests/stepper_check.py # A4/S9 — ten real buttons, keyboard operable 70 checks
python tests/launcher_check.py # B3 — can a brand-new account get started? 58 checks
python tests/pipeline_check.py # B4 surface — server counts, shareable links 43 checks
python tests/locations_check.py # CR-005 — codes not labels, nothing deleted 58 checks
pipeline_check.py reads the dashboard's state out of the iframe's DOM, not its
globals: dashFilter and currentView are declared with let in a classic script, so
they are not properties of window and a cross-frame read of either comes back
undefined — which is indistinguishable from a filter that never applied.
launcher_check.py is the only probe that runs itself in two subprocesses, and both
reasons are worth knowing before writing a third:
- The two states it tests are database states — an account with no projects, and an account with two — and faking "no projects" in the browser would test the fake.
server/db.pybuilds its engine at import time fromDATABASE_URL, so a secondseed()in one interpreter still points at the first phase's database, which has been deleted by then. That surfaces asunable to open database file, which reads like a broken environment rather than what it is.
It drives the rail with real key events over Input.dispatchKeyEvent rather than
page.key(), which dispatches a synthetic KeyboardEvent on document. That event
never reaches a listener bound to the rail and never triggers a button's native
Enter/Space activation, so a rail with no keyboard support at all would have reported a
clean pass — the same class of false green as a11y_check.py's focus emulation.
Each tests what was broken rather than what is easy to assert. aggregates_check.py poisons
localStorage and demands the dashboard still report the server's total; a test that only
checked the totals were correct would have passed before B4 was built. a11y_check.py runs
with CDP focus emulation on, without which :focus-visible never matches in headless and every
element reports a clean pass.
Run them one at a time. Chained back to back they exhaust the headless browser's ports and all three abort with "browser would not start"; that failure looks like a code fault and is not one.
Baseline counts, updated
| # | Metric | Wave 0 | Now | Changed by |
|---|---|---|---|---|
| 5 | sheets declaring their own #0f62fe |
4 (really 5, see §4) | 1 | T3.2 |
| 7 | aria-live / role="alert"|"status" sites |
0 | 13 | T4.4, T4.5 |
| 8 | pushState |
0 | 2 (6 call sites via wp-url.js) |
T4.2 |
| — | outline: none in stylesheets |
6 | 1, with its replacement one rule above | T3.4, T4.7 |
| — | helper-text contrast, worst case | 3.01:1 | 4.56:1 | T4.6 |
| 3 | <div onclick> |
12 | 2 | T5.1 |
| 1 | native dialogs app-wide | 79 | 77 | T5.1 (2), T5.2 (1), wave 4 (+1) |
Metrics 2, 4 and 6 (creator dialogs, <span onclick>, .help-tip badges) are wave 9's to
move and are unchanged.
Metric 3 after T5.1: the two survivors are wp-creation-index.html:383
(.cmt-overlay) and work-package-suite-app.js:1046 (the constraint-library row). Both
are wave 7 / wave 9 work. Nothing in the SOP wizard's markup carries a click handler on a
non-interactive element any more.
Metric 1 is noisier than it looks — the command matches alert( inside a comment as
readily as inside code, and waves 3 and 4 left several comments explaining dialogs they
were removing. That is why the figure was 80 before T5.1 rather than the 79 wave 0
recorded. T5.1 removed two real calls (validateStep's three conditions collapsed to
one) and reworded its own comments so they do not inflate it. Logged as BL-017;
T5.8 records a comment-stripped figure alongside the raw one.
server/smoketest.py is the one that is not self-contained: it drives a server you point
it at and aborts unless WP_SMOKE_USER and WP_SMOKE_PASSWORD are set, because every route
but /api/health needs a session. Use an admin account — it creates and deletes a project.
tests/browser_check.py and tests/baseline_shots.py are self-contained: each creates a
throwaway SQLite database, seeds a fixture, starts its own uvicorn on a free port, drives
headless Edge or Chrome over CDP, and tears everything down. Your real wpsuite.db is never
touched. Both need Edge or Chrome on the machine; set WP_BROWSER to override discovery.
Screenshots for a before/after pair:
python tests/baseline_shots.py --out docs/reference/baseline # before (committed)
python tests/baseline_shots.py --out /tmp/after --label after # after
python tests/baseline_shots.py --pages creator --widths 390,768,1024,1440
tests/ is referenced by no wave file. T0.2 needs screenshots and T7.3 requires "a
regression test covers the clear-last-constraint path" without naming a home for it — both
belong here.
tests/f_items.py is both halves of the same measurement: it recorded that all six defects
reproduce before wave 1, and it is how waves 1–3 prove each one stopped. An item is done when
its probe flips from REPRODUCES to FIXED. It never reports a silent pass — a probe that
cannot decide says INCONCLUSIVE.
The project switcher: what it shows at each width (B2, T2.3)
Written here because T2.3 says to, so it is not re-litigated. Test name is always
"Micron EUV Cleanroom Enable 2667008" — the real one, and the one that breaks things.
| Width | The app bar shows | Why |
|---|---|---|
| ≥ 1024px | the full project name | There is room. The cap is raised to 400px (button) / 340px (name) so a real name fits without an ellipsis. |
| < 1024px | the project number alone, e.g. 2667008 |
Short, stable, unambiguous. Dropping the name entirely beats shortening it to "Micron EUV Clean…" on the one control whose job is to tell you which job you are in. |
The number is not prefixed to the name at wide widths. Real project names already end
with their number, so 2667008 — Micron EUV Cleanroom Enable 2667008 printed it twice.
The full name is reachable at every width by three routes, none of them hover-only
(Field View is a touch surface — C1):
- The drawer —
.wp-sidenav-proj, under the drawer head, on every page. Wraps onto as many lines as it needs and is never truncated. This is the guaranteed one. - The switcher's
title— the fullnumber — name, so it surfaces on hover and on keyboard focus. - The switcher popover — every project is listed with its full name and number.
The ellipsis is kept only as a backstop for a name longer than anything real. Verified at 390, 768, 1024 and 1440: no truncation at any of them, and the drawer carries the whole name at all four.
T1.1 correctness is preserved — with a project active the switcher never reads
"Select a project"; with none active it reads exactly that at every width.
Discrepancies
Things the plan asserts that the repo contradicts. Listed per T0.1's fourth done-when.
D1 — "6 pages, 4 stylesheets" is wrong; it is 7 and 6
wave-0.md:21 says 6 pages and 4 stylesheets. There are 7 pages and 6 stylesheets. T0.1's
own parenthetical at wave-0.md:24-25 lists seven names, so the document contradicts itself
in the same task. The missing stylesheets are wp-chrome.css and wp-sidenav.css.
Consequence: every "all 6 pages" done-when in waves 2, 3, 5 and 9 is off by one, and T0.2
asks for "12 baseline screenshots (6 pages x 2 widths)" when the correct number is 14.
14 were captured. Treat "6 pages" as "all pages" wherever it appears.
D2 — A1's protected line reference points at the wrong function
This is the important one. CLAUDE.md, under Things that must not change, says:
The logged-override path for predecessors stays (A1). It is an audited business rule, not a bug. See
wp-creation-app.js:1962-1972.
IMPLEMENTATION.md's X2 cites the same range. Lines 1962–1972 are dashIssue(), which is
the opposite code: the dashboard guard that refuses to issue and tells you to
"Open the package to release it early with a logged reason". The reviewer read that sentence and
correctly concluded a logged override exists — but cited the mention, not the implementation.
The actual audited path is:
| Part | Location |
|---|---|
confirmEarlyRelease() — the override itself |
wp-creation-app.js:967-984 |
state pkgGateOverride |
:392, reset at :481, :488, :1744 |
| call site — status change | :998-1002 |
| call site — save | :1149-1150 |
| persisted onto the package | :1117 (gateOverride:) |
| rendered in the printed package | :1215 |
| rehydrated when loading a package | :1674 |
| the guard the plan actually cited | :1960-1974 (dashIssue) |
The comment at :967-969 states the rule plainly: "Releasing with an unclosed predecessor is
allowed but must be explained. The reason rides on the package (data.gateOverride) and the
server writes it to the audit log."
Consequence for T7.3: the "do not remove" instruction must be applied to 967-984 and
its seven satellites, not to 1962-1972. A task that preserved only 1962–1972 would delete the
audited business rule while believing it had protected it. dashIssue() must also survive — it
is what stops the dashboard becoming a quiet way around the gate — but it is a second thing to
protect, not the same thing.
X2's reasoning is unaffected: hold state genuinely is not purely derived from open
constraints, so CR-015 and A1 remain the same code and the same task.
D3 — four documents the plan reads from do not exist yet
Not errors; they are deliverables not yet produced. Recorded so no task treats one as a missing input and goes looking for a rename.
| Path | Created by | Also read by |
|---|---|---|
docs/reference/file-map.md |
T0.1 |
T0.2, T1.1, T2.1, T2.3 |
docs/reference/tokens.md |
T3.1 |
T3.2, T3.5, T9.3 |
docs/reference/accessibility-audit.md |
T9.5 |
— |
docs/reference/completion.md |
T9.7 |
— |
backlog.md:16 contains path/to/file.js:120 inside a fenced format template. It is a
placeholder, not a reference — do not resolve it.
D5 — T2.1's premise is already satisfied: there are no "three near-copies"
wave-2.md:25 asks to "lift the drawer into one shared implementation the pages include,
rather than three near-copies". There are no copies. html/wp-sidenav.js (221 lines) and
html/wp-sidenav.css (90 lines) are already one implementation, included by admin,
field view and directory. Verified: .wp-sidenav, .wp-navscrim and .wp-navbtn are
declared in exactly one file, and no page defines its own drawer.
The review said 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 appears to have read "3 of 6 pages" as "3 copies".
It already meets every T2.1 done-when as it stands, including the last one: activeProjectId()
(wp-sidenav.js:62-68) reads ?project= then 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 to go on the remaining pages.
Consequence: T2.1 is a no-op beyond this verification. The real work of wave 2 is T2.2.
Nothing was refactored, because refactoring a single shared component into a single shared
component would be churn with regression risk and no gain.
D4 — the creator overflows horizontally at 1440px, not just at 390px
Measured during T0.2, not in the review. Content width against the viewport it was given:
| Page | 390px | 1440px |
|---|---|---|
| launcher | 425 | ok |
| SOP wizard | 429 | ok |
| creator | 485 | 1551 |
| field view | 432 | ok |
| login, admin, users | ok | ok |
Four pages overflow at 390px, which is F2 and its neighbours. The creator also overflows by
111px at desk width, which no F item covers. Logged to docs/waves/backlog.md; not fixed
here, since wave 1 is scoped to F1–F5 and T7.x rebuilds this page anyway.