diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index f6f9336e29..cae0f35090 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -672,7 +672,7 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-06 | a24f74fdf0134487a03dce37dd9f1e9bd18502f5 | a24f74fdf0134487a03dce37dd9f1e9bd18502f5 | PR #1614 post-merge RAG index restoration audit | Pass - guard-only migration, no DDL, no ranking/RPC change; pr-policy ragRanking=false so no eval-canary required; 1 P3 doc nit (#248 renumber note says 237->246, row is #248) | check:migration-role; npx vitest run tests/supabase-schema.test.ts (74 passed); check:outstanding-issues | | 2026-08-06 | PR #1614 / codex/restore-rag-indexes-20260804 | a24f74fdf0134487a03dce37dd9f1e9bd18502f5 | PR #1614 post-merge RAG index restoration audit | Pass - guard-only migration, no DDL, no ranking/RPC change; pr-policy ragRanking=false so no eval-canary required; 1 P3 doc nit (#248 renumber note says 237->246, row is #248); supersedes 2026-08-06 row (ref column mistakenly held commit SHA instead of PR ref, breaking ledger:lookup per Devin/Sentry review on PR #1636) | check:migration-role; npx vitest run tests/supabase-schema.test.ts (74 passed); check:outstanding-issues | | 2026-08-06 | claude/implement-97vpz7 | 00ab7bfd34684bc854d15a3f28987674098a7130 | PR #1646 soft-tail answer-cache skip + soft-tail test hardening | fixed — answer-path soft-tail skip via rag-query-guard helpers; soft-tail fixture pins; duplicate memo test removed; in-corpus assert narrowed; budget 4362 | test:rag-query-guard+unsupported-cache+classifier-memo 22/22,check:maintainability-budgets 4362/4362 | -| 2026-08-06 | cursor/mode-nav-pr-1647-a5eb | 4c8d70612f3be4a1267ed16b81e926a9f2e1ef50 | PR #1647 mode-nav | no high-confidence defects; medium: record pages lose mode destinations after Subnav removal (section-nav early return); addon-slot guard still coincidence-tested not runtime; includes() activeId fragile for future slugs | read mode-nav/*, page-secondary-navigation, mode-secondary-navigation, specifier/formulation record anchors + tests; catalog slug collision scan (0 hits); test:focused blocked (test paths changed) | +| 2026-08-06 | claude/ds-truth-fixes | 7a9c41a971aa9111f050cd39d4429ada1b3f4409 | PR #1655 heavy review-and-fix | fixed DownloadLink tone DOM leak + stale ToggleSwitch/Links §9 docs; merge-tree clean; verify:cheap+verify:pr-local green; CI re-queued after push | bugbot+deep-review; vitest ui-primitives+ui-v2 60p; verify:cheap 5448p; verify:pr-local format+lint+typecheck+test+build+rag-fixtures; no provider gates | | 2026-08-06 | temp-rebase | 868a8a2800351ce85a2ad13e14d80550cbc3e668 | Merge conflict resolution and CI fixes | Verified and ready for PR | verify:pr-local | | 2026-08-06 | claude/pr-handoff-stop-hook (PR #1649) | fff524d73ebfafeef7bdc50752a6d14f253ebc2e | Run PR sweep: CI fix + threads + drift | before: 5 unresolved threads (Devin create-from-output + 4 CodeRabbit), mergeable/BLOCKED, Actions major outage leaving CI pending; after: hardened jq-less input/output separation + session fail-open + prefix unlock + tests (9 passed) in fff524d73ebfafeef7bdc50752a6d14f253ebc2e, threads replied+resolved, branch current with main, CI re-triggered (Actions outage — not babysat) | npx vitest run tests/pr-handoff-stop.test.ts (9 passed); bash -n hook OK; no provider-backed checks run | | 2026-08-06 | claude/pr-handoff-stop-hook (PR #1649) | f67e5c9103c3e1483ad8e034fe7a6757c2362d74 | Run PR sweep: CI fix + threads + drift | before: 5 unresolved threads (Devin create-from-output + 4 CodeRabbit), mergeable/BLOCKED, Actions major outage leaving CI pending; after: hardened jq-less input/output separation + session fail-open + prefix unlock + tests (9 passed) in f67e5c9103c3e1483ad8e034fe7a6757c2362d74, threads replied+resolved, branch current with main, CI re-triggered (Actions outage — not babysat) | npx vitest run tests/pr-handoff-stop.test.ts (9 passed); bash -n hook OK; no provider-backed checks run | @@ -682,3 +682,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-06 | claude/pr-handoff-stop-hook (PR #1649) | 057a5579e538fcacc15feb8a0ac7fa580b3decb4 | Run PR sweep: CI fix + threads + drift | supersede: prior stacked Run PR rows used unresolvable HEADs (fff524d7/f67e5c91/3403126b); reviewed product tip is this SHA (ledger bookkeeping may sit one commit above); Bugbot prune+context+comment/review fixes landed | vitest:pr-handoff-stop 11/11; verify:cheap pass; verify:pr-local pass | | 2026-08-06 | claude/settings-nav-freeze-desktop-tdzh7z (PR #1641) | d4ecd2b5fc48a8d8f37f081cac2d80beb749df01 | Run PR sweep: CI fix + threads + drift | before: mergeable/BLOCKED, 0 unresolved threads, 0 behind main; prior CI reds were infra (npm ECONNRESET on Unit coverage; e7f380f3 cancelled at Set up job). after: pushed d4ecd2b5 ResizeObserver pin-clamp distance reset; all review threads remain resolved; CI re-triggered but GitHub Actions major_outage — runs pending/queued, no product failure to fix; merge tree clean | local typecheck PASS; local lint (settings-dialog) PASS; format unchanged; no provider-backed checks run; hosted CI awaiting Actions recovery (run 31120931239) | | 2026-08-06 | claude/settings-nav-freeze-desktop-tdzh7z (PR #1641) | c31827e63c20788c5dabb22b96ddbf239c65f893 | Run PR sweep: CI fix + threads + drift | before: mergeable/BLOCKED, 0 unresolved threads, 0 behind main; prior CI reds were infra (npm ECONNRESET; cancelled Set up job). after: product fix d4ecd2b5 (ResizeObserver pin-clamp distance reset) + ledger row; all review threads resolved; merge tree clean; hosted CI pending on GitHub Actions major_outage (no product failure) | local typecheck PASS; local lint (settings-dialog) PASS; no provider-backed checks run; hosted CI awaiting Actions recovery | +| 2026-08-06 | cursor/mode-nav-pr-1647-a5eb | 4c8d70612f3be4a1267ed16b81e926a9f2e1ef50 | PR #1647 mode-nav | no high-confidence defects; medium: record pages lose mode destinations after Subnav removal (section-nav early return); addon-slot guard still coincidence-tested not runtime; includes() activeId fragile for future slugs | read mode-nav/*, page-secondary-navigation, mode-secondary-navigation, specifier/formulation record anchors + tests; catalog slug collision scan (0 hits); test:focused blocked (test paths changed) | diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 860e53ed04..5520d37a7a 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 and the 48px comment are resolved; ref forwarding and the client boundary remain | 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 still animates `left`/`right` | PR 9 (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 | -| Checkbox/RadioGroup | RadioGroup controlled/uncontrolled union done; raw dimensions · unsanitised ids · no group hint/error | PR 7 | -| Citation/CitationList | required interactive handler and stable-id list keys are resolved; static span labelling and 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 · inline styles/raw geometry · HCM token remaps proven (computed suite); mark shape still visual | 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 | `

