Skip to content

chore(deps): bump node from 24-bookworm-slim to 26-bookworm-slim in the docker-images group - #2470

Open
dependabot[bot] wants to merge 67 commits into
mainfrom
dependabot/docker/docker-images-263a700181
Open

chore(deps): bump node from 24-bookworm-slim to 26-bookworm-slim in the docker-images group#2470
dependabot[bot] wants to merge 67 commits into
mainfrom
dependabot/docker/docker-images-263a700181

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Cooldown could not be applied because no publication date was available from the registry.

Bumps the docker-images group with 1 update: node.

Updates node from 24-bookworm-slim to 26-bookworm-slim

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions

Bumps the docker-images group with 1 update: node.


Updates `node` from 24-bookworm-slim to 26-bookworm-slim

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26-bookworm-slim
  dependency-type: direct:production
  dependency-group: docker-images
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file docker Pull requests that update docker code labels Aug 31, 2026
@dependabot
dependabot Bot requested a review from BigSimmo as a code owner August 31, 2026 01:10
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file docker Pull requests that update docker code labels Aug 31, 2026
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

CI triage

CI failed on this PR. Automated classification of the 3 failed job(s):

  • Unit coverageneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.
  • Migration replayneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.
  • PR requiredneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.

Compared with main CI run #14717 (success).

Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger.

claude and others added 23 commits August 31, 2026 02:22
The follow-up recorded in PR #2462 and deliberately left undone there, now that
the artwork decision has been made: the app rendered the mark's *display*
geometry at every size, including the 28-32px chrome where the brand's own
construction calls for a different cut.

**Why this is the real fix.** The mark is a construction, not a bitmap, so it
does not simply get smaller. Two pieces of its negative space close up first —
the 4.2-unit cut between the strokes and the 7.26-unit crescent around the point
— and when the crescent closes, the dot fuses into the S. That fuses the top of
the mark into one solid blob above a lower stroke that is already the thinner
read, which is exactly the top-heavy look this branch previously compensated for
with the deeper `--brand-mark-emphasis` ink. The ink stays; the cause is now
addressed rather than offset.

`docs/brand/psychsift-logo.md` §"Small sizes" already specifies the remedy and
`brand-mark.ts` already ships it: `BRAND_STROKE_PATH_SMALL` opens the cut to 7.2
units and `BRAND_POINT_SMALL` slides the point 10 units out of its cradle,
opening the crescent from 7.26 to 11.55. Only the favicon and the raster icon
routes used them. Nothing in the app did.

**The one thing that did not exist, and how it was derived rather than drawn.**
The bare (untiled) glyph had no small-size placement. `brand-mark.ts` says in
terms not to redraw the construction by hand, and this does not: the doc fixes
both ink boxes (display spans 0-55.33, chrome 0-65.33, both 0-100.38 tall) and
every committed placement centres that box in the 512 viewBox — verified against
all three, each landing on 256.000 within 0.003, the rounding of a two-decimal
box figure.

