diff --git a/docs/branch-review-records/19d9b5ccfa4a8c3a52744d24cfc3bb7906a309c9f0d6beb92203bdd5717f47b3.record.md b/docs/branch-review-records/19d9b5ccfa4a8c3a52744d24cfc3bb7906a309c9f0d6beb92203bdd5717f47b3.record.md new file mode 100644 index 0000000000..e15e14f0b1 --- /dev/null +++ b/docs/branch-review-records/19d9b5ccfa4a8c3a52744d24cfc3bb7906a309c9f0d6beb92203bdd5717f47b3.record.md @@ -0,0 +1 @@ +| 2026-08-15 | claude/ds-ratchet-tighten | 41a1bd73749564954d7bf11143fd6541402f7691 | Tighten seven design-system ratchets to measured values after the #1982-#1986 merges | 17 units of stale headroom removed; no metric relaxed | check:design-system-contract passed at the tightened values (sub-floor min-heights 40, edge conflicts 19, legacy shadow aliases 114); mutation-verified — min-h-9 on the shortlist Clear button now fails 'increased from 40 to 41' plus the per-path line, which the old pin of 43 allowed silently; check:gate-manifest OK; verify:pr-local 17 gates, unit suite 611 files / 6647 passed | diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index 03aaf66e5a..9bb942a670 100644 --- a/docs/design-system/GATES.md +++ b/docs/design-system/GATES.md @@ -73,13 +73,13 @@ because it contributed nothing. | # | Gate | Status | Evidence / path to blocking | | --- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. | -| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking for new use** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Still open for the `h-10` case, and 9 Aug 2026 measured why.** An enumeration of _rendered interactive_ elements was written, shown to find genuine defects, and then **reverted rather than landed**: it is not deterministic on this route. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct sub-floor shapes, largely disjoint — one run saw the answer-suggestion chips and a sort band, another the settled results list. `waitForLoadState("networkidle")` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs turned out to be coincidence. This spec runs in the required `Production UI` job, so an intermittent version of it would block every merge in the repo; that is a worse outcome than the gap it closes. Path to blocking: give the audit a deterministic surface — a static route or a fixed seeded state — before re-attempting it. **What the enumeration did establish, in every one of the six runs, is a live defect:** controls that carry `min-h-tap` compute `min-height: 0px` and render at 16–36px, six distinct shapes in total. The declared-carrier audit cannot report them by construction — it only measures elements already computing at or above the floor, so a floor overridden downward is skipped rather than flagged. Tracked as `#293`. **Correction, same date — "`test:e2e:style-contract` is not part of `verify:cheap`" was true and badly misleading, and it sent one session looking for a wiring bug that does not exist.** The npm script is only a convenience alias for running this one spec; the spec matches `productionSpecPattern` in `playwright.config.ts` and is listed explicitly in `scripts/playwright-pr-shards.mjs`, so it already runs in the `Production UI` job that `pr-required` demands on any UI-scoped PR. It must **not** be added to `verify:cheap:internal`: `check:gate-manifest` requires every gate in that chain to also run in `static-pr`, which has no browser and no server. Per-surface geometry stays in the held visual harness. **Both named gaps are now closed, 15 Aug 2026.** The rendered enumeration landed first: `885c613` (PR #1962) added the deterministic phone-viewport `min-h-tap` audit this row was waiting on — `/forms`'s fixed-array home rather than a live-search route, polled until three consecutive reads agree, explicitly sorted, and asserted identical across three full navigate-and-enumerate cycles. The `h-10` case is closed here by `interactiveTapFloorDeclarations` in `check:design-system-contract`: an interactive element (`a`, `button`, `input`, `select`, `summary`, `textarea`) declaring its own **unprefixed** `min-h-*` below the 48px token. Comparable arbitrary lengths and mutually exclusive/composed class branches are evaluated independently, so one safe branch cannot hide a sub-floor runtime path. Ratcheted at **43** across 17 files with per-path pins, so a new sub-floor control anywhere in `src/**` fails today. Mutation-verified: lowering one shortlist button to `min-h-9` produces both the total and matching per-path regression. **Deliberately scoped to `min-h-*`, not `h-*`/`size-*`:** a short `h-4` on an interactive element is routinely the visible box of a control whose hit area is owned by a tap-sized wrapper (`SelectionCheckbox` in `differentials-home.tsx`, whose label `ui-smoke` asserts still meets the floor), so flagging those would pad the baseline with non-defects — the §5 failure mode. **One known limit, recorded rather than hidden:** the walker sees intrinsic lowercase tags only, so a floor declared on `` or another component wrapper is invisible to it (the pre-existing `legacyTapClasses` check has the same blind spot). The 43 recorded sites are real debt still owed, tracked in `#265`. | +| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking for new use** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Still open for the `h-10` case, and 9 Aug 2026 measured why.** An enumeration of _rendered interactive_ elements was written, shown to find genuine defects, and then **reverted rather than landed**: it is not deterministic on this route. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct sub-floor shapes, largely disjoint — one run saw the answer-suggestion chips and a sort band, another the settled results list. `waitForLoadState("networkidle")` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs turned out to be coincidence. This spec runs in the required `Production UI` job, so an intermittent version of it would block every merge in the repo; that is a worse outcome than the gap it closes. Path to blocking: give the audit a deterministic surface — a static route or a fixed seeded state — before re-attempting it. **What the enumeration did establish, in every one of the six runs, is a live defect:** controls that carry `min-h-tap` compute `min-height: 0px` and render at 16–36px, six distinct shapes in total. The declared-carrier audit cannot report them by construction — it only measures elements already computing at or above the floor, so a floor overridden downward is skipped rather than flagged. Tracked as `#293`. **Correction, same date — "`test:e2e:style-contract` is not part of `verify:cheap`" was true and badly misleading, and it sent one session looking for a wiring bug that does not exist.** The npm script is only a convenience alias for running this one spec; the spec matches `productionSpecPattern` in `playwright.config.ts` and is listed explicitly in `scripts/playwright-pr-shards.mjs`, so it already runs in the `Production UI` job that `pr-required` demands on any UI-scoped PR. It must **not** be added to `verify:cheap:internal`: `check:gate-manifest` requires every gate in that chain to also run in `static-pr`, which has no browser and no server. Per-surface geometry stays in the held visual harness. **Both named gaps are now closed, 15 Aug 2026.** The rendered enumeration landed first: `885c613` (PR #1962) added the deterministic phone-viewport `min-h-tap` audit this row was waiting on — `/forms`'s fixed-array home rather than a live-search route, polled until three consecutive reads agree, explicitly sorted, and asserted identical across three full navigate-and-enumerate cycles. The `h-10` case is closed here by `interactiveTapFloorDeclarations` in `check:design-system-contract`: an interactive element (`a`, `button`, `input`, `select`, `summary`, `textarea`) declaring its own **unprefixed** `min-h-*` below the 48px token. Comparable arbitrary lengths and mutually exclusive/composed class branches are evaluated independently, so one safe branch cannot hide a sub-floor runtime path. Ratcheted at **40** across 16 files with per-path pins, so a new sub-floor control anywhere in `src/**` fails today. Mutation-verified: lowering one shortlist button to `min-h-9` produces both the total and matching per-path regression. **Deliberately scoped to `min-h-*`, not `h-*`/`size-*`:** a short `h-4` on an interactive element is routinely the visible box of a control whose hit area is owned by a tap-sized wrapper (`SelectionCheckbox` in `differentials-home.tsx`, whose label `ui-smoke` asserts still meets the floor), so flagging those would pad the baseline with non-defects — the §5 failure mode. **One known limit, recorded rather than hidden:** the walker sees intrinsic lowercase tags only, so a floor declared on `` or another component wrapper is invisible to it (the pre-existing `legacyTapClasses` check has the same blind spot). The 40 recorded sites are real debt still owed, tracked in `#265`. | | 3 | Focus outline present, `--focus`, no companion ring | **planned** | Corrected 6 Aug 2026: the previous evidence ("`--focus` is referenced nowhere in the DS export", finding N3) was false — **[verified: grep]** 4 declarations (2 theme, 2 forced-colours) against **273** `var(--focus)` consumers, 260 of them in `.tsx`. The token is adopted; the check is what is missing. Path to blocking: assert a visible focus outline on every interactive role and reject a `ring-*` companion on the same node. A row that understates shipped work costs the document its authority as surely as one that overstates it, and nobody files a bug against pessimism. | | 4 | Non-colour encoding on every status indicator | **implemented-blocking for new use** | Blocked today by `ui-v2-answer-safety.dom.test.tsx`: an overdue `DoseLine` row is asserted to carry all three channels (amber inset rule **plus** the words "Source review overdue" **plus** a `StatusMark` shape), `MissingValue` is asserted never to contract to a dash at any density, `FieldError` is asserted to pair its text with an icon, and `RetrievalStateBanner` is asserted to carry its state in the headline text rather than the tone alone. Off-vocabulary status still degrades to a phrase (`source-badges-off-vocab.dom.test.tsx`, Gate 6). **Not blocked today:** there is no repository-wide enumeration of status indicators, so a _new_ colour-only indicator elsewhere in `src/components/**` — the bare `statusDot*` recipes are the obvious candidates — would not fail anything. **Closed 9 Aug 2026 — the repository-wide enumeration now ships.** `colourOnlyStatusIndicators` (`check:design-system-contract`) flags a status hue on a box that says nothing: no children, no `aria-label`/`aria-labelledby`/`title` on it or any ancestor, no text sibling, and not a `StatusMark`. It also flags shared _swatch recipes_ — a status hue plus a tiny round box and no text utility — because the analyzer is per-file and cannot follow an imported `statusDotReady` to its call sites, so the recipe is where the defect is catchable. Ratcheted at **4** with per-path pins: the two bare `statusDot*` recipes GATES.md named, plus a calculator risk band and a therapy meter fill. A _new_ colour-only indicator anywhere in `src/**` now fails. Still partial: those 4 recorded sites, and `--decoration-soft` is deliberately out of scope (it carries no state). | | 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-blocking** | `AccessibleTableProps.caption` is required; DOM and alignment tests prove the semantic ``, associated headers, and expander relationship. | | 6 | Enum resilience — neutral fallback, never throws | **implemented-blocking** | `source-badges-off-vocab.dom.test.tsx`. | | 7 | Elevation monotonicity — no child heavier than its parent | **implemented-partial** | Token-ramp monotonicity is tested; the per-surface child/parent rule is planned (needs a render-tree check). | -| 8 | Edge ownership — no `border-*` + `ring-*` on one surface; no 1px spread in a drop shadow | **implemented-partial** | Corrected 9 Aug 2026: the previous evidence — "the per-surface co-occurrence lint and spread-term check are planned (PR 9)" — was **false**, and had been since before this row was written. Both ship. `edgeOwnershipConflicts` is an AST co-occurrence check over resolved class roots (`BORDER_WIDTH_UTILITY` × `RING_WIDTH_UTILITY`), and `onePixelShadowSpreads` reads the fourth length of every `box-shadow` layer. Both ratchet **per path**, so a new conflict on any surface fails today. What is partial is the recorded debt, not the check: **25 conflicts across 12 files**, and 2 spreads in `globals.css`. (Corrected 15 Aug 2026 — this cell said 27 across 15, which PR #1942 had already paid down to 25 across 12 without updating the prose. The §3 row below and `scripts/design-system-contract-baseline.json` both already said 25, and per `docs/design-system/README.md` the baseline outranks this document, so the prose was the defect. Ledger `#265` carried the same stale figure.) Path to blocking: retire that debt and pin both at zero. The spread form was chosen _because_ it is mechanically lintable, and it duly was. | +| 8 | Edge ownership — no `border-*` + `ring-*` on one surface; no 1px spread in a drop shadow | **implemented-partial** | Corrected 9 Aug 2026: the previous evidence — "the per-surface co-occurrence lint and spread-term check are planned (PR 9)" — was **false**, and had been since before this row was written. Both ship. `edgeOwnershipConflicts` is an AST co-occurrence check over resolved class roots (`BORDER_WIDTH_UTILITY` × `RING_WIDTH_UTILITY`), and `onePixelShadowSpreads` reads the fourth length of every `box-shadow` layer. Both ratchet **per path**, so a new conflict on any surface fails today. What is partial is the recorded debt, not the check: **19 conflicts across 10 files**, and 2 spreads in `globals.css`. (Corrected 15 Aug 2026 — this cell said 27 across 15, which PR #1942 had already paid down to 25 across 12 without updating the prose. The §3 row below and `scripts/design-system-contract-baseline.json` both already said 25, and per `docs/design-system/README.md` the baseline outranks this document, so the prose was the defect. Ledger `#265` carried the same stale figure.) Path to blocking: retire that debt and pin both at zero. The spread form was chosen _because_ it is mechanically lintable, and it duly was. | | 9 | No layout-property animation; no hardcoded durations | **implemented-partial** | Corrected 9 Aug 2026: "a layout-property lint is still planned" was **false**, and leaving it would have preserved the exact contradiction this pass exists to remove. `layoutTransitionExceptions` ships and ratchets **per path** against `SAFE_TRANSITION_PROPERTIES`, so a new layout-property transition in any file fails today; the phone chrome's intentional `grid-template-rows` / `height` / `padding-bottom` are among the 12 recorded exceptions rather than a gap. The Tailwind duration form is a **hard zero** (`hardcodedMotionClasses`), on top of PR-Arch tokenising production `duration-*`/`ease-*` literals onto `--duration-*` / `--ease-*` (Gate 10). What stays partial is recorded debt, not a missing check: 12 layout transitions and 42 CSS `hardcodedCssMotionDurations`. Path to blocking: retire those and pin both at zero. | | 10 | Named z and motion tokens have real consumers; no unapproved `z-` value | **implemented-blocking** | PR-Arch: off-ladder `z-50` retired; toast at `--z-toast` (`z-[110]`, lint allowlist); motion literals map to named duration/ease tokens including phone-chrome hide/reveal pair. Dead `OverlayProvider` deleted. | | 11 | Published-type conformance — every `dtsPropsFor` entry exactly matches source props | **implemented-blocking** | `generate-design-sync-contract.mjs` uses the TypeScript checker to derive every public property from the exported source `*Props` type; `--check` and `check-design-sync-contract.mjs` reject drift, missing named types, or machine-specific declarations. Zero-prop visual roots are explicit. | @@ -107,19 +107,19 @@ theme-list parity, and remote design-project publication remain separate concern | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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** (9 Aug 2026) — ratcheted at 2 and 4 with per-path pins; see §5 | +| Paint a numeral with a status colour; use amber/red decoratively | `check:design-system-contract` — `statusColouredNumerals` + `colourOnlyStatusIndicators` | **implemented-blocking** (9 Aug 2026) — ratcheted at 1 and 4 with per-path pins; 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` (25) + `onePixelShadowSpreads` (2) | **implemented-blocking for new use** — AST/CSS ratchets with per-path pins; the recorded debt itself is Gate 8's remaining half | +| Border **and** ring on one surface, or a 1px spread in a drop shadow | `check:design-system-contract` — `edgeOwnershipConflicts` (19) + `onePixelShadowSpreads` (2) | **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 118 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 119 are the other six aliases, still `#262` | -| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (63), `rawRadiusLiterals` (24), `rawGapLiterals` (32), `rawLineHeightLiterals` (3) | **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 | +| Use `--shadow-tight`/any alias in new code | `check:design-system-contract` — `legacyShadowAliases`, ratcheted at 114 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 119 are the other six aliases, still `#262` | +| Raw pixel size, padding, radius, gap or line-height in markup | `check:design-system-contract` — `rawPaddingLiterals` (63), `rawRadiusLiterals` (23), `rawGapLiterals` (31), `rawLineHeightLiterals` (3) | **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 | +| Hardcode a transition duration | `check:design-system-contract` — `hardcodedMotionClasses` (**zero**) + `hardcodedCssMotionDurations` (41) | **implemented-blocking** for the Tailwind `duration-*`/`delay-*`/`transition-all` form; the CSS form is a ratchet, so its 42 are debt | | A `z-` value outside the named rungs | `require-z-index-ladder` (ESLint) | implemented-blocking | | 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` (43) + 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 43 recorded sites remain debt. Component-wrapper tags such as `` are a known blind spot | +| Lower a production tap target below 48px | `check:design-system-contract` — `interactiveTapFloorDeclarations` (40) + 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 40 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 `