` truncates · actions starve title · eyebrow ink moved off decoration (PR 3); layout starve remains | PR 7-adjacent layout fix | -| Tabs | `aria-controls` to unrendered panels · invalid selected value can empty the tab order | PR 4-adjacent | -| Pagination | unclamped props · 320px overflow · opacity disabled retired · no focus/announce policy | PR 8 | -| Links | `tone` leaks to DOM · `download` overridable by spread · `gap` animation · new-tab policy implicit | 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 | configurable heading level is resolved; print behaviour and truncation remain | PR 11 | -| Progress/StageList | determinate width animation is resolved via `scaleX`; hardcoded indeterminate timing, "step 0 of N" and whole-list live region remain | PR 8, PR 9 | -| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) | -| AccessibleTable | required semantic caption and `MissingValue` cells landed; dense headers, content-role widths and Button-based expander remain | PR 6/PR 12 remainder | -| ui-primitives.tsx | 572-line module mixing recipes/actions/feedback/clinical — split | PR 12 | +| Component | Open defects (compressed) | Closes in | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| Button | danger hover/active tokens and the 48px comment are resolved; ref forwarding and the client boundary remain | 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 | hint dropped on error (comment promises otherwise) · describedBy overwritten · no external id/refs (placeholder off decoration tier in PR-A) | PR 7 | +| Checkbox/RadioGroup | RadioGroup controlled/uncontrolled union done; raw dimensions · unsanitised ids · no group hint/error | PR 7 | +| Citation/CitationList | required interactive handler and stable-id list keys are resolved; static span labelling and 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 · inline styles/raw geometry · HCM token remaps proven (computed suite); mark shape still visual | 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 | `

