From e2e1655c0ef98f07fca0a121999ddc727b424f4a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 10:48:37 +0000 Subject: [PATCH 1/8] fix(clinical): align decision-support copy with the product boundary; pin design-system figures and token drift The app told users four times that it was clinical decision support while its own privacy page says the opposite. Two design-system documents had also drifted far enough from the code to mislead, and one silent trap had no gate at all. Clinical copy (the reason this change exists) - Differentials (3 sites) and Specifiers (1) said "Clinical decision support only" / "Use this as structured decision support". The privacy page and CLAUDE.md both scope this product as "a clinical reference prototype, not validated clinical decision support", and the PR preflight carries a device-classification item, so the clinical surfaces were the ones out of step. - All four now read "Clinical reference - not validated decision support", the wording already recorded as the clinical-owner copy decision on #PM9SP1. - The fifth site was the copied/exported differential text in src/lib/differential-detail.ts, which carried the same claim into anything a clinician pastes elsewhere. Its pinned assertion moved with it. - Therapy Compass was already correct and is untouched. New gate: token-layer divergence (scripts/token-layer-divergences.mjs) layout.tsx mounts .ckb-v2 on , so .ckb-v2.ckb-v2 (0,2,0) beats :root (0,1,0) and for any role both stylesheets declare the v2 value paints while the globals.css one is dead - editing it changes nothing, with no lint, type or screenshot signal. 53 roles are in that state today (29 light, 24 dark). The v2 migration is deliberate and unfinished, so divergence is pinned rather than banned: docs/design-system/token-layer-divergences.json is the reviewed set, and a role that starts diverging OR stops diverging fails. The comparison covers globals' @theme block as well as :root, which is where the structural roles live. Wired into the existing check:design-system-contract, so no gate-manifest change was needed. Mutation-verified both directions. New gate: GATES.md figures are generated (scripts/generate-gates-figures.mjs) GATES.md carried the same metric as three different hand-typed values in three sections. Measured against the baseline: interactiveTapFloorDeclarations appeared as 9, 32 and 2 (true value 2); rawMarginLiterals as 74 and 53 (34); legacyShadowAliases as 231, 224 and 87 (34). Nearly every figure overstated remaining debt, which costs a session in exactly the way the document's own section 5 warns about. A generated section 0 is now the single current count, with the prose figures in sections 2, 4 and 5 explicitly marked as the historical record. Comparison is normalised cell-wise so Prettier's table padding cannot report false drift. Mutation-verified both directions. Breakpoint tokens: reconciled toward the code, not away from it GATES.md 3b banned --breakpoint-* tokens; globals.css has five, added four days after that decision. The obvious fix - delete the three device-band names, which have zero product call sites - was attempted and reverted in the same session: MIN_WIDTH_BREAKPOINT_BANDS in design-system-contract-utils.mjs hard-codes them as same-threshold aliases of sm/md/lg, and they are the only fixtures exercising the tap-floor gate's alias-collision path. "Nothing imports it" was necessary and nowhere near sufficient, exactly as AGENTS.md warns. All five stay, with the real reason recorded in both the stylesheet and 3b, and the wrong "(Task #336)" attribution removed. AGENTS.md bundle-budget route list Documented five budgeted routes; the config has two. The config is right: /therapy-compass, /dsm and /forms became redirect stubs rendering the same shared home as /, so budgeting them measured / three more times (see tests/check-lighthouse-budget.test.ts, COMMITTED_ROUTES). Corrected the document and recorded why, so the next reader does not "fix" the config back. Also: indexed the one mockup route missing from mockups/README.md (answer-loading-redesign), taking that index to 79 of 79. Verification npm run verify:cheap exit 0. Full unit suite 934 files / 11,771 tests passed, 1 skipped, 0 failures. lint and typecheck clean. Clean-build bundle check passed (production 1742.1 KiB gzip, / at 240.0 KiB); it reports a +5.2% drift warning against a baseline commit that no longer resolves, which is pre-existing and tracked as #QSHHGK, not caused here. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL --- AGENTS.md | 11 +- data/outstanding-issues-snapshot.json | 20 +- data/repo-awareness-snapshot.json | 4 +- docs/design-system/GATES.md | 152 +++++++++--- .../token-layer-divergences.json | 226 ++++++++++++++++++ docs/scripts-index.md | 2 +- mockups/README.md | 7 +- package.json | 4 +- scripts/generate-gates-figures.mjs | 118 +++++++++ scripts/token-layer-divergences.mjs | 180 ++++++++++++++ src/app/globals.css | 12 +- .../differential-detail-page.tsx | 2 +- ...ifferential-presentation-workflow-page.tsx | 5 +- src/components/specifiers/specifier-ui.tsx | 4 +- src/lib/differential-detail.ts | 2 +- tests/design-token-contract.test.ts | 28 ++- tests/differential-detail.test.ts | 2 +- 17 files changed, 724 insertions(+), 55 deletions(-) create mode 100644 docs/design-system/token-layer-divergences.json create mode 100644 scripts/generate-gates-figures.mjs create mode 100644 scripts/token-layer-divergences.mjs diff --git a/AGENTS.md b/AGENTS.md index 380f1693ba..78b07de6f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -295,9 +295,14 @@ action must perform one; a page that ships must be reachable. - **`production`** — every chunk a non-mockup route reaches, plus chunks no route manifest claims (framework, polyfills, runtime). This is user-facing weight and the real regression guard. Tolerance 10%. A failure here means find the regression; do not refresh the baseline to clear it. -- **`routes`** — client JavaScript referenced by `/`, `/therapy-compass`, `/documents/search`, - `/dsm`, and `/forms`, the same journeys measured by Lighthouse. Each route has a 10% tolerance, - so local growth cannot hide inside a still-healthy repository aggregate. +- **`routes`** — client JavaScript referenced by `/` and `/documents/search`, the same journeys + measured by Lighthouse. Each route has a 10% tolerance, so local growth cannot hide inside a + still-healthy repository aggregate. `/therapy-compass`, `/dsm` and `/forms` were in this list + and were deliberately removed: home consolidation turned all three into redirect stubs that + render the same shared home as `/`, so budgeting them measured `/` three more times rather than + covering anything new (see `tests/check-lighthouse-budget.test.ts`, `COMMITTED_ROUTES`). Do not + re-add them without also un-consolidating the homes; if a mode's own surface needs its own + budget, the route to add is its `/search` view, which is a genuinely different bundle. - **`mockups`** — chunks reachable **only** from `/mockups/**`. Nobody downloads these, so this is a repo-hygiene ceiling for unbounded accumulation, not a per-mockup gate. Tolerance 25%. diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index 861f15c0e4..c2d39e4af4 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -10,7 +10,7 @@ "p2": 49, "p3": 31, "queued": 7, - "pending": 79, + "pending": 82, "resolved": 431 }, "queue": [ @@ -872,6 +872,12 @@ "summary": "#G693BZ: Verified PanelSection extracted in panel-primitives.tsx and adopted across routes, documentation, test-health, review-state, and ledger pages.", "created_at": "2026-08-27" }, + { + "request_id": "184404de-c45e-43e3-8048-2825799b349e", + "action": "add", + "summary": "Nothing compares the replayed migration chain against supabase/schema.sql, so a migration that changes a function body without a matching schema.sql edit passes every pre-merge gate and only surfaces post-merge in live-drift", + "created_at": "2026-09-01" + }, { "request_id": "2166a980-bac5-4565-b009-f7f4ea530e5c", "action": "done", @@ -1028,6 +1034,12 @@ "summary": "#EFETZT: detail → UPDATE 2026-08-27 (PR #2413, second and third occurrences): this is not only a staleness problem, it also produces hard git merge conflicts that block auto-merge. data/repo-awareness-snapshot.json conflicted twice in fifteen minutes on one PR head — first when main advanced to be65b8a, again when main advanced to fb88b47 — each time setting mergeable_state=dirty, which suppresses refs/pull//merge so pull_request CI (CI, Gitleaks, Semgrep) does not run at all and the check list reads empty rather than red. Both were resolved identically: git merge origin/main, git checkout --theirs the snapshot, npm run snapshot:repo-awareness, commit, push. The staleness half also reproduced: check:repo-awareness-snapshot failed Static PR checks with 'review_state differs from the repository'. Cost per occurrence is one full CI round trip plus a push, and it scales with how many PRs are open, since every merge to main regenerates the file for all of them. Reinforces the fix direction already proposed on this row: exclude docs/branch-review-records/ from the compared content the way captured_revision already is, or compare a narrower key set. Adding .gitattributes merge guidance for this path would blunt the conflict half but not the CI-staleness half.", "created_at": "2026-08-27" }, + { + "request_id": "86ba753c-f2fd-4be7-92f4-df3d54cf4a04", + "action": "update", + "summary": "#QCNE6N: detail → generate-drift-manifest.ts replays supabase/schema.sql and embeds ITS sha256; CI's db-reset-verify runs supabase migration up --local, proving the chain APPLIES but never diffing the result against schema.sql. A migration whose function or policy body diverges from the mirror passes every gate. Object-NAME parity does hold today (all create targets across 211 migrations resolve into schema.sql) but it is enforced by hand-written per-object tests, not systematically. FIX: after migration up --local, replay schema.sql into a second scratch database and diff schema_drift_snapshot() between the two. Fully offline and containerised, no provider access. DEMONSTRATED 2026-09-01, no longer theoretical: live-drift run 33484535655 (red on main at d3074946a) reported its sole unexpected finding as public.correct_clinical_query_terms(text,real) def_hash manifest e2356565 vs live 2ebaf978. Cause was exactly this gap - migration 20260831100000 (PR #2477) redefined that function with a duplicated 'and length(canonical) between 4 and 40' predicate and supabase/schema.sql was never updated to match, so the manifest disagreed with live while every pre-merge gate stayed green. The db-reset-verify assertion that did run is committed.schema_sha256 === generated.schema_sha256 (.github/workflows/ci.yml ~L1082-1095), which only catches an unrefreshed manifest, not a chain/mirror divergence. Behaviour impact of that instance was nil (the duplicate predicate is a boolean no-op) but it cost a red daily alarm and a remediation PR, and it is the second occurrence of this failure class after the #316 root cause (SET work_mem absent from schema.sql). Strengthens the case for scheduling the containerised two-database diff above.; source → repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f; demonstrated by live-drift run 33484535655, classified by the database coordination chat 2026-09-01", + "created_at": "2026-09-01" + }, { "request_id": "870d4673-65da-40b8-94ba-15ba3c6ec6db", "action": "cancel", @@ -1190,6 +1202,12 @@ "summary": "An agent push rewrote tests/ui-smoke.spec.ts from 6,019 lines to 1,081, deleting 74 of 82 UI journey tests, and only a merge conflict stopped it auto-merging", "created_at": "2026-08-31" }, + { + "request_id": "cbe7c359-b8e1-4218-a4df-59e869133e0a", + "action": "cancel", + "summary": "Cancel request 184404de-c45e-43e3-8048-2825799b349e: Duplicate of the existing open row #QCNE6N, which already records that CI never diffs the replayed migration chain against supabase/schema.sql and already proposes the same containerised two-database fix. Retracted the same session it was queued; the new 2026-09-01 evidence was reissued as an update to #QCNE6N instead (request 86ba753c-f2fd-4be7-92f4-df3d54cf4a04).", + "created_at": "2026-09-01" + }, { "request_id": "cd767916-0034-4e5a-b2db-276609d0deec", "action": "cancel", diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json index 66370ca729..404329848d 100644 --- a/data/repo-awareness-snapshot.json +++ b/data/repo-awareness-snapshot.json @@ -1,8 +1,8 @@ { "version": "repo-awareness-snapshot-v1", "captured_revision": { - "sha": "f328372f39ead3501c8a4ba09db4cbef492d4788", - "committed_at": "2026-09-01T01:52:31Z" + "sha": "d3074946a917cac378de64284c67cbc1d4dc58fa", + "committed_at": "2026-09-01T15:56:16+08:00" }, "routes": { "modes": [ diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index 9247ee14e2..5aee1547e3 100644 --- a/docs/design-system/GATES.md +++ b/docs/design-system/GATES.md @@ -10,6 +10,57 @@ than CI, two of them by the second reader; review does not scale past two carefu - **Date:** 5 August 2026 · companions: [SPEC.md](SPEC.md) · [TOKENS.md](TOKENS.md) · [COMPONENTS.md](COMPONENTS.md) · [DECISIONS.md](DECISIONS.md) +> **Figures.** The generated table in §0 is the only current count in this document. +> Numbers written into the prose of §2, §4 and §5 are the historical record of what was +> true when that passage was written, and several are now badly stale — the same metric +> once appeared here as three different hand-typed values, none of them right. Read the +> prose for _why_ a gate is shaped the way it is; read §0 for _how much_ is left. Where +> they disagree, §0 wins, and per [README.md](README.md) the baseline outranks both. + +--- + +## 0 · Current ratchet figures + + + + + +| Metric | Current pin | Files pinned | +| --------------------------------- | ------------------ | ------------ | +| `arbitraryTracking` | **0** (hard floor) | 0 | +| `colourOnlyStatusIndicators` | **0** (hard floor) | 0 | +| `darkColorOverrides` | **0** (hard floor) | 0 | +| `edgeOwnershipConflicts` | 5 | 2 | +| `elevationInversions` | 6 | 5 | +| `errorStateCountProps` | **0** (hard floor) | 0 | +| `failedStateResultCounts` | **0** (hard floor) | 0 | +| `handRolledCommandButtons` | 8 | 7 | +| `hardcodedCssMotionDurations` | 26 | 1 | +| `interactiveTapFloorDeclarations` | 2 | 1 | +| `layoutTransitionExceptions` | 10 | 4 | +| `legacyPaletteUtilities` | **0** (hard floor) | 0 | +| `legacyShadowAliases` | 34 | 28 | +| `legacyTapClasses` | **0** (hard floor) | 0 | +| `literalShadowClasses` | **0** (hard floor) | 0 | +| `onePixelShadowSpreads` | **0** (hard floor) | 0 | +| `rawColorLiterals` | **0** (hard floor) | 0 | +| `rawCssZIndices` | 4 | 1 | +| `rawGapLiterals` | 11 | 1 | +| `rawLineHeightLiterals` | **0** (hard floor) | 0 | +| `rawMarginLiterals` | 34 | 4 | +| `rawPaddingLiterals` | 22 | 4 | +| `rawRadiusLiterals` | 15 | 3 | +| `sameFileTextSmMinusMix` | 25 | 25 | +| `statusColouredNumerals` | **0** (hard floor) | 0 | +| `textSoftConsumers` | **0** (hard floor) | 0 | + +26 metrics, 13 of them pinned at zero. A metric at zero is a hard floor: +the check asserts `value <= baseline`, so any reintroduction fails. A non-zero pin is +recorded debt with per-path pins, so a new occurrence fails even while the total stands. + + + --- ## 1 · What actually runs today @@ -146,35 +197,35 @@ theme-list parity, and remote design-project publication remain separate concern ## 3 · The prohibition table -| Never do this | Gate | Status | -| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Reintroduce `.ckb-v2:not(.dark)` | Cascade computed-style test (PR 1) + contract-test selector filter update | implemented-blocking — landed #1538; contract parser + dark-ink regression guard | -| Add `prefers-color-scheme` rules to the v2 layer | — (users pin a theme against OS preference) | **manual** | -| Paint a numeral with a status colour; use amber/red decoratively | `check:design-system-contract` — `statusColouredNumerals` + `colourOnlyStatusIndicators` | **implemented-blocking** — both metrics and their per-path baselines are pinned at **0**; see §5 | -| Put `--decoration-soft`/`--text-soft` on a text node | Tier pinned both sides in the token contract; recipe AST (`decoration-on-text.contract.test.ts`) | implemented-blocking (recipes); broader AST follow-on | -| Uppercase a dose unit | `Quantity` pins `normal-case` + unit test | implemented-blocking (branch) | -| Invert a PDF, diagram or clinical image in any theme | `check:design-system-contract` — `imageInversions`, pinned at **zero**, not ratcheted | **implemented-blocking** (9 Aug 2026) — CSS `filter`/`backdrop-filter` plus the Tailwind `invert`/`hue-rotate` utilities; see §5 | -| Border **and** ring on one surface, or a 1px spread in a drop shadow | `check:design-system-contract` — `edgeOwnershipConflicts` (16) + `onePixelShadowSpreads` (0) | **implemented-blocking for new use** — AST/CSS ratchets with per-path pins; the recorded debt itself is Gate 8's remaining half | -| A child shadow heavier than its parent's | Gate 7 | implemented-partial | -| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 87 with per-path pins | **implemented-blocking for new use** — a new alias in any file fails. `--shadow-tight` itself is retired onto `--e1` and additionally pinned by a tracked-tree sweep in `tests/design-token-contract.test.ts` (it was silently reverted once by the `acf78bf` merge); the remaining 87 are the other six aliases, still `#262` | -| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (42), `rawRadiusLiterals` (20), `rawGapLiterals` (20), `rawMarginLiterals` (53), `rawLineHeightLiterals` (0) | **implemented-blocking for new use** (9 Aug 2026) — per-path ratchets over both the utility and the CSS-declaration spelling, so a literal cannot move into `globals.css` to escape. Values containing a CSS function (`env(`, `clamp(`, `max(`, `calc(`) are sanctioned computed forms and exempt. Raw _size_ is still covered only for tap/shadow/colour | -| Animate `width`, `height`, `grid-template-*`, `top`, `left`, `gap` | `check:design-system-contract` — `layoutTransitionExceptions`, ratcheted at 11 with per-path pins | **implemented-blocking for new use** — `SAFE_TRANSITION_PROPERTIES` carries the compositor-only allowlist; phone chrome's deliberate `grid-template-rows` is in the recorded 11 | -| Hardcode a transition duration | `check:design-system-contract` — `hardcodedMotionClasses` (**zero**) + `hardcodedCssMotionDurations` (42) | **implemented-blocking** for the Tailwind `duration-*`/`delay-*`/`transition-all` form; the CSS form is a ratchet, so its 42 are debt | -| A `z-[N]` literal outside the named rungs, in JS/TS only | `require-z-index-ladder` (ESLint) | implemented-blocking. Scope: `z-[N]` literals in JS/TS AST nodes. Plain `z-30` utilities and every `.css` / `.module.css` declaration are invisible to an ESLint rule - `sidebar-live-shell.module.css` carries an off-ladder `z-index: var(--z-sidebar-flyout, 50)` this cannot see. | -| Ship a registered component without a direct publication test | Gate 12 | implemented-blocking | -| Lower a production tap target below 48px | `check:design-system-contract` — `interactiveTapFloorDeclarations` (32) + Gate 2's phone-viewport `ui-style-contract` audit + `ui-smoke` flake history | **implemented-blocking for new use** (15 Aug 2026) — a new interactive control declaring a sub-floor `min-h-*`, including comparable arbitrary values and any reachable conditional branch, fails on both the total and its own path; the 32 recorded sites remain debt. Component-wrapper tags such as `` are a known blind spot | -| Construct an enabled control with no action, destination or name | PR 4 discriminated unions + DOM contracts; `require-button-wiring` covers raw `