Two consequences follow arithmetically. The point keeps its `cy` and `r`, so the
vertical extent is identical and the scale is unchanged at 5.1006 (100.3813 x
5.1006 = 512.0, which is the bare variant's whole purpose). The point moves
exactly 10 units and is the box's right edge, so the box widens by 10 and its
centre by 5: 114.8907 - 5 x 5.1006 = 89.3877. The same arithmetic reproduces the
committed tiled pair (143.1125 - 5 x 4.0804 = 122.7103, as shipped), which is
the check that this is the rule actually in use rather than one that merely fits.

**Threshold.** 32px, from the doc's own "at 32 px and below two things close up".
Sub-32px production sites now draw the chrome cut: the collapsed sidebar rail
(28px), the phone drawer header (28/32px), the account-setup workspace mark
(32px), and the ward sidebar glyph (32px). Everything at 36px and above keeps the
display cut. Design-scratch mockups are untouched.

The three pieces are selected together through one `brandMarkOptics()` call, so a
caller cannot half-apply the variant — the doc is explicit that mixing one
variant's point with the other's placement puts the glyph off-centre.

Verification
- `tests/brand-mark-optics.test.ts` — 14 passed. Every rule mutation-tested with
  the failure predicted before running: dropping `optical="chrome"` from a 28px
  site, adding it to a 40px one, shifting the new transform 5px off centre, and
  pairing the chrome placement with the display point each failed exactly the
  intended assertion; all four restored byte-identical.
- Writing that test surfaced a defect in the test itself, fixed before it could
  mislead: `ward-management-navigation.tsx` imports two CSS modules that BOTH
  define `.brandGlyph`, at 2.5rem and 2rem, so an identifier-blind resolver
  merged them and reported real sites as spanning the threshold. It now resolves
  the specific import. It also caught `sidebar-live-mockup.tsx` — design scratch
  named with the singular `-mockup`, which the usual `mockups` filter misses.
- `npm run brand:check` — `src/app/icon.svg matches the brand-mark source` (this
  adds exports; it changes no existing value, so the generated assets are
  untouched).
- Design-system contract passed, every ratchet at or under baseline; adoption 55
  components / 84 roots; design-sync contract 55 components / 7 guidelines.
- Full unit suite: `11744 passed | 1 skipped` across 931 files. Three
  pre-existing failures in `clinical-hazard-controls`, `privacy-readiness-contract`
  and `rag-plan-package-parity` — none touches brand geometry, and CI's Unit
  coverage passed these on the previous PR, so they are container-local.
- Lint clean at `--max-warnings 0`; typecheck exit 0, unscoped; whole-tree format
  committed with the work.

RAG impact: no retrieval behaviour change - this diff touches no file under
src/lib/rag/** and none of the protected ranking surfaces (clinical-search,
retrieval-selection, released-search-order, ranking-config, answer-ranking,
answer-verification), adds no comparator key, and leaves the golden fixture and
eval harness untouched. Verified by listing the diff's paths against that set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRGeyyiZNrXnFJjVVnykgp
…design-sha4cn

feat(brand): draw the small-size optical cut at chrome sizes
… updates (#2467)

Bumps the npm-production group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@next/env](https://github.com/vercel/next.js/tree/HEAD/packages/next-env) | `16.3.0` | `16.3.3` |
| [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) | `10.69.0` | `10.71.0` |
| [@supabase/ssr](https://github.com/supabase/ssr) | `0.12.4` | `0.12.5` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.29.0` | `1.34.0` |
| [mammoth](https://github.com/mwilliamson/mammoth.js) | `1.12.0` | `1.12.1` |
| [next](https://github.com/vercel/next.js) | `16.3.0` | `16.3.3` |
| [openai](https://github.com/openai/openai-node) | `7.4.0` | `7.8.0` |
| [pdfkit](https://github.com/foliojs/pdfkit) | `0.19.1` | `0.20.1` |



Updates `@next/env` from 16.3.0 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/next-env)

Updates `@sentry/nextjs` from 10.69.0 to 10.71.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/10.71.0/CHANGELOG.md)
- [Commits](getsentry/sentry-javascript@10.69.0...10.71.0)

Updates `@sentry/node` from 10.69.0 to 10.71.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/10.71.0/CHANGELOG.md)
- [Commits](getsentry/sentry-javascript@10.69.0...10.71.0)

Updates `@supabase/ssr` from 0.12.4 to 0.12.5
- [Release notes](https://github.com/supabase/ssr/releases)
- [Changelog](https://github.com/supabase/ssr/blob/main/CHANGELOG.md)
- [Commits](supabase/ssr@v0.12.4...v0.12.5)

Updates `@supabase/supabase-js` from 2.112.2 to 2.112.4
- [Release notes](https://github.com/supabase/supabase-js/releases)
- [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md)
- [Commits](https://github.com/supabase/supabase-js/commits/v2.112.4/packages/core/supabase-js)

Updates `lucide-react` from 1.29.0 to 1.34.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.34.0/packages/lucide-react)

Updates `mammoth` from 1.12.0 to 1.12.1
- [Release notes](https://github.com/mwilliamson/mammoth.js/releases)
- [Changelog](https://github.com/mwilliamson/mammoth.js/blob/master/NEWS)
- [Commits](mwilliamson/mammoth.js@1.12.0...1.12.1)

Updates `next` from 16.3.0 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](vercel/next.js@v16.3.0...v16.3.3)

Updates `openai` from 7.4.0 to 7.8.0
- [Release notes](https://github.com/openai/openai-node/releases)
- [Changelog](https://github.com/openai/openai-node/blob/main/CHANGELOG.md)
- [Commits](openai/openai-node@v7.4.0...v7.8.0)

Updates `pdfkit` from 0.19.1 to 0.20.1
- [Release notes](https://github.com/foliojs/pdfkit/releases)
- [Changelog](https://github.com/foliojs/pdfkit/blob/master/CHANGELOG.md)
- [Commits](foliojs/pdfkit@v0.19.1...v0.20.1)

---
updated-dependencies:
- dependency-name: "@next/env"
  dependency-version: 16.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-production
- dependency-name: "@sentry/nextjs"
  dependency-version: 10.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: "@sentry/node"
  dependency-version: 10.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: "@supabase/ssr"
  dependency-version: 0.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-production
- dependency-name: "@supabase/supabase-js"
  dependency-version: 2.112.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-production
- dependency-name: lucide-react
  dependency-version: 1.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: mammoth
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-production
- dependency-name: next
  dependency-version: 16.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-production
- dependency-name: openai
  dependency-version: 7.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: pdfkit
  dependency-version: 0.20.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
…th 13 updates (#2468)

Bumps the npm-development group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm) | `4.12.1` | `4.13.0` |
| [@next/bundle-analyzer](https://github.com/vercel/next.js/tree/HEAD/packages/next-bundle-analyzer) | `16.3.0` | `16.3.3` |
| [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) | `7.0.0` | `7.0.1` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.2` | `16.3.3` |
| [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.3` | `14.6.6` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.1.2` | `26.4.0` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.4` | `19.2.5` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.10` | `4.1.11` |
| [esbuild](https://github.com/evanw/esbuild) | `0.28.1` | `0.28.2` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.3.0` | `16.3.3` |
| [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.32.0` | `6.32.3` |
| [tsx](https://github.com/privatenumber/tsx) | `4.23.9` | `4.23.12` |



Updates `@axe-core/playwright` from 4.12.1 to 4.13.0
- [Release notes](https://github.com/dequelabs/axe-core-npm/releases)
- [Changelog](https://github.com/dequelabs/axe-core-npm/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/dequelabs/axe-core-npm/commits/v4.13.0)

Updates `@next/bundle-analyzer` from 16.3.0 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/next-bundle-analyzer)

Updates `@testing-library/jest-dom` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](testing-library/jest-dom@v7.0.0...v7.0.1)

Updates `@testing-library/react` from 16.3.2 to 16.3.3
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](testing-library/react-testing-library@v16.3.2...v16.3.3)

Updates `@testing-library/user-event` from 14.6.3 to 14.6.6
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](testing-library/user-event@v14.6.3...v14.6.6)

Updates `@types/node` from 26.1.2 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react-dom` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@vitest/coverage-v8` from 4.1.10 to 4.1.11
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/coverage-v8)

Updates `esbuild` from 0.28.1 to 0.28.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](evanw/esbuild@v0.28.1...v0.28.2)

Updates `eslint-config-next` from 16.3.0 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/eslint-config-next)

Updates `knip` from 6.32.0 to 6.32.3
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.32.3/packages/knip)

Updates `tsx` from 4.23.9 to 4.23.12
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](privatenumber/tsx@v4.23.9...v4.23.12)

Updates `vitest` from 4.1.10 to 4.1.11
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/vitest)

---
updated-dependencies:
- dependency-name: "@axe-core/playwright"
  dependency-version: 4.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@next/bundle-analyzer"
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@testing-library/jest-dom"
  dependency-version: 7.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@testing-library/react"
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@testing-library/user-event"
  dependency-version: 14.6.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: esbuild
  dependency-version: 0.28.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: eslint-config-next
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: knip
  dependency-version: 6.32.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: vitest
  dependency-version: 4.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
…10 in the github-actions group across 1 directory (#2469)

* chore(deps): bump anthropics/claude-code-action

Bumps the github-actions group with 1 update in the / directory: [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action).


Updates `anthropics/claude-code-action` from 1.0.199 to 1.0.210
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](anthropics/claude-code-action@dcb5774...a874e9e)

---
updated-dependencies:
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.210
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(ci): allowlist reviewed anthropics/claude-code-action@v1.0.210 pin

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com>
…e amber (#2481)

* fix(ui/answer): keep answer-surface counts neutral and the safety tile amber

Three colour defects found by opening the answer page at 390px in light, dark
and forced colours, all the same mistake — a status colour landing on a count
instead of on the icon that already carries the state.

- The `Evidence gaps` count was painted `--warning-soft`. §12.7 of the answer
  page handover settled this ("the evidence-gaps count is neutral, not amber. A
  count painted with a status colour is a status-coloured numeral, which the
  contract holds at zero, and the icon beside it already says what state it is
  in") and the approved specimen in answer-chat-perfected-v2-mockups.tsx draws
  it neutral, but the correction never reached production. It now matches the
  neutral count the safety row directly above it already used.

- The safety-findings sheet opened with an amber shield sitting in a *blue*
  tile. The call site passed `text-[color:var(--warning)]` on top of
  `iconTilePremium`, which recoloured only the glyph — the recipe's
  clinical-accent border and background stayed. The tile is now written out with
  the warning tones, identical to `AnswerSupportSummaryCard`'s tile, so the
  colour the design assigns to the icon tile is the same on both sides of the
  tap. `iconTilePremium` is no longer imported there.

- That sheet's header count was amber too, while the count on the card that
  opens it is neutral — the same number in two treatments. Now neutral, geometry
  unchanged.

Display only. No behaviour, no retrieval, no clinical wording changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkKHznd5E5KLnKbowRTLd8

* test(ui): compare answer status surfaces

---------

Co-authored-by: Claude <noreply@anthropic.com>
…#2482)

* feat(search): separate smart catalogue interpretation

* docs(search): record smart search handover

* docs(repo): refresh awareness snapshot for PR 2482

* fix(search): preserve composer and ranking contracts

* fix(search): align Smart catalogue results

* test(specifiers): allow duplicate anxious-distress catalogue labels in Smart default-lane assert

* test(specifiers): restore suite; allow duplicate anxious-distress labels in Smart assert

* test(specifiers): pin guides scope for Top match wording journey under Smart NL default
An agent push on PR #2481 rewrote tests/ui-smoke.spec.ts from 6,019 lines
to 1,081, dropping 74 of its 82 journey tests, under a commit message that
described a narrow comparison change. Squash auto-merge was armed at the
time and a gutted suite goes green trivially, so only an unrelated merge
conflict kept the deletion off main; the later merge of main restored the
file and the squash landed the intended two-file change.

No gate would have caught it: nothing asserts a floor on Playwright test
count or fails on a large test deletion, and the "never skip, disable, or
quarantine a test to get green" rule is agent policy rather than an
enforced check. The queued request proposes a per-path ratchet on test
counts, in the spirit of the bundle-budget baselines.


Claude-Session: https://claude.ai/code/session_01NkKHznd5E5KLnKbowRTLd8

Co-authored-by: Claude <noreply@anthropic.com>
…2484)

* feat(ui/answer): status chips above the answer, thumbs verdicts below it

Applies the approved specimen (artifact "Cited Answer Specimens") to the
live answer surface, with two owner decisions taken on 2026-08-31.

The header becomes one status line: the support chip the card already owned,
the safety-notes chip, the evidence-gaps chip, and the cited count. Every chip
carries an icon and words, so none of them relies on colour — they read the
same in forced-colors and in greyscale print.

The action row becomes Copy with sources on the left and the two verdicts on
the right. The thumb down is the way in to "report a problem": it opens the
list of problem types, which now omits the affirmative option, because
offering "Verified" inside a list opened to report a fault records the
opposite of what the reader meant. The thumb up records that one verdict in a
single tap. Evidence gaps left this row for the header, which is what keeps
the row to one line at 390px.

The governed verification sentence moves below the answer (owner decision).
`AnswerCard` still owns the wording and still refuses to render an answer
without it; `verificationPlacement="content"` is the same contract
`retrievalStatePlacement` already used, and the surface honours it.

The support card goes with it. Its safety row was the only route to the
safety-critical findings sheet, so that route moved to the header chip, which
is a real button with the repo's `before:-inset-y-*` hit expansion: drawn
small, tapped at full size. Removing the card left its `priority` derivation,
and `answerGrounded`, `safeAnswerSections`, `sourceLookup` and two imports in
ClinicalDashboard, with no consumers; those go too.

"Also in your library" is kept, deliberately against the specimen: that
specimen is one answer with no library matches to show, and the line is the
only route from an answer to the Medication and Differentials records.

Also: "Sources" becomes "Cited documents", "Ask next" becomes "Follow up",
and the generated design-system adoption and design-sync manifests are
regenerated for the new AnswerCard props.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkKHznd5E5KLnKbowRTLd8

* fix(ui/answer): give the status chips a real tap target, repoint the smoke suite

`verify:ui` found seven failures on the answer surface. All seven are fixed
here rather than accommodated.

The safety chip was a 24px target. It used the repo's `before:-inset-y-*` hit
expansion, which a thumb finds and `boundingBox()` does not — and this chip is
the only route to the safety-critical findings sheet, which is the last control
on this surface that should rest on a target no gate can measure. It is now a
48px button with the pill drawn inside it, carrying `-my-3` so the line keeps
the pill's own height instead of becoming 96px of chrome above a clinical
answer. Measured: safety 133x48, evidence gaps 138x48, both verdicts 48x48.

Six assertions pinned `answer-support-card`, removed with the owner's decision.
They are repointed at what carries the same guarantees now — the chip states
the support level, a degraded answer reads "limited"/"unassessed" rather than
looking fully supported, and the safety sheet still opens at full tap size —
not deleted. The document-order check gains the caution's new position below
the utilities, so the placement decision is pinned rather than assumed.

One failure was a latent defect this change exposed rather than caused. With
the support card gone the answer is shorter, so the phone runway test took its
`else` branch for the first time, and that branch measured a listitem inside
the library panel it had just collapsed — `display:none`, so nothing to
measure, so a 60s timeout. It now measures the last follow-up row, which is
what "the last content clears the dock" means on this surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkKHznd5E5KLnKbowRTLd8

* fix(ui/answer): keep status-chip hitboxes in layout so wrapped chips cannot cover prose

* fix(ui/answer): keep status-chip hitboxes in layout so wrapped chips cannot cover prose

* fix(ui/answer): keep status-chip hitboxes in layout so wrapped chips cannot cover prose

---------

Co-authored-by: Claude <noreply@anthropic.com>
PR #2484 landed the fix for the chip overlap — `-my-3` is gone and the
comment above `chipButton` forbids it — but nothing on main can tell if
it comes back. A negative margin leaves `boundingBox()` reading a correct
48px while the hit region floats outside the element's layout box, so
every DOM assertion in the suite passes with the chip sitting on top of
its neighbours. Only geometry sees it.

The critical answer smoke now measures the chip rectangles against the
support chip, the cited count, the prose and each other, at 390px and
320px. Reinstating `-my-3` fails it and names all four overlaps: the
safety chip over a 133x9px band of the support chip and a 133x2px band of
the prose, and the evidence-gaps chip over both.

`AnswerCard` also gives the chips a full-width row of their own. They are
48px controls, and the header line they were sharing aligns its children
on the text baseline, which is the pressure that made a negative margin
look reasonable in the first place. Measured on the production build, the
status block from the support chip to the top of the prose is 85px with
the row against 96px without it, at both 390px and 320px — so the honest
hitbox costs nothing and the alignment stops being incidental. The prop
doc now carries the rule, since the next author reaching for
`before:-inset-y-*` has the same problem and no gate.
…ssage

Sidebar: every control in the collapsed rail now sits on one centre line.
The scrolling group of mode icons used `scrollbar-gutter: stable`, which
reserves the gutter on the right edge only, so its centred column rendered
5.5px left of the brand mark, New chat, Settings and the account button —
measured in Chromium as -6px against -0.5px for every other control.
`stable both-edges` reserves symmetrically and holds the column on the
centre line whether or not a scrollbar is showing; the region's `px-3` goes
with it, because two 15px gutters plus that padding left a content box
narrower than a 48px control. Vertical rhythm follows: one 12px gap on the
column replaces four separate `mt-3`s, every group rule is the same recipe
with the same 12px of air on both sides, and a matching rule above the
footer brackets the rail at both ends.

Source drawer: the panel now opens on the cited passage. The front-page
cover thumbnail and the sentence beside it cost the top third of the panel
to say nothing about the page being shown — the thumbnail is the document's
front page and never a render of the cited page, so it needed a caption to
stop it reading as "p. 12", and a drawer opened from the source list paired
it with a sentence whose whole content was that there was no claim. The
support sentence now returns null in that case and renders only when a
claim opened the drawer, where it still states how far the page backs that
claim. A source with no captured passage says so rather than leaving a
panel whose only content is a button.

Verified: full offline unit suite (11722 passed; the 3 failures are a
pre-existing shallow-clone artefact that reproduce on a clean tree),
lint, typecheck, and the Chromium journeys covering both surfaces —
demo answer flow @critical, answer support popups at four widths, the
long-content stress pair, and the four sidebar journeys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fy5PcoANVc6Xj1EE3dLQ5
…qlwy

Answer page: measure the status chips instead of asking them nicely
…ckfill preset, add a stale-docs check (#2486)

* Trim Codex/Cursor-only sections out of AGENTS.md

Every AI session in this repo loads AGENTS.md in full, including three
sections that only apply to Codex or Cursor and were duplicated (in one
case, stale-duplicated) elsewhere already:

- Codex GitHub review behavior was already mirrored verbatim in
  docs/agents/codex-github-review.md, which is the file
  scripts/check-codex-autofix-workflow.mjs actually enforces against the
  live workflow. The AGENTS.md copy had drifted a line out of sync with
  the enforced version.
- Cursor Cloud specific instructions was a short stub of the much fuller
  docs/agents/cursor-cloud.md.
- Reasoning effort calibration (Codex-only: gpt-5.6-sol, .codex/config.toml,
  the Cloud xhigh gate) had no extraction yet; moved to a new
  docs/agents/codex-reasoning-effort.md, following the same
  heading+pointer pattern already used for the dependency shortcut, review
  throttling, and productivity defaults sections.

Net effect: AGENTS.md drops from ~23,000 to ~18,500 tokens with no loss of
content — every fact still exists in a linked file — and Claude Code
sessions (which never act on Codex-specific instructions) stop paying to
load them. Verified: docs link checker, codebase-index coverage, the
codex-autofix-workflow guard, and the five test files that read AGENTS.md
directly (234 tests) all still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdTPCjqdpM73nGVSUDsGp

* Retire one completed backfill preset, add an advisory stale-docs check

Deepened this shallow clone (git fetch --deepen=3000) before drawing any
conclusions from git history, per this repo's own documented lesson about
shallow-clone false signals (the check:dead-code-candidate rationale).
That surfaced that most of a prior "65 unreferenced commands" estimate was
a measurement artifact: checking only "npm run <name>" usage missed every
case where docs call the underlying script directly, which is most of
them. Re-checking both forms drops the real count to 5, and closer
inspection of those found:

- backfill:smart-v2-labels was a one-off preset over classify:documents
  for a migration that the corpus governance data already confirms is
  complete (missing_smart_v2_labels: 0). Removed; classify:documents
  itself is untouched and still does everything the preset did.
- eval:quality:release:offline turned out to be wired to a live release-
  governance acceptance record (docs/release-source-metadata-debt-2026-06-
  30.json) whose acceptance window expires today. Left untouched — that is
  a release-governance decision, not a tidiness one.
- The other three (verify:preflight, verify:release:offline,
  clean:artifacts) had no strong evidence either way and were left alone.

Also adds scripts/check-stale-docs.mjs (check:stale-docs): an advisory-
only report of docs untouched for N days (default 180) whose own path
isn't mentioned anywhere else in the repo. It refuses to run on a shallow
clone rather than report a false picture, and excludes docs/archive,
docs/adr, the immutable ledgers, and docs/superpowers/plans|specs — the
last because two of the three docs it initially flagged there turned out
to have 44 and 22 unchecked tasks respectively, the same "looks dead, has
an unwritten consumer" trap the code-symbol version of this check exists
to catch. It never deletes or fails CI; at --days 30 it currently surfaces
28 real candidates (dated audits, superseded redesign plans) worth a
human's look.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdTPCjqdpM73nGVSUDsGp

* fix(check-stale-docs): Windows-safe root path and relative Markdown link resolution

Address Codex P2 review threads: use fileURLToPath for checkout paths with
spaces/URL-encoding, and resolve Markdown link destinations relative to the
referring file before classifying a doc as unreferenced.

---------

Co-authored-by: Claude <noreply@anthropic.com>
… real edge

The follow-up recorded in PR #2462 and again in #2473: the privacy route's sticky
header read as a white band floating on a white page. Looking at it in a browser
rather than reasoning about the class list turned up a second, worse defect that
no screenshot of the top of the page could show.

**The band was translucent at every width, so scrolled content ghosted through
the title.** Scrolling to 600px put the amber obligation card behind the header,
and its fill and "READ MORE" label read straight through "Data handling". The
repo already has a rule for this: `.edge-glass-header` / `.universal-header` in
globals.css set `background: var(--surface); backdrop-filter: none` under
`max-width: 639px` — "Phone baseline: a full-width opaque header." This page had
never followed it, carrying `--surface-glass`/95 plus `backdrop-blur-xl` at all
widths. It now matches: opaque on phones, the shared 72%-surface glass from `sm`.

**The edge was too light to anchor the band.** In the live v2 token layer
`--background`, `--surface`, `--surface-chrome` and `--surface-raised` are all
`#ffffff`, so no surface tone can separate this band from the page beneath it —
the border and the elevation are the only signals available, and both were at
their faintest: `--border` (#e6ebf2) over `--e1` (5% alpha, 2px blur). Measured
in the browser, not inferred. The edge moves to `--border-strong` (#d3dbe5) and
the lift to `--e2`, which is one step and still below the `--e3`/`--e4` reserved
for hover chrome and modals.

Two corrections to my own earlier reasoning, both caught by measuring:

- I had assumed the page ground was the tinted `--background: #f1f4f8` from
  globals.css and that the band merely needed to match it. It is not: `.ckb-v2`
  is the layer in force and its `--background` is pure white, which is why no
  amount of re-tinting the band would have closed the seam.
- A first attempt moved the band to the shared 72% glass at every width. That is
  the correct value for `sm`+ and made the phone case measurably worse, because
  72% of white is more transparent than the 78% it replaced. The screenshot of
  the scrolled state is what caught it.

Verification
- `npm run verify:phone-chrome` — full gate green. lock-parity, runtime and the
  phone-chrome static/unit contracts pass; `changed-browser` ran the complete
  ui-smoke suite, `105 passed (3.5m)`; `focused-browser` `6 passed`. The gate
  selected focused ownership scope ("sufficient for this page-local scope") and
  did not escalate to full `verify:ui`.
- `tests/ui-smoke.spec.ts` gains a browser-proven assertion, as the chrome
  contract requires for exactly this case: tailwind-merge keeps both the base and
  the `sm:` utility, so stylesheet order rather than the class list decides the
  winner, and jsdom can resolve neither. It asserts a fully opaque phone
  background with no alpha channel, `backdrop-filter: none`, that the H1 is
  genuinely hidden behind the band once scrolled, and that `sm` restores blur and
  a different background.
- That assertion was mutation-tested by reintroducing the original bug verbatim
  (`--surface-glass`/95 + `backdrop-blur-xl` on phones). It failed on the alpha
  channel, reporting the real value `oklab(... / 0.778627)`, then restored.
- Design-system contract passed, every ratchet at or under baseline. Privacy and
  chrome contract units: `88 passed` across 6 files.
- Full unit suite `11770 passed | 1 skipped` across 931 files. Three pre-existing
  failures (`clinical-hazard-controls`, `privacy-readiness-contract`,
  `rag-plan-package-parity`) are unrelated and reproduce on a clean checkout.
- Lint clean at `--max-warnings 0`; typecheck exit 0, unscoped; format committed
  with the work.

RAG impact: no retrieval behaviour change - this diff touches no file under
src/lib/rag/** and none of the protected ranking surfaces (clinical-search,
retrieval-selection, released-search-order, ranking-config, answer-ranking,
answer-verification), adds no comparator key, and leaves the golden fixture and
eval harness untouched. Verified by listing the diff's two paths against that set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRGeyyiZNrXnFJjVVnykgp
BigSimmo and others added 30 commits September 1, 2026 10:48
…2495)

* test(design-sync): give the two subprocess contract tests a load-tolerant timeout

Both shell out to a Node child process. Measured on this worktree: the child
finishes in ~7 s standalone and the tests take ~9 s and ~10 s, so the global
30 s budget looks generous in isolation. It is not under the full run — the node
suite is CPU-bound on two workers across 900+ files, and on 2026-09-01 the
parity test exceeded 30 000 ms inside `npm run test` and failed the gate for a
diff that touched nothing under `.design-sync/**`.

Deliberately not quarantined: it did not fail an assertion, and a wrong time
budget is a different defect from an unstable test. `tests/flake-ledger.json` is
untouched, and the repo's three-reproductions rule does not apply here.

Proven rather than assumed: setting the same option to 1 ms makes the test fail
with "Test timed out in 1ms", so the option is read rather than silently ignored.

Committed with SKIP_DOCS_SYNC_HOOK=1 because the pre-commit generator refuses a
partially staged tree; the sibling commit that follows runs it in full on the
final tree, which is what gets pushed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(developer-hub): wire the environment strip to real environment facts

The strip shipped in Phase 1 with `demoMode`, `documentCount` and `email` all
`null`, rendering "environment unknown · document count unavailable · account
unknown". Phase 2's design deferred them to whichever phase owned each value's
data source and reserved any Supabase read from the hub for its own approval;
that approval was given on 2026-09-01.

`resolveHubEnvironmentFacts` gathers all three in one Supabase round trip. Two
of its rules are load-bearing, and each is pinned by a test proven to fail
without it:

- The count goes through the cookie-bound user client, so row-level security
  (`documents owner read`, `owner_id = auth.uid()`) scopes it to the caller's
  own documents. The service-role admin client bypasses RLS and would report
  every owner's total to whoever was signed in; a source assertion fails if that
  import ever appears.
- Every failure path returns `null`, never `0`, and an unauthenticated request
  skips the query rather than reporting the zero rows RLS would correctly hand
  it. Zero is a true answer here, so a failed read must not be able to
  impersonate an empty corpus.

Reading data makes the page an async Server Component, so the dom tests and the
shared in-page-nav route table now await their fixtures. `next build` classifies
`/mockups/development` as dynamic, which it already was via the layout's
`headers()` call, so nothing regressed there.

The Phase 2 handoff document is corrected here rather than left claiming the
strip is unwired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(developer-hub): degrade the environment strip when a Supabase read rejects

Raised in review of #2495. The helper handled a returned `{ error }` but not a
rejected promise, and the installed client rejects rather than resolves when a
request is aborted or exhausts its network retries. An unhandled rejection there
fails the whole developer hub page instead of degrading one line of it — during
exactly the Supabase outage that makes the page worth opening, and contrary to
the module's own stated contract.

Both awaits are now inside one guard, not just the count: an auth read can fail
the same way. `demoMode` is resolved before either of them and survives both,
because it never depended on the network, so an outage cannot make the strip
claim it is unable to tell demo data from live.

Proven, not assumed: two tests whose mocked `select` and `getUser` reject both
fail without the guard and pass with it, and they assert the whole returned
shape rather than merely that nothing threw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… pin design-system figures and token drift

The app told users four times that it was clinical decision support while its own
privacy page says the opposite. Two design-system documents had also drifted far
enough from the code to mislead, and one silent trap had no gate at all.

Clinical copy (the reason this change exists)

- Differentials (3 sites) and Specifiers (1) said "Clinical decision support only"
  / "Use this as structured decision support". The privacy page and CLAUDE.md both
  scope this product as "a clinical reference prototype, not validated clinical
  decision support", and the PR preflight carries a device-classification item, so
  the clinical surfaces were the ones out of step.
- All four now read "Clinical reference - not validated decision support", the
  wording already recorded as the clinical-owner copy decision on #PM9SP1.
- The fifth site was the copied/exported differential text in
  src/lib/differential-detail.ts, which carried the same claim into anything a
  clinician pastes elsewhere. Its pinned assertion moved with it.
- Therapy Compass was already correct and is untouched.

New gate: token-layer divergence (scripts/token-layer-divergences.mjs)

layout.tsx mounts .ckb-v2 on <html>, so .ckb-v2.ckb-v2 (0,2,0) beats :root (0,1,0)
and for any role both stylesheets declare the v2 value paints while the globals.css
one is dead - editing it changes nothing, with no lint, type or screenshot signal.
53 roles are in that state today (29 light, 24 dark).

The v2 migration is deliberate and unfinished, so divergence is pinned rather than
banned: docs/design-system/token-layer-divergences.json is the reviewed set, and a
role that starts diverging OR stops diverging fails. The comparison covers globals'
@theme block as well as :root, which is where the structural roles live. Wired into
the existing check:design-system-contract, so no gate-manifest change was needed.
Mutation-verified both directions.

New gate: GATES.md figures are generated (scripts/generate-gates-figures.mjs)

GATES.md carried the same metric as three different hand-typed values in three
sections. Measured against the baseline: interactiveTapFloorDeclarations appeared
as 9, 32 and 2 (true value 2); rawMarginLiterals as 74 and 53 (34);
legacyShadowAliases as 231, 224 and 87 (34). Nearly every figure overstated
remaining debt, which costs a session in exactly the way the document's own section
5 warns about.

A generated section 0 is now the single current count, with the prose figures in
sections 2, 4 and 5 explicitly marked as the historical record. Comparison is
normalised cell-wise so Prettier's table padding cannot report false drift.
Mutation-verified both directions.

Breakpoint tokens: reconciled toward the code, not away from it

GATES.md 3b banned --breakpoint-* tokens; globals.css has five, added four days
after that decision. The obvious fix - delete the three device-band names, which
have zero product call sites - was attempted and reverted in the same session:
MIN_WIDTH_BREAKPOINT_BANDS in design-system-contract-utils.mjs hard-codes them as
same-threshold aliases of sm/md/lg, and they are the only fixtures exercising the
tap-floor gate's alias-collision path. "Nothing imports it" was necessary and
nowhere near sufficient, exactly as AGENTS.md warns. All five stay, with the real
reason recorded in both the stylesheet and 3b, and the wrong "(Task #336)"
attribution removed.

AGENTS.md bundle-budget route list

Documented five budgeted routes; the config has two. The config is right:
/therapy-compass, /dsm and /forms became redirect stubs rendering the same shared
home as /, so budgeting them measured / three more times (see
tests/check-lighthouse-budget.test.ts, COMMITTED_ROUTES). Corrected the document
and recorded why, so the next reader does not "fix" the config back.

Also: indexed the one mockup route missing from mockups/README.md
(answer-loading-redesign), taking that index to 79 of 79.

Verification

npm run verify:cheap exit 0. Full unit suite 934 files / 11,771 tests passed, 1
skipped, 0 failures. lint and typecheck clean. Clean-build bundle check passed
(production 1742.1 KiB gzip, / at 240.0 KiB); it reports a +5.2% drift warning
against a baseline commit that no longer resolves, which is pre-existing and
tracked as #QSHHGK, not caused here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL
…app-review-sxd0mm

# Conflicts:
#	data/outstanding-issues-snapshot.json
#	data/repo-awareness-snapshot.json
Resolve conflicts:
- data/outstanding-issues-snapshot.json: keep main (superset pending incl. privacy inbox items)
- data/repo-awareness-snapshot.json: keep main (includes docs landed on main)
- package.json: keep main deps/engines; take PR design-system gate scripts
Codex P2: reject hand-edited/merge-skewed counts when divergences are unchanged.
…les in the new token-divergence gate

Two independent audits ran against the previous commit. Both found real defects,
and both are fixed here.

Clinical over-claim sweep — two more surfaces

The earlier fix found its sites by grepping one phrase. A sweep for the whole
class found two it missed, both on the medication/prescribing surface, which is
the highest-consequence screen in the app:

- `patient-profile-panel.tsx` (rendered by the prescribing workspace, the
  medication record page, and the patient-details dock) said "Decision support,
  not medical advice" under the panel where a clinician enters renal/hepatic
  function, allergies and a current medication list to get a per-patient
  interaction verdict. This asserted the feature IS decision support — worse than
  the five sites already corrected, which merely said "not ... decision support".
- `medication-record-page.tsx`'s standing footer claimed PsychSift "provides
  evidence summaries", an authority claim the product does not make anywhere
  else, and disclaimed with a generic "not medical advice" rather than the
  house governance phrasing every comparable mode uses.

Both now match the wording used on the other surfaces. Production is clean: the
only remaining "decision support" strings in src are the document-intent taxonomy
(classifying UPLOADED guideline documents, not the app's own voice) and the
privacy page's own boundary statement.

Token-divergence gate — four holes, three of them live

An adversarial review of the gate added in the previous commit demonstrated four
false passes. The parser has been rewritten:

1. **Forced colours was unmonitored.** Both stylesheets declare a third theme in
   `@media (forced-colors: active)`, and the same specificity trap applies there.
   Editing a real Windows High Contrast value produced no complaint. Three roles
   are genuinely diverging in it today and are now pinned:
   `--clinical-accent-border`, `--overlay-backdrop`, `--text-soft`.
2. **Indentation-sensitive parsing.** The old regex required exactly two leading
   spaces, so re-indenting a declaration — no rendered effect — dropped it from
   the comparison, and the tool then reported the divergence as RESOLVED. Acting
   on that advice removed a still-dead token from tracking and left the gate
   green. Parsing is now indentation-insensitive.
3. **`var()` aliases were never resolved.** Two layers can declare identical alias
   text and still paint different colours when the alias itself diverges: dark
   `--clinical-chat-document` is `var(--surface-inset)` on both sides while
   `--surface-inset` differs. Values are now resolved within their own layer
   before comparison, which also removes three false FAILS where different text
   resolved to the same value (dark `--text`, `--text-heading`,
   `--clinical-accent-soft`).
4. **Ambiguous markers in GATES.md** silently retargeted both the comparison and
   the `--write` overwrite at the wrong slice. Now a hard error.

Block extraction uses real brace matching instead of slicing to the next
line-initial `}`, so an `@media` wrapper is context rather than a terminator, and
comments are stripped before selectors are read. An empty result for any theme is
a hard error, never a quiet pass.

Counts move from light 29 / dark 24 to light 29 / dark 22 / forcedColors 3 — the
dark drop is the three false fails above, the forced-colours three are newly
visible real divergences.

One review claim was checked and NOT adopted: the review reported forced-colours
`--border`/`--border-strong`/`--border-lux` as diverging. globals.css declares
them twice in that media context and the later value already matches v2, so they
are not divergent; the manual diff had only read the first block.

Verification

Full unit suite 937 files / 11,796 tests passed, 1 skipped, 0 failures. lint,
typecheck and the design-system contract chain all clean. Each of the four holes
was re-attacked against the hardened code: the forced-colours edit and the
duplicated marker now fail as they should, and the re-indented declaration is
proven still tracked rather than silently dropped.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL
…sxd0mm' into claude/design-system-app-review-sxd0mm

# Conflicts:
#	data/outstanding-issues-snapshot.json
#	data/repo-awareness-snapshot.json
…, and pin two drifting design-system documents (#2497)

* fix(clinical): align decision-support copy with the product boundary; pin design-system figures and token drift

The app told users four times that it was clinical decision support while its own
privacy page says the opposite. Two design-system documents had also drifted far
enough from the code to mislead, and one silent trap had no gate at all.

Clinical copy (the reason this change exists)

- Differentials (3 sites) and Specifiers (1) said "Clinical decision support only"
  / "Use this as structured decision support". The privacy page and CLAUDE.md both
  scope this product as "a clinical reference prototype, not validated clinical
  decision support", and the PR preflight carries a device-classification item, so
  the clinical surfaces were the ones out of step.
- All four now read "Clinical reference - not validated decision support", the
  wording already recorded as the clinical-owner copy decision on #PM9SP1.
- The fifth site was the copied/exported differential text in
  src/lib/differential-detail.ts, which carried the same claim into anything a
  clinician pastes elsewhere. Its pinned assertion moved with it.
- Therapy Compass was already correct and is untouched.

New gate: token-layer divergence (scripts/token-layer-divergences.mjs)

layout.tsx mounts .ckb-v2 on <html>, so .ckb-v2.ckb-v2 (0,2,0) beats :root (0,1,0)
and for any role both stylesheets declare the v2 value paints while the globals.css
one is dead - editing it changes nothing, with no lint, type or screenshot signal.
53 roles are in that state today (29 light, 24 dark).

The v2 migration is deliberate and unfinished, so divergence is pinned rather than
banned: docs/design-system/token-layer-divergences.json is the reviewed set, and a
role that starts diverging OR stops diverging fails. The comparison covers globals'
@theme block as well as :root, which is where the structural roles live. Wired into
the existing check:design-system-contract, so no gate-manifest change was needed.
Mutation-verified both directions.

New gate: GATES.md figures are generated (scripts/generate-gates-figures.mjs)

GATES.md carried the same metric as three different hand-typed values in three
sections. Measured against the baseline: interactiveTapFloorDeclarations appeared
as 9, 32 and 2 (true value 2); rawMarginLiterals as 74 and 53 (34);
legacyShadowAliases as 231, 224 and 87 (34). Nearly every figure overstated
remaining debt, which costs a session in exactly the way the document's own section
5 warns about.

A generated section 0 is now the single current count, with the prose figures in
sections 2, 4 and 5 explicitly marked as the historical record. Comparison is
normalised cell-wise so Prettier's table padding cannot report false drift.
Mutation-verified both directions.

Breakpoint tokens: reconciled toward the code, not away from it

GATES.md 3b banned --breakpoint-* tokens; globals.css has five, added four days
after that decision. The obvious fix - delete the three device-band names, which
have zero product call sites - was attempted and reverted in the same session:
MIN_WIDTH_BREAKPOINT_BANDS in design-system-contract-utils.mjs hard-codes them as
same-threshold aliases of sm/md/lg, and they are the only fixtures exercising the
tap-floor gate's alias-collision path. "Nothing imports it" was necessary and
nowhere near sufficient, exactly as AGENTS.md warns. All five stay, with the real
reason recorded in both the stylesheet and 3b, and the wrong "(Task #336)"
attribution removed.

AGENTS.md bundle-budget route list

Documented five budgeted routes; the config has two. The config is right:
/therapy-compass, /dsm and /forms became redirect stubs rendering the same shared
home as /, so budgeting them measured / three more times (see
tests/check-lighthouse-budget.test.ts, COMMITTED_ROUTES). Corrected the document
and recorded why, so the next reader does not "fix" the config back.

Also: indexed the one mockup route missing from mockups/README.md
(answer-loading-redesign), taking that index to 79 of 79.

Verification

npm run verify:cheap exit 0. Full unit suite 934 files / 11,771 tests passed, 1
skipped, 0 failures. lint and typecheck clean. Clean-build bundle check passed
(production 1742.1 KiB gzip, / at 240.0 KiB); it reports a +5.2% drift warning
against a baseline commit that no longer resolves, which is pre-existing and
tracked as #QSHHGK, not caused here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL

* fix(design-system): pin token-layer counts metadata against divergences

Codex P2: reject hand-edited/merge-skewed counts when divergences are unchanged.

* docs: refresh scripts-index inventory counts for two new design-system scripts

---------

Co-authored-by: Claude <noreply@anthropic.com>
…app-review-sxd0mm

# Conflicts:
#	docs/design-system/token-layer-divergences.json
#	scripts/generate-gates-figures.mjs
#	scripts/token-layer-divergences.mjs
* test(design-sync): give the two subprocess contract tests a load-tolerant timeout

Both shell out to a Node child process. Measured on this worktree: the child
finishes in ~7 s standalone and the tests take ~9 s and ~10 s, so the global
30 s budget looks generous in isolation. It is not under the full run — the node
suite is CPU-bound on two workers across 900+ files, and on 2026-09-01 the
parity test exceeded 30 000 ms inside `npm run test` and failed the gate for a
diff that touched nothing under `.design-sync/**`.

Deliberately not quarantined: it did not fail an assertion, and a wrong time
budget is a different defect from an unstable test. `tests/flake-ledger.json` is
untouched, and the repo's three-reproductions rule does not apply here.

Proven rather than assumed: setting the same option to 1 ms makes the test fail
with "Test timed out in 1ms", so the option is read rather than silently ignored.

Committed with SKIP_DOCS_SYNC_HOOK=1 because the pre-commit generator refuses a
partially staged tree; the sibling commit that follows runs it in full on the
final tree, which is what gets pushed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(developer-hub): wire the environment strip to real environment facts

The strip shipped in Phase 1 with `demoMode`, `documentCount` and `email` all
`null`, rendering "environment unknown · document count unavailable · account
unknown". Phase 2's design deferred them to whichever phase owned each value's
data source and reserved any Supabase read from the hub for its own approval;
that approval was given on 2026-09-01.

`resolveHubEnvironmentFacts` gathers all three in one Supabase round trip. Two
of its rules are load-bearing, and each is pinned by a test proven to fail
without it:

- The count goes through the cookie-bound user client, so row-level security
  (`documents owner read`, `owner_id = auth.uid()`) scopes it to the caller's
  own documents. The service-role admin client bypasses RLS and would report
  every owner's total to whoever was signed in; a source assertion fails if that
  import ever appears.
- Every failure path returns `null`, never `0`, and an unauthenticated request
  skips the query rather than reporting the zero rows RLS would correctly hand
  it. Zero is a true answer here, so a failed read must not be able to
  impersonate an empty corpus.

Reading data makes the page an async Server Component, so the dom tests and the
shared in-page-nav route table now await their fixtures. `next build` classifies
`/mockups/development` as dynamic, which it already was via the layout's
`headers()` call, so nothing regressed there.

The Phase 2 handoff document is corrected here rather than left claiming the
strip is unwired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(developer-hub): degrade the environment strip when a Supabase read rejects

Raised in review of #2495. The helper handled a returned `{ error }` but not a
rejected promise, and the installed client rejects rather than resolves when a
request is aborted or exhausts its network retries. An unhandled rejection there
fails the whole developer hub page instead of degrading one line of it — during
exactly the Supabase outage that makes the page worth opening, and contrary to
the module's own stated contract.

Both awaits are now inside one guard, not just the count: an auth read can fail
the same way. `demoMode` is resolved before either of them and survives both,
because it never depended on the network, so an outage cannot make the strip
claim it is unable to tell demo data from live.

Proven, not assumed: two tests whose mocked `select` and `getUser` reject both
fail without the guard and pass with it, and they assert the whole returned
shape rather than merely that nothing threw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(developer-hub): surface clinical answer failures the ledger already records

The task ledger already records which clinical questions the answer engine gets
wrong -- today, three open items naming five questions between them, including
antipsychotic metabolic monitoring returning a placeholder instead of an answer.
Those facts sit unread among eighty-one open items. This puts them on the hub.

`resolveClinicalAnswerFailures` intersects two records the repository already
maintains: open ledger items, and the eval case list that names each clinical
question. Derived rather than curated on purpose -- a hand-kept list of ledger
ids is exact the day it is written and wrong a month later, with nothing to
catch the drift.

Matching is whole-token, and that is the load-bearing part rather than a detail.
Case ids nest: `discharge-documentation` and `patient-safety-plan` are both real
cases AND substrings of other real ones, so a plain `includes` reported five
questions where three were real -- two clinical questions on screen that nothing
had reported as broken. Proven by mutation: reverting the matcher to `includes`
turns both the synthetic nesting test and the real-data property test red.

The panel names itself for its evidence ("Open problems recorded against a named
clinical question"), and states above the list that a recorded problem which
does not name a case is not shown. On a clinical surface the reassuring-looking
empty state is the dangerous one, so the empty branch says in words that a quiet
ledger is not a well answer engine. Both are pinned by tests.

A hub band links straight to the panel, in warning rather than danger colours:
these are recorded problems, not blocking work, and painting them the same red
as the blocking band would teach the reader to discount both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(developer-hub): a named eval case is a reference, not a verdict on that case

Raised in review of #2498, and confirmed against the real ledger text rather than
taken on trust. `#J8SJQ9` is about `quality-antipsychotic-metabolic-monitoring`
and names `quality-discharge-documentation` only as the CONTRAST -- the case that
"deliberately drops mustContainAny" because a source pointer is a legitimate
answer there. The panel listed it among the questions and counted it, stating on
a clinical surface the opposite of what the ledger says.

Two tempting fixes were tried against the real data and rejected:

- Matching only the `source` field fixes `#J8SJQ9` and HIDES both genuinely
  broken questions in `#S4R2W3`, which names them only in its detail prose. On
  this panel a false negative is worse than a loose one.
- Guessing intent from surrounding words is the fragile heuristic the panel
  exists to avoid; it would be wrong quietly, later.

So the assertion moves to the level the data supports. The item-level claim was
always sound -- these are recorded problems about clinical answers -- and the
named cases are now presented as references: the section reads "Questions this
item names", the count is "questions referenced" (`affectedQuestionCount` becomes
`referencedQuestionCount`), and a second caveat says in words that an item may
name a question as the contrast that is behaving correctly.

Tests: the reviewer's requested fixture reproduces the real `#J8SJQ9` detail and
pins that BOTH cases come back as references, with a comment saying a later
change may only narrow that via an explicit per-case association in the ledger,
never a heuristic. A DOM test pins the wording, and fails when the caveat is
removed -- proven by mutation, then restored byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Resolve dirty/behind after #2498: keep main regenerated snapshots and deps;
overlay PR intentional clinical copy + token-divergence gate hardening.
…base theme comparison

Codex P2 on #2499, verified and confirmed. `collect()` filtered only on
`forced-colors`, so every OTHER `@media` block was admitted into the
unconditional theme map and its override replaced the base declaration.

Comparing a narrow-viewport override against an unconditional v2 declaration
compares two different contexts, and it reports "identical" for a pair that
diverges everywhere the condition does not apply.

Proven on real data, not argued: before this change the light compat map held
`--mode-home-copy-reserve` as the `(min-width: 412px)` override
(`calc(var(--text-hero) * …)`) rather than its unconditional declaration at
globals.css:1240 (`calc(2 * var(--text-hero) * …)`). globals.css has three such
`:root` blocks today — `--mode-home-copy-reserve` twice and
`--spacing-mode-home-composer-wide` once.

Reproduced as a false pass with the synthetic wide-viewport case the review asked
for: base compat `--radius-md: 0.7rem`, an override at `(min-width: 9999px)`
matching v2's `0.625rem`, v2 unconditionally `0.625rem`. The old predicate
reported no divergence; the new one flags it. Latent rather than live today only
because the v2 layer declares none of the three affected names.

Base themes now take unconditional blocks only. Conditional non-forced-colors
blocks are excluded rather than merged, which is the conservative direction: a
token declared only under such a condition goes uncompared instead of being
compared wrongly. Forced colours remains its own theme — it is a condition, and
it is modelled as one rather than treated as base.

Also fixes the return typing of `readLayers`, which inferred `{}` and broke
`npm run typecheck` for the new test's consumers.

Verification

Full unit suite 937 files / 11,798 tests passed, 1 skipped, 0 failures. lint,
typecheck and the design-system contract chain clean; pin unchanged at light 29 /
dark 22 / forcedColors 3, confirming this was latent. The new regression test is
mutation-verified: restoring the old predicate fails it, and it asserts against
the real stylesheet rather than a fixture, so it tracks the file as it changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL
…sxd0mm' into claude/design-system-app-review-sxd0mm
…lose four holes in the new token-divergence gate (#2499)

* fix(clinical): align decision-support copy with the product boundary; pin design-system figures and token drift

The app told users four times that it was clinical decision support while its own
privacy page says the opposite. Two design-system documents had also drifted far
enough from the code to mislead, and one silent trap had no gate at all.

Clinical copy (the reason this change exists)

- Differentials (3 sites) and Specifiers (1) said "Clinical decision support only"
  / "Use this as structured decision support". The privacy page and CLAUDE.md both
  scope this product as "a clinical reference prototype, not validated clinical
  decision support", and the PR preflight carries a device-classification item, so
  the clinical surfaces were the ones out of step.
- All four now read "Clinical reference - not validated decision support", the
  wording already recorded as the clinical-owner copy decision on #PM9SP1.
- The fifth site was the copied/exported differential text in
  src/lib/differential-detail.ts, which carried the same claim into anything a
  clinician pastes elsewhere. Its pinned assertion moved with it.
- Therapy Compass was already correct and is untouched.

New gate: token-layer divergence (scripts/token-layer-divergences.mjs)

layout.tsx mounts .ckb-v2 on <html>, so .ckb-v2.ckb-v2 (0,2,0) beats :root (0,1,0)
and for any role both stylesheets declare the v2 value paints while the globals.css
one is dead - editing it changes nothing, with no lint, type or screenshot signal.
53 roles are in that state today (29 light, 24 dark).

The v2 migration is deliberate and unfinished, so divergence is pinned rather than
banned: docs/design-system/token-layer-divergences.json is the reviewed set, and a
role that starts diverging OR stops diverging fails. The comparison covers globals'
@theme block as well as :root, which is where the structural roles live. Wired into
the existing check:design-system-contract, so no gate-manifest change was needed.
Mutation-verified both directions.

New gate: GATES.md figures are generated (scripts/generate-gates-figures.mjs)

GATES.md carried the same metric as three different hand-typed values in three
sections. Measured against the baseline: interactiveTapFloorDeclarations appeared
as 9, 32 and 2 (true value 2); rawMarginLiterals as 74 and 53 (34);
legacyShadowAliases as 231, 224 and 87 (34). Nearly every figure overstated
remaining debt, which costs a session in exactly the way the document's own section
5 warns about.

A generated section 0 is now the single current count, with the prose figures in
sections 2, 4 and 5 explicitly marked as the historical record. Comparison is
normalised cell-wise so Prettier's table padding cannot report false drift.
Mutation-verified both directions.

Breakpoint tokens: reconciled toward the code, not away from it

GATES.md 3b banned --breakpoint-* tokens; globals.css has five, added four days
after that decision. The obvious fix - delete the three device-band names, which
have zero product call sites - was attempted and reverted in the same session:
MIN_WIDTH_BREAKPOINT_BANDS in design-system-contract-utils.mjs hard-codes them as
same-threshold aliases of sm/md/lg, and they are the only fixtures exercising the
tap-floor gate's alias-collision path. "Nothing imports it" was necessary and
nowhere near sufficient, exactly as AGENTS.md warns. All five stay, with the real
reason recorded in both the stylesheet and 3b, and the wrong "(Task #336)"
attribution removed.

AGENTS.md bundle-budget route list

Documented five budgeted routes; the config has two. The config is right:
/therapy-compass, /dsm and /forms became redirect stubs rendering the same shared
home as /, so budgeting them measured / three more times (see
tests/check-lighthouse-budget.test.ts, COMMITTED_ROUTES). Corrected the document
and recorded why, so the next reader does not "fix" the config back.

Also: indexed the one mockup route missing from mockups/README.md
(answer-loading-redesign), taking that index to 79 of 79.

Verification

npm run verify:cheap exit 0. Full unit suite 934 files / 11,771 tests passed, 1
skipped, 0 failures. lint and typecheck clean. Clean-build bundle check passed
(production 1742.1 KiB gzip, / at 240.0 KiB); it reports a +5.2% drift warning
against a baseline commit that no longer resolves, which is pre-existing and
tracked as #QSHHGK, not caused here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL

* fix(design-system): pin token-layer counts metadata against divergences

Codex P2: reject hand-edited/merge-skewed counts when divergences are unchanged.

* docs: refresh scripts-index inventory counts for two new design-system scripts

* fix(clinical): correct two more over-claiming surfaces; close four holes in the new token-divergence gate

Two independent audits ran against the previous commit. Both found real defects,
and both are fixed here.

Clinical over-claim sweep — two more surfaces

The earlier fix found its sites by grepping one phrase. A sweep for the whole
class found two it missed, both on the medication/prescribing surface, which is
the highest-consequence screen in the app:

- `patient-profile-panel.tsx` (rendered by the prescribing workspace, the
  medication record page, and the patient-details dock) said "Decision support,
  not medical advice" under the panel where a clinician enters renal/hepatic
  function, allergies and a current medication list to get a per-patient
  interaction verdict. This asserted the feature IS decision support — worse than
  the five sites already corrected, which merely said "not ... decision support".
- `medication-record-page.tsx`'s standing footer claimed PsychSift "provides
  evidence summaries", an authority claim the product does not make anywhere
  else, and disclaimed with a generic "not medical advice" rather than the
  house governance phrasing every comparable mode uses.

Both now match the wording used on the other surfaces. Production is clean: the
only remaining "decision support" strings in src are the document-intent taxonomy
(classifying UPLOADED guideline documents, not the app's own voice) and the
privacy page's own boundary statement.

Token-divergence gate — four holes, three of them live

An adversarial review of the gate added in the previous commit demonstrated four
false passes. The parser has been rewritten:

1. **Forced colours was unmonitored.** Both stylesheets declare a third theme in
   `@media (forced-colors: active)`, and the same specificity trap applies there.
   Editing a real Windows High Contrast value produced no complaint. Three roles
   are genuinely diverging in it today and are now pinned:
   `--clinical-accent-border`, `--overlay-backdrop`, `--text-soft`.
2. **Indentation-sensitive parsing.** The old regex required exactly two leading
   spaces, so re-indenting a declaration — no rendered effect — dropped it from
   the comparison, and the tool then reported the divergence as RESOLVED. Acting
   on that advice removed a still-dead token from tracking and left the gate
   green. Parsing is now indentation-insensitive.
3. **`var()` aliases were never resolved.** Two layers can declare identical alias
   text and still paint different colours when the alias itself diverges: dark
   `--clinical-chat-document` is `var(--surface-inset)` on both sides while
   `--surface-inset` differs. Values are now resolved within their own layer
   before comparison, which also removes three false FAILS where different text
   resolved to the same value (dark `--text`, `--text-heading`,
   `--clinical-accent-soft`).
4. **Ambiguous markers in GATES.md** silently retargeted both the comparison and
   the `--write` overwrite at the wrong slice. Now a hard error.

Block extraction uses real brace matching instead of slicing to the next
line-initial `}`, so an `@media` wrapper is context rather than a terminator, and
comments are stripped before selectors are read. An empty result for any theme is
a hard error, never a quiet pass.

Counts move from light 29 / dark 24 to light 29 / dark 22 / forcedColors 3 — the
dark drop is the three false fails above, the forced-colours three are newly
visible real divergences.

One review claim was checked and NOT adopted: the review reported forced-colours
`--border`/`--border-strong`/`--border-lux` as diverging. globals.css declares
them twice in that media context and the later value already matches v2, so they
are not divergent; the manual diff had only read the first block.

Verification

Full unit suite 937 files / 11,796 tests passed, 1 skipped, 0 failures. lint,
typecheck and the design-system contract chain all clean. Each of the four holes
was re-attacked against the hardened code: the forced-colours edit and the
duplicated marker now fail as they should, and the re-indented declaration is
proven still tracked rather than silently dropped.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL

* fix(design-system): conditional media blocks no longer leak into the base theme comparison

Codex P2 on #2499, verified and confirmed. `collect()` filtered only on
`forced-colors`, so every OTHER `@media` block was admitted into the
unconditional theme map and its override replaced the base declaration.

Comparing a narrow-viewport override against an unconditional v2 declaration
compares two different contexts, and it reports "identical" for a pair that
diverges everywhere the condition does not apply.

Proven on real data, not argued: before this change the light compat map held
`--mode-home-copy-reserve` as the `(min-width: 412px)` override
(`calc(var(--text-hero) * …)`) rather than its unconditional declaration at
globals.css:1240 (`calc(2 * var(--text-hero) * …)`). globals.css has three such
`:root` blocks today — `--mode-home-copy-reserve` twice and
`--spacing-mode-home-composer-wide` once.

Reproduced as a false pass with the synthetic wide-viewport case the review asked
for: base compat `--radius-md: 0.7rem`, an override at `(min-width: 9999px)`
matching v2's `0.625rem`, v2 unconditionally `0.625rem`. The old predicate
reported no divergence; the new one flags it. Latent rather than live today only
because the v2 layer declares none of the three affected names.

Base themes now take unconditional blocks only. Conditional non-forced-colors
blocks are excluded rather than merged, which is the conservative direction: a
token declared only under such a condition goes uncompared instead of being
compared wrongly. Forced colours remains its own theme — it is a condition, and
it is modelled as one rather than treated as base.

Also fixes the return typing of `readLayers`, which inferred `{}` and broke
`npm run typecheck` for the new test's consumers.

Verification

Full unit suite 937 files / 11,798 tests passed, 1 skipped, 0 failures. lint,
typecheck and the design-system contract chain clean; pin unchanged at light 29 /
dark 22 / forcedColors 3, confirming this was latent. The new regression test is
mutation-verified: restoring the old predicate fails it, and it asserts against
the real stylesheet rather than a fixture, so it tracks the file as it changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL

---------

Co-authored-by: Claude <noreply@anthropic.com>
… measured bundle drift on #QSHHGK

- done #PM9SP1: the decision-support over-claim is fixed at seven sites across
  PRs #2497 and #2499, two of them (the prescribing patient-profile panel and
  the medication record footer) not named in the original row and worse than
  those that were. Production verified clean.
- add: the row's second clause, 'mount the footer on the other mode homes',
  contradicts a recorded decision at mode-home-template.tsx:216-219. Filed for
  an owner ruling rather than carried silently into the close.
- update #QSHHGK: clean-build measurement puts production at +5.2% (drift
  warning, over half way to the 10% ceiling), and the recorded baselineSource
  is unreachable even on a full 5415-commit clone, so the distance cannot be
  attributed to any reviewed change set.

Requests only; docs/outstanding-issues.md is untouched and reconciliation runs
on its own fresh-base branch.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL
Two pending done-mutations on #PM9SP1 made reconciliation ambiguous, which the
inbox correctly refuses to guess at. 44e5f87c (queued 2026-08-27 by PR #2448)
records only the Therapy Compass relabel; c772a1b8 records the same close plus
the five further sites the 2026-09-01 sweep found, including the prescribing
patient-profile panel and the medication record footer.

Cancelling the narrower one so the archived outcome describes everything that
was wrong rather than a subset. Nothing is lost - the surviving outcome states
Therapy Compass had already been corrected.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL
The previous close outcome asserted that the only remaining 'decision support'
strings under src/ were the document-intent taxonomy and the privacy page. That
is literally false: the agreed replacement wording is 'not validated decision
support', so all seven corrected sites still contain the phrase. A re-grep found
three further categories the claim omitted - the document-intent filter
placeholder, four negative guidance strings in calculator-fixtures.ts, and one
in hub-panels.ts.

Every occurrence is benign and the conclusion is unchanged: the app makes no
positive decision-support claim in its own voice. But an archived row must not
carry a false inventory, because a future auditor grepping the phrase will find
hits and conclude the sweep was unreliable. The replacement enumerates all four
categories and says to grep for positive claims rather than the bare phrase.

Replaced rather than edited, per the inbox immutability rule.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uQ7pGjA4hBxi9TRNsfjAL
…e they were tapped (#2500)

* fix(ui/answer): one cited count, and a follow-up that is a sentence

Two defects found by opening the answer page at 390px on the running app.

**The cited count was printed twice.** "2 cited" sat beside the support
chip and again on the source rail's heading 160px below, both visible in
one glance on a phone. The rail's is the better of the two: it reads
"2 cited · 1 also found" and is the only thing that explains why an
uncited card carries a dash instead of a number. The status-line copy is
gone, and with it `AnswerCard`'s `metaTrailing` prop, which had no other
consumer.

**A gap follow-up could not be a sentence.** `gapFollowUpTemplates` built
a question by wrapping a reported gap in `What does the source say about
<message lowercased>?`. Every message `detectConflictsOrGaps` writes is a
full advisory sentence, so that construction cannot produce English; the
live page showed "What does the source say about current evidence comes
from one document; broaden document scope if you need cross-document
comparison?", and the threshold-conflict message, being two sentences,
came out worse still. A gap is now offered verbatim only when it is
already a question.

Nothing is lost, but only because of the second half of this change. The
authored `source_gap` template asks the same thing properly — "What does
the indexed guidance not cover for <topic>?" — and it lived in the
`management` menu alone, so on a `dosing`, `escalation`, `threshold` or
`comparison` query the gap would simply have gone unmentioned. It is now
offered from the builder for every menu key, and still first, which is
where reported gaps have always ranked. That also repairs the
`source_gap` suppression: it tested `suggestions.length > 0`, which meant
"a gap already asked for itself" only while gaps ran first and alone.

The test that pinned the old wording pinned the bug — it asserted "What
does the source say about paediatric dosing is not covered?" — so it now
asserts the property instead: no suggestion carries sentence punctuation
before its own question mark.

* fix(ui/answer): count each safety passage once, and put disclosures where they were tapped

Four defects found by working the answer page at 390px on the running app.

**A safety passage could be counted twice.** `extractSafetyFindings`
draws candidates from `quoteCards` AND `sources`, and a quote card is an
extract of its own parent chunk — same document, same page, its text a
substring of the chunk's. The dedupe key was the text itself, so two
lengths of one passage were two different keys and both survived; they
could also carry different labels, because `safetyPatterns.find` returns
the first pattern a text matches and the longer text reaches severities
the extract does not. The live clozapine answer read "3 safety notes"
over two passages, the first two of them the same words under "Red flag"
and "Monitoring". It now reads 2.

`collapseDuplicateSafetyFindings` keeps the fuller text and the most
severe label of the group, and requires 40 characters of overlap before
containment counts as one passage, so a stray fragment cannot swallow a
finding it merely appears inside. It runs on both paths into the module —
fresh extraction and an answer that arrives with `safetyWarnings` already
computed — so a future producer of those warnings cannot reintroduce the
double count. The gather loop also collects past the display limit before
collapsing, so a duplicate can no longer crowd out a distinct finding.

**The safety sheet buried the finding under its own chrome.** The row led
with a kind pill, the source link and a governance pill, which at 390px
wrapped to three lines and put ~110px above the first word. The row now
reads kind, then finding, then source: the kind is an eyebrow beside its
icon rather than a pill saying what the icon already said, the governance
chip sits on that same line, and the text starts where the reader looks.

**Report a problem opened behind the composer.** Measured at 390x844, the
panel's last two options sat under the fixed composer and nothing moved,
so the list read as ending at "Outdated". It now scrolls itself clear on
open — centred when it fits, top-anchored when it does not — through
`resolveScrollBehavior`, so the in-app reduce-motion setting still
suppresses the animation.

**Evidence gaps opened ~450px from the chip that opened them.** The panel
rendered after the whole card, prose and source rail included, which on a
phone is off-screen: tapping the chip looked like nothing happening. It
now renders directly beneath the chips, through a new `metaDetail` slot
on `AnswerCard`, and the chip shows an open state instead of looking
identical either way. The panel is also mounted while collapsed, so
`aria-controls` points at an id that always resolves rather than being
conditionally dropped to work around the same problem.

* revert(ui/answer): drop the report-a-problem auto-scroll

It broke the phone chrome. Scrolling the panel into view is necessarily a
downward scroll, a downward scroll is what hides the phone composer, and
closing the panel then shrinks the page back to the top without producing
the upward travel `useScrollHideReporter` needs to reveal it again — so
the composer stays gone at the top of the page. `ui-smoke`'s critical
answer journey failed on exactly that (`data-scroll-hidden="true"` on
`form.answer-footer-search-dock` several steps later), and disabling only
this effect made it pass, so the attribution is measured rather than
assumed. `block: "nearest"` plus a scroll margin did not help: the panel
is genuinely obscured when it opens, so the scroll still happens.

What the panel actually needs is not to move the page at all. Its options
were never unreachable — measured at 390x844 the last one clears the
composer by 180px at full scroll — the fault is that the list LOOKS
complete where the composer cuts it. That wants the sheet treatment the
safety findings already use, which sidesteps the fixed chrome entirely,
and that is a design change to put to the owner rather than to slip in
behind a revert. Comment left at the site so the next author does not
re-derive the trap.

The other three fixes in this branch are untouched.

* fix(answer): tighten the safety collapse and stop the gap chip crowding the menu

Six defects found by three specialist reviewers reading this branch before
it was pushed. All six are in code added earlier on this branch.

**The safety collapse could disagree with itself.** One pass merges into
the FIRST passage-key match, so a finding containing two already-kept ones
landed on the first and left the second nested inside it. This function
runs twice on the same data — server-side into the payload, then again on
the client — so a pass short of a fixed point renders "2 safety notes"
before hydration and "1" after. It now iterates to a fixed point, bounded
by the input length since every effective pass removes at least one item.

**A short quote still double-counted against its own parent chunk.** The
40-character floor was applied to every containment, but a quote card
carries its parent chunk's `chunk_id`, so containment there is proof of one
passage however short the extract. The floor now applies only across
chunks, which is the case it was written for; same-chunk containment
merges outright. This was the original defect escaping through its own fix.

**A merged finding could contradict its own id.** The id encodes the kind,
and a merge that takes one finding's text and another's severity kept a
`monitoring:` id on a row labelled "Red flag". Rebuilt on merge.

**The gap follow-up could restate a section the answer already emitted.**
`source_gap` is a real emitted section kind — `rag.ts` maps gap, missing
and unclear headings onto it — and offering the question outside the menu
loop skipped that loop's `emittedSectionKinds` guard. The guard now applies
to the direct offer too.

**The gap follow-up displaced a concrete clinical question.** Offered
first, on every menu, it took a slot from the four-chip cap: traced on a
gapped `medication_dose_risk` answer it dropped "How is lithium dosed in
renal or hepatic impairment?" for a meta-question about coverage. It is now
offered last and only into a spare slot. Nothing is hidden by that — the
gap's own words already render verbatim as a caveat — and the cheaper thing
to lose is the meta-question.

**Two layout faults.** A citation carrying two governance labels gave each
its own `ms-auto`, which splits the free space between them so neither ends
up flush right; they now share one. And the evidence-gaps disclosure
rendered above the retrieval banner, pushing a caution that names which
sources are stale and how much was missed ~200px down at 390px when opened;
the disclosure the reader asked for now sits below the warning they did not.

Tests: four added (same-chunk collapse under the floor, fixed-point
convergence, the emitted-section suppression, and an unsupported topic),
and the cap assertions restored to exact ordering — the previous
`toBeLessThanOrEqual(4)` passed with a single suggestion and pinned nothing.

* fix(answer): do not call a conflict a coverage gap

`hasReportedGap` is true for `type: "conflict"` entries as well as gaps,
and the authored gap question says the wrong thing about a conflict. When
`detectConflictsOrGaps` reports that sources disagree on a withholding
threshold, that answer HAS coverage — from several sources — and the
problem is that they contradict each other. Offering "What does the
indexed guidance not cover for X?" there misstates the evidence and points
the clinician at the wrong follow-up.

The flaw predates this branch: `source_gap`'s evidence gate has always
been `reported_gap`, which is the same conflated flag. What this branch
did was widen its reach from one menu to every menu, which turns a rare
mis-wording into a routine one — so it is fixed here rather than deferred.

The gap question now requires at least one `type: "gap"` entry.
`hasReportedGap` keeps its original meaning for the menu template's own
evidence gate, which is unchanged. Two tests: a conflict-only answer is
never offered the chip, and an answer carrying both still is.

Codex review finding on #2500, verified against `evidence.ts` before
acting.

---------

Co-authored-by: Claude <noreply@anthropic.com>
…w-sxd0mm

issues: close #PM9SP1 with the full over-claim outcome, file the mode-home footer contradiction, record measured bundle drift
#2501)

`tsconfig.typecheck.json` exists to answer one question — "is the source
itself sound?" — without reading gitignored build artifacts, because a
stale generated validator reports errors that no longer exist in source
and CI, on a fresh checkout, never sees them. A permanently-red local gate
is why real type errors reach CI instead of being caught before push
(`docs/outstanding-issues.md` #210).

Excluding `.next/**` stopped being sufficient. An `exclude` filters what
the `include` globs collect; it cannot drop a file that an included file
IMPORTS. Next 16 regenerates `next-env.d.ts` with

    import "./.next/dev/types/routes.d.ts";
    import "./.next/dev/types/root-params.d.ts";

so the moment `next dev` or `next build` runs, the source-health typecheck
is reading build output again through the back door.

Measured on 2026-09-01: `next dev` left `.next/dev/types/routes.d.ts` with
a stray fragment (`d": {}`) where a shorter write had not truncated a
longer previous file, and `npm run typecheck` reported 106 syntax errors
against source that was completely sound. Deleting `.next` made it pass
instantly. Reproduced deliberately here — the same corrupt artefact gives
106 errors under the old config and exit 0 under the new one.

`next-env.typecheck.d.ts` carries the half of `next-env.d.ts` that is real
type information (the two reference directives, which resolve inside
`node_modules`) and none of the half that points at build output.
`tsconfig.typecheck.json` includes it and excludes `next-env.d.ts` by name
— `**/*.ts` matches `.d.ts`, so dropping it from `include` alone would not
keep it out.

Route-signature validation is not lost: `tsconfig.json` still uses the real
`next-env.d.ts`, so `next build` (CI's Build job) keeps typechecking the
generated types against the actual routes.

`tests/typecheck-config-isolation.test.ts` fails if either the include or
the exclude is undone, if the stand-in grows a reference into build output,
or if a Next upgrade adds a reference directive to `next-env.d.ts` that the
stand-in does not carry.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file docker Pull requests that update docker code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants