From c3a9cbecc24af04d76d5fcbafbbf4ba2d0b52388 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 7 Aug 2026 01:49:25 +0800 Subject: [PATCH 1/4] feat(tokens): name the letterspacing scale and ratchet the arbitrary family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section D, the half that carries no conflict with the two open PRs: this touches only `src/app/globals.css` and the contract scripts, none of which #1655 or #1658 holds. Corrects the measurement first. The backlog and the audit both quote the letterspacing sprawl as ~371 sites across 31 values; that number counts `*-mockups.tsx`, which is design scratch and gate-exempt by policy. Production only: 110 sites across 18 values. The gate's AST pass — which sees statically resolvable class roots — counts 68 across 40 files, and that is the number a ratchet can act on. The real defect turns out to be sharper than sprawl. 57 of the 110 production sites are one value, 0.06em, while the single pre-existing token `--tracking-eyebrow` is declared at 0.08em. The token has almost no consumers because it was named for a number nobody writes. Adds five named roles sized to the measured production mass, not to the mockup-inflated spread: --tracking-display -0.02em absorbs the six-site negative cluster --tracking-normal 0 --tracking-label 0.06em absorbs 66 sites including the 57-site mass --tracking-eyebrow 0.08em pre-existing, value deliberately unchanged --tracking-kicker 0.12em absorbs 23 sites Five roles rather than seven: production does not justify a second wide step or a separate tight-display step. No fold moves a call site by more than 0.04em. `--tracking-eyebrow` keeps 0.08em on purpose. Retuning it to the more common 0.06em would silently move every current consumer of the token — a different change from naming the scale, and one that wants its own measured step. Adds an `arbitraryTracking` metric to the design-system contract, mirroring the existing literal-shadow ratchet exactly, baselined per path at today's 68 so the family cannot regrow while the call-site migration waits for the open PRs to land. `tracking-[var(--…)]` is the sanctioned token form and is not counted, the same exemption `text-[color:var(--…)]` has in the type-scale check. No call sites migrated in this commit — that is 40 files including `ui-primitives.tsx`, which #1655 owns. Ratchet proven to bite, not assumed: injecting `tracking-[0.077em]` into mode-nav.tsx failed the checker with "arbitraryTracking increased from 68 to 69" and the per-path line, then the file was restored clean. Verification: check:design-system-contract exit 0 · typecheck 0 · lint 0 · ckb-v2-token-contract + design-token-contract + design-system-contract-utils 85 passed. Pre-existing baseline metrics were asserted unchanged before the baseline was rewritten; only `arbitraryTracking` was introduced. Co-Authored-By: Claude Opus 5 --- scripts/check-design-system-contract.mjs | 4 +- scripts/design-system-contract-baseline.json | 43 ++++++++++++++++++++ scripts/design-system-contract-utils.mjs | 8 ++++ src/app/globals.css | 42 +++++++++++++++++-- 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/scripts/check-design-system-contract.mjs b/scripts/check-design-system-contract.mjs index 41b879033c..6d71356b40 100644 --- a/scripts/check-design-system-contract.mjs +++ b/scripts/check-design-system-contract.mjs @@ -116,6 +116,7 @@ const metrics = { legacyPaletteUtilities: 0, darkColorOverrides: 0, legacyShadowAliases: 0, + arbitraryTracking: 0, layoutTransitionExceptions: 0, textSoftConsumers: 0, }; @@ -162,6 +163,7 @@ for (const file of files) { recordDebt("legacyPaletteUtilities", file.relativePath, classAnalysis.legacyPaletteUtilities.length); recordDebt("darkColorOverrides", file.relativePath, classAnalysis.darkColorOverrides.length); recordDebt("legacyShadowAliases", file.relativePath, classAnalysis.legacyShadowAliases.length); + recordDebt("arbitraryTracking", file.relativePath, classAnalysis.arbitraryTracking.length); densityOverrideFindings.push(...classAnalysis.densityOverrides); hardcodedMotionClassFindings.push(...classAnalysis.hardcodedMotionClasses); layoutTransitionFindings.push(...classAnalysis.layoutTransitions); @@ -374,7 +376,7 @@ console.log( `Design-system contract passed (${files.length} production files; raw colors ${metrics.rawColorLiterals}; literal shadows ${metrics.literalShadowClasses}; legacy tap classes ${metrics.legacyTapClasses}; edge conflicts ${metrics.edgeOwnershipConflicts}; 1px shadow spreads ${metrics.onePixelShadowSpreads}).`, ); console.log( - `Motion/z/palette ratchets: hardcoded CSS durations ${metrics.hardcodedCssMotionDurations}; layout transitions ${metrics.layoutTransitionExceptions}; raw CSS z-index ${metrics.rawCssZIndices}; legacy palette utilities ${metrics.legacyPaletteUtilities}; dark color overrides ${metrics.darkColorOverrides}; legacy shadow aliases ${metrics.legacyShadowAliases}.`, + `Motion/z/palette ratchets: hardcoded CSS durations ${metrics.hardcodedCssMotionDurations}; layout transitions ${metrics.layoutTransitionExceptions}; raw CSS z-index ${metrics.rawCssZIndices}; legacy palette utilities ${metrics.legacyPaletteUtilities}; dark color overrides ${metrics.darkColorOverrides}; legacy shadow aliases ${metrics.legacyShadowAliases}; arbitrary tracking ${metrics.arbitraryTracking}.`, ); console.log(`Text-role ratchet: --text-soft consumers ${metrics.textSoftConsumers}.`); console.log(`Raw-color exemptions: ${RAW_COLOR_EXEMPTIONS.map(({ category }) => category).join(", ")}.`); diff --git a/scripts/design-system-contract-baseline.json b/scripts/design-system-contract-baseline.json index 79857b1032..e95303ff02 100644 --- a/scripts/design-system-contract-baseline.json +++ b/scripts/design-system-contract-baseline.json @@ -10,6 +10,7 @@ "legacyPaletteUtilities": 2, "darkColorOverrides": 3, "legacyShadowAliases": 231, + "arbitraryTracking": 68, "layoutTransitionExceptions": 12, "textSoftConsumers": 0 }, @@ -147,6 +148,48 @@ "src/components/ui/tooltip.tsx": 1, "src/components/ui-primitives.tsx": 6 }, + "arbitraryTracking": { + "src/app/reference/colour-coding/page.tsx": 1, + "src/components/AccessibleTable.tsx": 2, + "src/components/calculators/bedside-sheet.tsx": 1, + "src/components/calculators/clinical-console.tsx": 1, + "src/components/calculators/directory-grid.tsx": 3, + "src/components/calculators/guided-flow.tsx": 4, + "src/components/calculators/search-detail.tsx": 4, + "src/components/calculators/search-page.tsx": 1, + "src/components/clinical-dashboard/account-setup-dialog.tsx": 1, + "src/components/clinical-dashboard/cross-mode-links.tsx": 1, + "src/components/clinical-dashboard/differentials-home.tsx": 3, + "src/components/clinical-dashboard/document-results.tsx": 1, + "src/components/clinical-dashboard/document-search-results.tsx": 2, + "src/components/clinical-dashboard/evidence-panels.tsx": 1, + "src/components/clinical-dashboard/favourites-command-library-page.tsx": 1, + "src/components/clinical-dashboard/library-health-strip.tsx": 1, + "src/components/clinical-dashboard/medication-prescribing-workspace.tsx": 1, + "src/components/clinical-dashboard/output-panel.tsx": 2, + "src/components/clinical-dashboard/search-pins-menu.tsx": 1, + "src/components/clinical-dashboard/settings-dialog.tsx": 1, + "src/components/clinical-dashboard/universal-search-command-surface.tsx": 1, + "src/components/ClinicalDashboard.tsx": 1, + "src/components/differentials/differential-detail-page.tsx": 1, + "src/components/differentials/differential-presentation-workflow-page.tsx": 1, + "src/components/document-viewer/document-clinical-summary.tsx": 3, + "src/components/document-viewer/document-overview-landing.tsx": 1, + "src/components/document-viewer/section-nav.tsx": 2, + "src/components/document-viewer/source-panels.tsx": 5, + "src/components/dsm/dsm-comparison-page.tsx": 1, + "src/components/factsheets/factsheet-detail-page.tsx": 6, + "src/components/factsheets/factsheets-search-page.tsx": 2, + "src/components/mode-nav/mode-nav.tsx": 1, + "src/components/patient-safety-plan.tsx": 2, + "src/components/pwa-lifecycle.tsx": 1, + "src/components/services/services-navigator-page.tsx": 2, + "src/components/specifiers/specifier-ui.tsx": 1, + "src/components/therapy-compass/screens/recommend-screen.tsx": 1, + "src/components/therapy-compass/screens/sheets-screen.tsx": 1, + "src/components/therapy-compass/therapy-card.tsx": 1, + "src/components/ui-primitives.tsx": 1 + }, "layoutTransitionExceptions": { "src/app/globals.css": 4, "src/components/calculators/guided-flow.tsx": 1, diff --git a/scripts/design-system-contract-utils.mjs b/scripts/design-system-contract-utils.mjs index 7a44da0455..07122ac0e8 100644 --- a/scripts/design-system-contract-utils.mjs +++ b/scripts/design-system-contract-utils.mjs @@ -201,6 +201,12 @@ const DENSITY_HEIGHT_UTILITY = /^(?:h|min-h|max-h|size)-/; const DENSITY_TEXT_UTILITY = /^text-(?:2xs|xs|sm-minus|sm|base|lg|xl|[2-9]xl|\[[^\]]*(?:px|rem|em|clamp\()[^\]]*\])$/; const HARDCODED_MOTION_UTILITY = /^(?:duration|delay)-(?:\d+|\[(?!var\(--duration-)[^\]]+\])$/; const LITERAL_SHADOW_UTILITY = /^shadow-\[(?!var\()[^\]]+\]$/; +// Same shape as the literal-shadow ratchet, for the same reason: 371 arbitrary +// letterspacing values across 31 distinct spellings, seven of them positive steps +// between 0.04 and 0.16em that no reader can tell apart. `tracking-[var(--…)]` is +// the sanctioned token form and is deliberately NOT counted, exactly as +// `text-[color:var(--…)]` is exempt from the type-scale check. +const ARBITRARY_TRACKING_UTILITY = /^tracking-\[(?!var\()[^\]]+\]$/; const LEGACY_SHADOW_ALIAS = /var\(--shadow-(?:tight|card|soft|hover|elevated|lux|lift)\)/g; const LEGACY_PALETTE_UTILITY = /^(?:bg|text|border|ring|outline|fill|stroke|placeholder|from|via|to)-(?:white|black|(?:slate|gray|zinc|neutral|stone)-\d{2,3})(?:\/\d{1,3})?$/; @@ -660,6 +666,7 @@ function uniqueTokenEntries(possibilities) { export function analyzeClassContractsInSource(relativePath, sourceText) { const analyzer = classExpressionAnalyzer(relativePath, sourceText); const result = { + arbitraryTracking: [], darkColorOverrides: [], densityOverrides: [], edgeOwnershipConflicts: [], @@ -709,6 +716,7 @@ export function analyzeClassContractsInSource(relativePath, sourceText) { result.hardcodedMotionClasses.push(`${relativePath}:${line} (${token})`); } if (LITERAL_SHADOW_UTILITY.test(base)) result.literalShadowClasses.push(`${relativePath}:${line} (${token})`); + if (ARBITRARY_TRACKING_UTILITY.test(base)) result.arbitraryTracking.push(`${relativePath}:${line} (${token})`); if (hasLegacyTapClass(token)) result.legacyTapClasses.push(`${relativePath}:${line} (${token})`); for (const match of token.matchAll(LEGACY_SHADOW_ALIAS)) { result.legacyShadowAliases.push(`${relativePath}:${line} (${match[0]})`); diff --git a/src/app/globals.css b/src/app/globals.css index b12c04daa7..08b70a5e2a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -133,10 +133,46 @@ --leading-display: 1.05; --leading-prose: 1.6; - /* Uppercase eyebrow tracking. 0.08em was spelled as `tracking-[0.08em]` at 60+ - call sites; this makes it a named utility (`tracking-eyebrow`) with the - identical computed value. */ + /* ---- Letterspacing scale ------------------------------------------------- + Measured 7 Aug 2026, production only (`*-mockups.tsx` and `mockups/` are + design scratch and gate-exempt): 110 arbitrary `tracking-[…]` call sites + across 18 distinct values, against one named token. Counting mockups gives + 372/31 — that is the number to quote about the *sprawl*, not about the debt + a gate can act on. + + The real defect is sharper than sprawl. 57 of the 110 sites are a single + value, 0.06em — and `--tracking-eyebrow` is declared at 0.08em, so the one + named token does not match the value its own call sites overwhelmingly use. + That is why it has almost no consumers: the token was named for a number + nobody writes. + + Named roles, with the measured production counts each one absorbs: + + --tracking-display -0.02em folds: -0.008 (1), -0.01 (3), -0.035 (1), -0.04 (1) + --tracking-normal 0 folds: 0.01 (1), 0.02 (2) + --tracking-label 0.06em folds: 0.04 (3), 0.05 (6), 0.06 (57) <- the mass + --tracking-eyebrow 0.08em folds: 0.08 (8), 0.09 (1) [pre-existing, value unchanged] + --tracking-kicker 0.12em folds: 0.1 (7), 0.11 (1), 0.12 (14), 0.16 (1) + + Five roles, not seven: production does not justify a second wide step or a + separate tight-display step — the negative cluster is six sites total. No + fold moves a call site by more than 0.04em, and most by 0.01–0.02em. + + `--tracking-eyebrow` keeps 0.08em deliberately. Retuning it to 0.06em would + silently move every existing consumer of the token; that is a different + change from naming the scale, and wants its own measured step. + + The `arbitraryTracking` contract ratchet holds the family at its current + per-path count (68 statically-resolvable sites across 40 files) so it cannot + regrow while the call-site migration waits on the open answer-shell and + doc-truth PRs. `tracking-[var(--…)]` is the sanctioned token form and is not + counted, exactly as `text-[color:var(--…)]` is exempt from the type-scale + check. */ + --tracking-display: -0.02em; + --tracking-normal: 0em; + --tracking-label: 0.06em; --tracking-eyebrow: 0.08em; + --tracking-kicker: 0.12em; /* Font families: bind Tailwind's font-sans / font-mono to the loaded Geist faces (variables set on by next/font). font-mono is used for From 91c5aeb688a8eddeee4a2e4f497d93948b9ee3bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 18:18:11 +0000 Subject: [PATCH 2/4] fix(tokens): fail-closed text scan for arbitrary tracking ratchet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the literal-shadow AST/text parity guard so unresolved or dynamic class strings cannot silently bypass the tracking-[…] debt ratchet. Co-authored-by: BigSimmo --- scripts/check-design-system-contract.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/check-design-system-contract.mjs b/scripts/check-design-system-contract.mjs index 6d71356b40..5fb4687193 100644 --- a/scripts/check-design-system-contract.mjs +++ b/scripts/check-design-system-contract.mjs @@ -25,6 +25,7 @@ const SOURCE_EXTENSIONS = new Set([".css", ".ts", ".tsx"]); const RAW_COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|oklch)\(/gi; /** Whole-file backstop for literal shadow utilities the AST class-root pass can miss. */ const LITERAL_SHADOW_TEXT = /(?:^|[\s"'`])shadow-\[(?!var\()[^\]]+\]/g; +const ARBITRARY_TRACKING_TEXT = /(?:^|[\s"'`])tracking-\[(?!var\()[^\]]+\]/g; const CUSTOM_CONTROL_CLASS_PROP = /(?:closeButtonClassName|sheetCloseButtonClassName|buttonClassName|triggerClassName)\s*=\s*(?:"([^"]*)"|`([^`]*)`)/g; @@ -150,6 +151,7 @@ for (const file of files) { const classTextSource = withoutComments(source); const textLegacyTap = countMatches(classTextSource, LEGACY_TAP_CLASS); const textLiteralShadow = countMatches(classTextSource, LITERAL_SHADOW_TEXT); + const textArbitraryTracking = countMatches(classTextSource, ARBITRARY_TRACKING_TEXT); assert( classAnalysis.legacyTapClasses.length >= textLegacyTap, `${file.relativePath} has ${textLegacyTap} legacy tap class text match(es) but the AST class-root pass only saw ${classAnalysis.legacyTapClasses.length}`, @@ -158,6 +160,10 @@ for (const file of files) { classAnalysis.literalShadowClasses.length >= textLiteralShadow, `${file.relativePath} has ${textLiteralShadow} literal shadow class text match(es) but the AST class-root pass only saw ${classAnalysis.literalShadowClasses.length}`, ); + assert( + classAnalysis.arbitraryTracking.length >= textArbitraryTracking, + `${file.relativePath} has ${textArbitraryTracking} arbitrary tracking text match(es) but the AST class-root pass only saw ${classAnalysis.arbitraryTracking.length}`, + ); const fileEdgeFindings = classAnalysis.edgeOwnershipConflicts; recordDebt("edgeOwnershipConflicts", file.relativePath, fileEdgeFindings.length); recordDebt("legacyPaletteUtilities", file.relativePath, classAnalysis.legacyPaletteUtilities.length); From 4b6d49e7a0466795c1d4c1b98ac05823baacd758 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 18:23:32 +0000 Subject: [PATCH 3/4] ci: retrigger checks after GitHub Actions outage Empty commit to re-fire pull_request synchronize for the current tip after stale/queued runs were cancelled during the Actions major outage. Co-authored-by: BigSimmo From 611e9f548594d4980f80f0945fdc1b6a71bf1649 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 18:28:06 +0000 Subject: [PATCH 4/4] ci: re-trigger checks for PR #1663 Empty commit to fire pull_request synchronize after head had no Actions runs. Co-authored-by: BigSimmo