diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 171e63bcd..c979f5aeb 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -855,3 +855,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | PR #1782 / cursor/fix-document-open-scroll-e5bf | 5709f2cc7a954197e02107c96d7896d8d13445c3 | document-viewer open-at-top | ship: remove chunk mount scrollIntoView so document opens stay at overview top | document-viewer-shell.dom 7 pass; document-section-summary.dom 8 pass; verify:pr-local dry-run | | 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 98029875db7d640d3e699829249bb33892296bff | PR #1782 unblock | before: static-pr+coverage failed on stale adoption-manifest (document-viewer-shell testFiles drift), merge-tree clean 0 behind, auto-merge armed, 1 advisory CodeRabbit waitFor thread; after: regenerated adoption-manifest, hardened scroll negative assertion, pre-commit+handoff adoption sync to prevent recurrence; CodeRabbit dispositioned as fixed by sync assert | check:design-system-adoption PASS; vitest design-system-adoption+document-viewer-shell+docs-inventory 63/63 PASS; format; no provider-backed checks | | 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 86698228533ebe10452c10c1bd7a3e1610d891ae | PR #1782 unblock | merged origin/main (behind-but-clean); fixed static-pr TS2322 on document-viewer-shell chunk fixture; fixed Production UI DSM compare remove stall via location.assign + DOM proof; prior adoption-manifest drift already fixed | tsc clean for changed files; vitest document-viewer-shell+dsm-compare-remove+design-system-adoption 59/59 PASS; check:design-system-adoption PASS; format; no provider-backed checks | +| 2026-08-10 | PR #1797 / claude/codex-m4a-retire-dead-type-8wq9ta | 6bf3c7b2a0600021290e165302fd07d721af6592 | retire the dead --text-2xl-compact type step (ledger #297): globals.css @theme, twMerge config, two test lists, the design-system-contract exemption, TOKENS.md/GATES.md | Executed the recorded next action on outstanding-issues #297. The step had zero class-utility and zero var(--text-*) consumers, so the deletion renders identically; UNUSED_TYPE_STEP_EXEMPTIONS is now empty and the declared-but-unconsumed gate holds the line with no carve-out. One test fixture using the token as a synthetic var() consumer was repointed at --text-2xl-minus. GATES.md corrected to eight non-standard steps; the 705-consumer total is unchanged because this step contributed 0. No clinical, RAG-ranking or operational risk paths touched (classifyPullRequestFiles: all false). | check:design-system-contract PASS (705 production files); check:type-scale --strict PASS; lint exit 0; typecheck exit 0; npm run build after rm -rf .next exit 0 (Compiled successfully in 63s); check:outstanding-issues PASS; verify:pr-local completed through typecheck then failed at test on a PRE-EXISTING root-permission failure in tests/pr-handoff-stop.test.ts that reproduces on clean d812c76 (5993 passed, 1 failed); build and check:rag:fixtures run/assessed separately. No UI gate: no rendered output can change. No provider-backed check run. | diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index b5cc3a57a..c7e678a32 100644 --- a/docs/design-system/GATES.md +++ b/docs/design-system/GATES.md @@ -45,10 +45,12 @@ flagged. **Partly enforced, corrected 9 Aug 2026.** The decidable half of step _selection_ now ships inside `check:design-system-contract`: a step declared in the `globals.css` `@theme` block that no production surface selects fails the build, naming the step. It found one on the day -it landed — `--text-2xl-compact`, zero consumers, carried as a documented exemption with its -retirement tracked as `#297`. The exemption itself is gated: the build also fails if an -exempted step stops being declared or gains a consumer (class utility or direct -`var(--text-*)` across walked production sources), so the list cannot rot. +it landed — `--text-2xl-compact`, zero consumers — which was carried as a documented exemption +with its retirement tracked as `#297`, and **retired from `@theme` on 10 Aug 2026**, closing +that row. The exemption list is empty now and that is its intended resting state; it is still +gated from both sides, so the build fails if an exempted step stops being declared or gains a +consumer (class utility or direct `var(--text-*)` across walked production sources), and the +list cannot rot. **Still not enforced:** which of the _existing_ steps a component picks. Nothing stops a surface choosing `text-sm-minus` over `text-sm`, and nothing mechanical can — that is a @@ -58,11 +60,13 @@ already ships. ⚠️ **The "1 318 call sites" figure this section used to quote was a repo-wide grep including `src/app/mockups/**`, which every one of these gates excludes** — 1 360 at `7aaf9349c`. -Production consumers of the nine non-standard steps total **705** **[verified: AST class-root +Production consumers of the eight non-standard steps total **705** **[verified: AST class-root pass over the contract check's own walk, 9 Aug 2026]**: `text-2xs` 421, `sm-minus` 160, -`base-minus` 57, `3xs` 42, `2xl-minus` 9, `3xl-minus` 9, `lg-minus` 6, `hero` 1, -`2xl-compact` 0. A third figure of 733 has also circulated; that one counts the `@theme` -declarations and doc comments as if they were usages. Nine steps, not eight. +`base-minus` 57, `3xs` 42, `2xl-minus` 9, `3xl-minus` 9, `lg-minus` 6, `hero` 1. A third +figure of 733 has also circulated; that one counts the `@theme` declarations and doc comments +as if they were usages. The count was nine steps until 10 Aug 2026, when the ninth — +`2xl-compact`, the one with 0 consumers — was retired; the total is unchanged precisely +because it contributed nothing. ## 2 · The twelve system gates, labelled diff --git a/docs/design-system/TOKENS.md b/docs/design-system/TOKENS.md index e8a3e34ae..ed9b4ab8c 100644 --- a/docs/design-system/TOKENS.md +++ b/docs/design-system/TOKENS.md @@ -109,15 +109,15 @@ The v2 layer _references_ or _depends on_ these; their values stay in `live` / ` ## 6 · Deprecations and deletions -| Token | Disposition | Gate | -| ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| `--text-soft` | Deprecated alias of `--decoration-soft`; both resolve identically during the window. Delete when zero references remain outside the alias declaration. | Contract test pins the tier from both sides; a lint for `--text-soft`/`--decoration-soft` on text-bearing nodes is planned (GATES §1). | -| `--shadow-focus` | **Delete** (PR 9) — encodes a companion focus ring the conventions forbid; a trap for the next person who greps "focus". | Planned lint after deletion. | -| `--shadow-lift` | Retire into the `--eN` ladder (PR 9). | Planned. | -| `--shadow-card`, `--shadow-soft` | Aliases of a ladder step; retire **inside the recipes first**, then delete. | Planned. | -| `--spring-bouncy` + two other dead springs | Delete (PR 9); byte-duplicate and unused curves. | Planned. | -| `--quantity-unit-scale` (design side) | Never lands; superseded per §1. | Next design sync removes it. | -| Legacy type steps (`text-2xs`/`3xs`, `sm-minus`, `base-minus`, `2xl-minus`, `lg-minus`, `3xl-minus`, `2xl-compact`, `3xl/4xl/5xl`) | Retired **last of all** — ≈663 call sites; `--text-md` arrives additively first. ⚠️ `Quantity` currently consumes `text-base-minus` — fix in the retirement tranche. | Contract ratchet extension, planned. | +| Token | Disposition | Gate | +| ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `--text-soft` | Deprecated alias of `--decoration-soft`; both resolve identically during the window. Delete when zero references remain outside the alias declaration. | Contract test pins the tier from both sides; a lint for `--text-soft`/`--decoration-soft` on text-bearing nodes is planned (GATES §1). | +| `--shadow-focus` | **Delete** (PR 9) — encodes a companion focus ring the conventions forbid; a trap for the next person who greps "focus". | Planned lint after deletion. | +| `--shadow-lift` | Retire into the `--eN` ladder (PR 9). | Planned. | +| `--shadow-card`, `--shadow-soft` | Aliases of a ladder step; retire **inside the recipes first**, then delete. | Planned. | +| `--spring-bouncy` + two other dead springs | Delete (PR 9); byte-duplicate and unused curves. | Planned. | +| `--quantity-unit-scale` (design side) | Never lands; superseded per §1. | Next design sync removes it. | +| Legacy type steps (`text-2xs`/`3xs`, `sm-minus`, `base-minus`, `2xl-minus`, `lg-minus`, `3xl-minus`, `3xl/4xl/5xl`) | Retired **last of all** — ≈663 call sites; `--text-md` arrives additively first. ⚠️ `Quantity` currently consumes `text-base-minus` — fix in the retirement tranche. `--text-2xl-compact` left this list early (`#297`): it had zero consumers, so retiring it needed no tranche and rendered identically. | Contract ratchet extension, planned. | ## 7 · Usage rules — allowed and forbidden, per group diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 11f6e230c..cd0c013ea 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -329,7 +329,6 @@ removed after current-main verification; it is not missing recommended work. | #293 | P2 | issue | Controls declare min-h-tap and compute min-height 0px; a rendered-interactive tap audit needs a deterministic surface first | Two findings, one robust and one that blocked the gate. FINDING 1 (robust, reproduced in ALL SIX runs): controls that carry min-h-tap compute min-height 0px and render far below the 48px floor. Six distinct shapes seen across runs - 'a.inline-flex min-h-tap items-center' 16px, 'a.inline-flex min-h-tap shrink-0' 16.5px, 'button.flex min-h-tap w-full' 26.6px, 'button.grid min-h-tap min-w-tap' 36px, 'button.inline-flex min-h-tap items-center' 16px, 'button.inline-flex min-h-tap min-w-[94px]' 36px. min-h-tap works in general (the existing declared-carrier audit still measures carriers at or above 48px), so these elements have the declaration overridden to 0 rather than the utility being absent; likely an unlayered component class in globals.css, which by design outranks Tailwind utilities here. This was invisible because the pre-existing audit in tests/ui-style-contract.spec.ts only measures elements whose COMPUTED min-height is already at or above the floor (declared < tapFloor - 0.5 continue), so a floor overridden downward is skipped rather than flagged - the same structural blind spot as the h-10 case #265 named. FINDING 2 (why gate 2 did NOT land 2026-08-09): a rendered-interactive enumeration on /services?q=CMHT&run=1 is NOT DETERMINISTIC. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct control shapes, largely disjoint - one run saw answer-suggestion chips and a sort band, another a settled services results list. waitForLoadState('networkidle') plus deduplication to distinct shapes (instance counts measure how many results the query returned, and gave 9 vs 39) did NOT fix it; two consecutive agreeing runs were coincidence, and the next run differed again. The enumeration was written, proven to find real defects, and then REVERTED rather than landed, because tests/ui-style-contract.spec.ts runs in the required Production UI job via productionSpecPattern and scripts/playwright-pr-shards.mjs, so an intermittent version of it would block every merge in the repo. Next, in order: (1) find a deterministic surface for the audit - a static route with no async search, or a fixed seeded state - before re-attempting the enumeration; (2) separately, find what zeroes min-height on the min-h-tap carriers and fix or write a stated exception. Stop: do not re-land the enumeration on a live-search route, do not quarantine a brand-new test to get it merged (quarantine is for keeping flaky tests we already trust, and repo policy needs three reproductions on one SHA via tests/flake-ledger.json), do not lower any production tap target, and never to min-h-11 (known ui-smoke sub-pixel flake; production uses min-h-12). | session 2026-08-09 — M2 gate 2 enumeration (#265) | 2026-08-09 | | #294 | P3 | rec | OffscreenCanvas for the PDF raster is unjustified until the page-flip cost is read from CI | **Outcome:** the worker-raster question is settled by a number rather than left as a standing 'optional' item in the redesign plan. **Detail:** docs/plans/document-viewer-redesign-plan.md conditions OffscreenCanvas on 'measured main-thread paint cost'. Phase 3 (Task 5) did not implement it, deliberately: virtualization now keeps the reader's page and one neighbour already rastered, so the cold-render-per-flip cost that motivated a worker raster is largely gone before any threading work starts, and moving pdf.js rendering off the main thread would put the canvas the clinical source is drawn into behind a transfer boundary — a real risk on the one surface where a blank page is a clinical failure. **No number exists yet and none could be produced locally:** pdfjs-dist@6 needs Map.prototype.getOrInsertComputed, which this container's Chromium 141 lacks and Node 24.13.0 also lacks, so neither a browser nor a headless harness here can raster a page (see #279). **Next:** read the measurement the gate already captures. tests/ui-document-canvas.spec.ts attaches page-flip-raster-cost.json (flipToPaintedMs, longTaskCount, longTaskTotalMs, longestTaskMs, canvasBackingPixels) and logs a '[viewer-canvas] page flip painted in Nms' line, on every Production UI run and on any host with the pinned Chromium 151 build: npm ci --include=dev && npx playwright install chromium && npm run ensure && npm run test:e2e -- tests/ui-document-canvas.spec.ts --project=chromium. Close this as not-worth-doing and strike the row from the plan's Phase 3 table only when a Production UI (or equivalent Chromium 151) run records decisive log lines for all three: longestTaskMs comfortably under ~50ms, plus explicit flipToPaintedMs and longTaskTotalMs budgets agreed for that host class and met on the same run. Do not close on longestTaskMs alone. **Stop:** do not implement OffscreenCanvas on principle because the plan lists it — the plan conditions it on the measurement, and the measurement is now cheap to obtain. | session 2026-08-09 document viewer Phase 3, Task 5; docs/plans/document-viewer-phase3-handover.md | 2026-08-09 | | #296 | P3 | issue | tests/pr-handoff-stop.test.ts fails whenever the unit suite runs as root | The case 'emits handoff context only when the marker file exists' chmods the fixture git dir to 0o555 to force the marker write to fail, then asserts the hook failed open with no marker. Root ignores permission bits, so the write succeeds and the assertion flips: 'expected true to be false' at tests/pr-handoff-stop.test.ts:180. Confirmed environmental and pre-existing, not diff-induced — reproduced on a clean checkout of af85cbc with every working change stashed (1 failed \| 10 passed), and 'id -u' returns 0 in the remote container. Cost is that 'npm run test' and therefore 'npm run verify:pr-local' cannot reach a clean exit in any root container, so a real regression later in the run is masked by a known-red file and the gate has to be interpreted by hand every time. CI is unaffected because its runner is non-root, which is why this has not surfaced there. Next action: make the test skip or change technique when 'process.getuid?.() === 0' — either skip with an explicit reason, or force the write failure a way root cannot bypass (point the marker path at a directory that does not exist, or at a path whose parent is a file), which is portable and keeps the assertion meaningful for every user. Stop: do not delete the case or relax it to 'marker may or may not exist' — failing open without telling the model that tools are denied is the actual contract it guards. | session 2026-08-09; verify:pr-local run on af85cbc | 2026-08-09 | -| #297 | P3 | task | Retire the --text-2xl-compact type step, which no production surface selects | Measured 2026-08-09 against origin/main 7aaf9349c while adding the step-selection gate (#262 part 2). --text-2xl-compact is declared at src/app/globals.css:112 and selected by ZERO production surfaces: the only other mentions anywhere are src/lib/tailwind-merge.ts:20 (a docstring), its entry in CLINICAL_TWMERGE_THEME.text at src/lib/tailwind-merge.ts:50, tests/tailwind-merge-config.test.ts:49 (which asserts the @theme block and the twMerge config agree), and a row in docs/design-system/TOKENS.md. No var(--text-2xl-compact) consumer exists either. For contrast, the next-rarest step, text-hero, does have one real consumer (src/components/mode-home-template.tsx:87). The new gate in scripts/check-design-system-contract.mjs fails on any declared-but-unconsumed step; this one is carried in UNUSED_TYPE_STEP_EXEMPTIONS with outstanding-issues.md #297 named as its tracking task, and the gate additionally fails if the exemption ever goes stale (step undeclared, or step gains a consumer). Next action: delete --text-2xl-compact from the globals.css @theme block, drop "2xl-compact" from CLINICAL_TWMERGE_THEME.text and from tests/tailwind-merge-config.test.ts, update the TOKENS.md row, then delete the exemption entry - the gate will hold the line afterwards. Deleting a step with zero consumers renders identically, but it edits @theme, so give it its own revertible PR rather than bundling it with a gate change. Gate: npm run check:design-system-contract plus npm run test. | session 2026-08-09 - M3 design-token debt (#262 parts 2 and 3) | 2026-08-09 | | #298 | P2 | task | ErrorState is built and registered but nothing enforces it - the '0 matches after a failed request' row is still planned | GATES.md still reads: Render "0 matches" after a failed request \| ErrorState adoption + check \| planned. The component now exists (src/components/ui/error-state.tsx) and is registered across all gate-11/12 surfaces, but grep over scripts/ and eslint-rules/ returns ZERO references to ErrorState, so nothing prevents a new surface rendering a count under a failed status. Building the component closed the 'documented gate with nothing behind it' half; the enforcement half is untouched, and the row was deliberately NOT flipped to implemented because that is exactly the drift GATES.md exists to stop. Next action: add a metric to check:design-system-contract that fails when a count-bearing node renders under an error/failed status - the analyzer already resolves class roots and JSX children (childrenAreNumeralOnly, used by statusColouredNumerals), so it needs no new npm script. Do this BEFORE adoption: a check with no adoption still stops the next regression, adoption with no check does not. Stop: do not flip the GATES.md row until a check actually runs in verify:cheap. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | | #299 | P3 | task | Adopt ErrorState at the three surfaces that genuinely hand-roll the failed-request guard | Three surfaces hand-roll the guard and their comments state the rule outright: src/components/clinical-dashboard/search-results-header-band.tsx:210 ('no number may reach the DOM'), src/components/services/services-navigator-page.tsx:634 ('a blocked registry must not reach the band as 0 matches'), src/components/clinical-dashboard/favourites-command-library-page.tsx:1182. They are CORRECT today, just not shared, so this is convergence rather than a bug fix. The band's fault panel is the richest existing implementation (role=alert, warning tokens, AsyncButton retry with busy state, faultAction slot) and ErrorState was modelled on it, so the shapes already line up. Live-look change: own PR, Chromium pass. Per the M4 brief it sits DOWNSTREAM of design decisions the owner has not made, so doing it before the site-wide redesign risks redoing it. Do NOT bundle with the enforcement check. Stop: only these three - see the sibling row for three sites that were miscarried as guards. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | | #300 | P2 | issue | Three sites carried into M4 as hand-rolled '0 matches' guards are not guards - do not convert them | Re-measured 2026-08-09. The M4 handover listed six surfaces hand-rolling the failed-request guard; three do not survive measurement and converting them to ErrorState would be WRONG. (1) src/components/clinical-dashboard/differentials-home.tsx:716,729 renders '0 matches' and 'No matches' when sourcesChecked is TRUE - sourcesChecked: boolean means the source search RAN, so that is a legitimate zero after a search that SUCCEEDED and must keep reporting its count. (2) specifiers-home-page.tsx is not under clinical-dashboard/ at all - the real path is src/components/specifiers/specifiers-home-page.tsx and its line 211 is a comment about not showing a stale zero ABOVE real catalogue results, a different problem. (3) document-search-results.tsx:1508 gates on recordStatus for LOADING (recordSearchStillRunning), not for a failed count; its genuine fault handling is recordBandOwnsFault, which delegates to the band. Only search-results-header-band, services-navigator-page and favourites-command-library-page are real. Next action: none - this row exists so the next reader does not convert the wrong three. Stop: do not 'fix' differentials-home to suppress its count. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | @@ -501,4 +500,5 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #295 | issue | Factsheet detail renders two h1 elements — hero plus the portaled print sheet | RESOLVED 2026-08-09 as WONTFIX-by-design, with the invariant now asserted instead. Investigated and concluded the print sheet's

is correct and must stay. The two headings are mutually exclusive by construction, so no state ever exposes both to the accessibility tree: on screen `.factsheet-print-sheet { display: none }` removes the print subtree entirely, and in print `html.factsheets-printing body > *:not(.factsheet-print-portal)` is `display: none !important`, which removes the entire shell that owns the hero heading (src/app/globals.css ~3409-3427). Demoting the print sheet to

would be a regression, not a fix: the printed PDF is a separate document whose section headings are already

, so it would ship with no top-level heading and an outline starting at level 2. The real residual was the one this row named — the DOM test had been scoped to the page testid, so the document-level invariant was asserted nowhere and a genuinely stray third

would not have been caught. Closed that instead: tests/factsheet-detail-header.dom.test.tsx now censuses every

in the document, pinning exactly two, one inside the page shell and one inside .factsheet-print-portal, both carrying the factsheet title, plus at least one

in the printed outline. jsdom applies no stylesheet, so a census is the right shape of guard rather than a visibility assertion. No component was moved: FactsheetPrintSheet stays in factsheet-detail-page.tsx, as scripts/design-system-contract-utils.mjs scopes its raw-colour exemption to the literal factsheet-print-sheet marker and fails closed if it moves. 6 tests pass. | 2026-08-09 | | #218 | issue | cn() has no tailwind-merge, so className size overrides resolve by stylesheet order rather than intent | CLOSED 2026-08-09 as already shipped - verified in source at origin/main 7aaf9349c, not inferred. cn() adopted tailwind-merge in PR #1678 (aeba5a254 feat(design-system): give cn() tailwind-merge (#218)). Evidence: src/components/ui-primitives.tsx:37 is now twMergeClinical(classes.filter(Boolean).join(" ")) rather than a plain join, importing twMergeClinical from @/lib/tailwind-merge at line 13; package.json:267 carries tailwind-merge ^3.6.0; src/lib/tailwind-merge.ts (119 lines) declares this repo's @theme text/leading/tracking/spacing/ease/animate scales to twMerge and documents the two silent failure modes it exists to stop (misclassification deleting the text-sm-minus family, and non-recognition of size-icon-md / tracking-label / leading-prose / ease-out-soft / animate-shimmer / pt-safe). The row's own concrete instance - cn(metadataPill, ... text-2xs) in document-search-results.tsx - now resolves by call-site intent rather than stylesheet order. The follow-up that #218 spawned, #270, is also closed. Note for the next reader: the docstring at src/lib/tailwind-merge.ts:34-36 still says present-tense that the --spacing-tap family 'is held back', which its own line 66 and the spacing array at line 104 contradict; that stale sentence is cosmetic and does not affect behaviour. | 2026-08-09 | | #270 | task | Declaring tap in tailwind-merge is no longer blocked by dead numeric heights — the 22-site premise did not survive re-measurement | CLOSED 2026-08-09 as already shipped - verified in source at origin/main 7aaf9349c, not inferred. The composition-aware sweep this row asked for was run and its conclusion acted on in PR #1738 (80cf78139 fix(tailwind-merge): declare the tap spacing token now its blocker is disproved), confirmed an ancestor of origin/main by git merge-base --is-ancestor. Evidence: 'tap' is now present in CLINICAL_TWMERGE_THEME.spacing at src/lib/tailwind-merge.ts:104, and the long comment at lines 66-93 has been rewritten from the stale 22-site/18-drop justification into a dated record of the disproof - zero same-variant tap/numeric pairs, the 84 survivors identified as cross-variant responsive step-downs that twMerge groups apart, the named call sites confirmed stale, and a composition-aware sweep over 1418 cn() call sites that resolves constant recipe identifiers finding zero same-variant pairs in either direction, mutation-tested against synthetic cn("h-tap","h-4") and cn("min-h-tap", recipe) probes so the zero is a measurement rather than a pattern that never matches. tests/tailwind-merge-config.test.ts was not deleted as the row proposed but inverted, which is better: line 116 now asserts cn("min-h-9","min-h-tap") === "min-h-tap", lines 125-127 assert the cross-variant pairs survive untouched, and line 161 asserts every --spacing-* token including tap is declared. No production target was lowered. Residual cosmetic drift: the module docstring at lines 34-36 still says present-tense that the --spacing-tap family 'is held back', contradicted by line 66 and the array itself. | 2026-08-09 | +| #297 | task | Retire the --text-2xl-compact type step, which no production surface selects | RESOLVED 2026-08-10. --text-2xl-compact is deleted from the src/app/globals.css @theme block, dropped from CLINICAL_TWMERGE_THEME.text in src/lib/tailwind-merge.ts (and its docstring), removed from the CUSTOM_TEXT_STEPS list in tests/tailwind-merge-config.test.ts, and its UNUSED_TYPE_STEP_EXEMPTIONS entry deleted from scripts/check-design-system-contract.mjs — the exemption Map is now empty, which the header records as its intended resting state. Docs updated: the TOKENS.md legacy-type-steps row no longer lists the step, and GATES.md now reads eight non-standard steps rather than nine (the 705-consumer total is unchanged precisely because this step contributed 0). One test fixture in tests/design-system-contract-utils.test.ts that used --text-2xl-compact as a synthetic var() consumer was repointed at --text-2xl-minus so no fixture names a token that no longer exists. Zero-consumer deletion, so the rendered output is identical. Verified: npm run check:design-system-contract (passed, 705 production files), npm run check:type-scale --strict (passed), npm run test (full offline unit suite). The gate now holds the line without an exemption — a re-added unconsumed step fails the build by name. | 2026-08-10 | diff --git a/scripts/check-design-system-contract.mjs b/scripts/check-design-system-contract.mjs index 034e4da06..c4e264d31 100644 --- a/scripts/check-design-system-contract.mjs +++ b/scripts/check-design-system-contract.mjs @@ -141,10 +141,13 @@ const recordDebt = (metric, relativePath, count) => { * change rather than riding along with a gate. Anything listed here is recorded * debt with a ledger row, not a permanent licence — an entry should leave this * list by being deleted from `globals.css`, not by being forgotten. + * + * Empty since `--text-2xl-compact` — the one entry this list ever carried, and + * the step the gate found on the day it landed — was retired from `@theme` + * (outstanding-issues `#297`). Empty is the intended resting state: an addition + * here needs a ledger row saying when it leaves again. */ -const UNUSED_TYPE_STEP_EXEMPTIONS = new Map([ - ["2xl-compact", "no consumer since it was added; retirement tracked as docs/outstanding-issues.md #297"], -]); +const UNUSED_TYPE_STEP_EXEMPTIONS = new Map([]); const densityOverrideFindings = []; const typeStepUsage = new Set(); diff --git a/src/app/globals.css b/src/app/globals.css index 6f2f8cf0a..85b876d7c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -109,7 +109,6 @@ --text-base-minus: 0.9375rem; --text-lg-minus: 1.0625rem; --text-2xl-minus: 1.375rem; - --text-2xl-compact: 1.45rem; --text-3xl-minus: 1.625rem; /* Fluid display heading for the hero / mode-home titles (hybrid-fluid type: diff --git a/src/lib/tailwind-merge.ts b/src/lib/tailwind-merge.ts index 2ef7b02ee..d2f4fa572 100644 --- a/src/lib/tailwind-merge.ts +++ b/src/lib/tailwind-merge.ts @@ -17,7 +17,7 @@ import { extendTailwindMerge } from "tailwind-merge"; * "text colour" for any `text-` it does not recognise as a font size. Its * built-in font-size scale is t-shirt-shaped, so `text-2xs` / `text-3xs` * happen to survive, but `text-sm-minus`, `text-base-minus`, `text-lg-minus`, - * `text-2xl-minus`, `text-2xl-compact`, `text-3xl-minus` and `text-hero` do + * `text-2xl-minus`, `text-3xl-minus` and `text-hero` do * not — each was measured being **deleted** when it met the sibling * `text-[color:var(--text-muted)]` in the same `cn()` call. `eyebrowText` is * exactly that pair. Declaring the scale under `theme.text` moves them into @@ -47,7 +47,7 @@ export const CLINICAL_TWMERGE_THEME = { // globals.css @theme --text-* — size-only steps (no baked leading/tracking). // `3xs`/`2xs` already resolve as t-shirt sizes; listed so the scale reads // whole and stays correct if that heuristic ever narrows. - text: ["3xs", "2xs", "sm-minus", "base-minus", "lg-minus", "2xl-minus", "2xl-compact", "3xl-minus", "hero"], + text: ["3xs", "2xs", "sm-minus", "base-minus", "lg-minus", "2xl-minus", "3xl-minus", "hero"], // globals.css @theme --leading-* — the two steps Tailwind's scale cannot // express. Tailwind still owns tight/snug/normal/relaxed. diff --git a/tests/design-system-contract-utils.test.ts b/tests/design-system-contract-utils.test.ts index 93c836054..ec7923274 100644 --- a/tests/design-system-contract-utils.test.ts +++ b/tests/design-system-contract-utils.test.ts @@ -431,16 +431,16 @@ describe("design-system contract helpers", () => { it("reports direct var(--text-*) consumers for the shared unused-step predicate", () => { expect( findTypeStepCssUsagesInSource( - '.a{font-size:var(--text-2xl-compact)}.b{font-size:var(--text-hero, 2rem)}.c{--text-sm-minus:1rem;content:"var(--text-sm-minus)"}', + '.a{font-size:var(--text-2xl-minus)}.b{font-size:var(--text-hero, 2rem)}.c{--text-sm-minus:1rem;content:"var(--text-sm-minus)"}', "src/probe.css", ), - ).toEqual(["2xl-compact", "hero"]); + ).toEqual(["2xl-minus", "hero"]); expect( findTypeStepCssUsagesInSource( - 'const style = { fontSize: "var(--text-2xl-compact)" };\n// var(--text-hero)\n', + 'const style = { fontSize: "var(--text-2xl-minus)" };\n// var(--text-hero)\n', "src/probe.tsx", ), - ).toEqual(["2xl-compact"]); + ).toEqual(["2xl-minus"]); }); it("rejects debt moved to a new path even when its global total is unchanged", () => { diff --git a/tests/tailwind-merge-config.test.ts b/tests/tailwind-merge-config.test.ts index 160cd9a79..2913296f6 100644 --- a/tests/tailwind-merge-config.test.ts +++ b/tests/tailwind-merge-config.test.ts @@ -46,7 +46,6 @@ describe("custom @theme scales are not misclassified", () => { "text-base-minus", "text-lg-minus", "text-2xl-minus", - "text-2xl-compact", "text-3xl-minus", "text-hero", ];