feat(design-system): ratchet raw scale literals and gate type-step selection (#262 parts 2 and 3) - #1780
Conversation
Both rows were still open in docs/outstanding-issues.md while their work was already live on main, which had scoped a third session from them. #218 (cn() lacks tailwind-merge) shipped in PR #1678, aeba5a2. src/components/ui-primitives.tsx:37 is twMergeClinical(...) rather than a plain join, package.json carries tailwind-merge ^3.6.0, and src/lib/tailwind-merge.ts declares the repo's @theme scales to twMerge. #270 (declare the tap spacing token) shipped in PR #1738, 80cf781, an ancestor of origin/main. "tap" is present in CLINICAL_TWMERGE_THEME.spacing, and tests/tailwind-merge-config.test.ts was inverted rather than deleted so the merge behaviour is now asserted rather than pinned out. Verified in source at origin/main 7aaf934, not inferred from the handover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rals (#262 part 3) The design-system contract ratcheted colour, shadow, tap and tracking but not spacing, radius or line-height, so a value could bypass the scale as a bare literal in either a class or a stylesheet and nothing objected. Adds three per-path ratchets, covering both halves the way the colour and legacy-shadow metrics already do: rawPaddingLiterals 67 (17 CSS declarations, 50 class utilities) rawRadiusLiterals 24 (22 CSS declarations, 2 class utilities) rawLineHeightLiterals 3 (3 CSS declarations) The exemption is deliberately "contains no CSS function", not the narrower `(?!var\()` the tracking rule uses. Padding is not only ever a token or a literal: production ships pb-[env(safe-area-inset-bottom)], pt-[max(0.75rem,var(--safe-area-top))], pt-[clamp(1.5rem,5vh,3rem)] and pb-[calc(7rem+env(safe-area-inset-bottom))]. Those are computed from the viewport or the safe-area inset, cannot be spelled as a scale step, and a `var(`-only lookahead would have flagged every one of them. On the CSS side, zero in any unit, the CSS-wide keywords and custom-property declarations (the token definitions themselves) are exempt for the same reason. Every one of the 94 baseline entries was verified present at its cited line before pinning, and the baseline change is additive: all fifteen pre-existing metrics and every pre-existing debtByPath entry are byte-identical. Mutation-tested rather than assumed. Class side, in a file with no prior debt: - rawPaddingLiterals increased from 67 to 68 - rawPaddingLiterals at src/components/ui-primitives.tsx increased from 0 to 1 - rawRadiusLiterals increased from 24 to 25 - rawRadiusLiterals at src/components/ui-primitives.tsx increased from 0 to 1 - rawLineHeightLiterals increased from 3 to 4 - rawLineHeightLiterals at src/components/ui-primitives.tsx increased from 0 to 1 The CSS half fails the same way. Both probes also carried the sanctioned computed forms, and each count rose by exactly one, so the exemptions are proved by the same runs rather than argued. No new npm script: the metrics live inside check:design-system-contract, so docs:check-inventory and check:gate-manifest are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…262 part 2) check:type-scale blocks arbitrary text-[12px] values. Nothing has stopped the scale itself growing a step no surface ever picks, which is the drift that makes a wrong selection possible in the first place. Whether a heading should have chosen text-sm over text-sm-minus is not mechanically decidable, and this does not pretend otherwise. A step that is declared and consumed by nobody is decidable, and there is one today: --text-2xl-compact (globals.css:112) has zero consumers -- no utility use, no var() use -- while the next-rarest step, text-hero, has one real consumer. The analyzer reports every bare text-<name> it sees and does not decide which names are steps; the checker intersects that against the @theme block it parses from globals.css. So the scale is never written down twice, and a step added to globals.css is covered without touching this gate. Retiring the dead step edits @theme, so it gets its own revertible PR rather than riding along here: it is carried in UNUSED_TYPE_STEP_EXEMPTIONS and tracked as docs/outstanding-issues.md #295. The exemption cannot rot silently -- the gate also fails if an exempted step stops being declared or gains a consumer. Mutation-tested, three ways: - type steps are declared in globals.css @theme but no production surface selects them: --text-2xl-compact (text-2xl-compact). Retire the step or use it; do not leave the scale carrying a step nobody picks. - (a newly added --text-probe-step fails identically, so this catches future drift rather than only today's known case) - --text-2xl-compact is exempted as unused but production now selects text-2xl-compact -- drop the exemption Measurement note, since three different figures were in circulation for this row: the "1318 sites" is a repo-wide grep INCLUDING mockups, which the gate excludes (1360 at this HEAD). Production consumers of the nine non-standard steps total 705 -- 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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GATES.md's own §1 is the list of what actually runs, and this series' recurring failure is that list lagging the code: four of #264's six prohibitions were already gated while it said "planned". Records the padding/radius/line-height ratchets and the type-step selection rule in the contract row, and rewrites the type-scale callout, which claimed a step-selection lint "does not exist". The decidable half now ships; the half that asks whether text-sm-minus was the right pick over text-sm still does not, and cannot. Also corrects the "1 318 call sites" figure quoted there. It was a repo-wide grep including src/app/mockups/**, which every one of these gates excludes (1 360 at 7aaf934). Production consumers total 705, and there are nine non-standard steps, not eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 12 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6e1fe7fc4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds design-system gates for raw scale literals and unused typography steps while updating related debt tracking and documentation.
Changes:
- Ratchets raw padding, radius, and line-height literals.
- Detects declared but unused typography steps.
- Archives completed issues and documents follow-up debt.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
scripts/design-system-contract-utils.mjs |
Adds scale-literal and type-step analysis. |
scripts/check-design-system-contract.mjs |
Enforces new ratchets and unused-step rules. |
scripts/design-system-contract-baseline.json |
Records current per-path debt. |
tests/design-system-contract-utils.test.ts |
Tests the new analyzers. |
docs/design-system/GATES.md |
Documents active design-system gates. |
docs/outstanding-issues.md |
Archives shipped work and adds follow-ups. |
docs/branch-review-ledger.md |
Records the PR review state. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Cover Tailwind arbitrary-property forms and modern CSS zero units in the raw scale ratchets, and validate unused-step exemptions against the same class-or-CSS consumer predicate used for ordinary steps. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Clear behind-but-clean drift after main advanced (#1775); merge-tree was clean. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
PR #1780 review-and-fix summaryTip: Sync / merge-tree
Review
Fixed (P2, pushed in
|
…1786) * feat(design-system): build ErrorState, the gate with nothing behind it GATES.md §3 lists the prohibition "Render '0 matches' after a failed request" with the gate "ErrorState adoption + check", status planned. Measured at origin/main 199b303, ErrorState existed nowhere in src or tests — only in COMPONENTS.md, GATES.md and SPEC.md. This builds it. The invariant is clinical, not cosmetic. A search that failed has no count to report, so reporting zero is a false statement about the corpus: on the services page "0 matches" asserts there are no crisis services when the search never ran, and on favourites it reads as "you have saved nothing" rather than "we could not load them". COMPONENTS.md:322 draws the same line from the other side — "no result count is available" is not a MissingValue. The component therefore takes no count and no children. There is no prop through which a number can arrive, and the generated dtsPropsFor entry now records that as the published API. The one remaining route, a caller writing a count into title or body, is covered by a development-time tripwire that matches a figure against a counted noun ("0 matches", "no results") so an error code or a duration does not trip it. It warns and never throws: on the one screen already reporting a failure, a thrown error is a blank page. Requirements came from the surfaces that hand-roll this guard today. Three do, and their comments state the rule outright: search-results-header-band.tsx:210 ("no number may reach the DOM"), services-navigator-page.tsx:634 ("a blocked registry must not reach the band as '0 matches'") and favourites-command-library-page.tsx:1182. They are correct, just not shared; converting them is a live-look change and deliberately not here. Three further sites carried into this task as hand-rolled guards are not that, measured at this HEAD, and are recorded so the next reader does not convert them: differentials-home.tsx:716,729 renders "0 matches"/"No matches" when sourcesChecked is true, i.e. a legitimate zero after a search that SUCCEEDED; specifiers-home-page.tsx:211 is a comment about not showing a stale zero above real catalogue results, and lives in src/components/specifiers/, not clinical-dashboard/; document-search-results gates on recordStatus for loading, not for a failed count. Registered per gates 11 and 12: source, design-sync export, preview, prop contract, publication test entry, behavioural DOM tests, adoption-contract family, and both generators regenerated (54 components, 59 roots). Uses the shared floatingControl recipe rather than a hand-rolled control, so the tap floor, focus ring and forced-colors border come from one owner. Adds zero arbitrary padding/gap/radius/line-height, so the ratchet landed in the previous commit is unaffected and the two stay separately revertible. Scope: ErrorState only. OfflineState, PermissionDeniedState, NotFoundState and UnavailableState share the pattern but have no gate pointing at them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(design-system): ratchet raw gap literals, the family #1780 left uncovered #1780 landed rawPaddingLiterals, rawRadiusLiterals and rawLineHeightLiterals for #262 part 3. Gap was the one remaining spacing surface a hand-picked value could hide in: gap-[9px] and `gap: 18px` were counted by no ratchet at all. This adds rawGapLiterals on that commit's own predicate and wiring. Measured against origin/main ef9bb51: 34 sites across 9 files — 21 Tailwind utilities, every one under src/components/therapy-compass/, plus 13 CSS declarations in globals.css that a utility-only scan misses. Covering both spellings is the same reason #1780 counts both: otherwise a literal escapes by moving from a class into globals.css. Kept as its own metric rather than folded into rawPaddingLiterals so the therapy-compass cleanup can be paid down and re-pinned independently of the padding debt, which is spread across fifteen unrelated files. Reuses RAW_LITERAL_VALUE unchanged, so a value containing a CSS function (env(, clamp(, max(, calc() stays a sanctioned computed form and is exempt. Also corrects the §3 prohibition row, which #1780 left reading "implemented-partial (colour/shadow/tap literals only)" and which named none of the metrics it had just shipped. A row that understates shipped work is what sends the next session to rebuild it — this change was itself started as a duplicate of #262 part 3 for exactly that reason. Mutation-verified in both halves, each naming the metric and the path: a gap-[3px] utility gives "rawGapLiterals increased from 34 to 35" plus "at src/components/ui/missing-value.tsx increased from 0 to 1"; a `gap: 19px` declaration gives the same total plus "at src/app/globals.css increased from 13 to 14". Baseline diff is additive only. legacyShadowAliases measures 218 against its pinned 220 on main; that slack is left exactly as found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(issues): capture the ErrorState and duplicate-work follow-ups Four rows (#298-#301), added with npm run issues:add. Ids 295-297 were claimed by main while this branch was open, which is ledger #156's read-modify-write race behaving exactly as recorded. - #298 (P2 task) the ErrorState enforcement check. GATES.md still reads "planned" for the 0-matches prohibition and nothing in scripts/ or eslint-rules/ references ErrorState, so the component exists but is not required. Deliberately not flipped to implemented. - #299 (P3 task) adopting ErrorState at the three surfaces that genuinely hand-roll the guard. Live-look change, downstream of the redesign. - #300 (P2 issue) three sites miscarried into M4 as guards that are not, so the next reader does not convert them. differentials-home renders its zero after a search that SUCCEEDED. - #301 (P3 issue) two sessions built #262 part 3 in parallel because the §3 row understated what had shipped. Proposes asserting that every baseline metric key appears in GATES.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(design-system): restrict the ErrorState copy tripwire to development Codex review on #1786 (P2). The doc comment described the tripwire as development-only, but the emitter only silenced NODE_ENV === "test", so a production caller supplying count-bearing title/body copy had the full caller-provided string written to console.warn. On a clinical surface that copy can quote the query — "0 results for <query>" — which turns a copy defect into a disclosure risk. Nobody reads a production browser console for design-system warnings, so the emit is now development-only and an unset NODE_ENV is treated as production: fail quiet. The gate is an exported predicate rather than an inline comparison because an inline comparison is untestable here. Vite statically replaces process.env.NODE_ENV inside src/ modules, so under Vitest the check compiles to `"test" === "development"` and no stubEnv can move it. The review asked for a production console-spy check; written that way it would have passed while proving nothing, staying silent for the wrong reason and continuing to pass even if the guard were deleted. shouldEmitErrorStateDiagnostic is asserted directly instead — development true; production, test and unset false — with the console spy kept alongside as the weaker check that catches an emitter which warns unconditionally. Verified: typecheck 0 errors, lint 0, format:check 0, check:design-system-contract 0, Tests 70 passed (70) across error-state.dom and design-sync-visual-exports. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…bt rows (#1942) * refactor(tokens): re-land the --shadow-tight retirement onto --e1 PR #1803 retired the --shadow-tight role alias in favour of the --e1 elevation tier across 49 files and squash-merged as 9d8370a on 2026-08-10. The acf78bf merge on 2026-08-11 silently reverted it, along with six other PRs. This re-applies the retirement against current main: 130 call sites across 67 files, plus both declarations. The alias was a pure pass-through -- `--shadow-tight: var(--e1)` in the light and dark role blocks -- so the substitution is value-preserving. Confirmed for forced-colors too rather than assumed: the `@media (forced-colors: active)` block scopes `:root, .dark`, the same `html` element the alias is declared on, so `--shadow-tight` already resolved through the flattened `--e1: none` there. The .ckb-v2 redeclaration hazard does not bite for the same reason -- .ckb-v2 sits on <html> and .ckb-v2.ckb-v2 outspecifies :root, so both spellings substitute against the winning v2 tier. Two comments survived acf78bf while the code they describe did not: the globals.css note that "the resting-hairline role is gone", and the token test's "unlike the --shadow-tight assertion above". Both are accurate again. The token contract test now sweeps the tracked src tree for both spellings (declaration and var() consumer) instead of only asserting the declaration. A declaration-only check would have caught this particular revert, but only because the declarations happened to come back with the call sites; sweeping both makes the gate independent of which half of a bad merge lands. Mutation-verified in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz * chore(design-system): re-pin the contract ratchets to their measured values `scripts/design-system-contract-baseline.json` is a ceiling, so paying debt down leaves silent headroom behind. Ledger #302 records that pattern: legacyShadowAliases was pinned at 220 against a measured 193, 27 units of unguarded slack, up from 3 units on 2026-08-10. With the previous commit's --shadow-tight retirement applied the gap is wider still -- 220 pinned against 119 measured -- because the reland pays down the debt the acf78bf revert had re-hidden. Four other ratchets had accumulated slack from unrelated work in the same window. legacyShadowAliases 220 -> 119 edgeOwnershipConflicts 27 -> 25 rawPaddingLiterals 67 -> 63 rawGapLiterals 34 -> 32 layoutTransitionExceptions 12 -> 11 Regenerated with --print-debt-baseline rather than hand-edited, so the per-path debtByPath counts move with the totals -- those are what findDebtPathRegressions compares, and the retirement moved them wholesale. Every metric in the diff decreases; nothing is absorbed upward. This is not the baseline refresh #262 warns against. That stop rule forbids refreshing to hide the movement; this pins the movement in so it cannot silently drift back a second time. Mutation-verified: reintroducing one alias in button.tsx now fails at both the total (119 -> 120) and the per-path level. Under the old 220 ceiling the same addition passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz * refactor(tokens): hold the search-band count bubble in a spacing token The active-filter badge sized itself with a raw `h-[1.0625rem] min-w-[1.0625rem]` pair. Ledger #275 tracks that value as leaked debt: it had reached five files, so the fix has always been to tokenise once rather than edit a call site. Re-measured on merged main, the badge role is down to a single call site. #170's convergence landed in the meantime -- document-search- results.tsx now renders the shared control and therapy-compass/ filter-sheet.tsx was deleted outright -- so the leak this row was written about has already been reabsorbed by the extraction. Holding the value in @theme is what stops it leaving again. Two arbitrary values in the same component are deliberately left raw: pr-[0.6875rem] and min-[414px]:max-[429px] -- the repo defines no --breakpoint-* tokens at all, and eight peer sites use the same raw min-[]/max-[] form (359px, 389px, 414px). Naming one window while the peers stay raw is the same drift #275 warns about on another axis, and Tailwind named breakpoints would add variants across the whole utility surface. That belongs in a repo-wide decision, filed separately. The three remaining 1.0625rem hits in mode-nav.tsx and nav-slot-ink.tsx are NOT this token. They size <Icon> glyphs -- a 17px icon against a 12/14/16/20/24 --spacing-icon-* scale -- so folding them under a badge token would merge two roles that only happen to share a number. check:icon-scale deliberately does not flag arbitrary h-[Nrem], so they are a real but separate finding, filed rather than guessed at. The token is also registered in CLINICAL_TWMERGE_THEME.spacing, which tests/tailwind-merge-config.test.ts asserts against the @theme block -- without it `cn()` cannot resolve a conflict on the new utility. Safe by that file's own `tap` reasoning: the single call site is a static string carrying no competing h-*/min-w-* class and never passes through `cn()`, so there is no same-variant pair for declaration to hand to the later class. The entry is protective for future use, not load-bearing today. Value-preserving, and proven rather than inferred: compiling globals.css through @tailwindcss/postcss emits .h-search-band-badge { height: var(--spacing-search-band-badge) } .min-w-search-band-badge { min-width: var(--spacing-search-band-badge) } No ratchet moved, so the ceilings pinned in the previous commit still sit at zero slack. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz * docs(design-system): close out DS Track A3 and refresh the stale gate rows Track A3 is `#262`. Its three parts are now all settled, each checked against code rather than against the row that describes it. Part 1 is the --shadow-tight retirement re-landed earlier in this PR. Part 3 shipped in PR #1780 per `#301`: rawPaddingLiterals, rawRadiusLiterals and rawLineHeightLiterals are live baseline keys enforced over both the class and CSS-declaration spellings, plus rawGapLiterals beyond the original ask. Part 2 needs no work, and that had already been adjudicated -- GATES.md section 3 records it, which is why nothing here builds it. The decidable half of step selection shipped on 9 Aug inside check:design-system- contract: a declared @theme step no production surface selects fails the build. The remaining half -- which existing step a component picks -- is documented there as something "nothing mechanical can" gate, being a judgement about the rendered design rather than a property of the source, with a standing instruction not to duplicate the arbitrary-value check check:type-scale already ships. Reading `#262` alone would have sent a session to build it; that is the `#301` failure mode, so the closure record says so explicitly. Section 3's live status rows carried numbers this PR moved. `#301`'s lesson is that a row understating shipped work is a duplicate-work generator, so they are corrected in the same change: legacyShadowAliases 224 -> 119, and the alias is now retired outright rather than "224 left to retire" edgeOwnershipConflicts 27 -> 25 rawPaddingLiterals 67 -> 63 rawGapLiterals 34 -> 32 layoutTransitionExceptions 12 -> 11 Section 5 is left alone deliberately: it is a dated record measured against 8db1e53, not a live status surface, and rewriting its figures would destroy the provenance it exists to hold. Ledger records are queued as immutable inbox requests: `#262`, `#302` and `#275` closed; two carve-outs split out of `#275` filed as their own rows (the repo-wide breakpoint-token decision, and three 17px mode-nav icon glyphs that sit off the --spacing-icon-* scale with no gate covering them). The queued re-land request 210e3db5 is cancelled rather than reconciled -- its headline "67 files on main still use the retired alias" is false as of this branch, so it would open a row wrong on arrival. The request file and the cancellation both survive as provenance for the acf78bf merge loss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz * chore(ledger): record the design-token relands review Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz * fix(issues): retarget the reland record after main reconciled it mid-flight CI failed `docs:check-links` on this branch with Error: cancel request 2e791c01... targets missing pending request 210e3db5... `check-docs-links.mjs` replays the inbox batch to resolve link targets, so an unresolvable request fails it. The cause was a race, not a bad record: PR #1936 reconciled 75 queued requests -- 210e3db5 among them -- while this branch was already in flight. Reconciling moves the request file into `docs/outstanding-issues-inbox/applied/` and allocates it a canonical row, so by the time this branch merged main there was no pending request left for the cancellation to name. Cancelling was the right call against a pending request and is the wrong one against a reconciled row. The cancel is dropped and replaced with a `done` against `#319`, the row 210e3db5 became. That is also the better record: the work is finished rather than withdrawn, so the ledger should carry its outcome and its guard, which a cancellation would have thrown away. Also merges origin/main (this branch was 3 behind) and files two findings the PR preflight surfaced, both deliberately not fixed here: - `check:medication-lexicon-report` has been failing on main for every local `verify:pr-local`, and no CI job runs it -- a grep over .github/workflows finds nothing. It is the last step of the local chain, so it fails preflights while CI stays green. The stale file is a clinical-facing generated document; regenerating it inside a CSS-token PR would bundle a clinical-risk artefact with unrelated chores. - Claude Code web containers can ship Node 22 with no node_modules, which fails `npm ci` on engine-strict before any repo script can run. Re-verified after the merge: the tracked tree still holds zero `--shadow-tight` references, and every pinned ratchet still measures exactly its baseline, so the merge moved no metric and the pins stay honest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz * docs(design): retire shadow-tight guidance * docs(design): retire shadow-tight guidance * docs(design): retire shadow-tight guidance * docs(design): retire shadow-tight guidance --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Three separately revertible commits, plus a docs correction. Two ledger rows were closed as already shipped, and
#262parts 2 and 3 add gates. No live-look change: the diff touches onlyscripts/,tests/anddocs/.Close
#218and#270, both shipped before this branch existed. Both rows were still open while their work was live onmain, which had already scoped a third session from them.#218(cn()lacks tailwind-merge) shipped in feat(design-system): give cn() tailwind-merge (#218) #1678,aeba5a254:src/components/ui-primitives.tsx:37istwMergeClinical(...)rather than a plain join,package.jsoncarriestailwind-merge ^3.6.0, andsrc/lib/tailwind-merge.tsdeclares the repo's@themescales to twMerge.#270(declare thetapspacing token) shipped in fix(tailwind-merge): declare the tap spacing token now its blocker is disproved #1738,80cf78139, confirmed an ancestor oforigin/main:"tap"is present inCLINICAL_TWMERGE_THEME.spacing, andtests/tailwind-merge-config.test.tswas inverted rather than deleted, so the merge behaviour is asserted rather than pinned out. Verified in source, not inferred from the handover.#262part 3 — ratchet raw padding, radius and line-height literals. The contract ratcheted colour, shadow, tap and tracking but not spacing, radius or line-height, so a value could bypass the scale as a bare literal in either a class or a stylesheet and nothing objected. AddsrawPaddingLiterals(67),rawRadiusLiterals(24) andrawLineHeightLiterals(3), each ratcheted per path and each covering both the class utilities and the CSS declarations, the way the colour and legacy-shadow metrics already do. The exemption is deliberately "contains no CSS function" rather than the narrower(?!var\()the tracking rule uses, because production shipspb-[env(safe-area-inset-bottom)],pt-[max(0.75rem,var(--safe-area-top))],pt-[clamp(1.5rem,5vh,3rem)]andpb-[calc(7rem+env(safe-area-inset-bottom))]— all computed from the viewport or the safe-area inset, none of them spellable as a scale step, and avar(-only lookahead would have flagged every one.#262part 2 — gate type-step selection on the decidable half.check:type-scalealready blocks arbitrarytext-[12px]; nothing stopped the scale itself growing a step no surface ever picks. Whether a heading should have chosentext-smovertext-sm-minusis not mechanically decidable and this does not pretend otherwise, but a step declared and consumed by nobody is. There is one today:--text-2xl-compacthas zero consumers — no utility use, novar()use — while the next-rarest step,text-hero, has one real consumer. The analyzer reports every baretext-<name>it sees and does not decide which names are steps; the checker intersects that against the@themeblock it parses fromglobals.css, so the scale is never written down twice. Retiring the dead step edits@theme, so it is carried as a documented exemption tracked by#295rather than bundled into a gate change, and the exemption cannot rot: the build also fails if an exempted step stops being declared or gains a consumer.Correct
GATES.md. Its §1 is the list of what actually runs, and this area's recurring failure is that list lagging the code — four of#264's six prohibitions were already gated while it saidplanned. Records the new ratchets and the step rule, and rewrites the callout that claimed a step-selection lint "does not exist".Verification
npm run check:design-system-contract— passes,Scale ratchets: raw padding literals 67; raw radius literals 24; raw line-height literals 3.npm run check:icon-scale—✓ icon-scale: no retired 4.5 (18px) half-step icon sizes in src.Run explicitly because it is not part of the contract check.npm run check:type-scale—✓ type-scale: no arbitrary text-[<n>px|rem|em] font sizes in src.npm run check:outstanding-issues—294 rows (145 open, 149 archived), unique ids, next-id=297 above the highest, no merge driver, no ids deleted from base 7aaf9349cd64.npx vitest run tests/design-system-contract-utils.test.ts—Tests 31 passed (31)(28 pre-existing plus 3 added).npm run format:check— whole tree,All matched files use Prettier code style!npm run verify:cheap— exit 1, from five pre-existing failing files, none of them this branch's. See below.npm run verify:ui— not run, and not applicable: no UI, routing, styling, reduced-motion or forced-colors behaviour changed. The diff contains nosrc/file.Every baseline entry was verified in source before pinning. All 94 findings were checked to be present at their cited line — a mechanical whole-population check, not a sample. Zero false positives, which is the point of putting these in the AST class-root and postcss declaration passes rather than a text scan.
The baseline change is additive. Every one of the fifteen pre-existing metrics and every pre-existing
debtByPathentry is byte-identical; only the three new keys were added. Asserted before the file was written, not after.Mutation-tested rather than assumed — a ratchet never seen to fail is not a gate. Part 3, class side, in a file with no prior debt in any of the three metrics:
The CSS half fails the same way against a probe stylesheet. Both probes also carried the sanctioned computed forms, and each count rose by exactly one, so the exemptions are proved by the same runs rather than argued.
Part 2 was mutation-tested three ways: dropping the exemption, adding a brand-new unused step, and giving the exempted step a consumer.
On the
verify:cheapexit code. It exits 1 on five pre-existing failing files:Test Files 5 failed | 541 passed (546),Tests 9 failed | 5857 passed | 14 skipped. That run predates mergingmain. After merging, three of the five pass —#1771fixed the runtime/setup ones, andtests/worker-observability.test.tsis the known order-dependent case that passes in a small batch. The two that remain,tests/pr-handoff-stop.test.tsandtests/document-viewer-page-virtualization.dom.test.tsx, cannot be caused by this branch: neither references anything in the diff, and both the test files and their subjects (.claude/hooks/pr-handoff-stop.sh, the document viewer) are byte-identical toorigin/main. The virtualization file also failed a different test name across two runs, which is a timing flake.Risk and rollout
src/file changes, so nothing rendered moves.git revertthe individual commit. The three are independent — the two gate commits touch different logic in the same two files, and the docs commit touches neither. Reverting part 3 alone requires also dropping its three baseline keys, since the checker asserts every metric has a baseline entry.Notes
Three figures for the same measurement were in circulation for
#262, and none of the documents agreed. All three are now reproduced, so the next reader does not have to re-derive them:GATES.md) is a repo-wide grep includingsrc/app/mockups/**, which every one of these gates excludes. It is 1 360 today.@themedeclarations and doc comments as if they were usages.text-2xs421,sm-minus160,base-minus57,3xs42,2xl-minus9,3xl-minus9,lg-minus6,hero1,2xl-compact0. There are nine non-standard steps, not eight.One correction that matters for
#262part 1, which is not in this PR: its scope is 93var(--shadow-tight)sites across 50 files, not 94 across 51. The 94th is a doc comment atsrc/lib/tailwind-merge.ts:40, which the contract check strips. Clearing them therefore leaves 127 aliases from the other six tokens, not 126, andlegacyShadowAliases: 0remains the wrong success criterion.Two ledger rows were opened from findings made while measuring:
#295(retire--text-2xl-compact) and#296(src/components/route-error-boundary.tsx:106renderstext-green-600, the only chromatic Tailwind palette utility in production — it escapes every colour gate legitimately, becauseLEGACY_PALETTE_UTILITYdeliberately matches only the neutral ramps).Auto-merge deliberately not armed: this changes a gate.