` truncates · actions starve title · eyebrow ink moved off decoration (PR 3); layout starve remains | PR 7-adjacent layout fix | +| Tabs | `aria-controls` to unrendered panels is resolved (emitted only for the selected tab when it owns a panel); invalid selected value can still empty the tab order | PR 4-adjacent | +| Pagination | unclamped props · 320px overflow · opacity disabled retired · no focus/announce policy | PR 8 | +| Links | `tone` leak is resolved (destructured before the spread in every link); `download` still overridable by spread · `gap` animation · new-tab policy implicit | 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 | configurable heading level is resolved; print behaviour and truncation remain | PR 11 | +| Progress/StageList | determinate width animation is resolved via `scaleX`; hardcoded indeterminate timing, "step 0 of N" and whole-list live region remain | PR 8, PR 9 | +| EmptyState | static live-off default with explicit polite/assertive opt-in | **done** (PR 8) | +| AccessibleTable | required semantic caption and `MissingValue` cells landed; dense headers, content-role widths and Button-based expander 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 | --- @@ -647,8 +647,11 @@ on it. `interactive` (label + checked + onCheckedChange required) · `status` (label + checked; `role="img"`-style read-only, still named). **Rules.** Compact 40×24 face is fine; the _target_ meets the tap floor via a transparent hit area · knob motion is `transform` with -the physical curve · reduced motion snaps. **Open defects → PR.** unnameable, `left`/ -`right` animation, opacity disabled → PR 4, PR 9. +the physical curve · reduced motion snaps. **Resolved (6 Aug 2026).** operable branch +requires `aria-label`; opacity disabled retired; knob travels on `transform` +(`translate-x-0` / `translate-x-4`) with tokenised duration and `motion-reduce` opt-out — +no longer animates `left`/`right`. **Open defects → PR.** none remaining on this +component; broader Gate 9 motion sweep still tracks other surfaces. ### 9.5 `Chip` @@ -810,8 +813,10 @@ focus/announce policy → PR 3, PR 8. links carry the `ExternalLink` icon and an explicit new-tab policy, not an accidental universal · `DownloadLink`'s `download` semantics are not overridable by spread; `tone` is destructured, never leaked to the DOM · `LinkAction`'s arrow animates with `transform`, -never `gap`. **Open defects → PR.** tone leak, spread override, gap animation, raw -underline offset → PR 9. +never `gap`. **Resolved (6 Aug 2026).** `tone` is destructured before the spread in +`TextLink`, `ExternalTextLink`, and `DownloadLink`. **Open defects → PR.** `download` +still overridable by spread · `LinkAction` `gap` animation · new-tab policy implicit / +raw underline offset → PR 9. ### 9.21 `Tooltip` diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index ee09d396b3..76dc1528e2 100644 --- a/docs/design-system/GATES.md +++ b/docs/design-system/GATES.md @@ -30,10 +30,23 @@ than CI, two of them by the second reader; review does not scale past two carefu | `npm run verify:ui` (+ `verify:phone-chrome`) | Chromium production journeys; phone-chrome owners | implemented-blocking (journey scope) | | Visual-baseline harness (PR #1404) | Screenshot baselines exist but ship **zero committed baselines with `continue-on-error` on, deliberately** — a held gate until the design is declared final (issue `#118`). Includes the `--spacing-tap` probe floor. | implemented-partial (non-blocking by hold) | -⚠️ **Named-but-absent checks.** Project docs mention "type-scale" and "icon-scale" lint -rules; no such rule files exist in `eslint-rules/` **[verified: glob]**. Type-step and -icon-step discipline is currently **unenforced** outside the token contract — do not cite -those rules as gates. +⚠️ **Type-scale and icon-scale enforcement — corrected 6 Aug 2026.** The previous wording +here ("no such rule files exist in `eslint-rules/`… currently **unenforced**") was true about +ESLint and wrong about enforcement, which is the more expensive error of the two. +`npm run check:type-scale` and `npm run check:icon-scale` (`scripts/check-type-scale.mjs`, +`scripts/check-icon-scale.mjs`, both `--strict`) have run inside `verify:cheap` since +30 July **[verified: package.json]**. They are hard-zero gates with no baseline, unlike the +ratcheting design-system contract. + +**Enforced:** arbitrary font-size and icon-size utilities (`text-[12px]`, `text-[1.45rem]`) +fail the build. `text-[color:var(--…)]` is the sanctioned token form and is deliberately not +flagged. + +**Not enforced:** which named step a component picks. Nothing stops a surface choosing +`text-sm-minus` over `text-sm` — 1 318 call sites across the eight non-standard steps +**[verified: grep]**. That is a step-_selection_ lint and it does not exist. Do not describe +the scale as ungated, and do not write an ESLint rule duplicating the arbitrary-value check +that already ships. ## 2 · The twelve system gates, labelled @@ -41,7 +54,7 @@ those rules as gates. | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. | | 2 | Tap targets ≥48px interactive (token + declared carriers today; fixed-height `h-10` controls and full interactive enumeration not yet blocked); static pills never carry `min-h-tap`; no production target reduced | **implemented-partial** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Not blocked today:** the Chromium audit skips elements whose declared height is below the floor (a fixed `h-10` control would not fail it), and `test:e2e:style-contract` is not part of `verify:cheap`. Path to blocking: enumerate rendered interactive elements with an explicit exception list (phone composer below 431px is already written — SPEC §4.10), then wire that audit into required CI. Per-surface geometry stays in the held visual harness. | -| 3 | Focus outline present, `--focus`, no companion ring | **planned** | `--focus` is referenced nowhere in the DS export (finding N3). Global fix first, then the check. | +| 3 | Focus outline present, `--focus`, no companion ring | **planned** | Corrected 6 Aug 2026: the previous evidence ("`--focus` is referenced nowhere in the DS export", finding N3) was false — **[verified: grep]** 4 declarations (2 theme, 2 forced-colours) against **273** `var(--focus)` consumers, 260 of them in `.tsx`. The token is adopted; the check is what is missing. Path to blocking: assert a visible focus outline on every interactive role and reject a `ring-*` companion on the same node. A row that understates shipped work costs the document its authority as surely as one that overstates it, and nobody files a bug against pessimism. | | 4 | Non-colour encoding on every status indicator | **implemented-partial** | Blocked today by `ui-v2-answer-safety.dom.test.tsx`: an overdue `DoseLine` row is asserted to carry all three channels (amber inset rule **plus** the words "Source review overdue" **plus** a `StatusMark` shape), `MissingValue` is asserted never to contract to a dash at any density, `FieldError` is asserted to pair its text with an icon, and `RetrievalStateBanner` is asserted to carry its state in the headline text rather than the tone alone. Off-vocabulary status still degrades to a phrase (`source-badges-off-vocab.dom.test.tsx`, Gate 6). **Not blocked today:** there is no repository-wide enumeration of status indicators, so a _new_ colour-only indicator elsewhere in `src/components/**` — the bare `statusDot*` recipes are the obvious candidates — would not fail anything. Path to blocking: an AST sweep that pairs every status-bearing tone class with a sibling text or shape channel, wired into `verify:cheap`. Deliberately not claimed as blocking on the strength of per-component tests: on a clinical safety gate an overstated label is worse than an honest partial. | | 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-blocking** | `AccessibleTableProps.caption` is required; DOM and alignment tests prove the semantic ``, associated headers, and expander relationship. | | 6 | Enum resilience — neutral fallback, never throws | **implemented-blocking** | `source-badges-off-vocab.dom.test.tsx`. | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 908dd988ce..3c555aa824 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -602,7 +602,7 @@ "preview": ".design-sync/previews/DownloadLink.tsx", "previewValid": true }, - "testFiles": ["tests/design-sync-visual-exports.test.ts"], + "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"], "baseline": { "targetLayer": "v2", "liveLayer": "v2", @@ -718,7 +718,7 @@ "preview": ".design-sync/previews/ExternalTextLink.tsx", "previewValid": true }, - "testFiles": ["tests/design-sync-visual-exports.test.ts"], + "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"], "baseline": { "targetLayer": "v2", "liveLayer": "v2", @@ -1746,7 +1746,7 @@ "preview": ".design-sync/previews/TextLink.tsx", "previewValid": true }, - "testFiles": ["tests/design-sync-visual-exports.test.ts"], + "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/ui-v2-components.dom.test.tsx"], "baseline": { "targetLayer": "v2", "liveLayer": "v2", diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index 0b3a14f177..90285d25e9 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -337,8 +337,11 @@ export function ToggleSwitch({ ); diff --git a/src/components/ui/link.tsx b/src/components/ui/link.tsx index 14dd2fff9a..c2054ca391 100644 --- a/src/components/ui/link.tsx +++ b/src/components/ui/link.tsx @@ -84,10 +84,18 @@ export function ExternalTextLink({ href, children, tone = "accent", className, . * on hospital wifi deciding whether to tap a 40 MB PDF needs that before the tap, * not after. */ -export function DownloadLink({ href, children, format, size, className, ...props }: DownloadLinkProps) { +export function DownloadLink({ + href, + children, + format, + size, + tone = "accent", + className, + ...props +}: DownloadLinkProps) { const detail = [format, size].filter(Boolean).join(", "); return ( - +