diff --git a/docs/branch-review-records/2151c86f85ae9cbf8e2e8530ac02019ce737d1565612b12828818d2377162c38.record.md b/docs/branch-review-records/2151c86f85ae9cbf8e2e8530ac02019ce737d1565612b12828818d2377162c38.record.md new file mode 100644 index 0000000000..a648f6f38c --- /dev/null +++ b/docs/branch-review-records/2151c86f85ae9cbf8e2e8530ac02019ce737d1565612b12828818d2377162c38.record.md @@ -0,0 +1 @@ +| 2026-08-14 | claude/ledger-reconcile-batch-2 | af68b3271922656ef97312dae513a3f6906aec76 | docs/outstanding-issues.md + inbox — second serial reconciliation of 35 queued requests | Applied 25 active mutations (13 done, 6 add, 6 update) plus 5 cancellation decisions. Ledger 106 open/222 archived -> 99/235; inbox 0 pending/129 applied. Three closures queued in PR #1940 (#235 #237 #238) were cancelled by review and stay open: each asked for visual or browser proof and had been closed on executable evidence. Zero live same-target collisions verified before applying. | issues:reconcile --dry-run; verify:pr-local (11 completed, 0 failed); check:ledger-write-discipline | diff --git a/docs/outstanding-issues-inbox/00428ff0-1b45-4066-9838-94216fa8b6eb.json b/docs/outstanding-issues-inbox/applied/00428ff0-1b45-4066-9838-94216fa8b6eb.json similarity index 100% rename from docs/outstanding-issues-inbox/00428ff0-1b45-4066-9838-94216fa8b6eb.json rename to docs/outstanding-issues-inbox/applied/00428ff0-1b45-4066-9838-94216fa8b6eb.json diff --git a/docs/outstanding-issues-inbox/01580a63-502e-4ee9-bdf2-a5bf492f7dee.json b/docs/outstanding-issues-inbox/applied/01580a63-502e-4ee9-bdf2-a5bf492f7dee.json similarity index 100% rename from docs/outstanding-issues-inbox/01580a63-502e-4ee9-bdf2-a5bf492f7dee.json rename to docs/outstanding-issues-inbox/applied/01580a63-502e-4ee9-bdf2-a5bf492f7dee.json diff --git a/docs/outstanding-issues-inbox/04a0b8d6-8af9-4779-8af8-8e3b6e9a3b2b.json b/docs/outstanding-issues-inbox/applied/04a0b8d6-8af9-4779-8af8-8e3b6e9a3b2b.json similarity index 100% rename from docs/outstanding-issues-inbox/04a0b8d6-8af9-4779-8af8-8e3b6e9a3b2b.json rename to docs/outstanding-issues-inbox/applied/04a0b8d6-8af9-4779-8af8-8e3b6e9a3b2b.json diff --git a/docs/outstanding-issues-inbox/11e56221-8d46-4c9a-a89c-78621f18e754.json b/docs/outstanding-issues-inbox/applied/11e56221-8d46-4c9a-a89c-78621f18e754.json similarity index 100% rename from docs/outstanding-issues-inbox/11e56221-8d46-4c9a-a89c-78621f18e754.json rename to docs/outstanding-issues-inbox/applied/11e56221-8d46-4c9a-a89c-78621f18e754.json diff --git a/docs/outstanding-issues-inbox/17e1baf4-7d8f-4494-acd6-a845ade305ca.json b/docs/outstanding-issues-inbox/applied/17e1baf4-7d8f-4494-acd6-a845ade305ca.json similarity index 100% rename from docs/outstanding-issues-inbox/17e1baf4-7d8f-4494-acd6-a845ade305ca.json rename to docs/outstanding-issues-inbox/applied/17e1baf4-7d8f-4494-acd6-a845ade305ca.json diff --git a/docs/outstanding-issues-inbox/1b483041-4f2e-4ccf-872e-f7657785cfb3.json b/docs/outstanding-issues-inbox/applied/1b483041-4f2e-4ccf-872e-f7657785cfb3.json similarity index 100% rename from docs/outstanding-issues-inbox/1b483041-4f2e-4ccf-872e-f7657785cfb3.json rename to docs/outstanding-issues-inbox/applied/1b483041-4f2e-4ccf-872e-f7657785cfb3.json diff --git a/docs/outstanding-issues-inbox/1e598935-a21b-4aa5-9bd2-c667ce16ebbd.json b/docs/outstanding-issues-inbox/applied/1e598935-a21b-4aa5-9bd2-c667ce16ebbd.json similarity index 100% rename from docs/outstanding-issues-inbox/1e598935-a21b-4aa5-9bd2-c667ce16ebbd.json rename to docs/outstanding-issues-inbox/applied/1e598935-a21b-4aa5-9bd2-c667ce16ebbd.json diff --git a/docs/outstanding-issues-inbox/2194da1d-c445-47db-8b97-09774056ab42.json b/docs/outstanding-issues-inbox/applied/2194da1d-c445-47db-8b97-09774056ab42.json similarity index 100% rename from docs/outstanding-issues-inbox/2194da1d-c445-47db-8b97-09774056ab42.json rename to docs/outstanding-issues-inbox/applied/2194da1d-c445-47db-8b97-09774056ab42.json diff --git a/docs/outstanding-issues-inbox/25f6b53a-86e2-42e9-9b38-1c2daae28892.json b/docs/outstanding-issues-inbox/applied/25f6b53a-86e2-42e9-9b38-1c2daae28892.json similarity index 100% rename from docs/outstanding-issues-inbox/25f6b53a-86e2-42e9-9b38-1c2daae28892.json rename to docs/outstanding-issues-inbox/applied/25f6b53a-86e2-42e9-9b38-1c2daae28892.json diff --git a/docs/outstanding-issues-inbox/33d68dca-16b7-4325-8c50-f046c2f71316.json b/docs/outstanding-issues-inbox/applied/33d68dca-16b7-4325-8c50-f046c2f71316.json similarity index 100% rename from docs/outstanding-issues-inbox/33d68dca-16b7-4325-8c50-f046c2f71316.json rename to docs/outstanding-issues-inbox/applied/33d68dca-16b7-4325-8c50-f046c2f71316.json diff --git a/docs/outstanding-issues-inbox/39b08439-bd16-4b6d-b218-f04ad0a9a8cd.json b/docs/outstanding-issues-inbox/applied/39b08439-bd16-4b6d-b218-f04ad0a9a8cd.json similarity index 100% rename from docs/outstanding-issues-inbox/39b08439-bd16-4b6d-b218-f04ad0a9a8cd.json rename to docs/outstanding-issues-inbox/applied/39b08439-bd16-4b6d-b218-f04ad0a9a8cd.json diff --git a/docs/outstanding-issues-inbox/413a0aec-0239-45b2-8880-d3ace65cfdaf.json b/docs/outstanding-issues-inbox/applied/413a0aec-0239-45b2-8880-d3ace65cfdaf.json similarity index 100% rename from docs/outstanding-issues-inbox/413a0aec-0239-45b2-8880-d3ace65cfdaf.json rename to docs/outstanding-issues-inbox/applied/413a0aec-0239-45b2-8880-d3ace65cfdaf.json diff --git a/docs/outstanding-issues-inbox/4b6930e6-fae2-4b4f-9f90-49bf0bcd548c.json b/docs/outstanding-issues-inbox/applied/4b6930e6-fae2-4b4f-9f90-49bf0bcd548c.json similarity index 100% rename from docs/outstanding-issues-inbox/4b6930e6-fae2-4b4f-9f90-49bf0bcd548c.json rename to docs/outstanding-issues-inbox/applied/4b6930e6-fae2-4b4f-9f90-49bf0bcd548c.json diff --git a/docs/outstanding-issues-inbox/63419f06-c12a-4a84-a684-6e177f527365.json b/docs/outstanding-issues-inbox/applied/63419f06-c12a-4a84-a684-6e177f527365.json similarity index 100% rename from docs/outstanding-issues-inbox/63419f06-c12a-4a84-a684-6e177f527365.json rename to docs/outstanding-issues-inbox/applied/63419f06-c12a-4a84-a684-6e177f527365.json diff --git a/docs/outstanding-issues-inbox/69b9cd4a-9c2a-4e37-a146-48c7e540b87e.json b/docs/outstanding-issues-inbox/applied/69b9cd4a-9c2a-4e37-a146-48c7e540b87e.json similarity index 100% rename from docs/outstanding-issues-inbox/69b9cd4a-9c2a-4e37-a146-48c7e540b87e.json rename to docs/outstanding-issues-inbox/applied/69b9cd4a-9c2a-4e37-a146-48c7e540b87e.json diff --git a/docs/outstanding-issues-inbox/71d61764-9d93-43bd-a3d3-230f5ad78418.json b/docs/outstanding-issues-inbox/applied/71d61764-9d93-43bd-a3d3-230f5ad78418.json similarity index 100% rename from docs/outstanding-issues-inbox/71d61764-9d93-43bd-a3d3-230f5ad78418.json rename to docs/outstanding-issues-inbox/applied/71d61764-9d93-43bd-a3d3-230f5ad78418.json diff --git a/docs/outstanding-issues-inbox/7268da45-5b77-4583-a3cb-27e5ec7067b1.json b/docs/outstanding-issues-inbox/applied/7268da45-5b77-4583-a3cb-27e5ec7067b1.json similarity index 100% rename from docs/outstanding-issues-inbox/7268da45-5b77-4583-a3cb-27e5ec7067b1.json rename to docs/outstanding-issues-inbox/applied/7268da45-5b77-4583-a3cb-27e5ec7067b1.json diff --git a/docs/outstanding-issues-inbox/74edc91b-042b-4e73-911c-286d0b38da45.json b/docs/outstanding-issues-inbox/applied/74edc91b-042b-4e73-911c-286d0b38da45.json similarity index 100% rename from docs/outstanding-issues-inbox/74edc91b-042b-4e73-911c-286d0b38da45.json rename to docs/outstanding-issues-inbox/applied/74edc91b-042b-4e73-911c-286d0b38da45.json diff --git a/docs/outstanding-issues-inbox/83ec71cf-db94-4110-ada8-ec7e730e5154.json b/docs/outstanding-issues-inbox/applied/83ec71cf-db94-4110-ada8-ec7e730e5154.json similarity index 100% rename from docs/outstanding-issues-inbox/83ec71cf-db94-4110-ada8-ec7e730e5154.json rename to docs/outstanding-issues-inbox/applied/83ec71cf-db94-4110-ada8-ec7e730e5154.json diff --git a/docs/outstanding-issues-inbox/a780ce8a-a373-4c95-974f-0692af775ff6.json b/docs/outstanding-issues-inbox/applied/a780ce8a-a373-4c95-974f-0692af775ff6.json similarity index 100% rename from docs/outstanding-issues-inbox/a780ce8a-a373-4c95-974f-0692af775ff6.json rename to docs/outstanding-issues-inbox/applied/a780ce8a-a373-4c95-974f-0692af775ff6.json diff --git a/docs/outstanding-issues-inbox/a860ce7a-5ecf-4f30-a3b9-5c22d1d914b9.json b/docs/outstanding-issues-inbox/applied/a860ce7a-5ecf-4f30-a3b9-5c22d1d914b9.json similarity index 100% rename from docs/outstanding-issues-inbox/a860ce7a-5ecf-4f30-a3b9-5c22d1d914b9.json rename to docs/outstanding-issues-inbox/applied/a860ce7a-5ecf-4f30-a3b9-5c22d1d914b9.json diff --git a/docs/outstanding-issues-inbox/b5f28517-89c9-455d-a4b4-202608141940.json b/docs/outstanding-issues-inbox/applied/b5f28517-89c9-455d-a4b4-202608141940.json similarity index 100% rename from docs/outstanding-issues-inbox/b5f28517-89c9-455d-a4b4-202608141940.json rename to docs/outstanding-issues-inbox/applied/b5f28517-89c9-455d-a4b4-202608141940.json diff --git a/docs/outstanding-issues-inbox/b7a5bee9-bddc-4a7a-9614-a49301c00cc8.json b/docs/outstanding-issues-inbox/applied/b7a5bee9-bddc-4a7a-9614-a49301c00cc8.json similarity index 100% rename from docs/outstanding-issues-inbox/b7a5bee9-bddc-4a7a-9614-a49301c00cc8.json rename to docs/outstanding-issues-inbox/applied/b7a5bee9-bddc-4a7a-9614-a49301c00cc8.json diff --git a/docs/outstanding-issues-inbox/b7c0f9f6-c95d-413f-9bdd-6b6ad9cbcb91.json b/docs/outstanding-issues-inbox/applied/b7c0f9f6-c95d-413f-9bdd-6b6ad9cbcb91.json similarity index 100% rename from docs/outstanding-issues-inbox/b7c0f9f6-c95d-413f-9bdd-6b6ad9cbcb91.json rename to docs/outstanding-issues-inbox/applied/b7c0f9f6-c95d-413f-9bdd-6b6ad9cbcb91.json diff --git a/docs/outstanding-issues-inbox/ce304701-1e01-4cfb-bf9c-202608141940.json b/docs/outstanding-issues-inbox/applied/ce304701-1e01-4cfb-bf9c-202608141940.json similarity index 100% rename from docs/outstanding-issues-inbox/ce304701-1e01-4cfb-bf9c-202608141940.json rename to docs/outstanding-issues-inbox/applied/ce304701-1e01-4cfb-bf9c-202608141940.json diff --git a/docs/outstanding-issues-inbox/d229e6b5-a31a-44a9-8a7b-536e7f8ccf50.json b/docs/outstanding-issues-inbox/applied/d229e6b5-a31a-44a9-8a7b-536e7f8ccf50.json similarity index 100% rename from docs/outstanding-issues-inbox/d229e6b5-a31a-44a9-8a7b-536e7f8ccf50.json rename to docs/outstanding-issues-inbox/applied/d229e6b5-a31a-44a9-8a7b-536e7f8ccf50.json diff --git a/docs/outstanding-issues-inbox/d3b2fe49-3282-4756-9135-7c555dede447.json b/docs/outstanding-issues-inbox/applied/d3b2fe49-3282-4756-9135-7c555dede447.json similarity index 100% rename from docs/outstanding-issues-inbox/d3b2fe49-3282-4756-9135-7c555dede447.json rename to docs/outstanding-issues-inbox/applied/d3b2fe49-3282-4756-9135-7c555dede447.json diff --git a/docs/outstanding-issues-inbox/d9a7e3fc-06e9-45ca-ab2a-202608141940.json b/docs/outstanding-issues-inbox/applied/d9a7e3fc-06e9-45ca-ab2a-202608141940.json similarity index 100% rename from docs/outstanding-issues-inbox/d9a7e3fc-06e9-45ca-ab2a-202608141940.json rename to docs/outstanding-issues-inbox/applied/d9a7e3fc-06e9-45ca-ab2a-202608141940.json diff --git a/docs/outstanding-issues-inbox/dcb09280-0436-4651-a707-b0007e872d7b.json b/docs/outstanding-issues-inbox/applied/dcb09280-0436-4651-a707-b0007e872d7b.json similarity index 100% rename from docs/outstanding-issues-inbox/dcb09280-0436-4651-a707-b0007e872d7b.json rename to docs/outstanding-issues-inbox/applied/dcb09280-0436-4651-a707-b0007e872d7b.json diff --git a/docs/outstanding-issues-inbox/ded27e2f-5c71-43b8-99dc-ffffa6294ba3.json b/docs/outstanding-issues-inbox/applied/ded27e2f-5c71-43b8-99dc-ffffa6294ba3.json similarity index 100% rename from docs/outstanding-issues-inbox/ded27e2f-5c71-43b8-99dc-ffffa6294ba3.json rename to docs/outstanding-issues-inbox/applied/ded27e2f-5c71-43b8-99dc-ffffa6294ba3.json diff --git a/docs/outstanding-issues-inbox/e4a11465-348c-49e5-8a0a-202608141941.json b/docs/outstanding-issues-inbox/applied/e4a11465-348c-49e5-8a0a-202608141941.json similarity index 100% rename from docs/outstanding-issues-inbox/e4a11465-348c-49e5-8a0a-202608141941.json rename to docs/outstanding-issues-inbox/applied/e4a11465-348c-49e5-8a0a-202608141941.json diff --git a/docs/outstanding-issues-inbox/e684a311-2a0d-4c21-ba18-13afde3b62f8.json b/docs/outstanding-issues-inbox/applied/e684a311-2a0d-4c21-ba18-13afde3b62f8.json similarity index 100% rename from docs/outstanding-issues-inbox/e684a311-2a0d-4c21-ba18-13afde3b62f8.json rename to docs/outstanding-issues-inbox/applied/e684a311-2a0d-4c21-ba18-13afde3b62f8.json diff --git a/docs/outstanding-issues-inbox/f825f6b9-94ec-4af1-929c-202608141941.json b/docs/outstanding-issues-inbox/applied/f825f6b9-94ec-4af1-929c-202608141941.json similarity index 100% rename from docs/outstanding-issues-inbox/f825f6b9-94ec-4af1-929c-202608141941.json rename to docs/outstanding-issues-inbox/applied/f825f6b9-94ec-4af1-929c-202608141941.json diff --git a/docs/outstanding-issues-inbox/f9f40594-a816-45a8-954a-3fcecf6d5d05.json b/docs/outstanding-issues-inbox/applied/f9f40594-a816-45a8-954a-3fcecf6d5d05.json similarity index 100% rename from docs/outstanding-issues-inbox/f9f40594-a816-45a8-954a-3fcecf6d5d05.json rename to docs/outstanding-issues-inbox/applied/f9f40594-a816-45a8-954a-3fcecf6d5d05.json diff --git a/docs/outstanding-issues-inbox/fc23f1ec-c597-4690-b256-2263ecf73c86.json b/docs/outstanding-issues-inbox/applied/fc23f1ec-c597-4690-b256-2263ecf73c86.json similarity index 100% rename from docs/outstanding-issues-inbox/fc23f1ec-c597-4690-b256-2263ecf73c86.json rename to docs/outstanding-issues-inbox/applied/fc23f1ec-c597-4690-b256-2263ecf73c86.json diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index a08da1fee5..0ed58896bd 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -87,38 +87,31 @@ removed after current-main verification; it is not missing recommended work. | 32 | `#168` | A3 | High — ledger architecture | With #156 / id-scheme redesign | design first | Sequential issue ids force every concurrent append to conflict — two sessions can append to this ledger at the same time without conflicting. | | 33 | `#169` | A3 | High — git hygiene | Next branch cleanup batch | 1–2 hours | Local branches carry work that exists on no remote — committed work is not lost when a machine or worktree is reclaimed. | | 34 | `#175` | A2 | Operator — clinical data + Standard | Next therapy catalogue curation window | 2–4 hours | Therapy modality is now null on all 205 records and needs curation or removal — the Therapy detail and recommend screens either show a curated modality or stop carrying the field at all. | -| 35 | `#178` | A3 | High — PR policy | Next pr-policy change | 1–2 hours | pr-policy does not flag operational risk bundled with clinical or UI risk — a PR that mixes operational-risk paths with clinical or UI risk is called out before it merges, because squash-merging that mix destroys per-it… | -| 36 | `#189` | A2 | Specialist — search/RAG budgets | After #098 route residual; before collapsing RPCs | 2–4 hours + canary if behaviour | Pin /api/search route-level round trips and disposition the x3 text RPC probes — a counting-proxy budget drives `POST` `/api/search` (auth/ratelimit/scope/enrichment/telemetry), and the retrieval-core finding that `matc… | -| 37 | `#036` | Optional | Specialist — privacy/schema | When visibility model is redesigned | design + migration | No explicit `is_public` visibility flag on documents — Public-corpus visibility is implicit: `owner_id IS NULL` on an `indexed` document (`resolveSearchScope`). The `metadata.public_corpus` marker is written by the prom… | -| 38 | `#101` | A3 | Specialist — RAG/retrieval | After #098 harness + canary approval | canary-gated | Canary-gated retrieval parallelisation candidates — metadata and memory hydration shipped in PR #1474; visual hydration, scope enumeration, typeahead caching, and universal-search coalescing remain, each behind the RAG flag and live-canary criteria. | -| 39 | `#190` | A3 | Specialist — RAG structure | On explicit X3 go-ahead | 1 PR per extraction unit | X3: Finish rag.ts monolith decomposition — `src/lib/rag/rag.ts` is decomposed into focused modules per `docs/maturity-backlog-workorders.md` X3, with existing offline RAG contracts green. | -| 40 | `#191` | A3 | Operator — DB + Specialist | Approved live-DB window only | provider-gated | X5: ACL-migration consolidation (provider-gated) — ACL-related migrations are consolidated per maturity work-order X5 without weakening owner-scope/RLS. | -| 41 | `#192` | A3 | High — test coverage | Next coverage-floor pass | 0.5–1 day | X6: Raise clinical/retrieval/answer coverage floors — coverage floors for clinical, retrieval, and answer domains meet the maturity X6 targets with CI enforcing them. | -| 42 | `#193` | A3 | High — src/lib structure | After/with X3 non-protected clusters | 1 PR per cluster | X7: Complete the remaining src/lib domain-directory reorg — remaining `src/lib` clusters sit in their domain directories per X7 follow-on to X2. | -| 43 | `#194` | A3 | High — scripts/docs hygiene | Next scripts archive pass | 1–2 hours | L1: Archive retired backfill one-shots and dead ci-change-scope token — retired `backfill:*` one-shots and the dead `ci-change-scope` token are archived/removed with docs/script index updated. | -| 44 | `#195` | A3 | Operator — GitHub maintainer | Maintainer UI window | 30–60 min | M1: Repo-host hardening (branch protection and required checks) — GitHub branch-protection rulesets and required checks match audit §8 / maturity M1. | -| 45 | `#183` | A2 | Operator — Sentry + Specialist | Next approved observability window with SENTRY_AUTH_TOKEN | 1–2 hours | Create Sentry metric alert for production DB span p95 > 500ms (`span.op:db`, environment production). **Stop:** no secret printing; blocked until token/env available. | -| 46 | `#206` | A2 | Specialist — answer UI contract | With AnswerState producer work (`#207`) | 2–4 hours | `partial_retrieval` has no app-facing producer — decide RAG contract vs UI-only mapping before AnswerCard. **Stop:** no retrieval behaviour change without RAG flag. | -| 47 | `#209` | A3 | High — design tokens / contrast | Next Gate 1 / warning-token pass | 1–2 hours | Add contrast pair for `--warning` used as body text (VerificationNotice / DoseLine). **Gate:** design-system contrast checks. **Stop:** do not invent a new status token without TOKENS.md. | -| 48 | `#211` | A3 | High — TypeScript strictness | Dedicated migration branch | multi-PR | Plan and start `noUncheckedIndexedAccess` migration (1266 errors); highest-risk files first. **Stop:** do not flip the flag on main without a staged plan. | -| 49 | `#212` | A3 | High — runtime validation | After highest-risk cast inventory | multi-PR | Replace `as unknown as` and unvalidated `JSON.parse` with Zod/guards at trust boundaries. **Stop:** RAG/provider boundaries need clinical/privacy care. | -| 50 | `#213` | A3 | High — error handling | Next fetch/stream hardening pass | 0.5–1 day | Stop swallowing fetch/stream errors with empty catches; check `response.ok`. **Stop:** do not change telemetry contracts silently. | -| 51 | `#215` | Optional | High — image perf | Next image/PWA pass | 2–4 hours | Image-optimization basics for lightbox, PWA lifecycle, demo PNGs. **Stop:** optional until measured need. | -| 52 | `#221` | A3 | High — design-system convergence | After `#218` cn() decision | 0.5–1 day | Converge remaining local EmptyState/LoadingState/Chip duplicates. **Stop:** not piecemeal before cn()/Chip decisions. | -| 53 | `#222` | A3 | High — headers / search chrome | During headers redesign decision | 2–4 hours | Decide whether mode-home-template / search-results-header-band are in PageHeader scope or permanently out. **Stop:** do not flatten phone composer ownership. | -| 54 | `#233` | A3 | High — design-system docs | Next COMPONENTS.md docs PR | 1–2 hours | Refresh section 0 maturity matrix and document FormField optionality-marker contract. **Gate:** docs checks. **Stop:** docs-only; no product behaviour change. | -| 55 | `#234` | A3 | High — design-system docs | With answer-surface docs | 30–60 min | Document `answer-copy-payload.ts` as the clipboard contract for three surfaces. **Stop:** do not add a second copy builder. | -| 56 | `#235` | A3 | High — design-system evidence | Next warmed local proof-shot pass | 1–2 hours | Capture missing ADOPTION.md §7 proof shots for adopted surfaces. **Stop:** not visual-baseline PNGs (`#118`); no Playwright snapshot commit. | -| 57 | `#237` | A3 | High — design-system a11y | Before freezing Linux visual baselines (#242) | 30–60 min | Eyeball low-confidence AccessibleTable densities at 320px; MissingValue phrases must remain readable. **Gate:** visual spot-check only. **Stop:** do not abbreviate MissingValue to a dash. | -| 58 | `#238` | A3 | High — overlays/UI | After Sheet portal default change (#1616) | 30–60 min | Visual pass for Sheet portal default on settings, sidebar, and answer overlays under OverlayRoot. **Stop:** do not revert portal default without evidence. | -| 59 | `#239` | Optional | High — phone chrome | When phone orientation QA is available | 15–30 min | Manual phone rotation check for ResizeObserver-only phone chrome reserve. **Gate:** `verify:phone-chrome` still owns automated coverage. **Stop:** do not widen reserve heuristics without reproduction. | -| 60 | `#240` | Optional | High — design tokens | Next design-owner review | 15–30 min | Confirm tooltip visual hard-clip asymmetry with design owner (sr-only keeps full text). **Stop:** no product change without that confirmation. | -| 61 | `#242` | A2 | High — design-system baselines | After human review of Linux baselines | 1–2 hours | Commit approved Linux visual baselines and promote adoption not-committed → committed. **Stop:** never commit baselines from an unreviewed machine run. | -| 62 | `#245` | A3 | High — cross-mode links | Next CrossModeLinks / analytics pass | 30–60 min | responsive-compact CrossModeLinks keeps duplicate rails in the DOM; prefer one mount or accept test double-counts. **Stop:** do not break phone-only rail contract. | -| 63 | `#248` | A2 | Operator — Supabase + Specialist | After PR #1614 symptom repair; approved live/history window | 1–2 hours | Investigate why 20260705180000 search-health indexes were missing on live despite applied history; decide if drift checks should catch this class. **Stop:** no hosted mutation without approval. | +| 35 | `#189` | A2 | Specialist — search/RAG budgets | After #098 route residual; before collapsing RPCs | 2–4 hours + canary if behaviour | Pin /api/search route-level round trips and disposition the x3 text RPC probes — a counting-proxy budget drives `POST` `/api/search` (auth/ratelimit/scope/enrichment/telemetry), and the retrieval-core finding that `matc… | +| 36 | `#036` | Optional | Specialist — privacy/schema | When visibility model is redesigned | design + migration | No explicit `is_public` visibility flag on documents — Public-corpus visibility is implicit: `owner_id IS NULL` on an `indexed` document (`resolveSearchScope`). The `metadata.public_corpus` marker is written by the prom… | +| 37 | `#101` | A3 | Specialist — RAG/retrieval | After #098 harness + canary approval | canary-gated | Canary-gated retrieval parallelisation candidates — metadata and memory hydration shipped in PR #1474; visual hydration, scope enumeration, typeahead caching, and universal-search coalescing remain, each behind the RAG flag and live-canary criteria. | +| 38 | `#190` | A3 | Specialist — RAG structure | On explicit X3 go-ahead | 1 PR per extraction unit | X3: Finish rag.ts monolith decomposition — `src/lib/rag/rag.ts` is decomposed into focused modules per `docs/maturity-backlog-workorders.md` X3, with existing offline RAG contracts green. | +| 39 | `#191` | A3 | Operator — DB + Specialist | Approved live-DB window only | provider-gated | X5: ACL-migration consolidation (provider-gated) — ACL-related migrations are consolidated per maturity work-order X5 without weakening owner-scope/RLS. | +| 40 | `#192` | A3 | High — test coverage | Next coverage-floor pass | 0.5–1 day | X6: Raise clinical/retrieval/answer coverage floors — coverage floors for clinical, retrieval, and answer domains meet the maturity X6 targets with CI enforcing them. | +| 41 | `#193` | A3 | High — src/lib structure | After/with X3 non-protected clusters | 1 PR per cluster | X7: Complete the remaining src/lib domain-directory reorg — remaining `src/lib` clusters sit in their domain directories per X7 follow-on to X2. | +| 42 | `#194` | A3 | High — scripts/docs hygiene | Next scripts archive pass | 1–2 hours | L1: Archive retired backfill one-shots and dead ci-change-scope token — retired `backfill:*` one-shots and the dead `ci-change-scope` token are archived/removed with docs/script index updated. | +| 43 | `#195` | A3 | Operator — GitHub maintainer | Maintainer UI window | 30–60 min | M1: Repo-host hardening (branch protection and required checks) — GitHub branch-protection rulesets and required checks match audit §8 / maturity M1. | +| 44 | `#183` | A2 | Operator — Sentry + Specialist | Next approved observability window with SENTRY_AUTH_TOKEN | 1–2 hours | Create Sentry metric alert for production DB span p95 > 500ms (`span.op:db`, environment production). **Stop:** no secret printing; blocked until token/env available. | +| 45 | `#206` | A2 | Specialist — answer UI contract | With AnswerState producer work (`#207`) | 2–4 hours | `partial_retrieval` has no app-facing producer — decide RAG contract vs UI-only mapping before AnswerCard. **Stop:** no retrieval behaviour change without RAG flag. | +| 46 | `#211` | A3 | High — TypeScript strictness | Dedicated migration branch | multi-PR | Plan and start `noUncheckedIndexedAccess` migration (1266 errors); highest-risk files first. **Stop:** do not flip the flag on main without a staged plan. | +| 47 | `#212` | A3 | High — runtime validation | After highest-risk cast inventory | multi-PR | Replace `as unknown as` and unvalidated `JSON.parse` with Zod/guards at trust boundaries. **Stop:** RAG/provider boundaries need clinical/privacy care. | +| 48 | `#215` | Optional | High — image perf | Next image/PWA pass | 2–4 hours | Image-optimization basics for lightbox, PWA lifecycle, demo PNGs. **Stop:** optional until measured need. | +| 49 | `#222` | A3 | High — headers / search chrome | During headers redesign decision | 2–4 hours | Decide whether mode-home-template / search-results-header-band are in PageHeader scope or permanently out. **Stop:** do not flatten phone composer ownership. | +| 50 | `#235` | A3 | High — design-system evidence | Next warmed local proof-shot pass | 1–2 hours | Capture missing ADOPTION.md §7 proof shots for adopted surfaces. **Stop:** not visual-baseline PNGs (`#118`); no Playwright snapshot commit. | +| 51 | `#237` | A3 | High — design-system a11y | Before freezing Linux visual baselines (#242) | 30–60 min | Eyeball low-confidence AccessibleTable densities at 320px; MissingValue phrases must remain readable. **Gate:** visual spot-check only. **Stop:** do not abbreviate MissingValue to a dash. | +| 52 | `#238` | A3 | High — overlays/UI | After Sheet portal default change (#1616) | 30–60 min | Visual pass for Sheet portal default on settings, sidebar, and answer overlays under OverlayRoot. **Stop:** do not revert portal default without evidence. | +| 53 | `#239` | Optional | High — phone chrome | When phone orientation QA is available | 15–30 min | Manual phone rotation check for ResizeObserver-only phone chrome reserve. **Gate:** `verify:phone-chrome` still owns automated coverage. **Stop:** do not widen reserve heuristics without reproduction. | +| 54 | `#240` | Optional | High — design tokens | Next design-owner review | 15–30 min | Confirm tooltip visual hard-clip asymmetry with design owner (sr-only keeps full text). **Stop:** no product change without that confirmation. | +| 55 | `#242` | A2 | High — design-system baselines | After human review of Linux baselines | 1–2 hours | Commit approved Linux visual baselines and promote adoption not-committed → committed. **Stop:** never commit baselines from an unreviewed machine run. | +| 56 | `#248` | A2 | Operator — Supabase + Specialist | After PR #1614 symptom repair; approved live/history window | 1–2 hours | Investigate why 20260705180000 search-health indexes were missing on live despite applied history; decide if drift checks should catch this class. **Stop:** no hosted mutation without approval. | - + ## Open items > **Merged-main canary update (2026-07-23, run `30018289898`):** the new structured report correctly recorded evaluated tree `c24f2e8f2d30d0c59fc1eba025d3dcd63478137e`, run/attempt identity and `cross-region-runner` latency context. Golden retrieval remained 36/36 with document/content recall 1.0 and no failed cases. The 44-case answer gate had grounded-supported and unsupported-correct rates of 1.0, but failed because `neuroleptic-side-effect-escalation` again returned one citation where two are required (citation-failure rate 0.0227). `admission-discharge-comparison` again omitted the specific AKG admission document after `comparison_source_extractive_fallback`; `admission-discharge-coverage-paraphrase` was advisory-only at 24,870 ms. Answer cost was reported as `$0.234736`. Do not retry immediately: retain this as the first structured datapoint, compare it with the scheduled 2026-07-26 report, and keep retrieval/ranking unchanged. @@ -164,15 +157,14 @@ removed after current-main verification; it is not missing recommended work. | #102 | P3 | task | Apply the additive `documents` index debt (operator) | **Outcome:** bare-column `ILIKE` and the paged status scan on `documents` are index-served on hosted. `documents_title_trgm_idx` indexes a CONCATENATED expression, so the bare-column predicates in `api/documents/route.ts:193` and `rag-candidate-sources.ts:477` (RAG path) cannot use it and fall back to scanning; `search-scope.ts:271-277` sorts per page against the single-column `documents_status_idx`. **Runbook prepared 2026-07-29 — NOT applied, item stays open:** three `CREATE INDEX CONCURRENTLY` statements authored and reviewed in `docs/operator-apply-performance-latency-remediation.md` — additive, though **the "recall is byte-identical" claim was RETRACTED on 2026-07-29 review**: `fetchDocumentTitleAliasRows` (`rag-candidate-sources.ts:482`) applies `.limit(12)` with no `ORDER BY`, so a new index can change which title-alias documents feed candidate assembly. Only the documents-list use stays ordering-safe; `(status,id)` is canary-gated too — see runbook, and making that `.limit(12)` deterministic first does **not** lift the gate — an unordered `LIMIT` has no stable selection to preserve, so imposing an order can pick a different twelve and is itself an ordering behaviour change on a retrieval surface, which AGENTS.md requires a canary pair for. Sequencing the ordering fix first is worthwhile (unordered `LIMIT` on a retrieval input is latent nondeterminism regardless) but yields two canary-gated changes, not one (PR #1377 review). **Deliberately NO migration file:** an additive-index migration without a synchronized `schema.sql` mirror and regenerated drift manifest is exactly what closed PR #1312, and the mirror cannot come first because `required_indexes` in `search_schema_health()` (`schema.sql:3178`) runs against live. **Next (operator):** **author the migration first** — `supabase/migrations/` is the source of truth and `schema.sql` only a mirror, so hand-run operator SQL never reaches staging, disaster-recovery replay, or a local `supabase db reset`, and a `required_indexes` registration would fail there (PR #1377 review); follow the `20260717170000_registry_projection_cleanup.sql` idempotent pattern. **That migration must also carry the health-function change** — `required_indexes` lives inside `search_schema_health()`, which is redefined by `create or replace function` in eleven migrations (copy `20260705180000_reconcile_search_health_indexes.sql:62`); editing `schema.sql:3177` alone moves only the mirror and leaves the indexes unmonitored on hosted (PR #1377 review). Then apply concurrently, confirm `indisvalid`, mirror both the index statements and the identical function body into `schema.sql`, run `npm run drift:manifest` (Docker), and deploy the migration LAST — in that order, in one change. Expect `check:drift` to report them as unexpected between steps 1 and 2. **Rollback is three deployed phases, not the reverse of one:** retract `required_indexes` via its own `create or replace function` migration and deploy → drop concurrently live → only then deploy the `schema.sql` removal plus an idempotent forward `drop index if exists` migration, because Supabase wraps migrations in a transaction and a plain `DROP INDEX` there takes the lock the concurrent procedure exists to avoid (PR #1377 review). | `docs/audit/latency-audit-2026-07-28.md` L2-3/L2-5; `docs/operator-apply-performance-latency-remediation.md` | 2026-07-29 | | #117 | P2 | rec | All live mobile routes breach LCP; shared render-blocking CSS and font are the current bottleneck | **Outcome:** `/therapy-compass` mobile LCP lands near the other mobile routes instead of double them. **Measured 2026-07-30** by the new pre-merge Lighthouse budget: mobile LCP 5229 ms, TBT 612 ms, CLS 0.142, against 2123-2460 ms on every other mobile route and 826 ms on desktop — so it is client-side work under mobile CPU/network throttling, not server latency. **Cause before this PR:** `useTherapyData` fetched `/therapy-compass-data/therapies-index.json` (the stable public alias served by a Next rewrite to the thin browse index; 205 records) for the home/search/pathways screens, so the download plus JSON parse sat on the critical path before content painted. **Current split:** home now fetches `public/therapy-compass-data/therapies-home.211dab554c4ec62d.json` (136,288 bytes raw), pathways use the thin browse index, and search loads the full prose corpus (#1471). 90% of the index weight is long-form clinical prose — indications 159 KB (26%), contraindicationsOrCautions 139 KB (23%), bestUsedFor 73 KB (12%), clinicalSummary 67 KB (11%), patientPopulation 59 KB (10%), targetSymptoms 48 KB (8%) — while name, slug, category, tags and setting together are 54 KB (7%). **Remaining decision for search/pathways: rendered on the card, matched by search, or neither.** `therapy-card.tsx` references five of those prose fields and the same index feeds the search screen, so stripping fields could silently change clinical display or search recall. **Next:** settle that per-field question, then either pre-truncate prose that only feeds card display, or move search matching server-side / load prose on first keystroke. **Gate:** `check:therapy-data-index` plus the therapy Playwright journeys; re-measure with `npm run verify:lighthouse`. **Stop:** do not drop a field from the catalogue payload without confirming no card renders it and no search path matches on it. Same class as #013 (route-chunk / catalogue JSON weight), different route and now measured. | PR #1915; live Web Vitals runs 31704500966 and 31704504389; codex/performance-css-delivery | 2026-07-30 | | #118 | P2 | task | Adopt the remaining visual baselines; Lighthouse now gates regressions | Lighthouse half resolved in PR #1915: authorized CI refresh run 31697669596 on current main produced all 10 route/strategy cells with one pinned HeadlessChrome/151 identity. The reviewed artifact was committed, lighthouse-budget.json enforce is true, the job no longer uses continue-on-error, merge_group coverage is restored, and pr-required now fails on a selected Lighthouse failure. The 2026-08-08 and 2026-08-13 complete baselines stayed within tolerance; the latter puts mobile LCP at 2357-2388 ms and Therapy is no longer an outlier. This relative local-production gate does not close #117 deployed-origin LCP work. Remaining #118 scope: adopt the CI-generated Linux visual snapshots and promote visual-baseline only after design-owner review and stable reruns. Stop: never use developer-machine snapshots or let a workflow update its own gate. | PR #1915; CI run 31697669596 artifact lighthouse-baseline-refresh-31697669596 | 2026-07-30 | -| #150 | P2 | issue | CodeRabbit reviewed none of a full day's PRs; spending cap reached | IN FLIGHT 2026-08-12 in PR #1836 (finalize tooling follow-through notes). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. **Outcome:** the repo's second automated reviewer is either funded or acknowledged as absent, rather than appearing to review while skipping. **Evidence 2026-07-30:** CodeRabbit posted "Review limit reached … Your organization has reached its usage spending cap" on **every** PR opened that day — #1404, #1430, #1444, #1445, #1479 — reviewing none of them. Each notice renders as an ordinary bot comment, so a skimming reader sees reviewer activity where there was no review. The Codex connector was the sole substantive reviewer across those PRs and found three real defects that had survived local gates and self-review: a proxy-variable inference in #1430, an `unset` vs `unspecified` git-attribute conflation in #1444, and an earlier P1 recursive-delete on an unvalidated `--dir`. **Next:** decide whether to raise the cap, switch to label-based opt-in so the budget lands on PRs that need it, or accept single-reviewer coverage explicitly. **ESCALATED 2026-07-30 — both reviewers are now capped, so this row's premise no longer holds.** The analysis above rests on the Codex connector being the surviving reviewer. On PR #1505 the Codex connector posted "You have reached your Codex usage limits for code reviews" while CodeRabbit posted its own spending-cap notice on the same PR. **That PR therefore received zero automated review**, and so will anything opened while both caps hold. This is not a second issue — it is the same one, with the fallback removed. **Why it is worth more than a status note:** on 2026-07-30 the single Codex finding on PR #1459 was correct and changed the outcome — it showed that a claimed `LoadingPanel` verification had matched `ModeHomePageSkeleton` instead, which caused `#105` to be closed on wrong evidence. Local gates did not catch it and neither did self-review; the review did. A window in which neither reviewer runs is a window in which that class of error lands. **Next:** the three options above now need deciding rather than deferring, because "accept single-reviewer coverage" is no longer one of them. Until then, treat any PR merged during a cap window as locally-gated only. **Stop:** do not read a CodeRabbit *or* Codex comment as a completed review without checking it is not a usage-limit notice — during this window both bots posted comments on every PR while reviewing none of them. | PRs #1404/#1430/#1444/#1445/#1479; `.coderabbit.yaml` | 2026-07-30 | -| #162 | P2 | task | Redesign Tools search results state (Compact Results Instrument) | IN FLIGHT 2026-08-12 in PR #1839 (three runnable directions for the Tools search results state). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. **Outcome:** `/tools?q=` is a committed results page: query-as-H1, one composer, dense tool rows; cross-mode demoted; no home hero / green filter banner / dual H1. **Product pick:** direction A from comps in `public/mockups/mode-page-redesign-2026-07/tools-search/`. **Next:** implement A on production Tools search; verify desktop+phone chrome ownership. **Stop:** do not redesign Tools home in the same PR unless asked. Renumbered after `main` took `#161` for mockup hover-token residue. | session 2026-07-31 mode-page design audit | 2026-07-31 | +| #150 | P2 | issue | CodeRabbit reviewed none of a full day's PRs; spending cap reached | IN FLIGHT annotation retired 2026-08-14: PR #1836 has merged, so the do-not-start note is stale and was blocking rather than protecting. The row itself is NOT code-verifiable from a container — CodeRabbit's spending cap is an account/billing state, so confirming whether the cap still suppresses reviews needs the operator's CodeRabbit dashboard. Next: check the subscription's review quota and either raise it or record the accepted coverage gap. Keeping open pending that operator read. | PRs #1404/#1430/#1444/#1445/#1479; `.coderabbit.yaml` | 2026-07-30 | +| #162 | P2 | task | Redesign Tools search results state (Compact Results Instrument) | IN FLIGHT confirmed still accurate 2026-08-14: PR #1839 is the one PR in this cluster that has NOT merged (no merge commit on origin/main; refs/pull/1839/merge still exists, which GitHub keeps only for open PRs). Every sibling in the same sweep — #1835 #1836 #1837 #1840 #1841 #1842 — has landed and their rows are archived or re-scoped. Do not start this row; it is genuinely in flight. | session 2026-07-31 mode-page design audit | 2026-07-31 | | #163 | P2 | task | Redesign Services search results (Progressive Referral Workflow) | **Outcome:** `/services?q=` uses query-as-H1 (not match-count), progressive shortlist/compare, no always-on decision panel or giant step rail. **Product pick:** direction B from comps in `public/mockups/mode-page-redesign-2026-07/services-search/`. **Next:** implement B; keep referral shortlist behaviour. **Stop:** do not change Services ModeHome in the same PR. | session 2026-07-31 mode-page design audit | 2026-07-31 | | #164 | P2 | task | Redesign Favourites as hybrid dashboard + search (no ModeHome) | **Outcome:** `/favourites` is one dashboard+search workspace; empty query shows Continue/recent/sets/table; typed query filters in place; no ModeHome hero. **Product pick:** Search-Led Workspace (direction B) from comps in `public/mockups/mode-page-redesign-2026-07/favourites-hybrid/`. User rejected ModeHome for Favourites. **Next:** implement B; retire command-library marketing H1 and redundant dual search. **Stop:** do not reintroduce ModeHome or a separate Favourites home route. | session 2026-07-31 mode-page design audit; user Favourites hybrid decision | 2026-07-31 | | #165 | P2 | task | Adopt a consolidated answer-home notice block — the studies exist, nothing adopts them | **Outcome:** the answer hero states its safety obligation, its scope, and its verification requirement as one block in one voice. **Detail:** `/mockups/warning-consolidation` (PR #1437) diagnoses today's three stacked notices — the APP-5 privacy warning at 11px muted, a bare `/privacy` link, and an accent-blue `ShieldCheck` capability claim at 14px semibold — and shows the hierarchy is inverted: the least important line is the loudest, and two shields with opposite meanings sit ~40px apart. Three consolidations are drawn at 1440px and 390px. Recommended: **02 Safety card** on the hero (obligation on a warning-tinted top row, everything descriptive in one grey voice below) and **01 Assurance bar** on the docked composer — the same content model at two densities, so one component with a `density` prop covers both. **This is a governance change, not just a design one:** `PrivacyInputNotice` is the single site-wide APP-5 line and renders on the answer, documents and calculators composers, so all three move together; `tests/privacy-ui.test.ts`, `tests/ui-accessibility.spec.ts` and the phone-chrome reserve coverage all assert against the current markup and must change in the same commit; and the PR will need a full `## Clinical Governance Preflight` (the mockup PR correctly did not). **Third study (before/after):** `/mockups/answer-home-proposal` draws the concrete D-direction proposal as a full hero before/after rather than an isolated notice. **Second study (words only):** `/mockups/warning-line` answers a narrower brief — no icon, border, tint or background, one line where width allows. Six variants A-F; line counts measured from the rendered DOM, not asserted. Only B (middot clauses), D (obligation + verify) and F (compressed obligation) hold one line at desktop width, and **none fit one line on a 390px phone while the pinned APP-5 sentence stays verbatim** — 46 characters of obligation plus the 27-character link exceeds the ~60 available at 11px. Recommended there: **D**, the only compliant variant that is both one line and keeps weight-only hierarchy, reached by dropping the scope claim (a capability statement already visible on the answer itself). F fits best but rewrites the pinned obligation to \|No patient-identifiable information.\| and so needs the same privacy sign-off as `#166` plus a matching `tests/privacy-ui.test.ts` update. **Status:** PR #1437 was closed unmerged on 2026-07-30 as a deliberate pause during an owner-authorized ordered merge sweep, to be reopened at its queued place; branch `claude/warning-consolidation-mockups-09jyj7` is preserved and merged onto current `main`; these follow-up rows have been renumbered on each sync because `main` kept claiming the next ids while the PR was paused; the superseded numbers are deliberately not listed, since they now belong to unrelated rows. **Next:** decide block (02 + 01) versus line (D) direction, get wording sign-off for `#166`, then implement behind one component and run `verify:phone-chrome` before `verify:ui`. | session 2026-07-30; PR #1437; `/mockups/warning-consolidation`; `/mockups/warning-line` | 2026-07-30 | -| #168 | P2 | rec | Sequential issue ids force every concurrent append to conflict | **Outcome:** two sessions can append to this ledger at the same time without conflicting. **Detail:** ids are allocated read-modify-write against the `issues:next-id` marker inside the file being edited, so two branches both read N and both write N. Because duplicate ids are unacceptable, a union merge driver is unsafe — .gitattributes says so explicitly — which is why this file deliberately has no driver and every overlapping append conflicts by hand. Manual resolution is where rows get dropped: PR #1490 was closed during one and took the only record of four snapshots with it (#152), and ids were renumbered under in-flight work three times in one session (#154, #155). The new writer (`scripts/outstanding-issues.mjs`) removes the mechanical errors but explicitly not this one. **Next:** replace the counter with a collision-free id (ULID, timestamp+suffix, or a content hash), keeping a short display number derived at render time if `#151` reads better than 01JQ…; then a union driver becomes safe to reinstate and concurrent appends stop conflicting at all. A larger variant is one row per file under `docs/issues` with the table generated, which the repo already does for `site-map.md`. **Stop:** do not reinstate `merge=union` while ids are sequential — that combination was tried in PR #1416 and removed for duplicating rows and the marker. Renumbered from this PR's original `#159` because `main` already used `#159` for the duplicated test-file-list finding. | session 2026-07-31; .gitattributes; #154/#155; PR #1524 sync | 2026-07-31 | +| #168 | P2 | rec | Sequential issue ids force every concurrent append to conflict | DESIGNED 2026-08-14 in PR #1944 — docs/ledger-id-scheme-proposal.md. Design only, nothing implemented, so this row stays open. Recommends a ULID as the durable id with a short derived display form, the property that matters being that the display form is derived rather than stored: a clash there is a rendering fix (take one more character) rather than a renumber. UUIDv7 noted as an equally good fit. Records why timestamp-plus-slug and content hashes were rejected — the slug wants to change when a row is re-scoped, which is renumbering under another name, and a content hash is neither sortable nor stable. Migration is additive because the 314 existing sequential ids keep their numbers permanently: they are cited across the ledger, docs/branch-review-records/, AGENTS.md, the skills and the commit history, so renumbering would invalidate every citation while producing exactly the churn this row exists to end. Four steps, widening validators before allocation changes, with every current #NNN assumption enumerated by file and symbol (ledger-inbox.mjs validateRequest twice; check-outstanding-issues.mjs ID_CELL, the MARKER parse, the nextId-above-highest assertion and its padStart formatting; outstanding-issues.mjs allocator; issues-report.mjs and the issues-surface hook). Stop unchanged and now load-bearing on step ordering: do not reinstate merge=union while ids are sequential — it only becomes safe after the marker is gone. | session 2026-07-31; .gitattributes; #154/#155; PR #1524 sync | 2026-07-31 | | #169 | P2 | issue | Machine-local branches, snapshots, worktrees, and dev servers remain at risk | **CONSOLIDATED 2026-08-13 from #152, #236, and #260 before those source rows are archived by PR #1920. Outcome:** every branch, snapshot, worktree, or process that exists on only one machine remains recoverable and receives an explicit owner disposition before machine or worktree cleanup. **Original unpushed branches:** `claude/clinical-kb-design-system-333a69` was verified to contain 57 files / +4069 at tip `feat(design-system): v2 token layer, 26 components, browser-crash fix`, including `.design-sync/previews/*.tsx` absent from main. Also inspect `design-sync-db0a54`, `fable-implementation-fc937c`, `frosty-mayer-2c6167`, and `issues-133-evidence`. **Preserved WIP snapshots from #152, all unpushed, unreviewed, and unverified:** `codex/reconcile-immediate-20260730` at `748ef018f` (21 files, +395/-200 across 19 tracked, including `.github/workflows/ci.yml`, `package.json`, and `docs/scripts-index.md`); `codex/document-results-mockup-20260730` at `5dbd9f965` (8 tracked files, +13/-3, plus an untracked `document-search-results/page.tsx` mockup); `codex/chat-ledger-triage-d344` at `b7eae51a4` (`docs/outstanding-issues.md` +59/-61); and `claude/section-spy-browser-coverage` at `d949859c3` (`tests/ui-smoke.spec.ts` +51). **Wave-5 inventory from #236:** content-compare `claude/ds-v2-builder-a` and `claude/ds-v2-builder-b` with current `origin/main` because squash merges make ancestry checks unreliable; retain the associated process evidence for ports 3258 (`Database-wt-ds-v2-capture`), 3135 (`Database-wt-ds-v2-correctness`), and 3672 (`Database-wt-ds-v2-empty-state-heading`) until the owner confirms each process is no longer needed. **Stranded Sentry work from #260:** on the originating Windows machine, inspect branch `claude/cloud-pr-loop-prevention-bc052b` commits `c3c9d6a31` and `abbcdc8e9` (~389 lines across `src/sentry.*.config.ts`, `src/lib/env.ts`, `src/lib/supabase/client.tsx`, and `src/components/ui-primitives.tsx`) plus the same four uncommitted files in `.claude/worktrees/pensive-borg-6be2f0`; content-compare them with remote branches `claude/sentry-nextjs-sdk-setup-2v24q5` and `cursor/sentry-nextjs-sdk-7cee`, then record whether the work is unique, remotely preserved, or proven superseded. **Verification rule:** do not use `git rev-list` counts, three-dot diff, or ancestry alone to declare squash-merged work represented; verify the branch-added files or content against current main. **Cloud-session stop:** fresh cloud containers cannot observe the originating machine's local branches, worktrees, or processes, so never close this row from a cloud inventory that reports them absent. **Next:** complete and record each disposition from the originating machine. **Stop:** retain every listed branch, snapshot, worktree, and process record until content proof and owner disposition exist. | sessions 2026-07-30/31 and 2026-08-04/07; #152/#169/#236/#260; PR #1920 review | 2026-07-31 | | #175 | P2 | task | Therapy modality is now null on all 205 records and needs curation or removal | VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: src/data/therapies-source.json holds 205 records and 0 carry a modality value, exactly as described. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. **Outcome:** the Therapy detail and recommend screens either show a curated modality or stop carrying the field at all. **Detail:** the source catalogue derived `modality` from each record's own tag list — all 205 records had one, every value was also present in that record's `tags`, and the whole catalogue collapsed to CBT/ACT/DBT. It mislabelled the treatments it could not describe: ECT and rTMS as "ACT", Psychoanalysis and Psychodynamic Psychotherapy as "CBT", MBT and TFP as "DBT". Pre-existing on main, surfaced by the PR #1489 review. The generator emits it only when the source curates a value that is not already a tag, which today means null for 205/205 on the index projections *and* the full catalogue the detail/recommend screens load (`catalogue: "full"`), so the two chips (`detail-screen.tsx:49`, `recommend-screen.tsx:115`) never render and `select.ts:117` contributes no same-modality point. Removal was provably search-neutral: `src/lib/therapies.ts` scores with boolean `haystack.includes(token)`, not term frequency, and every modality value was already contributed by `tags.join(" ")` in the same haystack. **Next:** one of two — curate real modality values in `src/data/therapies-source.json` (clinical work, needs the psychiatrist), or drop the field from `types.ts`, `src/lib/therapies.ts`, the two chips and `select.ts`. **Stop:** do not reinstate the tag-derived value to make the chips reappear; a guess rendered as curated fact is the defect. `tests/therapy-compass-pathways.test.ts` pins the echo invariant on both the index and the full catalogue asset. Renumbered from this PR's original `#169` because `main` claimed `#169`–`#174` while the branch was open. | PR #1489 review remediation; PR #1532; session 2026-07-31 | 2026-07-31 | -| #178 | P2 | rec | pr-policy does not flag operational risk bundled with clinical or UI risk | IN FLIGHT 2026-08-12 in PR #1837 (harden verification & PR policy guards). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. **Outcome:** a PR that mixes operational-risk paths with clinical or UI risk is called out before it merges, because squash-merging that mix destroys per-item revert. **Detail:** `classifyPullRequestFiles` already computes `operationalRisk`, `clinicalRisk`, `ragRanking` and `ui` independently, but nothing reacts to the combination. AGENTS.md's "PR bundling" section forbids bundling anything once `operationalRisk` is true; the classifier is where that could be enforced. PR #1489 is the worked example: 33 files spanning `.github/workflows/ci.yml`, both Dockerfiles, a rewrite of the bundle-budget gate, a phone-chrome scroll change and a therapy data restructure, merged as one squash (945148251). Reverting any single item now means hand-reverting hunks out of the squash commit, because the branch commits are unreachable. The remediation PR for that review repeats the pattern on a smaller scale (clinical data plus a one-line ci.yml timeout), which is why this is a recommendation rather than a hard gate — the right severity is probably a warning that names the mixed classes, not a merge block. **Next:** emit an advisory line from `evaluatePullRequestPolicy` when `operationalRisk` coincides with `clinicalRisk` or `ui`, listing which paths drove each; decide separately whether it ever blocks. Cover it in the `--self-test` block. **Stop:** do not make it a hard failure in the same change that introduces it — land the signal first and see how often it fires. Renumbered from this PR's original `#172`. | PR #1489 review remediation; PR #1532; session 2026-07-31 | 2026-07-31 | | #183 | P3 | task | Create Sentry metric alert for production DB span p95 > 500ms | **DEPRIORITISED 2026-08-12 (yield review against current main).** A production DB p95 latency alert for a system with one user; the alert has nobody to wake. Revisit alongside #027 when real usage exists. Still blocked 2026-08-01 closeout: SUPABASE_ACCESS_TOKEN and SENTRY_AUTH_TOKEN missing from session env; Sentry MCP OAuth can list/get alerts but has no create tool; browser hits login wall; no metric rules exist yet on clinibase-xz. Create Metric Alert: p95(span.duration), filter span.op:db, environment production, threshold >500ms, notify Active Members. Provide SENTRY_AUTH_TOKEN in session to finish via sentry alert metrics create. | session 2026-07-31 db-query-perf follow-up | 2026-07-31 | | #189 | P2 | task | Pin /api/search route-level round trips and disposition the x3 text RPC probes | **Outcome:** a counting-proxy budget drives `POST` `/api/search` (auth/ratelimit/scope/enrichment/telemetry), and the retrieval-core finding that `match_document_chunks_text_v2` and `match_document_table_facts_text_v2` each issue three times per search is either documented as intentional or collapsed under the RAG canary gate. **Source:** residual next actions on `#098` after answer-path and retrieval-core budgets landed. **Next:** (a) route-level budget following `tests/answer-route-preamble.test.ts`; (b) decide probe vs collapse — behaviour change needs RAG flag + canary. **Stop:** do not change retrieval assembly without approval. | session 2026-07-31; #098 residual; tests/search-round-trip-budget.test.ts | 2026-07-31 | | #190 | P3 | task | X3: Finish rag.ts monolith decomposition | **DEPRIORITISED 2026-08-12 (yield review against current main).** Structural churn on the most safety-critical and most protected file in the repo, with no user-facing benefit and real behaviour-drift risk on a live-validated clinical answer path. Do the extractions opportunistically when a feature change already requires being inside a region, not as a standalone project. VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: src/lib/rag/rag.ts measures 4,362 lines — still the monolith this row describes; the decomposition has not started. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. **Outcome:** `src/lib/rag/rag.ts` is decomposed into focused modules per `docs/maturity-backlog-workorders.md` X3, with existing offline RAG contracts green. **Status:** IN PROGRESS (DocumentViewer/Dashboard extractions done; rag.ts remains). **Next:** continue safe extractions only with the RAG flag before editing protected surfaces; one verified draft PR per unit. **Stop:** no behaviour change without canary when retrieval/answer paths move. | docs/maturity-backlog-workorders.md X3; #086 | 2026-07-31 | @@ -182,34 +174,26 @@ removed after current-main verification; it is not missing recommended work. | #194 | P3 | task | L1: Archive retired backfill one-shots and dead ci-change-scope token | VERIFIED CORRECT 2026-08-12 — re-checked against merged main and left open: Still open: five backfill one-shots remain under scripts/ (backfill-document-covers.mjs, backfill-document-tags.ts, backfill-enrichment.ts, backfill-gold-document-labels.ts, backfill-smart-index.ts). No dead ci-change-scope token was found, so that half may already be gone — confirm before archiving the row. **Outcome:** retired `backfill:*` one-shots and the dead `ci-change-scope` token are archived/removed with docs/script index updated. **Status:** IN PROGRESS (#1033 archived m13/july8; backfills still open). **Next:** finish backfill archive + token cleanup in a docs/scripts PR. **Stop:** do not break CI classifiers. | docs/maturity-backlog-workorders.md L1; #086 | 2026-07-31 | | #195 | P3 | task | M1: Repo-host hardening (branch protection and required checks) | **Outcome:** GitHub branch-protection rulesets and required checks match audit §8 / maturity M1. **Next:** maintainer GitHub UI work; not a repo-file change. Record evidence in the ledger when done. **Stop:** agents must not weaken required checks. | docs/maturity-backlog-workorders.md M1; #086 | 2026-07-31 | | #206 | P2 | task | AnswerState partial_retrieval has no app-facing producer | VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: `partial_retrieval` is declared in src/lib/answer-state-types.ts:63 and handled in answer-clipboard.ts:75, but nothing in src/app or the retrieval path produces it — still no app-facing producer, as the row says. Do not synthesise it from candidate counts. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. PR-E step 0 found nothing in the client payload names which expected sources were unavailable (retrievalDiagnostics = candidate counts; conflictsOrGaps = prose). RetrievalStateBanner supports the state but PR-J adoption can only emit ready/stale_evidence/source_only. Next action: decide whether a separate RAG contract PR should add a named missing-source signal (governance preflight + RAG impact line + offline eval); until then do not synthesise the state from counts. Pinned by tests/answer-state-contract.test.ts and SPEC 13 / COMPONENTS 2. | PR-E step 0, session 2026-08-02 | 2026-08-02 | -| #209 | P3 | task | DS V2 Gate 1: add contrast pair for --warning used as body text | IN FLIGHT 2026-08-12 in PR #1841 (adds an explicit --warning body-text contrast assertion in tests/design-token-contract.test.ts). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. VerificationNotice's caution variant and DoseLine's overdue label use --warning at text tier — the only place a status hue is used as body-text colour rather than a --text-* token. Gate 1's contrast checking must add that pair explicitly rather than assuming the text tiers cover it. Also note: the logged-once Sets in missing-value, date-display, verification-notice, answer-state and retrieval-state-banner are module-level, so on the server they are per-process and unbounded; a persistent data defect logs once at boot then is swallowed. Acceptable while unregistered. | clinical-governance-reviewer P3 findings on PR 6; recorded in docs/design-system/SPEC.md PR 6 clinical review note | 2026-08-02 | | #210 | P2 | task | npm run ensure generates .next/dev types that break typecheck and every Playwright build | RE-SCOPED AGAIN 2026-08-13 (re-filed: the 2026-08-12 correction was lost when PR #1880 landed under the inbox architecture without a request being written for it). Half of this row is already fixed and its prescribed fix is REFUTED — do not apply the first suggestion. (1) FIXED: `npm run typecheck` runs `tsconfig.typecheck.json` (added in 450690f citing this row), which sets its own include and excludes `.next/**`; verified green with `.next/dev/types/validator.ts` present. (2) REFUTED: dropping `.next/dev/types/**/*.ts` from tsconfig.json does NOT hold. Next 16 emits that glob itself — `getTypeDefinitionGlobPatterns` (node_modules/next/dist/lib/typescript/type-paths.js) adds both `.next/types` and `.next/dev/types` deliberately 'to avoid tsconfig churn when switching between dev/build modes', and `writeConfigurationDefaults` adds a missing glob back when Next reads the root config directly. Deleting the line only re-creates an uncommitted change. (3) STILL OPEN, narrower than originally written: `scripts/run-playwright.mjs` writes an isolated tsconfig with `extends: '../../tsconfig.json'` and no include of its own, so it inherits the repo-root globs. The recorded `tsc --showConfig` probe resolved `../../.next/dev/types/**/*.ts`, and `--listFilesOnly` pulled in the root dev types including validator.ts. Next's API checker filters dev types with `getDevTypesPath`, but the default `experimental.useTypeScriptCli: true` path uses `runTypeCheckCli` to invoke `tsc --project` against the child config, so it honours the inherited include verbatim. Next: give the isolated tsconfig its own include/exclude (its run root is `.next-playwright/`, not under `.next/`, so excluding the repo-root `.next` keeps the run's own dist types). NOT PROVEN end-to-end: the failing Playwright build was not reproduced. Correcting the previous explanation, `next build` does not mutate this child config: Next 16.3 `writeConfigurationDefaults` returns immediately when the parsed config contains `extends` or `references`, and this config always contains `extends`. Confirm the remaining inherited-include hypothesis with one focused `verify:ui` build before and after the child include/exclude change, and hash the child tsconfig immediately before and after the build to prove it remains byte-identical. Stop: do not remove typecheck from the gate, and do not retry the include deletion. | session 2026-08-02 /ledger sweep; docs/review-findings-2026-08-02.md | 2026-08-02 | -| #211 | P3 | task | Plan and start the noUncheckedIndexedAccess migration | **DEPRIORITISED 2026-08-12 (yield review against current main).** 1,266 sites, each a local judgment, and no open ledger row traces a defect to unchecked indexed access. Real hardening, but speculative against this repo's measured failure history, and the diff conflicts with every open PR. Do it in scoped batches after the clinical and CI-trust work. VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: `noUncheckedIndexedAccess` is absent from tsconfig.json — the migration has not begun. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. Enable noUncheckedIndexedAccess in a branch and remediate the 1,266 errors, starting with the 15-20 highest-risk source files. Hot spots include worker/main.ts:901-942, src/lib/rag/rag-extractive-answer.ts, and src/lib/answer-verification.ts. Prefer ?. or ?? guards, or non-null assertions only where invariants are provable. Re-run npm run test and npm run typecheck before merge. See docs/review-findings-2026-08-02.md section 6. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | +| #211 | P3 | task | Plan and start the noUncheckedIndexedAccess migration | **DEPRIORITISED 2026-08-12 (yield review against current main), and that judgment still holds** — each site is a local judgment, no open ledger row traces a defect to unchecked indexed access, and the diff conflicts with every open PR. Do it in scoped batches after the clinical and CI-trust work. This update carries that conclusion forward rather than replacing it; what has changed is that the batches now exist on paper and the count was wrong. **RE-MEASURED AND PLANNED 2026-08-14 in PR #1944.** The staged plan is docs/no-unchecked-indexed-access-migration-plan.md; the migration has NOT started and tsconfig.json is unchanged, so this row stays open and stays deprioritised. Measured against main at d47aa6d rather than reusing the 2026-08-02 figure: **1,445 errors across 269 files, up from 1,266**. The drift is itself a finding — the flag is off, so nothing stops new unchecked indexing landing, and any plan built on the stale count under-scopes. The measurement also reshapes the job in a way that supports doing it in batches: tests/ (713) plus design-scratch mockups (237) are two-thirds of the population and carry no production consequence, so the genuinely risky remainder is about 500 errors, not 1,445. Shape is 71 percent TS2532/TS18048, which a guard fixes; the 368 TS2345/TS2322 need a real decision about what the absent case means. Hot spots unchanged and confirmed: answer-verification.ts (41), rag-extractive-answer.ts (23), worker/main.ts (23), evidence.ts (19). Six stages, cheapest first, each flagged mechanical or manual with its own gate. Key constraint the plan records: noUncheckedIndexedAccess is a whole-project option and narrowing include does not isolate a directory, because TypeScript still reports errors in every transitively imported file — so the flag flips exactly once in the final PR and intermediate stages are verified by a baseline ratchet in the shape of scripts/design-system-contract-baseline.json. Stage 6 touches src/lib/rag/**, so the plan writes out the flag-before-editing, RAG impact line, and live-canary obligations. Stop unchanged: do not flip the flag on main ahead of the final stage. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | | #212 | P3 | task | Replace as unknown as casts and unvalidated JSON.parse with Zod or runtime guards | **DEPRIORITISED 2026-08-12 (yield review against current main).** 40 casts at trust boundaries. Same reasoning as #211: worth doing, no measured defect traces to it, and it competes with clinical work for review attention. VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: 40 `as unknown as` casts remain under src/ — the row's population is intact. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. 48 as unknown as casts and ~24 unvalidated JSON.parse calls across src/ trust Supabase, OpenAI, localStorage, file metadata and extraction boundaries. Start with src/lib/rag/rag.ts and src/app/api/* routes, mirroring existing Zod use in src/lib/validation/body.ts and src/lib/extractors/document.ts. See docs/review-findings-2026-08-02.md sections 2.2, 2.3 and 8. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | -| #213 | P2 | task | Stop swallowing fetch and stream errors with empty catch handlers | SCOPE RE-MEASURED 2026-08-12 on merged main: only **3** empty catch handlers remain under src/ (`catch {}` / `catch (e) {}`), down from the audit population this row was opened against. The principle is unchanged and the remaining three still need dispositioning — each should either handle, log through the observability path, or carry a comment saying why swallowing is correct — but this is now a small, closeable job rather than a sweep. Companion rows measured in the same pass for sequencing: #212 has 40 `as unknown as` casts left, #211's `noUncheckedIndexedAccess` is still absent from tsconfig.json. Do the three catches first; it is the cheapest of the three and no longer blocked behind the other two. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | | #215 | P3 | task | Add image-optimization basics for lightbox, PWA lifecycle and demo PNGs | **Outcome:** two of the four image-only findings from the 2026-08-02 audit are shipped; two remain open for an explicit implementation-or-drop decision. **RESTATED 2026-08-13 after inspection against main 2d270392 — two of the four items already shipped and the row no longer describes them as open.** DONE: src/components/clinical-dashboard/image-lightbox.tsx carries decoding="async" (Phase 0, PR #1660), asserted by tests/signed-image.dom.test.tsx. DONE: SignedImage has the priority prop for above-fold evidence — it also skips the IntersectionObserver deferral entirely — and document viewer Phase 3 (PR #1772) added the other half of that pair: an explicit fetchPriority of high when priority is set and low otherwise, so a deferred rail figure does not contend with the page's own above-the-fold work. The document rail additionally passes a 240px observer root margin against the shared 640px default. REMAINING, both confirmed by inspection rather than inferred: (a) src/components/pwa-lifecycle.tsx still has no decoding attribute; (b) public/demo-documents/ still contains no .webp — the PNGs are ~80 KB each and served as-is, so the conversion with a PNG fallback has not been done. **Next:** apply decoding=async in pwa-lifecycle.tsx, and either convert the demo PNGs to WebP with a PNG fallback or record that an ~80 KB synthetic demo asset is not worth the build step. **Stop:** do not treat this row as covering the broader performance findings — those live under #016, #013, #117 and #147. | session 2026-08-02 /ledger sweep — docs/audit/performance-image-cwv-audit-2026-08-02.md | 2026-08-02 | -| #221 | P3 | task | Local EmptyState, LoadingState and Chip duplicates still unconverged after PR-J | IN FLIGHT 2026-08-12 in PR #1841 and #1842 (cn()/LinkAction contracts and the EmptyState/Chip convergence). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. PR-J converged what it could inside its allowlists and left four known duplicates, each blocked for a stated reason rather than missed. therapy-compass/ui.tsx defines its own LoadingState AND its own EmptyState used across nine screens (whole-module job, not a one-call-site conversion). mode-home-template.tsx ModeHomeStatusNotice is an EmptyState duplicate that four catalogue homes delegate to, which is why those four files show no diff. differentials-home.tsx has a local two-density Chip blocked by the cn() tailwind-merge gap. favourites-command-library-page.tsx SmallChip is driven by an eight-entry type-token map that Chip's five-tone vocabulary cannot express. Next action: take these as one convergence PR after the cn() decision lands, not piecemeal. Found during PR-J adoption, 2026-08-03. | session 2026-08-03 (PR-J Wave 5, Builder B) | 2026-08-02 | | #222 | P3 | task | Headers surface only partially converged in PR-J: mode-home-template and search-results-header-band untouched | VERIFIED CORRECT 2026-08-12 — re-checked against merged main and left open: Still unconverged: src/components/mode-home-template.tsx defines ModeHomeStatusNotice locally (:232) and imports neither PageHeader nor the DS EmptyState; search-results-header-band.tsx is likewise untouched. Note the adjacency — in-flight PR #1842 delegates ModeHomeStatusNotice to the DS EmptyState under #221, which is a different conversion from the PageHeader question this row asks. Re-check after #1842 merges. Builder A converged DsmPageHeader, InformationPageHeader and InformationPageBreadcrumbs onto PageHeader plus Breadcrumb, and declined two files with reasons. mode-home-template.tsx ModeHomeHero is a centred display hero on the fluid text-hero token and is the slot the in-flow phone composer sits in, so converging it onto a left-aligned PageHeader is a redesign of 13 mode homes that collides with the one-composer-per-page contract. search-results-header-band.tsx is a results spine carrying status, counts and filters, not a page-title stack, so its pin tests/search-results-header-band.dom.test.tsx remains unflipped. Both are defensible; both leave the headers surface partially adopted. Next action: decide whether either is in scope at all, or record them as permanently out of the PageHeader vocabulary. Found during PR-J adoption, 2026-08-03. | session 2026-08-03 (PR-J Wave 5, Builder A) | 2026-08-02 | | #231 | P1 | issue | Generation fallbacks no longer stick in answer cache; lithium generation quality still falls back safely | PARTIAL 2026-08-12: This PR fixes the clinically consequential stale-fallback path: every answer whose routing or degraded reason contains generation_fallback is excluded from rag_response_cache. Offline evidence: 96 focused answer-route tests and 574 RAG fixture/contract tests passed. Approved live baseline/final canaries preserved 36/36 document and content recall at 1.0 with zero per-case reciprocal-rank regressions; the final 44-case answer gate had zero citation or numeric-grounding failures. A budget extension was tested and rejected: four cache-bypassed 'Lithium dosing?' probes remained grounded, cited safe extractive fallbacks at 35-40 second candidate budgets; the decisive 40-second probe completed generation in 25.272 seconds and 27.237 seconds total with route_deadline_exceeded=false, but failed generation quality. Therefore OPENAI_ANSWER_TIMEOUT_MS and the route budget are not the current residual binding cause. Next: instrument and reproduce the structured generation-quality failure using provider-safe metadata, then make a separate bounded output-quality fix with an offline fixture and live canary. Stop: do not increase route/provider timeouts or cache any generation fallback. | session 2026-08-04 (production triage, live /api/search + /api/answer) | 2026-08-04 | -| #233 | P3 | task | COMPONENTS.md section 0 describes the pre-adoption world, and the optionality-marker contract change is undocumented | IN FLIGHT 2026-08-12 in PR #1842 (records DS adoption evidence and convergence state). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. Two documentation debts left by PR-J, both in docs/design-system/COMPONENTS.md, naturally one PR. First: section 0's maturity matrix is stale. FormField, TextField, SearchField, Select, Checkbox, RadioGroup, PageHeader and Breadcrumb now have real product mounts, so 0.1 and 0.2 misdescribe what is registered versus built-but-unregistered, and 0.4's field-shell defects are closed by the five-control fold. A reader deciding whether a component is safe to adopt is reading the wrong answer. Second: FormField now marks only the requirement and leaves optional fields unmarked - (optional) was removed app-wide by design decision and is pinned by tests/ui-v2-form-field.dom.test.tsx - which is a design-system contract change that appears in no document. It belongs in COMPONENTS.md section 4 and probably DECISIONS.md. Next action: one docs PR updating section 0 from the actual mount list and recording the optionality rule with its rationale. Stop: do not re-add (optional) markers to satisfy a generic form-accessibility rule - the removal was deliberate and is test-pinned. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | -| #234 | P3 | task | answer-copy-payload.ts is the single clipboard payload builder for three surfaces and has no documentation | IN FLIGHT 2026-08-12 in PR #1842 (publishes the answer-copy clipboard contract). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. src/lib/answer-copy-payload.ts arrived in PR-J exporting answerStateForAnswer, buildAnswerClipboardText, resolveAnswerSources, citedSourcesOnly and singleDocumentClipboardMetadata. It is now the one place three product surfaces build a clipboard payload, which makes it a contract rather than a helper: a future caller that bypasses it can reintroduce the false-attribution defect the module exists to prevent (see #228). Nothing in docs/design-system mentions it. Next action: document the module and its five exports where the answer surface's copy contract is described, and state that new copy paths route through it rather than composing their own text. Found during PR-J close-out, 2026-08-04. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | | #235 | P3 | task | ADOPTION.md section 7 proof shots exist for only four of the adopted surfaces | IN FLIGHT 2026-08-12 in PR #1842 (records adoption evidence). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. The adoption contract asks for a proof shot per adopted surface. The Wave 5 adoption captured four - DSM header, settings rows, patient panel, answer surface - and none for the forms fold, the catalogue and docs surfaces, the headers convergence, or the empty states adopted since. Section 7 therefore reads as complete while most of the adoption is unevidenced, which matters because the proof shot is what a later reader uses to tell an intended restyle from a regression (the #229 DSM eyebrow was almost rediscovered as a defect for exactly this reason). Next action: capture the missing shots against a warmed local server and attach them to section 7. Cheap and mechanical - no gate, no provider access. Stop: this is not the visual-baseline harness (#118) - do not commit Playwright snapshot PNGs or flip that job to blocking. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | | #237 | P2 | rec | Eyeball low-confidence AccessibleTable densities at 320px before freezing Linux visual baselines | IN FLIGHT 2026-08-12 in PR #1841 (renders empty dense cells wrapping rather than truncated, with a 320px jsdom assertion). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. PR #1616 clinical MissingValue phrases increase text volume in sparse OCR grids. Contract forbids abbreviating to a dash. Next: open one real lowConfidence extraction at 320px phone width and accept or adjust dense preview column widths before committing Linux screenshots (#118). | PR #1616 review findings; session 2026-08-05 | 2026-08-05 | | #238 | P2 | rec | Visual pass for Sheet portal default on settings, sidebar, and answer overlays | IN FLIGHT 2026-08-12 in PR #1842 (exercises the Sheet portal default and adds tests/sheet.dom.test.tsx). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. PR #1616 flips Sheet portal default to true, moving ~10 product overlays into OverlayRoot. Token inheritance is safe; residual risk is ancestor-scoped CSS / contain / transform. Next: one visual pass of settings-dialog, ClinicalSidebar, answer-result sheets, launcher sheet, section-nav. | PR #1616 review findings; session 2026-08-05 | 2026-08-05 | | #239 | P3 | rec | Manual phone rotation check for ResizeObserver-only phone chrome reserve | PR #1616 phone overlay reserve publishes only from ResizeObserver quiet-window deliveries. Desktop↔phone and late-mount recovery are covered; orientation that does not change stack height is a narrower trigger. Next: rotate a physical phone on a chrome-overlay route and confirm --phone-overlay-chrome-h updates. | PR #1616 review findings; session 2026-08-05 | 2026-08-05 | | #240 | P3 | rec | Confirm tooltip visual hard-clip asymmetry with design owner | Tooltip keeps overflow-hidden visual clamp while sr-only/aria-label retain full text. Design contract says supplementary-only. Next: design-owner confirmation that sighted users losing the clipped tail is acceptable, or allow overflow-y-auto for long clinical strings. | PR #1616 review findings; session 2026-08-05 | 2026-08-05 | | #242 | P2 | task | Commit approved Linux visual baselines and promote adoption not-committed → committed | VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: Six linux/ PNGs are committed, but the adoption manifest still carries 68 `not-committed` entries — the surfaces flip is the remaining work, as stated. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. Baselines and provenance are DONE as of PR #1729 (branch claude/ds-adopt-visual-baselines): all six linux/ PNGs committed from ubuntu artifact visual-baseline-31251091603 (main @ bc33d414e), AWAITING_BASELINE emptied, and tests/__screenshots__/linux/provenance.json written with per-candidate SHA-256 + dimensions and an approved human review. Proven by that PR's own run: visual-junit tests=9 failures=0 skipped=0, and no visual-candidates/ directory, i.e. all six compared rather than skipped. REMAINING: only the surfaces flip to baseline.status committed. Blocked on ordering, measured 2026-08-08: validateLinuxVisualBaselineSet short-circuits on declaredPaths.length===0, so declaring files activates its rule that no non-allowlisted path may change since candidateSourceHead — and PR #1729 necessarily changed tests/design-system-adoption.test.ts, whose initialiseCandidateRepository seeded fixtures from the LIVE spec and so failed the moment AWAITING_BASELINE emptied. The two cannot land together. Next: after #1729 merges, re-capture candidates from a main run that already contains that fixture fix, then flip the surfaces against that head. Note this does not affect whether pixels compare — Playwright compares because the goldens exist on disk. | PR #1616 review findings; session 2026-08-05 | 2026-08-05 | -| #245 | P3 | rec | responsive-compact CrossModeLinks keeps duplicate rails in the DOM | IN FLIGHT 2026-08-12 in PR #1842 (CrossModeLinks rail behaviour). Checked against the open-PR list during the full ledger sweep. Do NOT start this row while that PR is open — duplicating a queued conversion is the exact failure #292 records, and it has happened twice. Re-verify this row against main after that PR merges, and close it there rather than here. Phone chip rail and md+ card rail both mount; display:none removes the inactive from the a11y tree. Tests/analytics counting role=link see doubles; cross-mode-links-rail is phone-only. Next: prefer the variant test ids; do not collapse to one rail with JS breakpoints (hydration risk). | PR #1616 review findings; session 2026-08-05 | 2026-08-05 | | #248 | P2 | issue | Investigate why 20260705180000 search-health indexes were missing on live despite applied history | APPEND 2026-08-13: the prior closure is withdrawn. Repository and live-drift evidence establishes that 20260705180000_reconcile_search_health_indexes.sql is recorded as applied while documents_title_trgm_idx and document_chunks_content_trgm_idx are missing on live. Supabase transaction semantics exclude a persisted partial migration, but the present record does not distinguish skipped DDL/history repair from indexes created and later dropped. In an approved read-only window, query supabase_migrations.schema_migrations for the 20260705180000 statements fingerprint and inspect the relevant audit/history evidence; retain both hypotheses until that evidence establishes the cause. Separately, scheduled check:drift did detect the missing indexes, but red runs were not routed. | PR #1614 review / session 2026-08-05 (renumbered on main merge) | 2026-08-05 | -| #258 | P2 | rec | The PR-handoff stop rule is enforced for Claude Code only; Codex and Cursor get prose with no gate | **Outcome:** a session that opens a PR stops following it in every agent this repo supports, not just Claude Code. **Detail:** PR #1649 added `.claude/hooks/pr-handoff-stop.sh` plus the AGENTS.md "Stop when the pull request is open" section. The hook is registered in `.claude/settings.json`, which only Claude Code reads, so the PostToolUse marker and the PreToolUse denials (shell `gh pr checks/status/view/run watch`, GitHub MCP tools named pull_request/workflow_run/workflow_job/check_run/check_suite/job_log/update_branch, and Monitor/ScheduleWakeup/CronCreate) simply do not exist for Codex or Cursor sessions. Those agents get the AGENTS.md prose and nothing else — and prose alone is exactly what was already in force, and already insufficient, before #1649. Cost is the same long tail of post-handoff CI polling the hook was built to cut, just relocated to whichever agent lacks the gate; a cloud Codex session is the worst case because nothing naturally ends it. **Next:** cheapest first — check whether Codex and Cursor expose any pre-tool interception this repo can register (Codex plugin hooks under `plugins/clinical-kb/`, Cursor rules under `.cursor/`); if neither offers a deny path, the fallback is a shared marker file plus a wrapper the agent is told to route `gh` through, which is weaker but still detectable. If no mechanism exists at all, record that explicitly here so the gap is a known limit rather than an open task. **Stop:** do not weaken the Claude Code hook to make the tools symmetric, and do not add a second copy of the deny list — one script, multiple registrations. | PR #1649; .claude/hooks/pr-handoff-stop.sh; .claude/settings.json; AGENTS.md "Stop when the pull request is open"; session 2026-08-07 | 2026-08-07 | -| #262 | P2 | task | DS Track A3: finish the design-token debt | Three parts. (1) DONE 2026-08-10 - --shadow-tight is retired outright: 90 gated production sites across 48 files (plus 60 mockup occurrences, migrated in the same pass so no file names a dead token) now reach for var(--e1), and all three declarations - both themes and the forced-colors flattening - are deleted. The alias resolved to exactly var(--e1) in every scope and the forced-colors block already flattened --e1 alongside the roles, so the substitution was value-preserving in light, dark and forced-colors and needed no visual review. Do NOT take that from the declarations alone for the remaining tranches: ckb-v2-tokens.css redeclares --e1 (light 13 40 71 / 5% vs globals 11 42 56 / 7%) and never redeclares the roles, and a custom property containing var() substitutes on the element it is DECLARED on - an alias declared in an outer scope and overridden in a narrower one freezes at the outer value. This migration is safe only because .ckb-v2 is on (layout.tsx) and .ckb-v2.ckb-v2 outspecifies :root, so the alias substitutes against the winning v2 tier; measured in Chromium, both spellings compute to rgba(13, 40, 71, 0.05) 0px 1px 2px 0px. Re-run that check per alias, it is about where a declaration sits. legacyShadowAliases 220 -> 127 with per-path counts pinned to measured, which also closed 3 aliases of re-accumulated stale slack across the other six roles (measured 217 against a 220 ceiling - the same drift #264 found on 9 Aug). design-token-contract.test.ts now asserts the token is absent from the whole stylesheet, mutation-verified. Remaining 127: soft 71, elevated 17, hover 17, card 12, lux 8, lift 2 - and count a token by reading the var() call, not the declaration it sits in, because two of the soft hits are the VALUE of the --shadow-focus declarations. Parts (2) and (3) below are untouched; (3) landed separately in PR #1780 per #301. ORIGINAL SCOPE NOTE, kept for the remaining tranches: SCOPE RE-MEASURED 2026-08-08 against origin/main 2675e6e1d, running analyzeClassContractsInSource + analyzeCssContractsInSource over the same walk check-design-system-contract.mjs uses (src/**, .ts/.tsx/.css, mockups excluded). The inherited figures were wrong in three ways. First, the legacyShadowAliases metric counts SEVEN tokens, not one: measured total 228 = tight 100, soft 72, elevated 17, hover 17, card 12, lux 8, lift 2. So the '229 --shadow-tight aliases' in HANDOVER-2026-08-07 is the all-token total mislabelled, and this row's earlier '155 consumers' was closer to a raw repo-wide grep (160 occurrences including mockups) than to the gated number. Second, the real scope is 100 production --shadow-tight sites across 55 files, so the inherited figure overstates the work by roughly 1.55x, and clearing all 100 will NOT zero the ratchet: 128 aliases across the six other tokens remain, so do not treat legacyShadowAliases=0 as the success criterion. Third, --shadow-focus is NOT in this metric at all: LEGACY_SHADOW_ALIAS has matched exactly tight\|card\|soft\|hover\|elevated\|lux\|lift since PR #1616 and has never included focus, so an earlier note claiming 'eight tokens, focus 2' and an overlap with #261 was wrong. #261 is a separate token with one consumer (src/app/globals.css:1476) and two theme declarations (lines 423, 664); the two tasks do not share this metric. Baseline pins legacyShadowAliases at 231 and the baseline is a ceiling, so today's 228 already passes. Re-measure before starting rather than trusting any of these numbers. (2) Add a step-SELECTION lint for the eight non-standard type steps (1318 sites) — check:type-scale already blocks arbitrary text-[12px], so do NOT write a lint duplicating the half that ships. (3) Extend the contract ratchet to raw padding / radius / line-height literals; it covers colour, shadow, tap and tracking today. Gate: npm run check:design-system-contract. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | +| #258 | P2 | rec | The PR-handoff stop rule is enforced for Claude Code only; Codex and Cursor get prose with no gate | GAP RECORDED 2026-08-14 in PR #1944 — docs/pr-handoff-stop-cross-agent-gap.md. This is the row's own stated fallback ("If no mechanism exists at all, record that explicitly here so the gap is a known limit rather than an open task"), so the row stays open but is no longer unexamined. Checked, not assumed: .claude/settings.json is read only by Claude Code; plugins/clinical-kb/.codex-plugin/plugin.json declares name/version/description/author/repository/keywords/skills and an interface block with NO hook, event, or pre-tool-interception field, shipping exactly one skill; .cursor/ holds settings.json (plugin enablement only), mcp.json, agents/ and skills/ with no deny path. So the cheapest-first option the row proposed is currently unavailable in both tools. Worth noting because it sharpens the cost: .cursor/agents/pr-babysit.md exists, meaning Cursor ships a documented agent for exactly the PR-following behaviour this rule restricts, with nothing bounding it. The doc records the Claude Code mechanism in enough detail to reimplement (session-scoped marker under the absolute git dir, fail-open on an unidentifiable session id, never pruning a sibling's marker, post-mode scanning only the request half so a command that merely prints a PR URL cannot arm it, and the CLAUDE_ALLOW_PR_FOLLOW=1 prefix unlock that a mention alone cannot trigger), plus the three questions any parity mechanism must answer. It is explicit that the wrapper fallback is advisory only — it cannot touch the MCP-connector or loop-machinery classes, so it makes a violation detectable after the fact rather than prevented. Next: re-check the Codex and Cursor manifests when either ships hook support; close only when a mechanism exists or the limit is accepted deliberately. Stop unchanged: do not weaken the Claude Code hook for symmetry, and do not keep a second copy of the deny list. | PR #1649; .claude/hooks/pr-handoff-stop.sh; .claude/settings.json; AGENTS.md "Stop when the pull request is open"; session 2026-08-07 | 2026-08-07 | | #265 | P2 | task | DS Track A6: move design-system gates 2, 4, 7 and 8 from partial to blocking | VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: docs/design-system/GATES.md still carries 10 `implemented-partial` rows; gates 2, 7 and 8 remain unclosed. Gate 2 is blocked behind #293, whose finding 1 is refuted — see that row before attempting the enumeration. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. RE-MEASURED AND PART-CLOSED 2026-08-09 against origin/main 8db1e53937. GATE 4 CLOSED: colourOnlyStatusIndicators in check:design-system-contract is the repository-wide enumeration this row asked for - a status hue on a box with no children, no aria-label/aria-labelledby/title on it or any ancestor, no sibling text, and not a StatusMark. It also flags shared swatch recipes, because the analyzer is per-file and cannot follow an imported statusDotReady to its call sites. Ratcheted at 4 with per-path pins (the two bare statusDot recipes GATES.md named, a calculator risk band, a therapy meter fill); a new colour-only indicator anywhere in src now fails. Mutation-verified. GATE 2 NOT CLOSED, and this row's description of it was wrong in a way that cost a session. It is NOT true that test:e2e:style-contract needs wiring into verify:cheap: the npm script is only an alias for running that one spec, the spec matches productionSpecPattern in playwright.config.ts and is listed in scripts/playwright-pr-shards.mjs, so it ALREADY runs in the required Production UI job. It must NOT be added to verify:cheap:internal, because check:gate-manifest then demands a matching step in static-pr, which has no browser and no server. The real gap is the h-10 blind spot inside the audit itself, and an enumeration for it was written, shown to find genuine defects, and then reverted rather than landed because it is not deterministic on a live-search route - see #293 for the six-run evidence and the follow-up. REMAINING: gate 2's enumeration (needs a deterministic surface first, #293), gate 7 (elevation child/parent, needs a render-tree check, untouched), and gate 8's recorded debt only - its two checks already ship and ratchet per path, so that work is retiring 27 edge conflicts across 15 files and 2 globals.css spreads, then pinning both at zero. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #266 | P3 | task | DS Track B1: adopt the 23 unadopted components demand-driven, never as a race to 53/53 | **DEPRIORITISED 2026-08-12 (yield review against current main).** Adoption counting toward 53/53 while a clinical P1 is open. The row's own title says never as a race to 53/53; the queue has been running the race anyway. Demand-driven means it activates when a surface needs a component, not on a schedule. COUNTS RE-MEASURED 2026-08-12 from docs/design-system/adoption-manifest.json on merged main: **54 registered, 31 adopted, 23 UNADOPTED**. (This supersedes the 2026-08-08 figures of 53/30/23, which a main-merge briefly restored over this correction.) The total held at 23 but the membership moved — DisclosureGroup joined the adopted set, and the newly built ErrorState joined the unadopted set; ErrorState's enforcement is closed (archived #298) but its adoption is still open under #299. Today's 23: AnswerFooter, Checkbox, Citation, CitationList, ConfirmDialog, Disclosure, DoseLine, DownloadLink, ErrorState, ErrorSummary, ExternalTextLink, FieldError, FieldHint, LinkAction, Pagination, Progress, RadioGroup, SearchField, StageList, Tabs, TextLink, ToastRegion, Tooltip. Approach unchanged and still correct: demand-driven adoption — pick a surface and let it pull, the way AccessibleTable pulled Button and the answer surface pulled AnswerCard (#216) — never a race to 54/54. Forms remain the largest single tranche: FieldError, FieldHint, ErrorSummary, SearchField, Checkbox and RadioGroup land together on one form conversion. Do not stub a component to move the count. Regenerate with npm run design-system:adoption:update AND npm run design-system:design-sync:update; both manifests are generated, never hand-edited. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #267 | P3 | task | DS Track B2: AnswerFooter and DoseLine need a provenance/dose payload the answer surface does not produce | **DEPRIORITISED 2026-08-12 (yield review against current main).** Blocked on a provenance/dose payload the answer surface does not emit, which is backend work nobody has scoped. Cannot start. VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: Neither AnswerFooter nor DoseLine has a product importer; the provenance/dose payload the answer surface would need still does not exist. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. Backend-shaped work, not a component swap: the two components cannot be adopted until the answer surface emits the provenance and dose data they render. Do not stub one to make the adoption count look better. Sequence after the payload exists, then adopt via the Track B1 demand-driven route. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #268 | P3 | task | DS Track B3: move the 19 genuine bare-dash sites onto MissingValue | **DEPRIORITISED 2026-08-12 (yield review against current main).** 19 bare-dash sites with no reported clinical misreading. Cosmetic consistency on a prototype with an open P1. VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: MissingValue is imported in 5 component files; the bare-dash conversion is partial. The ~5 calculator 'not started' sites stay permanently, per this row's own stop rule. This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. Therapy-compass getters, specifier sourceFamily, favourites counts when untrusted. Leave the roughly 5 calculator 'derived.started ? score : dash' sites PERMANENTLY — 'not started' is not a missing clinical value, MissingValueReason has no member for it, and converting them would render 'Not recorded' for a score the clinician simply has not entered. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #269 | P3 | task | DS Track B4: prove the per-component visual state matrix (blocked on the baseline hold) | **DEPRIORITISED 2026-08-12 (yield review against current main).** Blocked on #118 baselines, and proves a per-component state matrix for a design system on a single-user prototype. High cost, low yield at this stage. hover / active / disabled / busy / invalid / 320px / dark / forced-colours / print, per component. Currently proven for none. Blocked on #118: zero visual baselines are committed and the harness is continue-on-error, so nothing in Track B is safe at scale until baselines exist. CORRECTION 2026-08-08: the claim that baselines cannot be generated on Windows is half true and led to the wrong conclusion. It is true that snapshotPathTemplate carries {platform}, so win32 PNGs are invisible to the ubuntu CI job — but the CI job already produces the ubuntu ones. .github/workflows/ci.yml job visual-baseline runs on ubuntu-24.04 whenever ui_changed, runs npm run test:e2e:visual, and uploads tests/__screenshots__/ as artifact visual-baseline-; playwright.visual.config.ts records that on a missing baseline Playwright writes the golden and fails the first attempt, which is why retries are pinned at 0. So the mechanism exists and adoption is mechanical — see #118. Stop rule unchanged: do not commit baselines until the owner declares the design final, and do not adopt them from a developer machine. | session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678) | 2026-08-07 | | #271 | P3 | task | Decide whether to delete the now-consumer-less action kind in SecondaryNavigation | VERIFIED CORRECT 2026-08-12 — re-checked against merged main and left open: No production constructor of SecondaryNavigation exists — ` 1 and not fullscreen, so the holder becomes the scroller, and its overscroll changes from overscroll-contain to overscroll-x-contain precisely so vertical scroll chains OUT of the pane at its ends rather than trapping the reader. Single-page documents keep their previous geometry exactly and need no re-check. A nested vertical scroller inside a page is a known iOS hazard and no Chromium gate says anything about it, so it belongs on this same device pass. **On a real iPhone, in Safari and the installed PWA:** open a multi-page document (the 2-page synthetic clozapine demo doc, or any real guideline), confirm pages scroll inside the pane, and confirm that reaching its top or bottom continues scrolling the page rather than dead-ending. **Stop:** if it does trap, do not fix it by removing the pane — the pane is what makes a long guideline readable; adjust the overscroll behaviour or the pane height instead. | session 2026-08-08 document-viewer optimisation; docs/design-system/COMPONENTS.md phone clause | 2026-08-08 | | #281 | P2 | rec | The phone document route renders two clinical-summary surfaces and neither is canonical | **Outcome:** one clinical summary on the document route, chosen deliberately. **Detail:** a phone reader gets the gradient 'High-yield clinical summary' card (DocumentClinicalSummary, built by buildDocumentClinicalSummaryModel) and, further down, the rail's '#source-summary' / 'high-yield-summary' disclosure (DocumentSectionSummary + FormattedHighYieldSummary + BadgeCluster). They render the same document.summary row two different ways. The rail is not hidden on phones — only its DocumentSectionIndexCard is lg:block — so both appear. Only the rail panel carries the section anchor, so the more prominent card is the unnavigable one. Note the two disagree about emptiness as well: the card now renders nothing when the model yields no usable text, while the rail panel still renders for its label badges, which is why 'hasStoredSummary' was deliberately left keyed to the stored row rather than to card content. **Next:** decide which rendering is canonical — this is a clinical-content judgement about how a summary should read, not a layout fix — then delete the other and give the survivor the 'source-summary' anchor. If the rail's badges are the part worth keeping, they can move without the second summary body. **Stop:** do not merge the two renderings mechanically; they format clinical text differently and the difference is the decision. | session 2026-08-08 document-viewer optimisation; document-rail-panels.tsx; document-clinical-summary.tsx | 2026-08-08 | | #282 | P3 | task | Probe the corpus for JBIG2/JPX before deciding whether pdf.js needs its decoder assets shipped | **DEPRIORITISED 2026-08-12 (yield review against current main).** A probe to decide whether pdf.js decoder assets are needed. Worth doing eventually, but no reported rendering failure traces to JBIG2/JPX today, so it is speculative. **Outcome:** a measured decision about pdf.js's cMap/standard-font/WASM assets rather than an assumption either way. **Detail:** getDocument is configured with url plus the on-demand fetch flags and nothing else, so 'wasmUrl', 'standardFontDataUrl', 'cMapUrl' and 'iccUrl' are all unset. pdfjs-dist ships those assets (wasm 1.5 MB, standard_fonts 804 KB, cmaps 1.7 MB) and nothing copies them into public/. With wasmUrl null, 'useWorkerFetch' resolves false and the WASM image decoders cannot load, so JBIG2 and JPEG2000 images fall back to the JS decoders or fail; those are exactly the encodings a scanned guideline uses, and this repo runs an OCR pipeline, which implies scanned sources exist. Non-embedded standard-14 fonts fall back to system fonts, which is a fidelity risk on a clinical document rather than a failure. **Next:** sample the real corpus for JBIG2/JPX-encoded images and for PDFs relying on the standard 14 before shipping ~2 MB of static assets; if the corpus does use them, copy into public/pdfjs, set the URLs, and add immutable cache headers in next.config.ts (public/ is not counted by check:bundle-budget, so there is no budget risk — the cost is bytes over the wire on first use). **Stop:** do not ship the assets on the assumption alone. | session 2026-08-08 document-viewer optimisation; node_modules/pdfjs-dist/types/src/display/api.d.ts | 2026-08-08 | @@ -217,20 +201,16 @@ removed after current-main verification; it is not missing recommended work. | #292 | P2 | rec | Two assistants built the same queued conversion twice because neither workflow checks the open-PR list before starting | **Outcome:** picking up a queued ledger item cannot silently duplicate work another session already has in flight. **Detail:** on 2026-08-09 two assistants took the same queued `/issues` item roughly four hours apart and independently built the same in-page-nav conversion — PR #1766 (merged) and PR #1767 (closed as duplicate). Neither had any way to see the other: the ledger row was the only shared state. Correcting an earlier version of this row after CodeRabbit's review on PR #1773: it is not true that the ledger "has no in-progress state" — some rows do carry a progress marker in their prose (`IN PROGRESS` appears on two, and `IMPLEMENTED in PR #1766` on another). The accurate gap is narrower and worse: there is no structured status field and no atomic claim, so a marker is written by whoever did the work, usually after the fact, and nothing requires or checks one — which means the ABSENCE of a marker carries no information at all. Both sessions read it, both correctly concluded it was open, both built it. The wasted effort is the smaller cost; the larger one is that the two implementations diverged in shape, which is what forced the separate `PageSection` ownership decision recorded in `docs/search-chrome-behaviour.md`. Distinct from `#156`/`#168`, which are about two branches colliding on an **id** while appending; this is two sessions colliding on the **work** a row describes, and a collision-free id scheme would leave it untouched. **Mitigation landed 2026-08-09 (same PR as this row):** the check is now written into the three places an assistant actually reads before starting queued work — `.claude/skills/newtask/SKILL.md` "Before you start" (which already performed an open-PR read for PR bundling, so this asks that same list a second question and costs no extra call), `.claude/skills/issues/SKILL.md` after the read-only flow, and the `/issues` section of `AGENTS.md` so Codex and Cursor get it too rather than Claude Code only. All three say to scan for the **route, component or surface**, not the ledger id, because a duplicate PR rarely quotes the id; all three degrade to a warning when GitHub is unreachable so an offline session can still start work. **Next:** leave open for one or two queued-item cycles to see whether prose is enough. If a second duplicate lands anyway, this becomes the same class as `#258` — a rule enforced for one tool by prose with no gate — and the answer is a check, not more wording. **Stop:** do not implement a claim marker written back into the row when a session starts an item; that reintroduces exactly the read-modify-write contention `#168` exists to remove. Do not make the open-PR read a hard blocker. | session 2026-08-09; PR #1766 (merged); PR #1767 (closed duplicate) | 2026-08-09 | | #293 | P2 | issue | Gate 2 needs a phone-viewport deterministic surface; the `min-h-tap` 0px finding is REFUTED | **CORRECTS this row's original text, which was wrong on its central claim.** FINDING 1 IS REFUTED (2026-08-12). As first written it asserted that controls carrying `min-h-tap` have their declaration "overridden to 0", blamed "likely an unlayered component class in globals.css", and treated the six shapes as a live 48px-floor defect. All of that is wrong, and acting on it would have caused a regression. **What actually zeroes the min-height is the source itself, deliberately:** the sites carry an explicit `sm:` step-down beside `min-h-tap`. The two 36px shapes are exact matches — `services-navigator-page.tsx:217` is `grid min-h-tap min-w-tap … sm:h-9 sm:min-h-0 sm:w-9 sm:min-w-0` and `:286` is `inline-flex min-h-tap min-w-[94px] … sm:h-9 sm:min-h-0`. `sm:min-h-0` IS the computed `min-height: 0px`, and `sm:h-9` IS the rendered 36px. Seven `min-h-tap` sites carry `sm:min-h-0`; the wider pattern is larger still — `inline-flex min-h-tap items-center` alone appears with `sm:min-h-0` (4), `sm:min-h-7` (2), `sm:min-h-8` (2), `sm:min-h-9` (4), `sm:min-h-10` (8) and `sm:min-h-12` (1). **`min-h-tap` is a PHONE floor that desktop deliberately releases**, which is why the audit only sees it below the floor: `tests/ui-style-contract.spec.ts:97` navigates at the project's desktop viewport, so every `sm:`-and-up override is in force at measurement time. The audit was measuring intended design and reporting it as an overridden floor. **Do NOT "fix" these** — removing the step-downs would pin every desktop control to 48px and is a visual regression across the app, not a WCAG improvement (the phone contract already exceeds both AA 2.5.8 and AAA 2.5.5). The `declared < tapFloor - 0.5 continue` skip at `:116` is therefore correct at desktop width and is NOT the same structural blind spot as the `h-10` case in `#265`. FINDING 2 STANDS UNCHANGED and is the whole of the remaining work: a rendered-interactive enumeration on `/services?q=CMHT&run=1` is NOT DETERMINISTIC — six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct control shapes, largely disjoint; `waitForLoadState('networkidle')` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs were coincidence. The enumeration was written, shown to find genuine defects, and REVERTED rather than landed, because that spec runs in the required Production UI job via `productionSpecPattern` and `scripts/playwright-pr-shards.mjs`, so an intermittent version would block every merge. **Next, revised:** (1) build the deterministic surface — a static route with no async search, or a fixed seeded state; (2) run the tap enumeration **at a phone viewport**, where `min-h-tap` is unreleased and the measurement is meaningful, rather than at desktop where the floor is intentionally lifted; a phone layout is also the simpler, more deterministic surface, so (1) and (2) push the same way. Step (2) of the original row — "find what zeroes min-height on the min-h-tap carriers" — is CLOSED by this correction: the answer is `sm:min-h-0`, and it is intended. **Stop:** do not re-land the enumeration on a live-search route; do not quarantine a brand-new test to get it merged (quarantine is for flaky tests already trusted, and policy needs three reproductions on one SHA via `tests/flake-ledger.json`); do not lower any production tap target, and never to `min-h-11` (known `ui-smoke` sub-pixel flake; production uses the 48px token). | session 2026-08-09 — M2 gate 2 enumeration (#265); finding 1 refuted session 2026-08-12 against `origin/main` 4587f78 (`services-navigator-page.tsx:217,286`; `tests/ui-style-contract.spec.ts:97,116`) | 2026-08-09 | | #299 | P3 | task | Adopt ErrorState at the three surfaces that genuinely hand-roll the failed-request guard | **DEPRIORITISED 2026-08-12 (yield review against current main).** Three surfaces hand-roll a guard that works. Converting them is consistency, not a fix. VERIFIED CORRECT 2026-08-12 — re-checked against merged main during the full ledger sweep and left unchanged: ErrorState has no product importer beyond src/components/ui/error-state.tsx, so the three hand-rolled surfaces are still unconverted. (Its ENFORCEMENT is closed — see archived #298.) This stamp exists so a later reader can tell "checked and still true" from "never looked at"; the two were indistinguishable before. Three surfaces hand-roll the guard and their comments state the rule outright: src/components/clinical-dashboard/search-results-header-band.tsx:210 ('no number may reach the DOM'), src/components/services/services-navigator-page.tsx:634 ('a blocked registry must not reach the band as 0 matches'), src/components/clinical-dashboard/favourites-command-library-page.tsx:1182. They are CORRECT today, just not shared, so this is convergence rather than a bug fix. The band's fault panel is the richest existing implementation (role=alert, warning tokens, AsyncButton retry with busy state, faultAction slot) and ErrorState was modelled on it, so the shapes already line up. Live-look change: own PR, Chromium pass. Per the M4 brief it sits DOWNSTREAM of design decisions the owner has not made, so doing it before the site-wide redesign risks redoing it. Do NOT bundle with the enforcement check. Stop: only these three - see the sibling row for three sites that were miscarried as guards. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | -| #302 | P3 | rec | Design-system contract ratchets re-accumulate slack because paying debt down does not re-pin the ceiling | RE-MEASURED 2026-08-12 and the gap has WIDENED, which strengthens this row rather than dating it. `scripts/design-system-contract-baseline.json` still pins legacyShadowAliases at **220** while `node scripts/check-design-system-contract.mjs` measures **193** today — 27 units of slack, up from the 3 units (220 vs 217) this row recorded on 2026-08-10. The cause is exactly what the row names: paying debt down does not re-pin the ceiling, so #262's --shadow-tight retirement bought 24 more units of unguarded headroom instead of tightening the gate. Twenty-seven files could each gain an alias without failing. Next unchanged: re-pin ratchet ceilings to the measured value whenever debt is paid, the way ledger #264 corrected edgeOwnershipConflicts on 2026-08-09 — ideally as part of the same PR that pays the debt, so the two cannot drift. Companion measurements from the same run, for whoever re-pins: edge conflicts 27, 1px shadow spreads 2, layout transitions 11, hardcoded CSS durations 42, raw CSS z-index 9. | session 2026-08-10 shadow-tight retirement (PR #1803) | 2026-08-10 | | #305 | P3 | rec | Canary has no latency-mode coverage and its cost readout is a known lower bound | Two informational gaps from the 2026-08-12 canary review, deferred by scope decision. (1) eval:retrieval:latency (p90 20s gate) is never wired into eval-canary.yml, so live retrieval latency regressions are invisible to the weekly canary while the answer step relaxes its own gates via EVAL_LATENCY_CONTEXT=cross-region-runner. (2) estimated_cost_usd applies one rate set (gpt-5.6-terra) to all usage including 2x-priced strong-model retries, so any cost trend understates strong-retry runs — the workflow comments say so, but eval:trend consumers may not read them. Also noted: the workflow-wide concurrency group (eval-canary, cancel-in-progress false) can queue a dispatched pair run behind a scheduled run, interleaving pair evidence; and fixture coverage gaps tracked in #018 remain uncatchable by the canary. Next: decide whether a monthly latency-mode dispatch is worth the spend; add a strong-usage split to the estimator if cost trends start driving decisions. | session 2026-08-12 RAG canary review | 2026-08-12 | | #308 | P3 | issue | Desktop /documents/search CLS is 0.119, above threshold and stable across runs and baselines | Measured 2026-08-12 during the #147 close-out, twice, on the offline Lighthouse harness (Chromium 141): desktop /documents/search CLS **0.119**, against a committed baseline that also reads **0.119**. So this is long-standing and deterministic, not a regression — and it is above the 0.1 threshold. It sits outside #147's scope, which was mobile only, and it contradicts that row's claim that 'desktop passes everywhere: 0.016-0.097' — that range is stale. Companion desktop values from the same runs, all passing: /dsm 0.014, /forms 0.059-0.064, / 0.006, /therapy-compass 0.000. Next: attribute it the way #147 was attributed — drive Chromium against the offline production build with a PerformanceObserver on layout-shift reading entry.sources[].node, at DESKTOP emulation this time. Do not assume it is the same phone-overlay reserve cause as #147; that reserve publishes 0px above the phone breakpoint by construction, so this is a different shifter. Stop: do not raise the budget to accommodate it, and do not read local LCP or TBT from that harness (loopback has no network latency). | Local offline verify:lighthouse runs 2026-08-12 (two runs, identical CLS); #147 close-out; lighthouse-budget.json | 2026-08-12 | | #309 | P2 | task | Facet groups of 6-20 options render as chips, not the dense list docs/filter-contract.md section 5 requires | Raised by the Codex reviewer on PR #1858 and correct. docs/filter-contract.md section 5 sets density by option count: <=5 chips, 6-20 dense full-width list with a right-aligned count column and group headings, >20 or >3 groups adds find-a-filter and collapse-by-default. **PARTIALLY DELIVERED 2026-08-13, and the part this row was opened for is NOT done.** PR F (#1910) ported documents' implementation up into the shared ResultFilterSheet, so the >20-or->3-groups tier now exists there: find-a-filter, per-group collapse-by-default, a group opening itself when it holds a selection, and a live needle owning openness. That is the tier documents needed. **The 6-20 band is still unimplemented.** result-filter-control.tsx computes `const dense = facetGroups.length > 3 \|\| totalFacetOptions > 20`, so a mode with one facet group of nine options — formulation, the exact case that opened this row — evaluates dense=false and still renders ResultFilterFacetChips as a wrapping chip row. Verified 2026-08-13 on main 2d27039: formulation passes one group with formulationDomainsInUse.length === 9, so neither condition fires. An earlier attempt to close this row as delivered was wrong and was caught in review on PR #1925; the mistake was conflating "the dense tier landed" with "this row's band landed" — section 5 has two thresholds and only the upper one shipped. **Next:** either implement the 6-20 full-width renderer with the right-aligned count column and add the nine-option DOM assertion this row already asked for, or amend section 5 to drop the middle band deliberately and record that the contract was reversed rather than satisfied. **Stop:** do not close this row on the strength of the >20 tier, and do not add a per-mode dense list — a second hand-rolled facet layout is the drift the shared renderer was extracted to remove. | Codex review on PR #1858; docs/filter-contract.md section 5 | 2026-08-12 | -| #310 | P2 | issue | Fuzzy catalogue search can match a DIFFERENT drug: fluoxetine to duloxetine at edit distance 2 | NOT REACHABLE ON MAIN AS OF 2026-08-13, AND NOT BECAUSE IT WAS FIXED. The whole matcher is gone: git show origin/main:src/lib/catalog-search.ts \| grep -c typoDistanceLimit returns 0, and eight of the 11 files PR #1800 touched are byte-identical to their pre-#1800 state after merge acf78bf. Three files (`src/components/therapy-compass/data/select.ts`, `src/lib/formulation.ts`, and `tests/formulation.test.ts`) contain later unrelated changes, but the fuzzy-search hunks are absent from them too; preserve those newer changes during the re-land. So this row is not currently a live clinical hazard, but it must NOT be closed: the fix belongs in the re-land of #1800, not as a patch to main. See the two rows filed 2026-08-13 for the re-land and for the merge-loss detector. Re-run 2026-08-13 against the algorithm re-confirmed every measurement below, including that citalopram and escitalopram correctly do not match because the substring guard fires first. Original 2026-08-12 measurement retained: MEASURED 2026-08-12 by running the matcher itself, not by reading it. PR #1851 adds Damerau-Levenshtein typo recovery to src/lib/catalog-search.ts (fuzzySearchTokenCount, boundedTypoDistance, typoDistanceLimit) and folds it into the score. The tier term.length >= 8 -> 2 edits is the problem: Damerau counts an adjacent transposition as ONE edit, so fluoxetine -> duloxetine is distance 2 (substitute f->d, transpose lu->ul) and both are 10 characters. Confirmed hits against the PR's own algorithm: **fluoxetine -> duloxetine** (SSRI vs SNRI, different drugs), **prednisone -> prednisolone** (different drugs). Intended cases also confirmed working: sertraline -> setraline, olanzapine -> olanzepine. The existing guards DO hold — SSRI/SNRI, ADHD/ODD, citalopram/escitalopram, clozapine/clonazepam and quetiapine/olanzapine all correctly return no match. ONE MITIGATION, stated so this is not over-read: terms under 5 characters are excluded entirely. The fuzzy trigger is evaluated independently for each candidate record, so the hazard persists when both the exact drug and a two-edit near-match are present: the exact record receives a literal score while the wrong drug can independently receive a fuzzy score and appear as an additional result. Blast radius is wide because catalog-search.ts feeds ELEVEN modules — medications.ts (prescribing), dsm.ts, differentials.ts, differential-stream.ts, universal-search.ts, specifiers-search-index.ts, tools-catalog.ts, form-ranker.ts, service-ranker.ts. TESTED FIX: capping the >=8 tier at 1 edit removes both cross-drug hits and preserves every legitimate typo recovery in the sample — a one-line change to typoDistanceLimit. Next: do not patch main -- there is nothing there to patch. Apply the >= 8 tier cap of 1 edit inside the #1800 re-land, in the same commit, with a test over real catalogue drug names carrying both the exact and the near-match record and asserting the wrong drug is excluded while the exact drug remains. Stop: do not remove fuzzy search outright -- the typo recovery is genuinely useful and the guards are otherwise well judged, and do not close this row on the grounds that the code is currently absent. Note classifyPullRequestFiles returns clinicalRisk true for this path so the governance preflight fires, but ragRanking false, which is correct: this is catalogue ranking, not the pgvector retrieval path. | session 2026-08-12; PR #1851 (codex/investigate-recent-regression-issues); algorithm re-run locally against real drug-name pairs; src/lib/catalog-search.ts | 2026-08-12 | | #311 | P3 | task | Promote the derived ledger loss-detector into scripts/ — it has now earned its place twice | During the 2026-08-12 sweep, two main-merges silently reverted edits to `docs/outstanding-issues.md`, including the ENTIRE #293 refutation (a `grep sm:min-h-0` returned 0; the text survived only in commit a6bfc6f). It went unnoticed because the recovery script was HAND-ENUMERATED — it listed 15 archives and 8 updates from one commit and could therefore only restore what the author remembered. The replacement is derived rather than listed: read every row id this branch has ever stamped out of `git rev-list ..HEAD` plus `git show :docs/outstanding-issues.md`, then assert each of those ids that is still OPEN carries its stamp text, and exit non-zero listing any that lost it. It has now proved itself twice — it caught the intentional #262 divergence (main's version was newer than the branch's, correctly left alone) and would have caught the #293 loss the hand-written list missed. The plan that created it said it should stay a scratch script 'unless it proves useful more than once'; that condition is met. Next: port it to scripts/ (suggested `check-ledger-stamp-retention.mjs`), generalise the stamp token from the hard-coded 2026-08-12 date to a `--since` or marker argument, add a self-test in the style of the other ledger scripts, and document it beside `ledger:dedupe` for use after any main sync that touches the ledger. Stop: do NOT wire it into verify:cheap or CI — it is a branch-local safety net for a human or agent mid-sweep, and it has no meaning on a branch that has not stamped rows. Related: #156 and #168, which track the id-allocation race that produces these merges in the first place. | session 2026-08-12 ledger sweep; scratch loss-check.mjs; #293 restoration from a6bfc6f | 2026-08-12 | | #312 | P3 | issue | check:playwright-browser-revision reporting OK does NOT mean browsers are installed — and installing the matching revision is a cheap first option | Two corrections learned the expensive way on 2026-08-12, both about browser proof in a cloud container. (1) **The check is easy to misread.** `npm run check:playwright-browser-revision` returned 'Playwright browser revision check OK (managed-or-unconstrained): No designated container browser root is forced; use the Playwright-managed cache or install matching browsers.' That reports that no browser root is FORCED — it does not assert any browser exists. It was read as a green light for `verify:ui`, and two subsequent Playwright runs died at preflight instead: the container carried chromium-1194 while Playwright 1.62.1 requires chromium_headless_shell-1234, with firefox-1538 and webkit-2336 absent entirely. Suggested fix: have the check say plainly which browsers are present and which the locked Playwright version requires, so 'OK' cannot be mistaken for 'ready'. (2) **Installing the matching revision works and is fast**, which archived #255's 'delegate browser proof to CI Production UI' guidance does not mention. `npx playwright install chromium` fetched 114.7 MiB in about a minute and made local Chromium proof possible — three full ui-smoke runs then completed at 2.8-3.0m each (this is how #290 was settled). It is a cheaper first option than deferring to CI. Two things that matter alongside it: `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` was EMPTY in this container, so the download was never blocked despite the environment note implying otherwise; and only Chromium is needed, because `scripts/playwright-browser-preflight.mjs:127-152` honours `--project`, so `--project=chromium` skips the firefox/webkit requirement rather than forcing two unused ~100MB downloads. Stop: do NOT set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH at the stale 1194 binary to get past the preflight — archived #255 warns against forcing a mismatched path, and the preflight's own message warns that a later 'N failed' summary must not then be read as a product regression. | session 2026-08-12; scripts/playwright-browser-preflight.mjs:127-152; scripts/run-playwright.mjs:50-53; #290 close-out; archived #255 | 2026-08-12 | -| #313 | P2 | issue | check:ledger-write-discipline reports a pass when run against an uncommitted working tree | Found 2026-08-13 while closing #170/#309. The gate compares a committed range (it reported `Ledger write discipline passed for 2d270392f9cf..HEAD`), so when the ledger edit is still unstaged or uncommitted the range is empty and it prints a pass having evaluated nothing. I edited docs/outstanding-issues.md directly, ran the gate, saw green, and only discovered the violation after committing to a branch and re-running — at which point it correctly failed with "does not exactly match the audited application of 0 moved inbox request(s) from the base". The green was real but meaningless, which is the worst kind: it actively told me a forbidden edit was fine. **Outcome:** the gate either refuses to report a verdict when the working tree is dirty for the files it governs, or evaluates the working tree as well as the committed range. **Next:** make check-ledger-write-discipline.mjs detect uncommitted changes to docs/outstanding-issues.md (and the inbox directory) and either fail with an explicit "commit before checking" message or include them in the audited diff. Add a self-test covering the dirty-tree case, since the existing self-test passes today. **Related contributing factor worth fixing in the same pass:** `node scripts/outstanding-issues.mjs done ...` and `npm run issues:done` are different tools — the first edits the canonical ledger in place (reconcile-side), the second queues a merge-safe inbox request via scripts/ledger-inbox.mjs (branch-side). Nothing at the call site says so, and the raw script is what AGENTS.md's usage header documents. Consider making scripts/outstanding-issues.mjs refuse to run outside a reconcile context, or print a pointer to issues:done. **Stop:** do not "fix" this by relaxing the discipline check — the check itself was right, it just was not asked the right question. | session 2026-08-13 closing #170/#309; scripts/check-ledger-write-discipline.mjs; scripts/ledger-inbox.mjs vs scripts/outstanding-issues.mjs | 2026-08-13 | | #314 | P2 | issue | Ship compact compressed registry projections and verify live transfer | Next: land the existing view=summary/search and gzip implementation, deploy it, then verify /api/registry/records on the exact deployment SHA returns counts-only home responses and compressed compact search responses. Why: the live full payloads measured on 2026-08-13 were 482786 bytes for Forms and 1096689 bytes for Services and were downloaded by count/search-only consumers without Content-Encoding. The local projections reduce raw search data by about 91.3% and 82.0%, with gzip responses about 4.9 KB and 27.3 KB. Context: latency and Sentry review. Owner: assistant. Confidence: high. Depends on: #013 and #016. Gate: focused registry/consumer tests, production build and bundle budget, then post-deploy headers/bytes and live LCP rerun. Stop: do not close from local-only payload measurements or deploy without explicit authorization. | session 2026-08-13 latency review; src/app/api/registry/records/route.ts | 2026-08-13 | | #315 | P3 | rec | If the ui-smoke scroll-hide flake (archived #290) recurs, start from the reporter-stranding mechanism — and treat the old regression window as unconfirmed | Independent verification on 2026-08-13 (second session, fresh cloud container, pinned Chromium 1234 installed per #312) measured the archived #290 flake at BOTH ends of its recorded window and corrects the archive's causal story: the bad SHA 9ab3b73ad itself passed 16 recorded executions — reproducer isolated --repeat-each=5 (5 passed, ~1.0s each), one full tests/ui-smoke.spec.ts --project=chromium run (98 tests passed, 2.5m, 0 flaky), and reproducer x10 under deliberate CPU contention (6 busy-loop processes on 4 cores, run times 1.2-1.5s: 10 passed). Current main a76f280 also 5/5. So the recovery was NOT drift — the exact commit that measured 2/5-3/5 failures passes cleanly here — and the e8adde1b9..9ab3b73a window is unconfirmed; the failure was specific to the original machine's environment/load profile. Recorded as a comment on PR #1884 (issuecomment-5272932999). On recurrence, do not re-bisect first: test the stranding mechanism. computeScrollHideUpdate (src/components/clinical-dashboard/use-hide-on-scroll.ts) re-evaluates only on scroll/resize events, and its viewportHeightChanged / maxOffset-range-change guards deliberately zero accumulated down-travel (contract-asserted in tests/use-hide-on-scroll.test.ts) — so geometry churn consuming the final steps of a gesture strands the not-hidden state permanently until the next event, matching the recorded ~11.5s toHaveAttribute timeout signature (the assertion DOES auto-retry for 10s; the attribute genuinely never flips). Fastest confirmation: a diagnostic page.on('console') trace logging which guard fires per evaluation. The window itself was one PR (#1744 mode-routing, true merge a503c22) whose net diff touched no scroll-hide code — content-bisect axes, if ever needed: tests/ vs src/ split, use-home-mode-seed/use-last-app-mode neutralized, prefetchModeDestination reverted, positional heading click restored to a settle wait. Stop: any guard change is a behaviour change to protected phone chrome — needs a failing trace first, never speculatively; do not weaken the assertion or tap targets. | session 2026-08-13; PR #1884 comment; archived #290; #312 | 2026-08-13 | -| #316 | P1 | issue | Live DB is missing 21 repo-defined indexes and 10 retrieval RPC bodies diverge; weekly live-drift has been red since 2026-07-26 with no routing | APPEND 2026-08-13: full remediation + future-proofing plan is at docs/database-remediation-plan.md (phases 0-7: routing + post-migration drift trigger, read-only forensics, staging rehearsal #056, RPC reconciliation before index restoration, batched CREATE INDEX CONCURRENTLY + 20260804110240-pattern guard migrations, EXPLAIN/#231 re-measure, history-integrity probe, guard-migration contract, DR codification #196-#200, deferred #022/#025/#036/#191). This row is the tracking anchor and follows the plan approval map. The 2026-08-09 scheduled live-drift run (Actions run 31330856982) reports 21 missing_live indexes across multiple migrations, 2 unexpected_live indexes, and def_hash mismatches on 10 match_* retrieval RPCs (protected RAG surface — diff before touching). The migration-history cause remains unresolved: distinguish skipped DDL/history repair from later index drops with the approved read-only fingerprint and audit check before attributing the drift. No hosted mutation without approval. | session 2026-08-13 / Actions runs 30763871562 + 31330856982 / open #248 | 2026-08-13 | +| #316 | P1 | issue | Live DB is missing 21 repo-defined indexes and 10 retrieval RPC bodies diverge; weekly live-drift has been red since 2026-07-26 with no routing | Phase 0 delivered — drift routing + post-migration trigger + evidence scaffold, PR #1938. live-drift.yml now creates/updates one pinned issue 'Live drift check failing' (label live-drift-failure) with the captured finding lines and run URL on failure, and comments+closes it on the next green run; issues: write is scoped to a separate drift-routing job so the job running npm ci keeps contents: read. The workflow also runs on pushes to main touching supabase/migrations/** or supabase/schema.sql. docs/audit/live-drift-forensics-2026-08.md now carries dated empty Phase 1-5 evidence sections anchored here. Still outstanding: a forced workflow_dispatch failure to observe the pinned issue end-to-end (provider-backed, operator to run), SUPABASE_ACCESS_TOKEN per #183, and Phases 1-5, which all need approved hosted windows. Note: the Phase 0 task prompt named #312 as the anchor; that is the unrelated Playwright-browser P3, and the anchor was resolved to #316 by exact title per the playbook. | session 2026-08-13 / Actions runs 30763871562 + 31330856982 / open #248 | 2026-08-13 | | #317 | P2 | task | Verify registry-backed service records preserve facet metadata | #1878 introduced the services filter-contract tree and #1882 later merged the identical tree, so no merge-conflict audit is required. Current main uses ServiceRecord.catalogPayload.tags and fixture coverage verifies 219 records. Add focused offline tests that recordToRow and rowToServiceRecord preserve all six tag dimensions and degrade safely when payloads are malformed or absent. Do not add a second facets carrier unless a failing test proves the current contract inadequate. | PR #1921 review; #1878/#1882 tree comparison; service-facets.ts; registry-records.ts | 2026-08-13 | | #318 | P1 | task | The medication interaction lexicon has never been clinically reviewed and its sign-off block is empty | docs/medication-interaction-lexicon-review.md is generated by npm run medications:lexicon-report and expands every lexicon term to the catalogue drugs it resolves to, with how many CRITICAL/HIGH rows depend on it, sorted by severe usage. It is marked UNREVIEWED and its sign-off table is unfilled, so every red and amber drug-drug interaction alert is currently an unvalidated mapping over source-backed text. The wording shown to a clinician is always verbatim catalogue prose; what is unreviewed is which drugs a phrase like 'NSAIDs' or 'CNS depressants' was taken to mean. The sheet has already produced three defects on generation alone (ARB matching Carbapenem across 16 CRITICAL/HIGH rows; two divergent Warfarin records; lithium unreachable from eight HIGH rows), which is a fair indication of what reading it would still find. Next: a clinician reads the term table top-down (it is sorted so the top ten terms carry most of the severe usage) and fills in the sign-off block. Stop: do not treat check:medication-lexicon-report passing as review - that check only proves the sheet describes the current lexicon, not that the mappings are correct. | PR #1923; docs/medication-interaction-lexicon-review.md; docs/samd-classification-medication-considerations.md | 2026-08-13 | -| #319 | P2 | task | Re-land PR #1803 (--shadow-tight retirement onto --e1); 67 files on main still use the retired alias | PR #1803 squash-merged as 9d8370a on 2026-08-10, retiring the --shadow-tight role alias onto the --e1 elevation tier across 49 files. All 49 are byte-identical to their pre-#1803 state on main. Independent confirmation that does not rely on blob identity: git grep -l shadow-tight over src/ on main returns 67 files. Cause is the same merge, acf78bf; see the merge-loss detector row filed alongside this one. Commit 6f8c70d 'fix(pr-1815) resolve main merge conflict and keep shadow-tight switch migration' shows the migration was consciously preserved once and reverted again by a later merge in the same chain. Interaction with existing rows: #302 records scripts/design-system-contract-baseline.json pinning legacyShadowAliases at 220 while the checker measures 193 -- that gap is partly this loss, so re-landing #1803 should move the measurement sharply and #302 should be re-measured afterwards rather than actioned on its current numbers. #262 (DS Track A3, design-token debt) is also downstream of this. Next: cherry-pick 9d8370a onto current main and resolve against the token work that has landed since; this is mechanical but wide. Gate: npm run test on the design-token contract tests plus tests/tailwind-merge-config.test.ts, then check:design-system-contract. Stop: do not refresh the design-system contract baseline to absorb the change -- the point is that the measurement moves. Do not bundle with the #1800 re-land; that one carries a clinical governance preflight and this one does not. | session 2026-08-13; 9d8370a; acf78bf; 6f8c70d; git grep shadow-tight on origin/main at 63526ee; rows #302 and #262 | 2026-08-13 | | #320 | P3 | task | Crop-to-page overlay remains unbuilt; bbox already reaches viewer state at runtime but is untyped, unvalidated, and unused | **Outcome:** selecting an indexed table or diagram can highlight its region on the PDF page, or the capability is deliberately retired — either way it stops living only in a plan document. **Detail:** this is the one Phase 3 capability never built (docs/plans/document-viewer-redesign-plan.md, Phase 3 table, 'Out of scope'). It had no ledger row until now, which is how work disappears between sessions: the plan doc marks it out of scope and nothing in durable memory says it remains owed. **The data path is partially live, not dropped.** src/lib/document-detail.ts SELECTs bbox alongside the other image columns, and withImageTableMetadata spreads every selected field except metadata. bbox therefore survives the runtime response and reaches DocumentViewer's image state. The gap is static and behavioural: DocumentDetailImage in src/lib/document-detail-contract.ts does not declare bbox, ImageRow in src/components/document-viewer/types.ts aliases that contract, no normalisation validates the stored value, and no viewer code renders it. Verified against exact PR head 2ac0f48a820be62947112efbb5d0845a702dad8e on 2026-08-13. **Shape of the work, in order:** (1) establish the ingestion coordinate space and stored shape, add a normalised bbox field to DocumentDetailImage, and add a focused loader or route-serialization test proving bbox survives with the promised shape. Do not change the selected-field mapping unless that test demonstrates an actual loss. (2) Only then draw the highlight over the rendered page when a figure is selected, accounting for the virtualized page column, the per-page raster scale from resolveViewportScale, and rotation. **Why it was scoped out rather than overlooked:** the contract and normalisation work has a wider blast radius than the component-only Phase 3 diff, and crop geometry quality from ingestion is separate debt — the redesign plan's residual-risk section says not to block viewer UX on perfect crops. **Stop:** do not land the typed-contract and normalisation half inside a viewer-only PR; it changes what the document-detail API promises and needs its own review and governance preflight. Do not render raw, unvalidated bbox values — a highlight over the wrong region of a clinical source is worse than no highlight. | session 2026-08-13 document-viewer remaining-work inventory; docs/plans/document-viewer-redesign-plan.md Phase 3 table; src/lib/document-detail.ts bbox projection | 2026-08-13 | | #321 | P3 | task | Four follow-up groups cover nine controls after #291 | Six controls in the differential comparison page stay coupled to its planned rewrite and pinned density test. The filmstrip Page unknown control is a later mechanical change. DocumentViewer needs its persistent access reason split from transient loading before classification. The pin-limit control remains a capacity-state judgement. These are four source groups and nine controls, not four controls. | PR #1778 body; verified against main 2d27039 | 2026-08-14 | | #322 | P2 | issue | Two catalogue records are both named Warfarin and share no interaction rows, so which one a clinician opens changes the warnings | data/medications-snapshot.json holds warfarin-vka and warfarin-anticoagulant, both displayed as 'Warfarin', both class Anticoagulant / subclass Vitamin K Antagonist. They carry three interaction rows each with ZERO in common, so the alerts a clinician sees depend on which record they happened to open, and nothing on screen distinguishes them. A lexicon class term resolves to both. This is a catalogue DATA defect, not a lexicon fault - merging, deleting one, or relabelling them is a clinical content decision, which is why it is reported rather than patched. Surfaced automatically by duplicateCatalogueNames in scripts/build-medication-lexicon-report.ts, which compares the row sets and states the divergence rather than asking about it, and pinned by a test in tests/medication-interaction-lexicon-coverage.test.ts that goes red when the records are reconciled so the flag can be retired with it. Next: a named clinical owner decides the disposition. Stop: do not de-duplicate by display name in the report or the UI - that hides the divergence rather than resolving it. | PR #1923; docs/medication-interaction-lexicon-review.md flag section; tests/medication-interaction-lexicon-coverage.test.ts | 2026-08-13 | @@ -242,6 +222,12 @@ removed after current-main verification; it is not missing recommended work. | #328 | P2 | issue | A row can outlive its own completion — nothing closes a ledger row when its work merges | **Found during the 2026-08-12 yield review; re-confirmed on main 2026-08-13.** The then-#304 row described a ranking-snapshot freshness fuse due to trip around 2026-08-19 and sat in the recommended queue as time-critical, but its work had already landed as commit d182844 (PR #1876) — the snapshot's generatedAt and sourceRunId no longer matched anything the row said. Nothing closes a row when its work merges: `issues:done` is a manual call, and the session that ships the work is often not the session that owns the row. This is the mirror of #292, which covers duplication BEFORE work starts; this is staleness AFTER it finishes, and it is more dangerous because the row keeps advertising urgency to every session that reads the queue. **Next:** the cheapest useful guard is a periodic re-verification pass that re-measures each open row against current main and flags rows whose stated evidence no longer reproduces — several rows already carry a hand-written VERIFIED CORRECT stamp, which shows the need but does it manually and unevenly. A stronger version has the handoff skill close the row in the same commit that lands the work. **Stop:** do not auto-close on keyword match; a row can be partially delivered (#215, #231) and auto-closing those would lose real remaining work. | session 2026-08-12 ledger yield review; re-verified 2026-08-13 | 2026-08-13 | | #329 | P2 | issue | All live mobile routes breach LCP; shared CSS delivery and JavaScript are the current bottleneck | PR #1927 is merged and deployed to Railway production at exact SHA f2abf5baf3f449a1803bedef9dc107f30b70db93. Three-sample live medians on that SHA are Documents 3374 ms, DSM 3961 ms, Forms 3507 ms, root 3819 ms, Therapy 3422 ms, and Services 3793 ms; desktop LCP is 580-679 ms and mobile CLS remains within the rule. The production CSS split is retained and reduced four canonical medians modestly, but every mobile route still breaches 2500 ms. Root trace attribution is now concrete: TTFB 283 ms, LCP render delay 3449 ms, the 46,724-byte transferred shared stylesheet completes at 3644 ms under the throttled critical-request contention, total main-thread work is 1785 ms, script evaluation is 1030 ms, and shared chunk 8322 alone consumes 870 ms CPU. This is separate from canonical #117, which continues to track the unresolved Therapy catalogue payload and per-field safety decision. Next: split the 4,251-line global stylesheet by route ownership and reduce the shared search-shell/root client boundary before repeating the same bounded live matrix. Therapy field safety review remains required for search/pathways. INP remains unverified because Lighthouse does not measure it and no usable CrUX result exists. Stop: do not strip clinical fields, weaken the Lighthouse budget, refresh a passing baseline to hide latency, or claim an INP pass. | PR #1927; Railway deployments 1224ed55-210d-443b-94e5-20f87475468c and 810cc8b3-e39a-493f-b18f-8c63d150d53f; live Web Vitals runs 31719448766 and 31719451951; PR #1933 review | 2026-08-13 | | #330 | P2 | task | Re-land PR #1800 (fuzzy catalogue search), applying the #310 one-edit cap in the same commit | PR #1800 squash-merged as 022c83b on 2026-08-10 and its entire content is absent from main: git show origin/main:src/lib/catalog-search.ts \| grep -c typoDistanceLimit returns 0, eight of its 11 source and test files are byte-identical to their pre-#1800 state. The remaining three (`src/components/therapy-compass/data/select.ts`, `src/lib/formulation.ts`, and `tests/formulation.test.ts`) contain later unrelated changes, but the fuzzy-search hunks are absent from them too; preserve those newer changes during the re-land. Cause and evidence in the merge-loss detector row filed alongside this one. Consequence today is a MISSING FEATURE, not a live hazard: because the matcher is gone, the #310 cross-drug defect is not reachable on main. Do not close #310 on that basis, and do not re-land #1800 unchanged. RE-LAND WITH THE FIX: #310 measured that the tier term.length >= 8 -> 2 edits is the problem, because Damerau scores an adjacent transposition as one edit, so fluoxetine to duloxetine is distance 2 and both are ten characters. Re-run 2026-08-13 against the algorithm confirms it, and confirms prednisone to prednisolone as the second real cross-drug hit. Capping that tier at 1 edit removes both while preserving sertraline to sertralin style recovery. The row's other claims also held on re-run: citalopram and escitalopram do not fuzzy-match, because the substring guard fires first, and clozapine/clonazepam and quetiapine/olanzapine are correctly out of range. Next: cherry-pick 022c83b onto current main, change typoDistanceLimit's >= 8 tier from 2 to 1, and add a test over real catalogue drug names with both the exact and the near-match record present, asserting the wrong drug is excluded while the exact drug remains. Gate: focused Vitest on `tests/catalog-search.test.ts` plus the other four test files #1800 touched. Stop: this path is clinicalRisk true under classifyPullRequestFiles because catalog-search.ts feeds medications.ts and prescribing, so the PR needs a complete Clinical Governance Preflight and must not be bundled with unrelated chores. ragRanking is correctly false; this is catalogue ranking, not pgvector retrieval. | session 2026-08-13; 022c83b; origin/main at 63526ee; row #310; algorithm re-run locally against real drug-name pairs | 2026-08-13 | +| #331 | P2 | issue | check:medication-lexicon-report fails on 3 independent branches despite zero diff on the flagged file or its inputs | Reproduced identically across three independently-authored branches on 2026-08-14 (PR #1947 archive-backfill-scripts, PR #1949 visual-layout-polish, PR #1950 search-round-trip-budget) during otherwise-unrelated verify:pr-local runs. Each session confirmed via git diff origin/main --name-only that docs/medication-interaction-lexicon-review.md and its generator inputs (src/lib/medication-interaction-lexicon, the medication snapshot, the medication interaction index) were untouched on their branch, yet check:medication-lexicon-report still reported the file stale. This is a tooling/process finding distinct from #1bfaf0ef (the lexicon's clinical content has never been signed off) -- this row is about the staleness CHECK itself firing on unchanged files, which suggests a bug in how the generator's staleness comparison works (timestamp vs content hash, or a comparison against the wrong base) rather than a real content drift. Next: investigate scripts/medications-lexicon-report.mjs (or equivalent) staleness-detection logic directly against origin/main; if it is a comparison bug, fix it; if the report genuinely is stale on main independent of these branches, regenerate it. Stop: do not treat repeated non-fixes of this check across unrelated PRs as acceptable long-term -- three independent confirmations is enough to act on. | PR #1947, PR #1949, PR #1950 verify:pr-local runs, 2026-08-14 | 2026-08-14 | +| #332 | P3 | task | Three mode-nav icon glyphs sit at 17px, off the --spacing-icon-* scale, and no gate flags them | Split out of #275 rather than folded into its badge-box token. mode-nav/mode-nav.tsx:64 and :214 and mode-nav/nav-slot-ink.tsx:44 size their with h-[1.0625rem] w-[1.0625rem] — 17px against an icon scale of 12/14/16/20/24 (--spacing-icon-xs..xl in the globals.css @theme block). #275 counted these among its five files because they share the badge's number, but they are a different role: the badge is a text-bearing box sized around its own --text-2xs numeral, these are glyphs. They are now the only consumers of that value, since the badge moved to --spacing-search-band-badge. Nothing gates this: check-icon-scale.mjs enforces only the retired 4.5 (18px) half-step and its header states it deliberately does NOT flag arbitrary h-[Nrem], because non-icon boxes legitimately use that form. So this is unguarded and will not self-report. Why it was not just fixed: snapping to size-icon-md (16px) or size-icon-lg (20px) visibly changes nav chrome at every breakpoint, and 17px is close enough to 16 that the choice looks arbitrary without seeing it rendered — a design call, not a token swap. Next: get a Chromium look at mode-nav at phone and desktop widths with the icon at 16 and at 20, pick one, then migrate all three together. If 17px turns out to be deliberate, say so in a comment at the call site and consider whether check:icon-scale should flag off-scale arbitrary icon sizes on -typed elements specifically, which would have surfaced this. Stop: do not add a 17px step to --spacing-icon-* to make the problem go away — that token block's own comment argues against widening the scale off the 4px grid, and it would sanction the drift rather than resolve it. | session 2026-08-14; split from #275; check-icon-scale.mjs header | 2026-08-14 | +| #333 | P2 | issue | check:medication-lexicon-report has been failing on main for every local verify:pr-local, and no CI job runs it | Found 2026-08-14 while running the PR preflight for an unrelated design-token change. 'npm run check:medication-lexicon-report' reports 'docs/medication-interaction-lexicon-review.md is stale. Run npm run medications:lexicon-report and commit the result.' and exits 1. Two things make this worth a row rather than a quick fix in a passing PR. FIRST, it is on main, not on any branch: reproduced in a clean worktree checked out at pristine origin/main (both d47aa6d and, after a merge, 79b01b3), with a diff touching zero medication, lexicon or data/ files. SECOND, and this is the part that explains why it went unnoticed, NOTHING IN CI RUNS IT — a grep for medication-lexicon-report across .github/workflows/ returns nothing. It is reached only through the local verify:pr-local chain, where it is the LAST step, so it fails every local PR preflight while every CI run stays green. The failure mode is therefore self-concealing in the direction that matters: the gate is invisible to the required checks and visible only to whoever is about to hand off, who then has to decide whether an unrelated stale generated doc is theirs to fix. It was not fixed in the design-token PR that found it, deliberately: the report is a clinical-facing generated document and regenerating it inside a CSS-token PR would bundle a clinical-risk artefact with unrelated chores, which AGENTS.md PR bundling explicitly forbids. Next: run 'npm run medications:lexicon-report', read the resulting diff to confirm it is a pure regeneration and not a content change needing clinical review, and commit it in its own PR. Then decide the real question this exposes — either wire the check into CI so it cannot silently rot again, or move it out of verify:pr-local so it stops failing preflights it does not gate. A check in the local chain but not in CI is the worst of both. Stop: do not simply delete the check or drop it from verify:pr-local to get a green preflight; the staleness is real and the generated file is a clinical artefact. | session 2026-08-14; PR #1942 preflight; reproduced on pristine origin/main d47aa6d and 79b01b3; grep over .github/workflows | 2026-08-14 | +| #334 | P3 | issue | Claude Code web containers can ship Node 22 with no node_modules, so npm ci fails engine-strict before any work starts | Hit 2026-08-14 at the start of a Claude Code on the web session, and it blocks a session completely until worked around, so it is worth recording even though the cause is the container image rather than this repo. The container provided /opt/node20, /opt/node21 and /opt/node22 with node22 on PATH, no nvm, and no node_modules in either the primary checkout or a fresh worktree. package.json requires node >=24.15.0 <25 with engine-strict, so 'npm ci --include=dev' aborts immediately with 'notsup Required: {node: >=24.15.0 <25, npm: 11.x} Actual: {npm: 10.9.7, node: v22.22.2}'. Nothing in the repo can fix this from inside, because the failure happens before any repo script can run — .nvmrc correctly says 24 and is simply not consulted, and there is no nvm for it to drive. Workaround used, which took about a minute and is safe: fetch the current 24.x from the nodejs.org dist index, untar to /opt/node24, and prefix subsequent commands with 'export PATH=/opt/node24/bin:/opt/node24/bin:/root/.local/bin:/root/.cargo/bin:/usr/local/go/bin:/opt/node22/bin:/opt/maven/bin:/opt/gradle/bin:/opt/rbenv/bin:/root/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'. Everything downstream then behaved normally — npm ci, the full unit suite, build, and the Playwright-free gates all passed. Worth knowing that this is a DIFFERENT surface from the Codex Cloud provisioning path: scripts/setup-codex-cloud.sh and scripts/setup-codex-worktree.mjs cover Codex, and docs/codex-cloud.md is explicit that Cloud mirrors the tracked toolchain, but neither runs for a Claude Code web session, so that hardening does not carry over. Next: decide whether this deserves repo-side help at all. Options are a short note in the AGENTS.md or CLAUDE.md orientation telling an agent to install Node 24 to /opt/node24 and re-export PATH rather than concluding the environment is broken, or a small bootstrap script equivalent to the Codex ones that a web session can run first. Prefer the note: a bootstrap script that downloads a runtime is a bigger surface than the problem. Stop: do not relax the engines range, drop engine-strict, or pass --force to get npm ci through — the Node 24 floor is enforced deliberately in several places (preinstall, check:runtime, scripts/dev-free-port.mjs) and loosening it to accommodate a bad container would disable a real guard. | session 2026-08-14; Claude Code web container for PR #1942 | 2026-08-14 | +| #335 | P2 | rec | Merge-loss detection covers file-level reverts and inbox-request loss separately; neither covers the other, and the scheduled run is undecided | **Outcome:** one decision about how merge loss is detected on this repo, rather than two half-overlapping checks and an undecided schedule. **Detail.** Two detectors now exist for the same underlying hazard — content that reached main and then stopped being there — and they measure different things. (1) PR #1944 added scripts/audit-merge-loss.mjs (npm run audit:merge-loss): for every PR landing on origin/main in a bounded window it compares the ref's current blob for each file that landing changed against the blob at the landing's first parent, so it catches a landing whose CONTENT was reverted by a later merge resolution. Validated by independently rediscovering the acf78bf casualties (#1803 with 53 files, #1800, #1804, #1796, #1811). (2) PR #1937 filed a request about a queued inbox request that existed on a branch and never reached main through that branch's squash — a file that never landed at all, which detector (1) cannot see, because it only ever examines what a landing actually contributed. Conversely #1937's own cancel request warns that comparing all historical branch additions against the squash produces FALSE losses when a PR deliberately removes a file during review; detector (1) avoids that by construction (it diffs merge^1 against merge, not the branch's whole history), which is worth reusing rather than rediscovering. **Three things to decide, ideally together.** (a) Whether detector (1) gets a scheduled or post-merge run. PR #1944 deliberately shipped script-plus-test only: scheduling is an operational change needing its own PR and explicit approval, and joining verify:cheap:internal would force a matching static-pr step in ci.yml via check-gate-manifest. Until something runs it, it only helps whoever remembers to type it. (b) Whether the branch-versus-squash case becomes a second check or a mode of the same script. (c) What a positive costs a human: detector (1) is advisory and exits 0 on purpose, because a deliberate revert is byte-identical to an accidental one at blob level — a scheduled run therefore needs a named owner to triage it, or it becomes ignorable noise. **Next:** decide (a) first; it is the cheapest and it is what turns an existing script into an actual control. **Stop:** do not make either detector auto-fail without deciding (c) — an advisory check flipped to blocking on a signal that cannot distinguish intent will be silenced rather than triaged. | PR #1944 (scripts/audit-merge-loss.mjs); PR #1937 and its cancel request 63419f06; inbox request 829597d4; acf78bf; session 2026-08-14 | 2026-08-14 | +| #336 | P3 | rec | Decide whether responsive breakpoint windows get named tokens, or stay raw min-[]/max-[] everywhere | Split out of #275 rather than guessed at. The repo defines ZERO --breakpoint-* tokens, and at least nine sites hand-write the arbitrary form: min-[414px]:max-[429px] at clinical-dashboard/result-filter-control.tsx:231, plus max-[359px] (search-heading-mockups, differentials/diagnosis-map-panel.tsx:1036, clinical-dashboard/account-setup-dialog.tsx:98) and max-[389px] (factsheets/factsheets-search-page.tsx:176, clinical-dashboard/search-results-header-band.tsx:532, factsheets-compact-view-mockups). #275 asked for the 414-429 window to be tokenised alongside the badge box; that was deliberately NOT done, because naming one window while eight peers stay raw reintroduces exactly the one-call-site drift #275 exists to stop, just on a different axis. This is a real decision with two defensible answers and it should be made once, for all of them. (a) Stay raw and say so in docs/design-system/GATES.md: the values are per-device band edges carrying measured justifications in their own comments, they are not a scale, and a Tailwind 4 --breakpoint-* entry adds BOTH the min and max variant to every utility in the build for a single consumer. (b) Name them: Tailwind 4 --breakpoint- generates : and max-:, so the 414-429 window needs two entries (414px and 430px, since max-[429px] is inclusive and max- is exclusive), and 359/389 would want their own. Note the mockup hits are design scratch and out of scope for any gate. Next: pick (a) or (b), record it in GATES.md section 3 so the next session does not re-derive it, and only then migrate. Stop: do not migrate one window ahead of the decision. | session 2026-08-14; split from #275 during the design-token relands PR | 2026-08-14 | ## Resolved / archive @@ -478,3 +464,16 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #188 | task | Document and track disaster-recovery re-creation checklist as ledger work | Umbrella index only, and its children are retired to the runbook in the same batch. The disaster-recovery checklist is canonical in docs/operator-backlog.md and fires only after a schema restore, which is a runbook trigger rather than queued work. Note main separately opened a single consolidated DR row to keep the work visible; that supersedes both this umbrella and its five children. | 2026-08-13 | | #301 | issue | Two sessions built #262 part 3 in parallel because the GATES.md row understated what had shipped | Merged into #292 — the same failure mode (two sessions build the same queued item because the ledger has no claim mechanism), recorded twice from two incidents. #292 carries both the in-page-nav duplication (PR #1766 merged, #1767 closed) and the #262 part-3 ratchet collision, plus the understated-GATES.md-row contribution this row identified. | 2026-08-13 | | #200 | task | DR: Re-enter dashboard config after schema restore | Retired to the runbook (docs/operator-backlog.md disaster-recovery checklist). See #196 for the rationale. | 2026-08-13 | +| #310 | issue | Fuzzy catalogue search can match a DIFFERENT drug: fluoxetine to duloxetine at edit distance 2 | Fixed on main by 247a359 ("Add tappable phone suggestion ticker and conservative fuzzy catalog search (#1851)", 2026-08-14 02:03 +0800; confirmed via git merge-base --is-ancestor 247a359 origin/main). The hazard is closed at its source: src/lib/catalog-search.ts typoDistanceLimit now returns 1 for term.length >= 5 and 0 below that, with no >= 8 two-edit tier at all, so the two-edit cross-drug window that produced fluoxetine -> duloxetine and prednisone -> prednisolone no longer exists. Coverage landed with it: tests/catalog-search.test.ts carries "never cross-matches a distinct drug two edits away, even with both records present" (cites this row by number, asserts Duloxetine and Prednisolone are absent while Fluoxetine and Prednisone rank first, and that setraline still recovers Sertraline), plus a dedicated tests/catalog-search-drug-name-regression.test.ts. Verified 2026-08-14 by running both files: Test Files 2 passed (2), Tests 19 passed (19). Also re-measured against the algorithm itself rather than read off the source, matching the method that originally opened this row -- hazards fluoxetine->Duloxetine, prednisone->Prednisolone and both reverses all return 0; intended recovery setraline, olanzepine, clozpaine, monitroing, fluoxetne, prednisne, schizophrnia and lithum all return 1; all five guards (SSRI/SNRI, ADHD/ODD, citalopram/escitalopram, clozapine/clonazepam, quetiapine/olanzapine) return 0; and with the exact and near-match records both present, fluoxetine ranks [Fluoxetine] and prednisone ranks [Prednisone]. Fuzzy search was not removed, per this row's stop rule. The earlier instruction not to close on the ground that the code was absent from main is discharged: the code is present, capped and tested. | 2026-08-14 | +| #262 | task | DS Track A3: finish the design-token debt | CLOSED 2026-08-14 — all three parts settled, verified against code rather than rows. (1) The --shadow-tight retirement is re-landed: 130 call sites across 67 files onto var(--e1) and both declarations deleted. It had closed 2026-08-10 via PR #1803 and was silently reverted by the acf78bf merge on 2026-08-11; two stranded comments (globals.css 'the resting-hairline role is gone', and the token test's 'unlike the --shadow-tight assertion above') survived that merge while the code they describe did not, which is how the loss stayed invisible. The alias was a pure pass-through in both themes, and the forced-colors block scopes ':root, .dark' — the same html element the alias is declared on — so it already resolved through the flattened '--e1: none'; value-preserving in light, dark and forced-colors. tests/design-token-contract.test.ts now sweeps the tracked src tree for BOTH spellings (declaration and var() consumer) rather than asserting the declaration alone, so the gate no longer depends on which half of a bad merge lands. Mutation-verified in both directions. (2) NOT ACTIONABLE BY DESIGN, and this was already adjudicated — docs/design-system/GATES.md section 3 records that the decidable half of step selection shipped 9 Aug inside check:design-system-contract (a declared @theme step that no production surface selects fails the build; it caught --text-2xl-compact, retired 10 Aug, closing #297, and the exemption list is empty by design and gated from both sides). The remaining half — which existing step a component picks — is explicitly documented there as something 'nothing mechanical can' gate, being a judgement about the rendered design rather than a property of the source, with a standing instruction not to write an ESLint rule duplicating the arbitrary-value check that already ships via check:type-scale. So this part needs no work and should not be re-attempted. (3) SHIPPED in PR #1780 per #301 — rawPaddingLiterals, rawRadiusLiterals and rawLineHeightLiterals are all live baseline keys enforced at check-design-system-contract.mjs:215-232 over both the class and CSS-declaration spellings, plus rawGapLiterals beyond the original ask. Note for anyone re-measuring part 2: a raw grep for text- overcounts, because it matches the --text-*: declarations and doc comments too — that is the 733-vs-705 discrepancy GATES.md line 66 already warns about, and it reproduces today (a naive sweep returns 773). Use the AST class-root pass. | 2026-08-14 | +| #233 | task | COMPONENTS.md section 0 describes the pre-adoption world, and the optionality-marker contract change is undocumented | DELIVERED — verified on main 2026-08-14. Both halves are answered: COMPONENTS.md:16 section 0 is now the generated maturity matrix ('the generated maturity snapshot below is the claim'), not the pre-adoption world; the optionality-marker contract is documented at COMPONENTS.md:387-390 including the deliberate removal of the (optional) suffix. PR #1842 merged. | 2026-08-14 | +| #221 | task | Local EmptyState, LoadingState and Chip duplicates still unconverged after PR-J | DELIVERED — verified on main 2026-08-14. The remaining local names are delegating wrappers, not duplicates: therapy-compass/ui.tsx:161-173 forwards to SharedEmptyState, :158 to LoadingPanel, :3 imports DS Chip. Recorded in ADOPTION.md section 7.1. PRs #1841/#1842 merged. | 2026-08-14 | +| #178 | rec | pr-policy does not flag operational risk bundled with clinical or UI risk | DELIVERED — verified on main 2026-08-14. scripts/pr-policy.mjs:335 flags exactly the bundling this row asked for: if (classification.operationalRisk && (classification.clinicalRisk \|\| classification.ui)). PR #1837 merged. | 2026-08-14 | +| #319 | task | Re-land PR #1803 (--shadow-tight retirement onto --e1); 67 files on main still use the retired alias | RESOLVED 2026-08-14 — the retirement is re-landed. 130 var(--shadow-tight) call sites across 67 files now read var(--e1), and both role-alias declarations are deleted; a tracked-tree grep for the token returns zero. This row was queued as inbox request 210e3db5 and reconciled into the ledger by PR #1936 while the work was already in flight, which is why the fix arrives as a done rather than a cancel. Value-preservation was confirmed before editing rather than inherited from #1803: the alias was a pure pass-through in both themes, and the forced-colors block at globals.css:3613 scopes ':root, .dark' — the same html element the alias is declared on — so --shadow-tight already resolved through the flattened '--e1: none' there. Identical in light, dark and forced-colors. The .ckb-v2 redeclaration hazard this row's neighbours warn about does not bite for the same reason: .ckb-v2 sits on and .ckb-v2.ckb-v2 outspecifies :root, so both spellings substitute against the winning v2 tier. Guard added, and it is deliberately stronger than the one #1803 shipped: tests/design-token-contract.test.ts now sweeps the tracked src tree for BOTH spellings (a '--shadow-tight:' declaration and a 'var(--shadow-tight)' consumer) rather than asserting only that the declaration points at --e1. A declaration-only assertion would have caught the acf78bf revert, but only because the declarations happened to come back alongside the call sites; the tree sweep is independent of which half of a bad merge lands. Mutation-verified in both directions. Two artefacts confirm the original loss and are now consistent again: the globals.css comment 'the resting-hairline role is gone' and this test file's 'unlike the --shadow-tight assertion above' both survived acf78bf while the code they describe did not. This row's stop rule was honoured — the contract baseline was NOT refreshed to absorb the change; it was tightened DOWN to the moved measurement (legacyShadowAliases 220 -> 119) in its own commit, so the movement is pinned rather than hidden. #302 was re-measured after the reland rather than actioned on its stale numbers, and is closed in the same PR. Not bundled with the #1800 re-land, per this row's other stop rule. | 2026-08-14 | +| #302 | rec | Design-system contract ratchets re-accumulate slack because paying debt down does not re-pin the ceiling | RESOLVED 2026-08-14 — the five ratchets carrying slack are re-pinned to measured, in the same PR that paid the debt, which is the coupling this row asked for. legacyShadowAliases 220 -> 119 (the row measured 193 on 2026-08-12; the reland of --shadow-tight pays down the debt the acf78bf revert had re-hidden, so the real gap was 101 units, not 27), edgeOwnershipConflicts 27 -> 25, rawPaddingLiterals 67 -> 63, rawGapLiterals 34 -> 32, layoutTransitionExceptions 12 -> 11. Regenerated with 'node scripts/check-design-system-contract.mjs --print-debt-baseline' rather than hand-edited, so per-path debtByPath moved with the totals — those are what findDebtPathRegressions compares, and the retirement moved them wholesale. Every metric in the diff decreases; nothing was absorbed upward. Verified zero slack on every ratchet afterwards by diffing the baseline against a fresh --print-metrics run. Mutation-verified: reintroducing one alias in button.tsx now fails at both the total (119 -> 120) and the per-path level, where the old 220 ceiling passed it silently. This is not the baseline refresh #262 warns against — that stop rule forbids refreshing to HIDE the movement, whereas this pins the movement in. The stale GATES.md section 3 numbers for all five were corrected in the same commit per #301. Residual risk this row should still be read for: nothing enforces the coupling. A future PR can still pay debt down and leave the ceiling, and only a manual --print-metrics diff will notice. A cheap guard would be a check that fails when any baseline metric exceeds its measured value by more than a stated tolerance. | 2026-08-14 | +| #313 | issue | check:ledger-write-discipline reports a pass when run against an uncommitted working tree | Closed 2026-08-14 by PR #1944. scripts/check-ledger-write-discipline.mjs now reads git status for the paths it governs (docs/outstanding-issues.md, docs/branch-review-ledger.md, and docs/outstanding-issues-inbox/ including applied/) and refuses to report any verdict while one of them is dirty, naming each offending path and its status. Fixed as the row asked — the check was right, it just was not being asked the right question — rather than by relaxing the discipline. Two things only surfaced by running it: the module git() helper trims its output, which ate the leading space of porcelain's " M path" status field and shifted every path by one character so the guard silently never fired (the refusal now reads porcelain untrimmed, and tests/ledger-write-discipline.test.ts pins that specific shift); and scripts/guard-push.mjs:899 invokes this gate with an explicit committed --head at a moment when the tree is legitimately dirty, so the refusal fires only when head resolves to the default HEAD, leaving pre-push and clean CI unaffected. No override env var: both callers are unaffected by construction, so an escape hatch would only reopen the hole. Self-test extended with the dirty-tree case plus 9 focused tests. NOT addressed here, still open: the row's related contributing factor that node scripts/outstanding-issues.mjs done and npm run issues:done are different tools with nothing at the call site saying so. | 2026-08-14 | +| #209 | task | DS V2 Gate 1: add contrast pair for --warning used as body text | DELIVERED — verified on main 2026-08-14. tests/design-token-contract.test.ts:202-204 asserts --warning against --surface at >= 4.5 (AA body text) per theme. PR #1841 merged. | 2026-08-14 | +| #275 | task | The shared filter trigger carries arbitrary spacing values inherited from DocumentFilterTrigger | RESOLVED 2026-08-14 for the badge box; two carve-outs re-filed rather than guessed. SCOPE RE-MEASURED AGAIN on merged main and it has SHRUNK back, in the opposite direction to this row's 2026-08-12 re-measure: the badge role is down from five files to ONE. #170's convergence landed in between — document-search-results.tsx now renders the shared control and therapy-compass/filter-sheet.tsx was deleted outright (PRs #1885, #1889, #1910) — so the extraction reabsorbed the leak this row was written about. Fixed: 1.0625rem is now --spacing-search-band-badge in the globals.css @theme block, consumed as h-search-band-badge / min-w-search-band-badge at result-filter-control.tsx:236. Value-preserving and PROVEN, not inferred: compiling globals.css through @tailwindcss/postcss emits '.h-search-band-badge { height: var(--spacing-search-band-badge) }' and the matching min-width rule. No ratchet moved. NOT done, deliberately, each now its own follow-up: (a) pr-[0.6875rem] and min-[414px]:max-[429px] stay raw — the repo defines ZERO --breakpoint-* tokens and eight peer sites use the same raw min-[]/max-[] form (359px, 389px, 414px), so naming one window while the peers stay raw is this row's own drift on another axis, and Tailwind named breakpoints would add variants across the whole utility surface; that is a repo-wide decision. (b) The three remaining 1.0625rem hits in mode-nav.tsx:64,214 and nav-slot-ink.tsx:44 are NOT this token — they size glyphs, a 17px icon against a 12/14/16/20/24 --spacing-icon-* scale, so folding them under a badge token would merge two roles that only share a number. check:icon-scale deliberately does not flag arbitrary h-[Nrem], so they are unguarded but real. Snapping them to 16 or 20px is a visible nav-chrome change and a design call. This row's stop rule ('do not change the measurements themselves') was honoured — every measured justification comment is intact and no value moved. | 2026-08-14 | +| #234 | task | answer-copy-payload.ts is the single clipboard payload builder for three surfaces and has no documentation | DELIVERED — verified on main 2026-08-14. answer-copy-payload.ts (now src/components/clinical-dashboard/) carries a header documenting the single-builder contract, the three consuming surfaces, and why it sits outside src/lib and outside the design system. PR #1842 merged. | 2026-08-14 | +| #213 | task | Stop swallowing fetch and stream errors with empty catch handlers | Closed 2026-08-14. The 2026-08-12 re-measure counted correctly but described the wrong thing: the 3 remaining bare catches under src/ were not fetch/stream swallowing at all. All 3 lived inside render-blocking inline bootstrap script strings — src/lib/theme.ts:46 (localStorage.getItem, then document.cookie) and src/app/layout.tsx:149 (JSON.parse of stored preferences) — where a throw means storage/cookies are unavailable and the correct behaviour is the documented fallback chain (cookie, then OS preference; defaults for density/motion). Each now carries an inline comment stating the throwing condition and the fallback that covers it; no behaviour changed, because there is no logger or toast before React mounts and surfacing the error would trade a correct default appearance for a broken first paint. The genuine fetch/stream catches this row was opened against were already dispositioned by earlier passes (api/answer/stream/route.ts:178,291 and api/search/universal/route.ts:102 carry comments and propagate via controller.error). Added tests/empty-catch-disposition.test.ts, a raw source-text scan asserting every empty catch under src/ carries a comment — raw text rather than an AST because ESLint's no-empty cannot see catches inside template-literal script strings, which is exactly where these 3 hid. Population is 21 empty catches, all dispositioned, 0 bare. | 2026-08-14 | +| #245 | rec | responsive-compact CrossModeLinks keeps duplicate rails in the DOM | RESOLVED AS INTENTIONAL — verified on main 2026-08-14. The premise still holds literally (both rails are mounted) but it is now a documented decision, not a defect: cross-mode-links.tsx:220-224 states both rails stay mounted so SSR and first paint agree, hidden/md:hidden use display:none which removes the inactive rail from the accessibility tree, and distinct test ids stop phone vs wide selectors double-counting. Removing a rail would reintroduce the hydration mismatch this comment exists to prevent. PR #1842 merged. | 2026-08-14 |