From ccb6d1e70567fed4624b0d823eb10d72507352fa Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:09:43 +0800 Subject: [PATCH 1/4] docs(issues): correct the re-measured A3 scope and B1 adoption count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replays a correction stranded in a local-only worktree when PR #1712 squash-merged and its branch was deleted before the commit could be pushed. Both figures were re-measured today against origin/main 2675e6e1d rather than copied forward, and one claim in the stranded version turned out to be wrong. #262: legacyShadowAliases counts SEVEN tokens, not one and not eight — the regex has matched tight|card|soft|hover|elevated|lux|lift since PR #1616 and has never included focus. Measured total 228 = tight 100, soft 72, elevated 17, hover 17, card 12, lux 8, lift 2. So --shadow-tight is 100 production sites across 55 files rather than the 155 or 229 previously recorded, clearing all 100 will not zero the ratchet, and #261's --shadow-focus does not appear in this metric at all. #266: adoption is 23 unadopted, not 24, since Button moved via #263 — its sole production importer is src/components/AccessibleTable.tsx. Co-Authored-By: Claude Opus 5 --- docs/outstanding-issues.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 060cb337ad..f0f3c5e836 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -301,10 +301,10 @@ removed after current-main verification; it is not missing recommended work. | #258 | P2 | rec | The PR-handoff stop rule is enforced for Claude Code only; Codex and Cursor get prose with no gate | **Outcome:** a session that opens a PR stops following it in every agent this repo supports, not just Claude Code. **Detail:** PR #1649 added `.claude/hooks/pr-handoff-stop.sh` plus the AGENTS.md "Stop when the pull request is open" section. The hook is registered in `.claude/settings.json`, which only Claude Code reads, so the PostToolUse marker and the PreToolUse denials (shell `gh pr checks/status/view/run watch`, GitHub MCP tools named pull_request/workflow_run/workflow_job/check_run/check_suite/job_log/update_branch, and Monitor/ScheduleWakeup/CronCreate) simply do not exist for Codex or Cursor sessions. Those agents get the AGENTS.md prose and nothing else — and prose alone is exactly what was already in force, and already insufficient, before #1649. Cost is the same long tail of post-handoff CI polling the hook was built to cut, just relocated to whichever agent lacks the gate; a cloud Codex session is the worst case because nothing naturally ends it. **Next:** cheapest first — check whether Codex and Cursor expose any pre-tool interception this repo can register (Codex plugin hooks under `plugins/clinical-kb/`, Cursor rules under `.cursor/`); if neither offers a deny path, the fallback is a shared marker file plus a wrapper the agent is told to route `gh` through, which is weaker but still detectable. If no mechanism exists at all, record that explicitly here so the gap is a known limit rather than an open task. **Stop:** do not weaken the Claude Code hook to make the tools symmetric, and do not add a second copy of the deny list — one script, multiple registrations. | PR #1649; .claude/hooks/pr-handoff-stop.sh; .claude/settings.json; AGENTS.md "Stop when the pull request is open"; session 2026-08-07 | 2026-08-07 | | #260 | P2 | task | Two unpushed Sentry commits are stranded on a Windows-only branch and will be lost with that machine | **Outcome:** the Sentry setup/logging-hardening work is either shipped or consciously discarded, not left sitting in one machine's reflog. **Detail:** `claude/cloud-pr-loop-prevention-bc052b` carries two commits — `c3c9d6a31` and `abbcdc8e9`, ~389 lines across `src/sentry.*.config.ts`, `src/lib/env.ts`, `src/lib/supabase/client.tsx`, `src/components/ui-primitives.tsx` — that were never pushed and are not the authoring session's own work. The branch does not exist on the remote, so the commits are unreachable from any cloud or remote container; a 2026-08-07 remote session could not inspect, verify, or ship them and could only record their existence. The same worktree (`.claude/worktrees/pensive-borg-6be2f0`) still holds the same four files uncommitted. Two Sentry branches DO exist on origin — `claude/sentry-nextjs-sdk-setup-2v24q5` and `cursor/sentry-nextjs-sdk-7cee` — but whether either already carries this change is unconfirmed: a three-dot diff against `origin/main` from the remote container returned empty for both, which is not trustworthy as proof either way and was not pursued further. Note this touches `src/lib/env.ts` and `src/lib/supabase/client.tsx`, so it is not a docs-class change and needs a real gate whenever it does ship. **Next:** from the Windows machine, diff those two commits against the two remote Sentry branches to decide whether the work is already represented. If it is, delete the branch; if it is not, push it and open a PR rather than leaving it local. **Stop:** do not discard the commits blind, and do not assume the remote Sentry branches supersede them without a content diff — nothing has yet compared them. | session 2026-08-07 remote container; handoff notes from the PR #1649 session; origin branches claude/sentry-nextjs-sdk-setup-2v24q5 and cursor/sentry-nextjs-sdk-7cee | 2026-08-07 | | #261 | P2 | task | DS Track A2: retire --shadow-focus from the search composer | Replace the composer's companion focus ring with the sanctioned outline / --focus treatment used everywhere else, then delete the token (both theme declarations). Live consumer is .chat-composer-shell-delta:focus-within in globals.css — a --include=*.tsx grep reports zero consumers and is wrong. This is a visible focus-state change on the search composer: read docs/search-chrome-behaviour.md first and get a Chromium look. Gate: npm run check:design-system-contract + npm run verify:phone-chrome. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | -| #262 | P2 | task | DS Track A3: finish the design-token debt | Three parts. (1) Move --shadow-tight's 155 consumers onto the --eN elevation ladder. (2) Add a step-SELECTION lint for the eight non-standard type steps (1 318 sites) — check:type-scale already blocks arbitrary text-[12px], so do NOT write a lint duplicating the half that ships. (3) Extend the contract ratchet to raw padding / radius / line-height literals; it covers colour, shadow, tap and tracking today. Gate: npm run check:design-system-contract. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | +| #262 | P2 | task | DS Track A3: finish the design-token debt | Three parts. (1) Move --shadow-tight onto the --eN elevation ladder. SCOPE RE-MEASURED 2026-08-09 against origin/main 2675e6e1d, running analyzeClassContractsInSource + analyzeCssContractsInSource over the same walk check-design-system-contract.mjs uses (src/**, .ts/.tsx/.css, mockups excluded). The inherited figures were wrong in three ways. First, the legacyShadowAliases metric counts SEVEN tokens, not one: measured total 228 = tight 100, soft 72, elevated 17, hover 17, card 12, lux 8, lift 2. So the '229 --shadow-tight aliases' in HANDOVER-2026-08-07 is the all-token total mislabelled, and this row's earlier '155 consumers' was closer to a raw repo-wide grep (160 occurrences including mockups) than to the gated number. Second, the real scope is 100 production --shadow-tight sites across 55 files, so the inherited figure overstates the work by roughly 1.55x, and clearing all 100 will NOT zero the ratchet: 128 aliases across the six other tokens remain, so do not treat legacyShadowAliases=0 as the success criterion. Third, --shadow-focus is NOT in this metric at all: LEGACY_SHADOW_ALIAS has matched exactly tight\|card\|soft\|hover\|elevated\|lux\|lift since PR #1616 and has never included focus, so an earlier note claiming 'eight tokens, focus 2' and an overlap with #261 was wrong. #261 is a separate token with one consumer (src/app/globals.css:1476) and two theme declarations (lines 423, 664); the two tasks do not share this metric. Baseline pins legacyShadowAliases at 231 and the baseline is a ceiling, so today's 228 already passes. Re-measure before starting rather than trusting any of these numbers. (2) Add a step-SELECTION lint for the eight non-standard type steps (1318 sites) — check:type-scale already blocks arbitrary text-[12px], so do NOT write a lint duplicating the half that ships. (3) Extend the contract ratchet to raw padding / radius / line-height literals; it covers colour, shadow, tap and tracking today. Gate: npm run check:design-system-contract. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #264 | P2 | task | DS Track A5: gate the six ungated design-system prohibitions | Highest-value item in Track A — a prohibition with no gate row is a suggestion, which is the failure mode GATES.md exists to prevent. Colour-boundary lint (no numeral painted with a status colour); PDF/diagram invert refusal; layout-property animation lint; border+ring co-occurrence; --shadow-tight alias lint; dark: override lint (3 live instances). Gate: npm run check:design-system-contract + npm run lint. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #265 | P2 | task | DS Track A6: move design-system gates 2, 4, 7 and 8 from partial to blocking | Gate 4 needs a repo-wide status-indicator enumeration — today a new colour-only indicator slips through. Gate 2 needs the fixed-height h-10 case and test:e2e:style-contract wired into verify:cheap. Gates 7 and 8 likewise. Gate: npm run verify:cheap once wired. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | -| #266 | P2 | task | DS Track B1: adopt the 24 unadopted components demand-driven, never as a race to 53/53 | Pick a surface and let it pull, the way PR #1658 did for AnswerCard. Forms are the largest single tranche: FieldError, FieldHint, ErrorSummary, SearchField, Checkbox and RadioGroup all land together on one form conversion. Do not stub a component to move the adoption count. Regenerate with npm run design-system:adoption:update after any import change; the manifest is generated, never hand-edited. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | +| #266 | P2 | task | DS Track B1: adopt the 23 unadopted components demand-driven, never as a race to 53/53 | Pick a surface and let it pull, the way PR #1658 did for AnswerCard. COUNT RE-MEASURED 2026-08-09 from docs/design-system/adoption-manifest.json on origin/main: 53 registered, 30 with at least one productImportFiles entry, 23 UNADOPTED — not 24. Button moved into the adopted set when AccessibleTable's expand control stopped being a hand-rolled recipe (#263, PR #1712); its sole production importer is src/components/AccessibleTable.tsx, which is the demand-driven route this row describes, so it is the pattern to copy rather than an exception. The 23 measured today: AnswerFooter, Checkbox, Citation, CitationList, ConfirmDialog, Disclosure, DisclosureGroup, DoseLine, DownloadLink, ErrorSummary, ExternalTextLink, FieldError, FieldHint, LinkAction, Pagination, Progress, RadioGroup, SearchField, StageList, Tabs, TextLink, ToastRegion, Tooltip. Forms are still the largest single tranche: FieldError, FieldHint, ErrorSummary, SearchField, Checkbox and RadioGroup all land together on one form conversion. Do not stub a component to move the adoption count. Regenerate with npm run design-system:adoption:update after any import change; ALSO run npm run design-system:design-sync:update, because changing any *Props type or adopting a component fails check:design-sync-contract with 'dtsPropsFor must be generated from source public Props types' if only the first is run. Both manifests are generated, never hand-edited. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #267 | P3 | task | DS Track B2: AnswerFooter and DoseLine need a provenance/dose payload the answer surface does not produce | Backend-shaped work, not a component swap: the two components cannot be adopted until the answer surface emits the provenance and dose data they render. Do not stub one to make the adoption count look better. Sequence after the payload exists, then adopt via the Track B1 demand-driven route. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #268 | P3 | task | DS Track B3: move the 19 genuine bare-dash sites onto MissingValue | Therapy-compass getters, specifier sourceFamily, favourites counts when untrusted. Leave the roughly 5 calculator 'derived.started ? score : dash' sites PERMANENTLY — 'not started' is not a missing clinical value, MissingValueReason has no member for it, and converting them would render 'Not recorded' for a score the clinician simply has not entered. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #269 | P2 | task | DS Track B4: prove the per-component visual state matrix (blocked on the baseline hold) | hover / active / disabled / busy / invalid / 320px / dark / forced-colours / print, per component. Currently proven for none. Blocked on #118: zero visual baselines are committed and the harness is continue-on-error, so nothing in Track B is safe at scale until baselines exist. Baselines cannot be generated on Windows — snapshotPathTemplate carries {platform}, so win32 PNGs are invisible to the ubuntu CI job. Stop rule: do not commit baselines until the owner declares the design final. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | From 534405600dca67317b4d60266cda03ec95f028e7 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:10:03 +0800 Subject: [PATCH 2/4] docs(design-system): mark the TextField/SearchField/Select row done, it was stale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replays a correction stranded in a local-only worktree when PR #1712 squash-merged and its branch was deleted before the commit could be pushed. Re-measured today against origin/main 2675e6e1d: all three fold onto FormField, which renders hint AND error unconditionally (form-field.tsx:131-132) and merges aria-describedby caller -> hint -> error (form-field.tsx:107-109); TextField and SearchField each take an external id and a Ref, Select an external id and a Ref. Landed in PR 13, not the PR 7 the row pointed at. Section 4's 'Do: fold TextField/SearchField/Select/Checkbox/RadioGroup onto this shell (PR 7)' was stale in a second way and is corrected too: Checkbox and RadioGroup deliberately do NOT fold onto FormField — a group keeps fieldset/legend and composes FieldHint/FieldError directly (choice.tsx:207-210). Left as written, that line asks a future session to 'finish' a fold that is finished and to break one that was a deliberate decision. Co-Authored-By: Claude Opus 5 --- docs/design-system/COMPONENTS.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index bdb5f23c9b..9857002adc 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -67,7 +67,7 @@ print primitives (`PrintHeader`, `PrintFooter`, `CitationFootnote`, `PrintOnly`, | IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) | | ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) | | Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** | -| TextField/SearchField/Select | hint dropped on error (comment promises otherwise) · describedBy overwritten · no external id/refs (placeholder off decoration tier in PR-A) | PR 7 | +| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-09: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | | Checkbox/RadioGroup | all four closed. Two rows were stale when re-measured on 2026-08-08: ids already sanitise through `optionId`, and the group already carries `hint`/`error`/`describedBy`. Raw dimensions moved to `size-5`/`h-0.5` (`size-4.5` retired by `check:icon-scale`) | **done** | | Citation/CitationList | required interactive handler, stable-id list keys and static-span labelling (`aria-label` on a role-less span was dropped, losing the currency phrase on print) are resolved; route/source modes remain | follow-on | | DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) | @@ -390,7 +390,10 @@ focus into the field. **Must refuse to render.** A field without a `label` (type-level) · an error rendered as colour alone · overwriting caller `describedBy` (merge is the only path). -**Do:** fold TextField/SearchField/Select/Checkbox/RadioGroup onto this shell (PR 7). +**Done, not pending (re-measured 2026-08-09):** `TextField`, `SearchField` and `Select` fold +onto this shell — landed in PR 13, not the PR 7 this line used to point at. `Checkbox` and +`RadioGroup` deliberately do **not**: a group keeps `
`/`` and composes +`FieldHint`/`FieldError` directly (see `choice.tsx`). Do not "finish" a fold that is finished. **Don't:** add per-control bespoke hint/error markup again; don't put block content in the hint (string type is deliberate). From f3a91c67c8f518af8ad43206e2c45223dd2c69f2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 07:53:34 +0000 Subject: [PATCH 3/4] docs: correct future-dated re-measurement records to 2026-08-08 Codex review on PR #1719 flagged SCOPE/COUNT RE-MEASURED and COMPONENTS.md rows dated 2026-08-09 while the recording commits were authored on 2026-08-08. Co-authored-by: BigSimmo --- docs/design-system/COMPONENTS.md | 4 ++-- docs/outstanding-issues.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index ec97551f1c..4e825ec794 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -67,7 +67,7 @@ print primitives (`PrintHeader`, `PrintFooter`, `CitationFootnote`, `PrintOnly`, | IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) | | ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) | | Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** | -| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-09: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | +| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-08: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | | Checkbox/RadioGroup | all four closed. Two rows were stale when re-measured on 2026-08-08: ids already sanitise through `optionId`, and the group already carries `hint`/`error`/`describedBy`. Raw dimensions moved to `size-5`/`h-0.5` (`size-4.5` retired by `check:icon-scale`) | **done** | | Citation/CitationList | required interactive handler, stable-id list keys and static-span labelling (`aria-label` on a role-less span was dropped, losing the currency phrase on print) are resolved; route/source modes remain | follow-on | | DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) | @@ -390,7 +390,7 @@ focus into the field. **Must refuse to render.** A field without a `label` (type-level) · an error rendered as colour alone · overwriting caller `describedBy` (merge is the only path). -**Done, not pending (re-measured 2026-08-09):** `TextField`, `SearchField` and `Select` fold +**Done, not pending (re-measured 2026-08-08):** `TextField`, `SearchField` and `Select` fold onto this shell — landed in PR 13, not the PR 7 this line used to point at. `Checkbox` and `RadioGroup` deliberately do **not**: a group keeps `
`/`` and composes `FieldHint`/`FieldError` directly (see `choice.tsx`). Do not "finish" a fold that is finished. diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index f0f3c5e836..ab465e4900 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -301,10 +301,10 @@ removed after current-main verification; it is not missing recommended work. | #258 | P2 | rec | The PR-handoff stop rule is enforced for Claude Code only; Codex and Cursor get prose with no gate | **Outcome:** a session that opens a PR stops following it in every agent this repo supports, not just Claude Code. **Detail:** PR #1649 added `.claude/hooks/pr-handoff-stop.sh` plus the AGENTS.md "Stop when the pull request is open" section. The hook is registered in `.claude/settings.json`, which only Claude Code reads, so the PostToolUse marker and the PreToolUse denials (shell `gh pr checks/status/view/run watch`, GitHub MCP tools named pull_request/workflow_run/workflow_job/check_run/check_suite/job_log/update_branch, and Monitor/ScheduleWakeup/CronCreate) simply do not exist for Codex or Cursor sessions. Those agents get the AGENTS.md prose and nothing else — and prose alone is exactly what was already in force, and already insufficient, before #1649. Cost is the same long tail of post-handoff CI polling the hook was built to cut, just relocated to whichever agent lacks the gate; a cloud Codex session is the worst case because nothing naturally ends it. **Next:** cheapest first — check whether Codex and Cursor expose any pre-tool interception this repo can register (Codex plugin hooks under `plugins/clinical-kb/`, Cursor rules under `.cursor/`); if neither offers a deny path, the fallback is a shared marker file plus a wrapper the agent is told to route `gh` through, which is weaker but still detectable. If no mechanism exists at all, record that explicitly here so the gap is a known limit rather than an open task. **Stop:** do not weaken the Claude Code hook to make the tools symmetric, and do not add a second copy of the deny list — one script, multiple registrations. | PR #1649; .claude/hooks/pr-handoff-stop.sh; .claude/settings.json; AGENTS.md "Stop when the pull request is open"; session 2026-08-07 | 2026-08-07 | | #260 | P2 | task | Two unpushed Sentry commits are stranded on a Windows-only branch and will be lost with that machine | **Outcome:** the Sentry setup/logging-hardening work is either shipped or consciously discarded, not left sitting in one machine's reflog. **Detail:** `claude/cloud-pr-loop-prevention-bc052b` carries two commits — `c3c9d6a31` and `abbcdc8e9`, ~389 lines across `src/sentry.*.config.ts`, `src/lib/env.ts`, `src/lib/supabase/client.tsx`, `src/components/ui-primitives.tsx` — that were never pushed and are not the authoring session's own work. The branch does not exist on the remote, so the commits are unreachable from any cloud or remote container; a 2026-08-07 remote session could not inspect, verify, or ship them and could only record their existence. The same worktree (`.claude/worktrees/pensive-borg-6be2f0`) still holds the same four files uncommitted. Two Sentry branches DO exist on origin — `claude/sentry-nextjs-sdk-setup-2v24q5` and `cursor/sentry-nextjs-sdk-7cee` — but whether either already carries this change is unconfirmed: a three-dot diff against `origin/main` from the remote container returned empty for both, which is not trustworthy as proof either way and was not pursued further. Note this touches `src/lib/env.ts` and `src/lib/supabase/client.tsx`, so it is not a docs-class change and needs a real gate whenever it does ship. **Next:** from the Windows machine, diff those two commits against the two remote Sentry branches to decide whether the work is already represented. If it is, delete the branch; if it is not, push it and open a PR rather than leaving it local. **Stop:** do not discard the commits blind, and do not assume the remote Sentry branches supersede them without a content diff — nothing has yet compared them. | session 2026-08-07 remote container; handoff notes from the PR #1649 session; origin branches claude/sentry-nextjs-sdk-setup-2v24q5 and cursor/sentry-nextjs-sdk-7cee | 2026-08-07 | | #261 | P2 | task | DS Track A2: retire --shadow-focus from the search composer | Replace the composer's companion focus ring with the sanctioned outline / --focus treatment used everywhere else, then delete the token (both theme declarations). Live consumer is .chat-composer-shell-delta:focus-within in globals.css — a --include=*.tsx grep reports zero consumers and is wrong. This is a visible focus-state change on the search composer: read docs/search-chrome-behaviour.md first and get a Chromium look. Gate: npm run check:design-system-contract + npm run verify:phone-chrome. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | -| #262 | P2 | task | DS Track A3: finish the design-token debt | Three parts. (1) Move --shadow-tight onto the --eN elevation ladder. SCOPE RE-MEASURED 2026-08-09 against origin/main 2675e6e1d, running analyzeClassContractsInSource + analyzeCssContractsInSource over the same walk check-design-system-contract.mjs uses (src/**, .ts/.tsx/.css, mockups excluded). The inherited figures were wrong in three ways. First, the legacyShadowAliases metric counts SEVEN tokens, not one: measured total 228 = tight 100, soft 72, elevated 17, hover 17, card 12, lux 8, lift 2. So the '229 --shadow-tight aliases' in HANDOVER-2026-08-07 is the all-token total mislabelled, and this row's earlier '155 consumers' was closer to a raw repo-wide grep (160 occurrences including mockups) than to the gated number. Second, the real scope is 100 production --shadow-tight sites across 55 files, so the inherited figure overstates the work by roughly 1.55x, and clearing all 100 will NOT zero the ratchet: 128 aliases across the six other tokens remain, so do not treat legacyShadowAliases=0 as the success criterion. Third, --shadow-focus is NOT in this metric at all: LEGACY_SHADOW_ALIAS has matched exactly tight\|card\|soft\|hover\|elevated\|lux\|lift since PR #1616 and has never included focus, so an earlier note claiming 'eight tokens, focus 2' and an overlap with #261 was wrong. #261 is a separate token with one consumer (src/app/globals.css:1476) and two theme declarations (lines 423, 664); the two tasks do not share this metric. Baseline pins legacyShadowAliases at 231 and the baseline is a ceiling, so today's 228 already passes. Re-measure before starting rather than trusting any of these numbers. (2) Add a step-SELECTION lint for the eight non-standard type steps (1318 sites) — check:type-scale already blocks arbitrary text-[12px], so do NOT write a lint duplicating the half that ships. (3) Extend the contract ratchet to raw padding / radius / line-height literals; it covers colour, shadow, tap and tracking today. Gate: npm run check:design-system-contract. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | +| #262 | P2 | task | DS Track A3: finish the design-token debt | Three parts. (1) Move --shadow-tight onto the --eN elevation ladder. SCOPE RE-MEASURED 2026-08-08 against origin/main 2675e6e1d, running analyzeClassContractsInSource + analyzeCssContractsInSource over the same walk check-design-system-contract.mjs uses (src/**, .ts/.tsx/.css, mockups excluded). The inherited figures were wrong in three ways. First, the legacyShadowAliases metric counts SEVEN tokens, not one: measured total 228 = tight 100, soft 72, elevated 17, hover 17, card 12, lux 8, lift 2. So the '229 --shadow-tight aliases' in HANDOVER-2026-08-07 is the all-token total mislabelled, and this row's earlier '155 consumers' was closer to a raw repo-wide grep (160 occurrences including mockups) than to the gated number. Second, the real scope is 100 production --shadow-tight sites across 55 files, so the inherited figure overstates the work by roughly 1.55x, and clearing all 100 will NOT zero the ratchet: 128 aliases across the six other tokens remain, so do not treat legacyShadowAliases=0 as the success criterion. Third, --shadow-focus is NOT in this metric at all: LEGACY_SHADOW_ALIAS has matched exactly tight\|card\|soft\|hover\|elevated\|lux\|lift since PR #1616 and has never included focus, so an earlier note claiming 'eight tokens, focus 2' and an overlap with #261 was wrong. #261 is a separate token with one consumer (src/app/globals.css:1476) and two theme declarations (lines 423, 664); the two tasks do not share this metric. Baseline pins legacyShadowAliases at 231 and the baseline is a ceiling, so today's 228 already passes. Re-measure before starting rather than trusting any of these numbers. (2) Add a step-SELECTION lint for the eight non-standard type steps (1318 sites) — check:type-scale already blocks arbitrary text-[12px], so do NOT write a lint duplicating the half that ships. (3) Extend the contract ratchet to raw padding / radius / line-height literals; it covers colour, shadow, tap and tracking today. Gate: npm run check:design-system-contract. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #264 | P2 | task | DS Track A5: gate the six ungated design-system prohibitions | Highest-value item in Track A — a prohibition with no gate row is a suggestion, which is the failure mode GATES.md exists to prevent. Colour-boundary lint (no numeral painted with a status colour); PDF/diagram invert refusal; layout-property animation lint; border+ring co-occurrence; --shadow-tight alias lint; dark: override lint (3 live instances). Gate: npm run check:design-system-contract + npm run lint. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #265 | P2 | task | DS Track A6: move design-system gates 2, 4, 7 and 8 from partial to blocking | Gate 4 needs a repo-wide status-indicator enumeration — today a new colour-only indicator slips through. Gate 2 needs the fixed-height h-10 case and test:e2e:style-contract wired into verify:cheap. Gates 7 and 8 likewise. Gate: npm run verify:cheap once wired. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | -| #266 | P2 | task | DS Track B1: adopt the 23 unadopted components demand-driven, never as a race to 53/53 | Pick a surface and let it pull, the way PR #1658 did for AnswerCard. COUNT RE-MEASURED 2026-08-09 from docs/design-system/adoption-manifest.json on origin/main: 53 registered, 30 with at least one productImportFiles entry, 23 UNADOPTED — not 24. Button moved into the adopted set when AccessibleTable's expand control stopped being a hand-rolled recipe (#263, PR #1712); its sole production importer is src/components/AccessibleTable.tsx, which is the demand-driven route this row describes, so it is the pattern to copy rather than an exception. The 23 measured today: AnswerFooter, Checkbox, Citation, CitationList, ConfirmDialog, Disclosure, DisclosureGroup, DoseLine, DownloadLink, ErrorSummary, ExternalTextLink, FieldError, FieldHint, LinkAction, Pagination, Progress, RadioGroup, SearchField, StageList, Tabs, TextLink, ToastRegion, Tooltip. Forms are still the largest single tranche: FieldError, FieldHint, ErrorSummary, SearchField, Checkbox and RadioGroup all land together on one form conversion. Do not stub a component to move the adoption count. Regenerate with npm run design-system:adoption:update after any import change; ALSO run npm run design-system:design-sync:update, because changing any *Props type or adopting a component fails check:design-sync-contract with 'dtsPropsFor must be generated from source public Props types' if only the first is run. Both manifests are generated, never hand-edited. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | +| #266 | P2 | task | DS Track B1: adopt the 23 unadopted components demand-driven, never as a race to 53/53 | Pick a surface and let it pull, the way PR #1658 did for AnswerCard. COUNT RE-MEASURED 2026-08-08 from docs/design-system/adoption-manifest.json on origin/main: 53 registered, 30 with at least one productImportFiles entry, 23 UNADOPTED — not 24. Button moved into the adopted set when AccessibleTable's expand control stopped being a hand-rolled recipe (#263, PR #1712); its sole production importer is src/components/AccessibleTable.tsx, which is the demand-driven route this row describes, so it is the pattern to copy rather than an exception. The 23 measured today: AnswerFooter, Checkbox, Citation, CitationList, ConfirmDialog, Disclosure, DisclosureGroup, DoseLine, DownloadLink, ErrorSummary, ExternalTextLink, FieldError, FieldHint, LinkAction, Pagination, Progress, RadioGroup, SearchField, StageList, Tabs, TextLink, ToastRegion, Tooltip. Forms are still the largest single tranche: FieldError, FieldHint, ErrorSummary, SearchField, Checkbox and RadioGroup all land together on one form conversion. Do not stub a component to move the adoption count. Regenerate with npm run design-system:adoption:update after any import change; ALSO run npm run design-system:design-sync:update, because changing any *Props type or adopting a component fails check:design-sync-contract with 'dtsPropsFor must be generated from source public Props types' if only the first is run. Both manifests are generated, never hand-edited. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #267 | P3 | task | DS Track B2: AnswerFooter and DoseLine need a provenance/dose payload the answer surface does not produce | Backend-shaped work, not a component swap: the two components cannot be adopted until the answer surface emits the provenance and dose data they render. Do not stub one to make the adoption count look better. Sequence after the payload exists, then adopt via the Track B1 demand-driven route. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #268 | P3 | task | DS Track B3: move the 19 genuine bare-dash sites onto MissingValue | Therapy-compass getters, specifier sourceFamily, favourites counts when untrusted. Leave the roughly 5 calculator 'derived.started ? score : dash' sites PERMANENTLY — 'not started' is not a missing clinical value, MissingValueReason has no member for it, and converting them would render 'Not recorded' for a score the clinician simply has not entered. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #269 | P2 | task | DS Track B4: prove the per-component visual state matrix (blocked on the baseline hold) | hover / active / disabled / busy / invalid / 320px / dark / forced-colours / print, per component. Currently proven for none. Blocked on #118: zero visual baselines are committed and the harness is continue-on-error, so nothing in Track B is safe at scale until baselines exist. Baselines cannot be generated on Windows — snapshotPathTemplate carries {platform}, so win32 PNGs are invisible to the ubuntu CI job. Stop rule: do not commit baselines until the owner declares the design final. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | From b4051d21f38755f7d37dbc2b49994f689af801b5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:13:37 +0800 Subject: [PATCH 4/4] docs(design-system): separate FormField integration from product adoption, re-measure the ui-primitives row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings on PR #1719, both valid. Section 4 said 'Select/choice controls remain separate adoption work', which contradicted the section above it and was wrong on both axes for Select: select.tsx consumes FormField (folded in PR 13), and adoption-manifest.json gives Select 2 production importers, so it is adopted. The genuinely unadopted member of the trio is SearchField, at zero, which the sentence did not mention. Rewritten to state the two axes explicitly — integration is complete for TextField, SearchField and Select, and deliberately declined for Checkbox/RadioGroup; adoption is measured TextField 3, Select 2, SearchField/Checkbox/RadioGroup 0 — so a control cannot be read as pending on one axis because it is pending on the other. The ui-primitives row carried three stale figures. Re-measured on this head: the module is 698 lines, not 686; 157 production files import it, not 200 — that figure is 202 including mockups, the same mockup-inclusive mislabelling this PR already corrects for the shadow aliases; and 30 of the 53 registered components are adopted, not 27. Future-dated 2026-08-09 records were already corrected to 2026-08-08 by f3a91c67c before this commit; verified none remain under docs/. Co-Authored-By: Claude Opus 5 --- docs/design-system/COMPONENTS.md | 69 ++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 4e825ec794..6890c0be1e 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -60,32 +60,32 @@ print primitives (`PrintHeader`, `PrintFooter`, `CitationFootnote`, `PrintOnly`, ### 0.4 Open-defect ledger (existing components → closing PR) -| Component | Open defects (compressed) | Closes in | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -| Button | danger hover/active tokens, the 48px comment and `ref` forwarding (plus a `testId` passthrough — React 19 types give components no `data-*` index signature) are resolved; the client boundary remains | follow-on | -| AsyncButton | `type` applied after spread (default `button`; explicit `submit` preserved). Prefer `Button` busy API for new sites. | **done** (PR-A) | -| IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) | -| ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) | -| Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** | -| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-08: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | -| Checkbox/RadioGroup | all four closed. Two rows were stale when re-measured on 2026-08-08: ids already sanitise through `optionId`, and the group already carries `hint`/`error`/`describedBy`. Raw dimensions moved to `size-5`/`h-0.5` (`size-4.5` retired by `check:icon-scale`) | **done** | -| Citation/CitationList | required interactive handler, stable-id list keys and static-span labelling (`aria-label` on a role-less span was dropped, losing the currency phrase on print) are resolved; route/source modes remain | follow-on | -| DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) | -| StatusMark | app-type coupling resolved — `DocumentStatus` is declared here and `@/lib/types` conforms to it (asserted at compile time in the DOM suite), not the reverse; inline styles/raw geometry remain | PR 12 | -| AnswerCard | required verification/state, structured actions and five-state vocabulary landed | **done** (PR 6) | -| AnswerFooter | machine ISO values composed through `DateDisplay` + `MissingValue` landed | **done** (PR 6) | -| PageHeader/Breadcrumb | all resolved. The wrap alone was not enough — the actions track still took full max-content first, so the title column now has a `minmax(20ch,1fr)` floor and the actions track is the one that gives way | **done** | -| Tabs | both resolved. An invalid `value` no longer empties the tab order: the first enabled tab becomes reachable and owns the panel wiring. Reachability only — no `onChange` is fired to repair caller state | **done** | -| Pagination | all resolved. Props clamp (`page=0` no longer emits `onPageChange(-1)`), the control row wraps at 320px, and reaching a boundary hands focus to the current page and announces "Page N of M" through `LiveAnnouncer` | **done** | -| Links | `tone` leak, spread-overridable `download` (now type-omitted AND written after the spread) and the `gap` "animation" (`gap` is not in Tailwind's `transition` list, so it jumped; now a composited `translate-x`) are resolved; new-tab policy still implicit, and `LinkAction` silently ignores `tone` | PR 9 | -| Tooltip | composed handlers/description, string content, OverlayRoot portal, collision and delay contract | **done** (PR 10) | -| Toast | independent tone/priority/persistence, OverlayRoot portal, pause, dedupe and queue contract | **done** (PR 10) | -| Sheet/ConfirmDialog | required names/action labels, portal default, tokened layers/duration and wrapping titles | **done** (PR 10) | -| Disclosure | heading level and print resolved — a collapsed panel is `print:block`, so a section no longer prints as if the guideline never mentioned it; truncation remains. The old docstring's Ctrl-F/print claim was false | PR 11 | -| Progress/StageList | all four resolved: `scaleX`, the theme `animate-shimmer` in place of a hardcoded `1.4s`, a step index clamped to ≥1, and an sr-only `role="status"` sibling in place of `aria-live` on the whole `
    ` | **done** | -| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) | -| AccessibleTable | semantic caption, `MissingValue` cells, dense headers (clipped header keeps its full string as `title`) and the expander (now the registered `Button`, off the local ring-focus recipe) landed; content-role widths remain | PR 6/PR 12 remainder | -| ui-primitives.tsx | 686-line module mixing recipes/actions/feedback/clinical — split. 200 product files import it, against the 53 registered components' 27 adopted; this module, not the registry, is what the product actually runs on | PR 12 | +| Component | Open defects (compressed) | Closes in | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | +| Button | danger hover/active tokens, the 48px comment and `ref` forwarding (plus a `testId` passthrough — React 19 types give components no `data-*` index signature) are resolved; the client boundary remains | follow-on | +| AsyncButton | `type` applied after spread (default `button`; explicit `submit` preserved). Prefer `Button` busy API for new sites. | **done** (PR-A) | +| IconButton | disabled encoding uses `controlDisabled` (opacity retired in PR-A) | **done** (PR-A) | +| ToggleSwitch | operable branch requires `aria-label`; opacity disabled retired; knob now travels on `transform` (`translate-x-4`), tokenised duration, reduced-motion opt-out | **done** (motion) | +| Chip | final `appearance`/size API, removable label contract, tap target and full-value title | **done** | +| TextField/SearchField/Select | all three closed, and the row was stale when re-measured 2026-08-08: each folds onto `FormField`, which renders `hint` AND `error` unconditionally and merges `aria-describedby` caller → hint → error; each takes an external `id` and a `ref`. Landed in PR 13, not the PR 7 the row pointed at | **done** | +| Checkbox/RadioGroup | all four closed. Two rows were stale when re-measured on 2026-08-08: ids already sanitise through `optionId`, and the group already carries `hint`/`error`/`describedBy`. Raw dimensions moved to `size-5`/`h-0.5` (`size-4.5` retired by `check:icon-scale`) | **done** | +| Citation/CitationList | required interactive handler, stable-id list keys and static-span labelling (`aria-label` on a role-less span was dropped, losing the currency phrase on print) are resolved; route/source modes remain | follow-on | +| DoseLine | must compose `Quantity` · structured dose model · overdue text + non-colour mark + open action | **done** (PR 6) | +| StatusMark | app-type coupling resolved — `DocumentStatus` is declared here and `@/lib/types` conforms to it (asserted at compile time in the DOM suite), not the reverse; inline styles/raw geometry remain | PR 12 | +| AnswerCard | required verification/state, structured actions and five-state vocabulary landed | **done** (PR 6) | +| AnswerFooter | machine ISO values composed through `DateDisplay` + `MissingValue` landed | **done** (PR 6) | +| PageHeader/Breadcrumb | all resolved. The wrap alone was not enough — the actions track still took full max-content first, so the title column now has a `minmax(20ch,1fr)` floor and the actions track is the one that gives way | **done** | +| Tabs | both resolved. An invalid `value` no longer empties the tab order: the first enabled tab becomes reachable and owns the panel wiring. Reachability only — no `onChange` is fired to repair caller state | **done** | +| Pagination | all resolved. Props clamp (`page=0` no longer emits `onPageChange(-1)`), the control row wraps at 320px, and reaching a boundary hands focus to the current page and announces "Page N of M" through `LiveAnnouncer` | **done** | +| Links | `tone` leak, spread-overridable `download` (now type-omitted AND written after the spread) and the `gap` "animation" (`gap` is not in Tailwind's `transition` list, so it jumped; now a composited `translate-x`) are resolved; new-tab policy still implicit, and `LinkAction` silently ignores `tone` | PR 9 | +| Tooltip | composed handlers/description, string content, OverlayRoot portal, collision and delay contract | **done** (PR 10) | +| Toast | independent tone/priority/persistence, OverlayRoot portal, pause, dedupe and queue contract | **done** (PR 10) | +| Sheet/ConfirmDialog | required names/action labels, portal default, tokened layers/duration and wrapping titles | **done** (PR 10) | +| Disclosure | heading level and print resolved — a collapsed panel is `print:block`, so a section no longer prints as if the guideline never mentioned it; truncation remains. The old docstring's Ctrl-F/print claim was false | PR 11 | +| Progress/StageList | all four resolved: `scaleX`, the theme `animate-shimmer` in place of a hardcoded `1.4s`, a step index clamped to ≥1, and an sr-only `role="status"` sibling in place of `aria-live` on the whole `
      ` | **done** | +| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) | +| AccessibleTable | semantic caption, `MissingValue` cells, dense headers (clipped header keeps its full string as `title`) and the expander (now the registered `Button`, off the local ring-focus recipe) landed; content-role widths remain | PR 6/PR 12 remainder | +| ui-primitives.tsx | 698-line module mixing recipes/actions/feedback/clinical — split. Re-measured 2026-08-08: **157** production files import it (202 including mockups — the old "200 product files" was the mockup-inclusive figure mislabelled), against the 53 registered components' **30** adopted; this module, not the registry, is what the product actually runs on | PR 12 | --- @@ -397,10 +397,19 @@ onto this shell — landed in PR 13, not the PR 7 this line used to point at. `C **Don't:** add per-control bespoke hint/error markup again; don't put block content in the hint (string type is deliberate). -**Current field contract.** `TextField` and `SearchField` now consume `FormField`: `id`, -`hint`, `error`, `required`, `autoComplete`, and caller `aria-describedby` are public field -contracts. Their hints/errors are strings because assistive technology receives them as one -description. `Select`/choice controls remain separate adoption work. +**Current field contract.** `TextField`, `SearchField` **and `Select`** all consume +`FormField`: `id`, `hint`, `error`, `required`, `autoComplete`, and caller +`aria-describedby` are public field contracts. Their hints/errors are strings because +assistive technology receives them as one description. + +**Two different axes, and this section is about the first one.** _Component integration_ — +which controls fold onto `FormField` — is complete for all three above, and deliberately +declined for `Checkbox`/`RadioGroup`, which keep `
      `/``. _Product adoption_ +— whether a production file imports the component — is separate and partial. Measured +2026-08-08 from `adoption-manifest.json`: `TextField` 3 production importers and `Select` 2, +so both are adopted; `SearchField`, `Checkbox` and `RadioGroup` have **zero** and are part of +the forms tranche in `#266`. A control can be fully integrated and still unadopted; do not +read one axis as the other, and do not count `Select` as pending on either. ---