From a6bfc6f2707975d9b9c649843e083965c80afb9c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 06:00:57 +0000 Subject: [PATCH 01/13] docs(issues): close six verified-delivered Tier 1 rows and refute the min-h-tap finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-verified the Tier 1 design backlog against origin/main rather than implementing it. Six rows were already delivered and one was wrong. Archived as verified-delivered: - #171 documents filtering — one Filter panel; scope chips and utilityControls are gone, source-type kept as its own axis per #181's correction - #172 Sources relocated and renamed to "Browse all sources" - #174 facets now OR within a group and AND across groups - #181 the correction to #171 was applied, so it retires with it - #273 max-[413px] wrap threshold deleted (8374c3b) - #274 services Clear now clears the q param, both controls share one path, and the ui-tools URL assertion landed (8374c3b) - #302 style-contract exemption artifacts cleaned up Corrected #293: finding 1 is refuted. Controls carrying min-h-tap are not having the floor overridden by an unlayered class — they carry an explicit sm: step-down beside it, and the audit measures at the desktop viewport where that step-down is in force. services-navigator-page.tsx:217 and :286 are exact matches for the two 36px shapes via sm:h-9 sm:min-h-0. min-h-tap is a phone floor that desktop deliberately releases, so "fixing" these would pin every desktop control to 48px. Finding 2 stands and is the whole remaining task; the revised next step is to run the enumeration at a phone viewport on a deterministic surface. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017paT42ZVMf8jaLtkjFxdy5 --- docs/outstanding-issues.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 4cadf4ea42..77cd3f9a97 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -99,7 +99,6 @@ removed after current-main verification; it is not missing recommended work. | 44 | `#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. | | 45 | `#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. | | 46 | `#170` | A2 | High — phone UI | Next documents/filter phone pass | 0.5–1 day | Documents and therapy already have page-owned phone filter sheets; remaining modes still use inline controls — shared-band Filter+Sheet adoption without regressing those two or sheetless Sort. | -| 47 | `#171` | A2 | High — documents UI | With #170 / filter consolidation | 0.5–1.5 days | Documents mode has four overlapping filtering surfaces, two of them the same job — one Filter control opening one panel, so a reader learns filtering once. | | 48 | `#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. | | 49 | `#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… | | 50 | `#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… | @@ -109,12 +108,9 @@ removed after current-main verification; it is not missing recommended work. | 54 | `#151` | Optional | Operator — GitHub PAT | When writing the durable note (#187) | 15–30 min | `gh pr checks` cannot read CI, but the Actions API can — nobody concludes CI is unverifiable when it is merely reached through a different endpoint. | | 55 | `#154` | Optional | High — agent process | When writing the durable note (#187) | 15–30 min | Row ids are not stable identifiers for "did my change land" — an agent confirms work reached `main` by content, never by id, title or PR state. | | 56 | `#156` | A3 | High — ledger architecture | With #168 id-scheme work | design first | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge — two branches cannot silently claim the same outstanding-issues id, and no merge path can commit a file where they have. | -| 57 | `#172` | A3 | High — documents UI | With #171 filter consolidation | 1–2 hours | `Sources` sits in the results bar but is navigation, not a filter — the results bar holds only controls that act on the current results. | -| 58 | `#174` | A3 | High — search facets | When facet UX is redesigned | 0.5–1 day | Facets AND within a group, so two values from one group almost always return nothing — a decision on record, either way. | | 59 | `#177` | A3 | High — therapy catalogue build | Next therapy-index build change | 1–2 hours | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file — the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. | | 60 | `#179` | A3 | High — therapy catalogue build | With #177/#180 | 1–2 hours | The full therapy catalogue silently switched from minified to pretty-printed — the full catalogue's on-disk format is a decision someone made, not a side effect. | | 61 | `#180` | A3 | High — therapy catalogue build | With #177/#179 | 1–2 hours | build-therapies-index now overwrites its own source input — the therapy catalogue generator has a source it does not also destroy. | -| 62 | `#181` | Optional | High — documents UI clarification | When updating #171 | 15–30 min | Correction to `#171`: source-type does NOT duplicate the `Document type` facet group — `#171` states that the documents source-type control "duplicates the facet group already named `Document type`". That is wrong, and … | | 63 | `#188` | A3 | Operator — DR/SRE | After any schema restore drill, or next DR review | checklist-owned | Document and track disaster-recovery re-creation checklist as ledger work — the five DR items that do not survive a schema restore are tracked with owners and verify steps, not only in `docs/operator-backlog.md`. | | 64 | `#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. | | 65 | `#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. | @@ -231,15 +227,11 @@ removed after current-main verification; it is not missing recommended work. | #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 | | #169 | P2 | issue | Local branches carry work that exists on no remote | **Outcome:** committed work is not lost when a machine or worktree is reclaimed. **Detail 2026-07-31:** six `claude/*` branches in this checkout have commits and no `origin/` counterpart. Verified real for `claude/clinical-kb-design-system-333a69` — 57 files / +4069, tip `feat(design-system): v2 token layer, 26 components, browser-crash fix` dated 2026-07-31 17:40, whose added `.design-sync/previews/*.tsx` files are absent from main. Others unverified: `design-sync-db0a54`, `fable-implementation-fc937c`, `frosty-mayer-2c6167`, `issues-133-evidence`. **How to check, because the obvious measure lies:** `git rev-list --count origin/main..` and a three-dot diff both report landed work as unmerged, since this repo squash-merges and the original commits never become ancestors — my own merged branch reported 1 commit and +476 by that measure. Test instead whether files the branch adds exist on main (`git ls-tree origin/main `). **Next:** per branch, push it for review or confirm it is superseded and delete it; do not bulk-delete on the commit count. Sibling of #152, which covers uncommitted work in worktrees rather than unpushed commits on branches. | session 2026-07-31; local branch audit | 2026-07-31 | | #170 | P2 | task | Phone filter sheets exist for documents and therapy; shared-band adoption remains | **Outcome:** phone filtering opens the repo's own bottom sheet instead of competing for width in the utilities rail. **Current state (2026-08-04):** documents already mounts a Filter documents Sheet, and therapy-compass mounts TherapyFilterSheet; both also pass appliedFilters into SearchResultsHeaderBand. The remaining modes still keep filter controls inline (or page-owned chips/nav), and the shared band has not yet adopted a Filter trigger + Sheet that wraps mobileControls for every mode. **Detail:** SearchResultsHeaderBand still renders mobileControls inline below sm for pages that supply them that way. src/components/ui/sheet.tsx already provides the primitive — bottom sheet on mobile via sheet-up, centred dialog at sm+, safe-area aware, focus-trapped, Escape and backdrop dismiss — so shared-band adoption is the remaining work, not new UI. Design settled in the round-7 study: one Filter control at both widths, badge counting applied filters, sheet titled Filter and sort because a phone bar cannot fit a labelled Sort beside Filter at 390 px. **Next:** replace the inline mobileControls render with a Filter trigger + Sheet containing mobileControls ?? filterControls for the modes that still lack a page-owned sheet, without regressing documents/therapy sheets or removing Sort from sheetless Sort consumers (differentials, forms, services). **Stop:** at least four Playwright specs drive those inline controls directly (document-source-type-select, search-query-ribbon-mobile-control-pair); budget for updating them, and do not ship without verify:ui — PR #1523 showed Production UI critical does gate this surface properly. | Round-7 design study; PR #1523 notes; `src/components/ui/sheet.tsx` | 2026-07-31 | -| #171 | P2 | issue | Documents mode has four overlapping filtering surfaces, two of them the same job | **Outcome:** one Filter control opening one panel, so a reader learns filtering once. **Detail:** `/documents` currently offers (a) smart-tag facets — 11 groups from `smartDocumentFacetGroups`, built by `buildSmartDocumentTagFacetIndex`, the real system; (b) a **source-type** control over `all/tables/images/pdfs`, which duplicates the facet group already named `Document type`; (c) scope chips from `searchCommandSurfaceConfig` (`Guidelines`, `Tables`, `Quotes`, `Current only`) — note `Tables` appears in both (b) and (c); (d) the `Sources` button, which is `openDocumentsDrawer("library")` and is not a filter at all. Traced 2026-07-31; `commandScopes` filter client-side after retrieval in the pages checked (services, prescribing), so merging looks safe from the RAG-protection rules — **verify every mode before editing**, this was not exhaustively checked. **Next:** fold (b) and (c) into the facet panel as groups; take (d) out of the results bar (see `#172`). **Stop:** this is a behaviour change across pages — own PR, own reasoning; do not bundle it with copy or layout work. | `document-search-results.tsx:920-937`; `document-tags.ts:798,849`; session 2026-07-31 | 2026-07-31 | -| #172 | P3 | task | `Sources` sits in the results bar but is navigation, not a filter | **Outcome:** the results bar holds only controls that act on the current results. **Detail:** `Sources` is passed as `utilityControls` from `document-search-results.tsx:1026` and calls `onOpenLibrary` -> `openDocumentsDrawer("library")`. Its own accessible name is *"Open source filters"*, which is why it reads as a second Filter beside the real one. It is also **documents-mode only**, so it can never be a fixture of a shared band. It reaches the ~2,000-document corpus; Filter narrows the ~12 a query returned — zero overlap, which is exactly why they cannot merge. Its one genuinely unique job is telling *"no results"* apart from *"not indexed"*, which matters in a clinical reference. **Next:** move to nav as **Browse library**; consider surfacing *Recently opened* separately since it is used far more often than corpus browsing but currently sits at the same depth. **Stop:** do not delete the capability — only relocate and rename it. | `document-search-results.tsx:1026`; `ClinicalDashboard.tsx:2752`; session 2026-07-31 | 2026-07-31 | -| #174 | P3 | rec | Facets AND within a group, so two values from one group almost always return nothing | **Outcome:** a decision on record, either way. **Detail:** `filterDocumentsBySmartTagFacetIndex` applies `selected.every(...)` across **all** selected keys, with no notion of grouping. So ticking `Medication:Lithium` and `Medication:Clozapine` asks for documents about **both** and returns zero — demonstrated in the round-7 study, where that exact pair is the "No matches" preset. Conventional faceted search ORs within a group and ANDs across groups, which would make that pair mean "either medication". **Next:** decide. If OR-within-group is wanted, it is a small change to the same helper plus its callers, but it changes what every existing multi-select does. **Stop:** this is a product decision, not a defect to fix unilaterally — the current behaviour is self-consistent and may be intended. Flagged, not changed. | `document-tags.ts:849-858`; round-7 study; session 2026-07-31 | 2026-07-31 | | #175 | P2 | task | Therapy modality is now null on all 205 records and needs curation or removal | **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 `public/therapy-compass-data/therapies.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 | | #177 | P3 | rec | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file | **Outcome:** the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. **Detail:** `public/therapy-compass-data/` ships each catalogue twice — `therapies.json` (2,529,429 bytes) alongside `therapies..json` with identical content, and the same for the index and home projections. Git stores one blob for each identical pair, so history is unaffected, but the working tree and every Docker image carry both. PR #1489's remediation adds a one-deploy grace generation, so a stem whose content changed now holds three copies until the next regeneration. **Next:** serve the alias as a Next rewrite to the current hashed filename instead of writing a duplicate file — the alias would keep its `max-age=0, must-revalidate` header (the hashed target keeps `immutable`, so the rewrite must not inherit it) and the generator would stop copying bytes. Check that a rewrite still satisfies the alias-equality assertions in `check:therapy-data-index` and `tests/therapy-compass-mode-wiring.test.ts`, which currently compare file bytes. **Stop:** do not drop the aliases outright — they are the fallback `useTherapyData` uses when a pre-deploy bundle names a hash that no longer exists. Renumbered from this PR's original `#171`. | 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 | **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 | | #179 | P3 | issue | The full therapy catalogue silently switched from minified to pretty-printed | **Outcome:** the full catalogue's on-disk format is a decision someone made, not a side effect. **Detail:** `public/therapy-compass-data/therapies.json` was a single minified line (2,529,429 bytes) until PR #1532 routed it through `syncTarget` to scrub the tag-echo `modality`. `syncTarget` serialises with `JSON.stringify(records, null, 2)`, so the file is now 18,400 pretty-printed lines at 2,652,783 bytes. **Measured cost:** +123,354 bytes raw (+4.9%), but only +3,409 bytes gzipped (333,428 -> 336,837, +1.0%) — gzip absorbs almost all the added whitespace, and the file is not JavaScript so `check:bundle-budget` never sees it. The visible cost is git churn: each future data revision now produces ~18,400 changed lines in this file plus another ~18,400 in its content-addressed twin, which is why PR #1532 read as 42,071 insertions when only 381 of them were hand-written. **The benefit is real and probably wins:** a 205-record clinical dataset with a one-line diff is unreviewable, and a reviewer needs to see which clinical field changed. The two index projections were already pretty-printed, so this is now consistent. **Next:** confirm the format deliberately, then either leave it (recommended — 1% wire cost buys reviewable clinical diffs) or pass a minified serialiser for the full payload only, which would land below main's original size now that `modality` is gone. Either way record the reason next to `syncTarget`. **Stop:** do not minify the index or home projections to "match" — they are small and their diffs are the review surface for catalogue changes. | PR #1532; session 2026-07-31 | 2026-07-31 | | #180 | P3 | issue | build-therapies-index now overwrites its own source input | **Outcome:** the therapy catalogue generator has a source it does not also destroy. **Detail:** `scripts/build-therapies-index.mjs` reads `public/therapy-compass-data/therapies.json` as `source`, and since PR #1532 also writes the curated full payload back to that same path (`syncTarget(legacyFullTarget, curatedFull)`). Input and output are now the same file, so the generator is no longer idempotent with respect to its own input in the strict sense: it is stable in practice only because `curatedModality` is a fixed point (a null modality stays null, a genuinely curated non-tag value survives a second pass). The original tag-echo values are now recoverable only from git history. **Why it matters beyond tidiness:** `#175` asks someone to curate real modality values in this file. They will be editing the generator's output, and any field the generator ever decides to rewrite will silently discard their edit on the next run — with no gate to catch it, because `--check` compares the file against what the generator would produce, so a clobbered hand edit reads as "current". **Next:** separate the two roles — keep an unprocessed source (`therapies.source.json`, or move curation upstream of `public/`) and let `therapies.json` be purely generated, or at minimum add a header comment to the file stating it is generated and hand edits to processed fields will not survive. **Stop:** do not fix this by reverting the full-catalogue scrub; detail and recommend load `catalogue: "full"`, so the scrub is what keeps the ECT/rTMS mislabel off the only screens that render the chip. | PR #1532; session 2026-07-31 | 2026-07-31 | -| #181 | P3 | issue | Correction to `#171`: source-type does NOT duplicate the `Document type` facet group | **CORRECTS a claim already merged to `main` in `#171`.** `#171` states that the documents source-type control "duplicates the facet group already named `Document type`". That is wrong, and it was asserted from a shared word rather than from the code. **What is actually true:** `resultTypeTabs`/`filterMatchesByResultType` (`document-search-results.tsx:253-273`) filter on artefact properties of the file — `match.tableCount > 0`, `match.imageCount > 0`, `match.file_name.endsWith(".pdf")`. The `Document type` **facet** group comes from `label_type: "document_type"` labels, meaning the *kind* of document: policy, guideline, form. A guideline containing a table is both; neither implies the other. They are complementary axes, not duplicates. **The real duplication** is the scope chip `{ id: "tables", label: "Tables" }` in `search-command-surface.ts` against the source-type `Tables` tab — same meaning, two controls. **Consequence for `#171`:** the merge it recommends is still worth doing (four entry points for narrowing one list is too many), but the shape changes — source-type becomes its own group in the panel (Format) rather than being absorbed into `Document type`, and the scope chips are the part that genuinely folds away. `#171`'s other claim, that `Sources` is navigation rather than a filter, is unaffected and stands. **Stop:** do not implement the absorption `#171` describes. **Before merging any of it,** verify the scope-chip semantics per mode — only services and prescribing were traced, and this row exists because an untraced inference reached `main` as fact. | `document-search-results.tsx:253-273`; `document-tags.ts` groupLabels; session 2026-07-31 | 2026-07-31 | | #183 | P2 | task | Create Sentry metric alert for production DB span p95 > 500ms | 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 | | #202 | P3 | rec | Recommendation answers must revalidate the ledger against origin/main, not a stale checkout | **Outcome:** agents stating next recommendations read `origin/main:docs/outstanding-issues.md` (or fast-forward first) and say how far the local checkout lags. **Evidence 2026-07-31:** a Cloud Agent answered `/issues`-style recommendations from a worktree 211 commits behind `origin/main`, so it re-proposed already-closed items (`#113`, `#114`, `#091`, `#105`, …) and missed newer opens (`#147`, `#117`, `#118`). Related to `#154` (ids/titles are unstable) and `#109` (shallow clones), but the failure mode here is stale full history, not missing history. **Next:** add a one-line check to the `issues` skill and AGENTS productivity defaults. **Stop:** do not treat a lagging worktree ledger as current memory. | session 2026-07-31 ledger hygiene review | 2026-07-31 | | #186 | P3 | task | Update #101 for shipped hydration parallelisation before any further canary work | **Outcome:** `#101` states which parallelisation candidates remain after PR `#1474` (`perf(rag): parallelise independent retrieval hydration`) so the row does not re-propose already-landed work. **Next:** rewrite the open `#101` detail to mark the metadata/memory/visual hydration triples done (with canary evidence citation if recorded) and keep only the still-open candidates (scope enumeration, typeahead cache, universal-search coalescing) with their canary gates. **Stop:** no further retrieval parallelisation without the usual RAG flag + live canary approval. | session 2026-07-31 ledger hygiene review; PR #1474 | 2026-07-31 | @@ -304,8 +296,6 @@ removed after current-main verification; it is not missing recommended work. | #269 | P2 | task | DS Track B4: prove the per-component visual state matrix (blocked on the baseline hold) | hover / active / disabled / busy / invalid / 320px / dark / forced-colours / print, per component. Currently proven for none. Blocked on #118: zero visual baselines are committed and the harness is continue-on-error, so nothing in Track B is safe at scale until baselines exist. 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 | PR #1679 removed the last live consumer of SecondaryNavigationActionItem. UPDATE 2026-08-09 (in-page-nav PR 2): the question is now the whole component, not one kind. That PR removed the section kind — SecondaryNavigationSectionItem, useActiveSection, sectionFragmentId, nearestScrollOwner, sectionActivationClearance and the internal secondary-navigation:section-select event — because the six information routes that fed it now mount InPageNavHeader. That was the last PRODUCTION constructor of any kind: RegistryModeNav renders ModeNav, not SecondaryNavigation, so the surviving route and action kinds have no caller in src/ at all and the only file that builds either is tests/secondary-navigation.dom.test.tsx. Kept rather than deleted in that PR deliberately: deleting a component plus its test file alongside a seven-route conversion is a second unrelated change, and this row already tracks the same keep-or-delete call. Next: decide keep-or-delete for src/components/secondary-navigation.tsx as its own change. If delete, also remove tests/secondary-navigation.dom.test.tsx and the therapy-compass action entries in the registry, and check tests/mode-nav-contract.test.ts, which string-matches source lines in page-secondary-navigation.tsx. Stop: do not do half of each — a deleted branch with its tests left behind, or vice versa, is worse than either. | session 2026-08-07; PR #1679; in-page-nav PR 2 2026-08-09 | 2026-08-07 | | #272 | P3 | rec | Header addon-slot single-owner rule is enforced by two lists agreeing by coincidence, not a guard | The universal header's addon slot must hold exactly ONE page-owned header. Nothing in PageSecondaryNavigation states that rule. What actually enforces it is that every route claiming the slot (DocumentViewer, differentials/differential-detail-page) also happens to be hasLocalInformationPageNavigation, which returns null before the mode branch is reached — two independently maintained lists agreeing by accident. tests/mode-nav-addon-slot.dom.test.tsx asserts the agreement route-for-route and is what will go red when a future claimant falls outside that cover. The original incidental protection (a claimant mode had fewer than MODE_NAV_MIN_ITEMS destinations so ModeNav rendered nothing) has already expired twice: for differentials in PR #1647 and for factsheets in PR #1674. Next: no action required while the lists agree; if documents or another slot claimant ever gains a second routed destination, add an explicit isHeaderAddonSlotOwnedRoute guard at the mode branch in PageSecondaryNavigation rather than widening either list. Stop: do not delete isHeaderAddonSlotOwnedRoute as unused — no production code calls it, but it is the named claimant list the test asserts against. Renumbered from this PR's original #262 → #272 because main claimed #261–#270 via PR #1678 design-system tracks. | session 2026-08-07; PRs #1647, #1674, #1679 | 2026-08-07 | -| #273 | P2 | task | The results band's max-[413px] wrap threshold is stale now that Sort and the phone selects are both gone | **Outcome:** the one-line phone results bar extends down to 320px instead of stopping at 414px, so the Filter trigger stops taking an otherwise-empty second row on the most common phone widths. **Detail:** `search-results-header-band.tsx` wraps the band's first line with `max-[413px]:flex-wrap max-[413px]:py-2`, justified in its own comment as "below 414px one line provably cannot hold count + query + sort + filter even with the query fully truncated". Both halves of that premise have since been removed: Sort became `sm`-and-up (PR #1689), and every mode's phone control became a compact trigger rather than a `w-full` select (#247). Measured in a real browser on the differentials band with the wrap class stripped at runtime, `scrollWidth - clientWidth` is **0 at 320, 360, 375, 390 and 402px** — the line fits at every width with room to spare. Left unchanged deliberately: the fix also changes documents, which is the reference layout the user approved, and it is a shared-geometry change with a 320-540px clip sweep asserting against it in `ui-smoke`. **Next:** delete the two `max-[413px]:` utilities, re-run the `ui-smoke` clip sweep and `ui-tools` phone rail assertions, and re-measure band height at 320/360/390 (expect 60px everywhere, versus 89px today below 414). **Stop:** do not raise the threshold instead of deleting it — it is not a narrower window now, it is an empty one. Do not re-measure with Sort mentally re-added; that control is gone below `sm` and is not coming back. | browser measurement 2026-08-07 on claude/search-bar-mobile-layout-buu0io; band comment; #247 | 2026-08-07 | -| #274 | P2 | task | Services' Clear quick filters leaves the q param in the URL, so a reload restores the filter | **Outcome:** clearing a services quick filter survives a reload. **Detail:** `services-navigator-page.tsx`'s Clear runs `setLocalQuery({ urlQuery, value: "" })`, which clears local state only; the route still carries `?q=crisis`, so a remount or reload re-derives `activeQuickFilter` and the filter comes back. Raised by CodeRabbit against the phone sheet's `onClearAll` in PR #1706, but the phone path was deliberately wired to the *same* handler the desktop Clear button already used — the behaviour is pre-existing on desktop and was mirrored, not introduced. Fixing it in that PR would have changed a shipped desktop control's semantics under a phone-scoped change, so it was deferred rather than bundled. **Next:** add one clear path that both resets local state and pushes a route without `q` (or with the mode's default query), and point both the desktop Clear and the sheet's `onClearAll` at it; assert the URL after Clear in `ui-tools`. **Stop:** do not fix only the phone path — two Clear buttons that disagree about whether the URL changes is worse than one that is consistently wrong. | CodeRabbit review on PR #1706; services-navigator-page.tsx Clear handler | 2026-08-07 | | #275 | P2 | task | The shared filter trigger carries arbitrary spacing values inherited from DocumentFilterTrigger | **Outcome:** the phone filter trigger expresses its measurements as named tokens rather than bracketed values. **Detail:** `result-filter-control.tsx`'s `ResultFilterTrigger` uses `pr-[0.6875rem]`, `h-[1.0625rem]`/`min-w-[1.0625rem]` for the badge, and the raw breakpoint window `min-[414px]:max-[429px]` for the label. CodeRabbit flagged these against the design-token rule in PR #1706. Every one of them is copied *verbatim* from `DocumentFilterTrigger`, which shipped on main earlier and is the component this one was deliberately lifted from so the two cannot drift — so the finding is real but its scope is both call sites, not the new one. Changing only the copy would reintroduce exactly the drift the extraction removed, and each value carries a measured justification in its own comment (the asymmetric padding answers a stroked glyph against a filled pill; the breakpoint window is the one band that is single-line and short of width). **Next:** tokenise in `@theme` once, then update the trigger — there is now only one implementation, so it is a single edit. Confirm the badge and padding render identically at 393/402/414/430px before and after. **Stop:** do not tokenise the trigger without also retiring the values from the documents original, and do not treat this as licence to change the measurements themselves. | CodeRabbit review on PR #1706; DocumentFilterTrigger on main | 2026-08-07 | | #278 | P3 | issue | The document-viewer visual baseline bakes in viewport-pinned chrome that overlaps content | Measured 2026-08-08 while adopting the baselines (#118 / PR #1729). The document-viewer target clips #main-content, which is 1196x2903 against a 900px viewport, and contains viewport-pinned chrome: the sm:sticky sm:top-0 document header (DocumentViewer.tsx:1028) and the sm:fixed search composer (DocumentViewer.tsx:1511). Playwright stitches an oversized element clip, so both composite partway down the image and OVERLAP the content behind them — the cited-excerpt card and a source passage are partly covered in the committed golden. Position tracks total content height, so any content-height change above them moves the pinned chrome and inflates the diff well beyond what actually changed. NOT a product bug and NOT a #1705 regression: the pre-#1705 candidate from run 31249978408 shows the same overlap, so it is inherent to the target's design. The capture is deterministic, so the comparison still means something — five of six candidates were byte-identical by SHA-256 across two independent CI runs. Next: narrow that target's clip to a smaller locator, or add the pinned chrome to the target's mask array (the spec already supports mask, with a comment warning a mask is a hole in the gate). Stop: do not fix this by capturing fullPage — the spec bans it because ledger #093 leaves a hidden duplicate page root under CI load. | session 2026-08-08 — visual baseline adoption, #118 | 2026-08-08 | | #280 | P2 | task | Physical iPhone acceptance is owed for the viewer pinch gesture and the canvas pixel budget | **Outcome:** the two phone-only viewer fixes are confirmed on the device class they were written for. **Detail:** the viewer-optimisation branch revives pinch-to-zoom in fit mode (it was gated off in the default state, so a pinch reached neither the viewer nor the browser) and adds a canvas pixel budget so WebKit stops blanking the page above roughly 2.3x zoom on a dpr-3 display. Neither is verifiable in this container (see the Chromium/pdf.js row) and neither is a Chromium behaviour anyway — the canvas ceiling is a WebKit limit and the touch-action contention is a Safari gesture question. **Next:** on a real iPhone, in Safari and in the installed PWA: pinch a freshly opened document and confirm it zooms without first tapping a control; zoom to maximum and confirm the page stays painted rather than going blank; confirm a pinch that drifts vertically is not cancelled mid-gesture by the holder's 'touch-action: pan-y' (the mitigation if it is, is switching touch-action to none while two pointers are down — the gesture hook already tracks pointer count and exposes 'pinching'). Record the result against docs/phone-chrome-physical-acceptance.md. **Stop:** do not re-gate pinch on '!fitWidth' to resolve a gesture-contention finding — that restores the original defect. | session 2026-08-08 document-viewer optimisation; docs/design-system/COMPONENTS.md phone clause | 2026-08-08 | @@ -320,14 +310,13 @@ removed after current-main verification; it is not missing recommended work. | #290 | P2 | issue | ui-smoke phone scroll-hide tests are flaky on main at 9ab3b73a | Blocking Chromium tests in tests/ui-smoke.spec.ts fail intermittently at base commit 9ab3b73ad. Observed members: 'answer glass header overlays main and fully hides while scrolling on phones' (the reliable reproducer), 'document viewer content disclosures are naturally closed and mutually exclusive by default', 'non-answer phone header keeps the in-flow collapse hide', and 'document search stays separate from the shared answer stream and summary action'. A different subset fails each full-suite run. Measured 2026-08-09 during the in-page-nav PR 2. The decisive control: reverting ALL of src/ to the base commit in the SAME worktree, and moving the branch's new untracked modules aside so the build was pristine, still failed 2/5 — so this is main, not the branch. Repeat-each runs on the branch gave 3/5 and 2/5; a targeted 3x3 across the three names above gave 8/9 with only the answer-glass one flaking. The passing case takes ~1.4s; the failing case times out at ~11.5s on expect(header).toHaveAttribute('data-scroll-hidden','true'), i.e. the universal header never hides after the test scrolls the document owner. A worktree at the older origin/main e8adde1b9 passed 5/5 twice on the same machine, so the regression window is e8adde1b9..9ab3b73a. Not quarantined: repo policy needs three reproductions on the same SHA recorded in tests/flake-ledger.json, and the right fix is probably the scroll-hide reporter's settle timing rather than a quarantine tag. Next: reproduce on a clean checkout of 9ab3b73a, bisect e8adde1b9..9ab3b73a, and fix or quarantine per docs/testing.md. Stop: do not weaken the assertion to a longer poll — it already waits 10s, far past the animation, so a longer wait would hide a real hang. **Quarantine arithmetic (recorded 2026-08-09):** this is the row's blocking constraint, so state it as a count rather than as policy prose. `tests/flake-ledger.json` is currently empty (`"flakes": []`, with a comment recording that stale non-reproducible entries were removed), so this flake has ZERO recorded reproductions in the ledger that governs quarantine. `docs/testing.md` requires three reproductions on the same SHA before a `@quarantine` tag is accepted. The 2026-08-09 measurement above is one — it is same-SHA (9ab3b73a) and reproducible on demand, but it was never written to the ledger. Net: **two more same-SHA reproductions** are needed before a quarantine entry can be added at all, and they must be recorded in `tests/flake-ledger.json`, not merely observed in a session. Until then the test stays blocking and every red run on it is unexplained-by-record. **Next (supersedes nothing above; this is the cheapest unblocking step):** run the reliable reproducer ('answer glass header overlays main and fully hides while scrolling on phones') twice more against 9ab3b73a and record each observation in THIS row — not in `tests/flake-ledger.json`. Two separate constraints make an early append invalid rather than merely premature, and both were missed by the original wording here: `docs/testing.md` (Flake policy) requires three same-SHA reproductions BEFORE an entry is added or retained, and `scripts/flake-ledger.mjs:48` throws `exact title must include @quarantine` while the test at `tests/ui-smoke.spec.ts:4470` is currently untagged — so an entry written today fails validation outright and the ledger is not a per-run log. Only after the third reproduction: tag that test `@quarantine` (never alongside `@critical`, which the next line rejects), then add ONE complete entry carrying owner, reproduction command, local tracking reference, first/last-seen dates and an expiry no more than 30 days out. Or, preferably, spend the same effort on the bisect of e8adde1b9..9ab3b73a already recorded above, since a fix retires the row outright and a quarantine only defers it. **Stop:** do not backfill the two outstanding reproductions from memory or from the 3/5 and 2/5 repeat-each runs on the branch — those were on branch SHAs, not 9ab3b73a, and the same-SHA requirement is the whole point of the count. | session 2026-08-09; in-page-nav PR 2; controls in scratchpad pw-truebase2 / pw-growth-base logs | 2026-08-08 | | #291 | P2 | issue | Placeholder buttons pin `disabled` and `disabled`+`aria-disabled` as two contradictory shapes; decide which wins | **CORRECTS this row's original text, which was wrong on its central claim.** As first written (2026-08-09) this row asserted that pairing `disabled` with an `aria-describedby` sr-only reason means the reason "is never announced to keyboard or screen-reader users", and that `docs/wiring-conventions.md` and `eslint-rules/require-button-wiring.mjs" blessed the broken pairing so a repo-wide pass was required. **All three claims are refuted.** A natively `disabled` button remains in the accessibility tree with its accessible description intact — a screen reader reaching it by virtual cursor or swipe still announces the reason. That is asserted, not assumed: `tests/favourites-hub-unavailable-controls.dom.test.tsx:31-46` pins `toBeDisabled()`, `not.toHaveAttribute("aria-disabled")` AND `toHaveAccessibleDescription(...)` together on all three favourites-hub placeholders, under the title "keeps unavailable actions natively disabled and exposes their reasons" — i.e. native `disabled` is a deliberate, tested decision, not drift. `require-button-wiring` already accepts both `disabled` and `aria-disabled`, so no rule change was ever needed. What `disabled` does remove is the tab stop, so a keyboard-only user without a screen reader cannot reach the control to discover why it is unavailable — a product judgement, not a WCAG failure (a disabled control is exempt from focus-order requirements, and the `title` covers pointer users). **Fixed in this PR:** the one genuine defect was documentation — `docs/wiring-conventions.md` named `favourites-hub.tsx` as "the reference markup" while showing `aria-disabled="true"`, which is not what that file does. The block now shows native `disabled`, states why the description survives it, cites the pinning test, and says when `aria-disabled` + no-op is correct instead (a roving-tabindex group such as `ResultFilterSheet`). **What remains open, and is the only reason this row is still here:** `disabled` and `aria-disabled="true"` together is a third shape pinned two contradictory ways inside one test file — `tests/mobile-interaction-regressions.test.ts` asserts density placeholders in `differential-presentation-workflow-page.tsx` are native-only (`not.toContain("aria-disabled")`), and asserts `disabled aria-disabled="true"` together for the Add placeholders in `visual-evidence.tsx` and `evidence-panels.tsx`. The pairing is redundant and the two attributes disagree about focusability. **Next:** an owner decision on which shape wins, then align the two pinned assertions and the four sites carrying the pairing (`visual-evidence`, `evidence-panels`, `forms-search-results-page`, `calculators/search-page`). **Stop:** do not swap `disabled` for `aria-disabled` in `favourites-hub.tsx` — that was attempted on 2026-08-09 and reverted; it contradicts the deliberate tested decision above. Do not treat `account-setup-dialog.tsx` as part of this: its placeholder styles itself through `disabled:` Tailwind variants, so any change there needs `aria-disabled:` variant renames as well. | session 2026-08-09; docs/wiring-conventions.md; eslint-rules/require-button-wiring.mjs; favourites-hub.tsx:367,382,544 | 2026-08-09 | | #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 | Controls declare min-h-tap and compute min-height 0px; a rendered-interactive tap audit needs a deterministic surface first | Two findings, one robust and one that blocked the gate. FINDING 1 (robust, reproduced in ALL SIX runs): controls that carry min-h-tap compute min-height 0px and render far below the 48px floor. Six distinct shapes seen across runs - 'a.inline-flex min-h-tap items-center' 16px, 'a.inline-flex min-h-tap shrink-0' 16.5px, 'button.flex min-h-tap w-full' 26.6px, 'button.grid min-h-tap min-w-tap' 36px, 'button.inline-flex min-h-tap items-center' 16px, 'button.inline-flex min-h-tap min-w-[94px]' 36px. min-h-tap works in general (the existing declared-carrier audit still measures carriers at or above 48px), so these elements have the declaration overridden to 0 rather than the utility being absent; likely an unlayered component class in globals.css, which by design outranks Tailwind utilities here. This was invisible because the pre-existing audit in tests/ui-style-contract.spec.ts only measures elements whose COMPUTED min-height is already at or above the floor (declared < tapFloor - 0.5 continue), so a floor overridden downward is skipped rather than flagged - the same structural blind spot as the h-10 case #265 named. FINDING 2 (why gate 2 did NOT land 2026-08-09): 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 - one run saw answer-suggestion chips and a sort band, another a settled services results list. waitForLoadState('networkidle') plus deduplication to distinct shapes (instance counts measure how many results the query returned, and gave 9 vs 39) did NOT fix it; two consecutive agreeing runs were coincidence, and the next run differed again. The enumeration was written, proven to find real defects, and then REVERTED rather than landed, because tests/ui-style-contract.spec.ts runs in the required Production UI job via productionSpecPattern and scripts/playwright-pr-shards.mjs, so an intermittent version of it would block every merge in the repo. Next, in order: (1) find a deterministic surface for the audit - a static route with no async search, or a fixed seeded state - before re-attempting the enumeration; (2) separately, find what zeroes min-height on the min-h-tap carriers and fix or write a stated exception. 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 keeping flaky tests we already trust, and repo 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 min-h-12). | session 2026-08-09 — M2 gate 2 enumeration (#265) | 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 | | #294 | P3 | rec | OffscreenCanvas for the PDF raster is unjustified until the page-flip cost is read from CI | **Outcome:** the worker-raster question is settled by a number rather than left as a standing 'optional' item in the redesign plan. **Detail:** docs/plans/document-viewer-redesign-plan.md conditions OffscreenCanvas on 'measured main-thread paint cost'. Phase 3 (Task 5) did not implement it, deliberately: virtualization now keeps the reader's page and one neighbour already rastered, so the cold-render-per-flip cost that motivated a worker raster is largely gone before any threading work starts, and moving pdf.js rendering off the main thread would put the canvas the clinical source is drawn into behind a transfer boundary — a real risk on the one surface where a blank page is a clinical failure. **No number exists yet and none could be produced locally:** pdfjs-dist@6 needs Map.prototype.getOrInsertComputed, which this container's Chromium 141 lacks and Node 24.13.0 also lacks, so neither a browser nor a headless harness here can raster a page (see #279). **Next:** read the measurement the gate already captures. tests/ui-document-canvas.spec.ts attaches page-flip-raster-cost.json (flipToPaintedMs, longTaskCount, longTaskTotalMs, longestTaskMs, canvasBackingPixels) and logs a '[viewer-canvas] page flip painted in Nms' line, on every Production UI run and on any host with the pinned Chromium 151 build: npm ci --include=dev && npx playwright install chromium && npm run ensure && npm run test:e2e -- tests/ui-document-canvas.spec.ts --project=chromium. Close this as not-worth-doing and strike the row from the plan's Phase 3 table only when a Production UI (or equivalent Chromium 151) run records decisive log lines for all three: longestTaskMs comfortably under ~50ms, plus explicit flipToPaintedMs and longTaskTotalMs budgets agreed for that host class and met on the same run. Do not close on longestTaskMs alone. **Stop:** do not implement OffscreenCanvas on principle because the plan lists it — the plan conditions it on the measurement, and the measurement is now cheap to obtain. | session 2026-08-09 document viewer Phase 3, Task 5; docs/plans/document-viewer-phase3-handover.md | 2026-08-09 | | #296 | P3 | issue | tests/pr-handoff-stop.test.ts fails whenever the unit suite runs as root | The case 'emits handoff context only when the marker file exists' chmods the fixture git dir to 0o555 to force the marker write to fail, then asserts the hook failed open with no marker. Root ignores permission bits, so the write succeeds and the assertion flips: 'expected true to be false' at tests/pr-handoff-stop.test.ts:180. Confirmed environmental and pre-existing, not diff-induced — reproduced on a clean checkout of af85cbc with every working change stashed (1 failed \| 10 passed), and 'id -u' returns 0 in the remote container. Cost is that 'npm run test' and therefore 'npm run verify:pr-local' cannot reach a clean exit in any root container, so a real regression later in the run is masked by a known-red file and the gate has to be interpreted by hand every time. CI is unaffected because its runner is non-root, which is why this has not surfaced there. Next action: make the test skip or change technique when 'process.getuid?.() === 0' — either skip with an explicit reason, or force the write failure a way root cannot bypass (point the marker path at a directory that does not exist, or at a path whose parent is a file), which is portable and keeps the assertion meaningful for every user. Stop: do not delete the case or relax it to 'marker may or may not exist' — failing open without telling the model that tools are denied is the actual contract it guards. | session 2026-08-09; verify:pr-local run on af85cbc | 2026-08-09 | | #298 | P2 | task | ErrorState is built and registered but nothing enforces it - the '0 matches after a failed request' row is still planned | GATES.md still reads: Render "0 matches" after a failed request \| ErrorState adoption + check \| planned. The component now exists (src/components/ui/error-state.tsx) and is registered across all gate-11/12 surfaces, but grep over scripts/ and eslint-rules/ returns ZERO references to ErrorState, so nothing prevents a new surface rendering a count under a failed status. Building the component closed the 'documented gate with nothing behind it' half; the enforcement half is untouched, and the row was deliberately NOT flipped to implemented because that is exactly the drift GATES.md exists to stop. Next action: add a metric to check:design-system-contract that fails when a count-bearing node renders under an error/failed status - the analyzer already resolves class roots and JSX children (childrenAreNumeralOnly, used by statusColouredNumerals), so it needs no new npm script. Do this BEFORE adoption: a check with no adoption still stops the next regression, adoption with no check does not. Stop: do not flip the GATES.md row until a check actually runs in verify:cheap. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | | #299 | P3 | task | Adopt ErrorState at the three surfaces that genuinely hand-roll the failed-request guard | 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 | | #300 | P2 | issue | Three sites carried into M4 as hand-rolled '0 matches' guards are not guards - do not convert them | Re-measured 2026-08-09. The M4 handover listed six surfaces hand-rolling the failed-request guard; three do not survive measurement and converting them to ErrorState would be WRONG. (1) src/components/clinical-dashboard/differentials-home.tsx:716,729 renders '0 matches' and 'No matches' when sourcesChecked is TRUE - sourcesChecked: boolean means the source search RAN, so that is a legitimate zero after a search that SUCCEEDED and must keep reporting its count. (2) specifiers-home-page.tsx is not under clinical-dashboard/ at all - the real path is src/components/specifiers/specifiers-home-page.tsx and its line 211 is a comment about not showing a stale zero ABOVE real catalogue results, a different problem. (3) document-search-results.tsx:1508 gates on recordStatus for LOADING (recordSearchStillRunning), not for a failed count; its genuine fault handling is recordBandOwnsFault, which delegates to the band. Only search-results-header-band, services-navigator-page and favourites-command-library-page are real. Next action: none - this row exists so the next reader does not convert the wrong three. Stop: do not 'fix' differentials-home to suppress its count. | session 2026-08-09 M4 - ErrorState build | 2026-08-09 | | #301 | P3 | issue | Two sessions built #262 part 3 in parallel because the GATES.md row understated what had shipped | On 2026-08-09 two branches implemented the same raw-value ratchet independently. PR #1780 landed rawPaddingLiterals/rawRadiusLiterals/rawLineHeightLiterals; a concurrent session built arbitraryPadding/arbitraryGap/arbitraryRadius/arbitraryLeading against the same four files and discovered the collision only when syncing before PR. The duplicate was dropped and only the uncovered gap family was rebuilt on #1780's predicate (rawGapLiterals, 34 sites). Root cause is the same failure this document keeps producing: the §3 row read 'Contract ratchet \| implemented-partial (colour/shadow/tap literals only)' and named none of the metrics #1780 had just shipped, so the row still advertised the work as unstarted. Identical to the 2026-08-09 finding that four of #264's six prohibitions were already gated while their rows read 'planned'. Both rows are corrected now. Next action: when a gate lands, update its §3 row IN THE SAME COMMIT - a row that understates shipped work is not a stale doc, it is a duplicate-work generator. Consider asserting in a test that every metric key in design-system-contract-baseline.json appears somewhere in GATES.md. Stop: do not rely on the ledger alone to prevent this - both sessions had ledger access. | session 2026-08-09 M4; PR #1780 collision | 2026-08-09 | -| #302 | P3 | issue | `tests/helpers/style-contracts.ts` contains escaped line-break artifacts in the exemption map | `smart-search-phone-ticker*` entries were merged with literal backtick-`r`n escapes, which makes the style-exemptions object invalid for the required parse and blocks local checks. Cleanly split each ticker exemption to one line and keep the same reason text so the exception intent is preserved. | PR #1815 unblock follow-up (`tests/helpers/style-contracts.ts`) | 2026-08-11 | | #303 | P3 | task | `issues:next-id` is out of sync with declared rows | The outstanding-issues marker is `issues:next-id=302` with no `#302`/`#303` rows in either open or resolved tables, which `check:outstanding-issues` flags as missing-issue failures. Add both rows and bump marker to `304` to keep the ledger monotonic. | `docs/outstanding-issues.md` | 2026-08-11 | ## Resolved / archive @@ -337,6 +326,13 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | ID | Type | Summary | Outcome | Resolved | | ---- | ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | +| #171 | issue | Documents mode has four overlapping filtering surfaces, two of them the same job | RESOLVED 2026-08-12 — verified delivered on `main` in the shape `#181` corrected to, not the shape this row originally proposed. `/documents` now has one Filter control opening one panel: `document-search-results.tsx` no longer references `utilityControls`, `searchCommandSurfaceConfig` or `commandScopes`, so (c) the scope chips (`Guidelines`/`Tables`/`Quotes`/`Current only`) are gone and (d) the `Sources` button has left the results bar (see `#172`). (b) source-type was NOT absorbed into `Document type` — per `#181` they are complementary axes — it stayed its own axis and now joins the same single panel, counting into `activeFilterCount` and rendering as a chip on the shared applied-filters shelf (`document-search-results.tsx:1499-1552`). | 2026-08-12 | +| #172 | task | `Sources` sits in the results bar but is navigation, not a filter | RESOLVED 2026-08-12 — verified delivered on `main`. `Sources` is out of the results bar (`utilityControls` is no longer referenced in `document-search-results.tsx`) and the capability was relocated and renamed rather than deleted, exactly as the stop rule required: `browseLibraryControl` renders as **Browse all sources** with the corpus count, shared across the filter-sheet footer (`data-testid="document-filter-browse-library"`) and the zero-result empty state (`document-results-browse-library`). The *Recently opened* half of the recommendation was advisory and is not part of this closure. | 2026-08-12 | +| #174 | rec | Facets AND within a group, so two values from one group almost always return nothing | RESOLVED 2026-08-12 — verified delivered on `main`: the decision this row asked for was made in favour of OR-within-group. `filterDocumentsBySmartTagFacetIndex` (`document-tags.ts:952-959`) no longer applies `selected.every(...)` across all keys; it partitions the selection with `partitionSelectionByGroup` and evaluates it with `entryMatchesSelection`, which breaks on the first hit within a group (OR) and requires every group to hit (AND). Ticking two medications now means "either medication" rather than returning zero. | 2026-08-12 | +| #181 | issue | Correction to `#171`: source-type does NOT duplicate the `Document type` facet group | RESOLVED 2026-08-12 — the correction was applied. `#171` shipped in this row's shape: source-type stayed a separate axis instead of being absorbed into the `Document type` facet group, and the scope chips this row identified as the genuine duplication are the part that folded away. Archived with `#171`. | 2026-08-12 | +| #273 | task | The results band's max-[413px] wrap threshold is stale now that Sort and the phone selects are both gone | RESOLVED 2026-08-12 — fixed on `main` in `8374c3b` ("fix: complete high-yield reliability tasks"). Both `max-[413px]:flex-wrap` and `max-[413px]:py-2` were deleted rather than re-thresholded, per the stop rule; `max-[413px]` no longer appears anywhere under `src/`. `tests/ui-smoke.spec.ts` and `tests/search-results-header-band.dom.test.tsx` were updated in the same commit. | 2026-08-12 | +| #274 | task | Services' Clear quick filters leaves the q param in the URL, so a reload restores the filter | RESOLVED 2026-08-12 — fixed on `main` in `8374c3b`. `clearServiceQuery()` (`services-navigator-page.tsx:612-615`) now resets local state AND calls `router.replace(appModeHomeHref("services"), { scroll: false })`, and both the desktop Clear (`:711`) and the phone sheet's `onClearAll` (`:763`) point at that one path — so the two controls cannot disagree, which was the stop rule. The `ui-tools` URL assertion the row asked for landed in the same commit: `await expect(page).toHaveURL(/\/services$/)` after Clear. | 2026-08-12 | +| #302 | issue | `tests/helpers/style-contracts.ts` contains escaped line-break artifacts in the exemption map | RESOLVED 2026-08-12 — fixed on `main`. The three `smart-search-phone-ticker*` exemptions in `tests/helpers/style-contracts.ts:311-313` are one clean line each with their original reason text preserved; no escaped line-break artifacts remain and the exemption map parses. | 2026-08-12 | | #173 | issue | Facet counts and format counts are computed against different sets, so half the filter panel goes stale | RESOLVED 2026-07-31 in PR #1526. `projectSmartTagFacetGroups` recounts an already-built facet index against the live selection so each row answers how many documents remain if that facet is also ticked; zero-count facets stay visible (not removed) and the facet rail disables unselected zeros so they cannot advertise a dead end. Originally captured as open `#172` on this branch before `main` claimed `#169` for the local-branches finding; renumbered to `#173` on merge. | 2026-07-31 | | #160 | task | Reland closed PR #1515 (#093 + #138 fixes never reached main) | RESOLVED 2026-07-31: capture recorded while #1515 was closed unmerged; #1515 then landed on `main` as squash `ca2c4de51faae9a0502b0b0570b6866acbb943fe`, which also archived `#093`/`#138`. **Content-verified on `origin/main` (not SHA/PR state alone):** `tests/playwright-settlement.ts` exports `visibleByTestId` (`.filter({ visible: true })`) and it is used from `tests/ui-tools.spec.ts`, `tests/ui-smoke.spec.ts`, and `tests/ui-accessibility.spec.ts`; `.github/workflows/ci-triage.yml` enables by default with `vars.CI_TRIAGE_ENABLED != 'false'`. Reland no longer needed; chat archive unblocked. | 2026-07-31 | | #093 | issue | Next streaming `S:` clone causes Playwright strict-mode violations under CI load | RESOLVED 2026-07-31: shared `visibleByTestId` scopes page-root/shell testids to the visible DOM owner (not bare `.first()`), applied to the known hotspots in `ui-tools` / `ui-smoke` / `ui-accessibility`. `expectSingleSettledOwner` remains for full-convergence races. Product mount bisect remains optional if a new surface appears. | 2026-07-31 | From 433add31879134827ded4204bc5700f7cb40cfae Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 06:06:32 +0000 Subject: [PATCH 02/13] chore(ledger): record the Tier 1 design-issue re-verification Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017paT42ZVMf8jaLtkjFxdy5 --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b1c6f2b5ce..eb7770d266 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -896,3 +896,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-11 | 1820 | 897ff11a4cdb13ae1c01f5eb149007847028f5aa | review-and-fix | fixed | Semgrep:IN_PROGRESS, Gitleaks:IN_PROGRESS, Semgrep ingestion gate:IN_PROGRESS, Static PR checks:QUEUED, Safety and config checks:QUEUED, Unit coverage:QUEUED, Build:QUEUED, Production UI critical:QUEUED, Lighthouse budget:QUEUED | | 2026-08-12 | PR #1815 / claude/spacing-icon-design-review-rxwh28 | 9f266210f02081be54d407c70a85f52fed436128 | babysit | no remaining actionable findings; one pre-existing thread resolved as no-change (Dockerfile.worker follow-up needed) | required checks: Gitleaks PR policy PR required (all pass); targeted vitest passed: tests/document-frame-contract.test.ts + tests/in-page-nav-header.dom.test.tsx | | 2026-08-12 | 1815 | 27ce96e1755055ceee2eeae02d6efdf11259fcde | babysit | fixed | Unit coverage: targeted vitest passed: tests/shared-home-empty-state.dom.test.tsx (17 passed). PR required still blocked on pre-existing check failure at old remote head before sync. | +| 2026-08-12 | claude/design-issues-triage-wnr7k9 | a6bfc6f2707975d9b9c649843e083965c80afb9c | Tier 1 design-issue re-verification: archive #171/#172/#174/#181/#273/#274/#302, correct #293 | docs-only; six rows verified delivered on main, min-h-tap finding refuted as deliberate sm: step-down | verify:pr-local (10/10 green); check:outstanding-issues 138 open/163 archived | From f4d28930287f2b6c1c6410800518ad2fbaedddce Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 07:03:42 +0000 Subject: [PATCH 03/13] =?UTF-8?q?docs(issues):=20ledger=20sweep=20pass=201?= =?UTF-8?q?-3=20=E2=80=94=20duplicates,=20tooling=20and=20design-system=20?= =?UTF-8?q?rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Archived as verified-delivered or duplicate: #303, #284 (dup of #296), #149, #159, #286, #287, #288, #256, #261, #291. Re-scoped with re-measured evidence: #210 (gate was never missing; the defect is the .next/dev types include), #262 (aliases 228 -> 193), #266 (counts moved, 23 unadopted holds), #275 (premise 'only one implementation' is false — the values leaked to five files). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017paT42ZVMf8jaLtkjFxdy5 --- docs/outstanding-issues.md | 188 ++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 97 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 77cd3f9a97..70d1dc1dae 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -77,89 +77,83 @@ removed after current-main verification; it is not missing recommended work. | 22 | `#027` | Optional | Operator — SRE/provider | When an owned external alert path is wanted | 1–2 hours | Decide vendor/cost/privacy/owner; if accepted, prove one non-PHI outage and recovery alert. Stop when no responder owns it. | | 23 | `#039` | Optional | High — frontend architecture | During a concrete catalogue-toolbar project | 0.5–1 day inventory; 1–3 days code | Converge only repeated toolbar behavior without flattening search semantics. Stop when there is no bounded implementation target. | | 24 | `#079` | Optional | High — repository hygiene | In explicitly scheduled batches | 30–60 minutes per batch | Disposition at most ten retained worktrees per pass using owner, PR, review-ledger, ancestry, and patch evidence. Preserve every dirty, active, secret-bearing, post-freeze, or ambiguous worktree and stop rather than broad-cleaning. | -| 25 | `#149` | A2 | High — install/gate integrity | Next verify-tooling pass | 1–3 hours | Widen `check:installed-lock-parity` beyond the seven top-level packages so transitive drift (e.g. `brace-expansion` CVE patch) fails closed. Prove with a fixture where only a nested dependency mismatches. **Stop:** do not weaken SessionStart skip-install behaviour without the wider check. | -| 26 | `#086` | A3 | High — repository structure + Specialist | On explicit go-ahead for X3; later packages own their gates | 1 PR per work order | Ship remaining maturity backlog (X3 rag.ts; X7 src/lib reorg; X6 coverage floors; X5 ACL consolidation; L1 one-shot archive; M1 host hardening) as verified draft PRs from `docs/maturity-backlog-workorders.md`. L4 ledger rotation shipped in #1418. Start with X3 after go-ahead; stop before RAG edits without the flag or X5 without live-DB approval. | -| 27 | `#098` | A3 | High — test infrastructure | Before `#099` or `#101`; it is their enabler | 2–4 hours | Generalise the answer-route preamble guard into a counting-proxy round-trip budget harness over the existing offline fixtures. Must enforce admission-before-scope, never the reverse. No providers, no DB. Stop if it would require live credentials. | -| 28 | `#102` | A3 | Operator — Supabase + Specialist | Next approved index window, after the ordering question is settled | 1–2 hours plus apply | Author the migration (operator SQL alone never reaches staging/DR/local replay), then apply → mirror `schema.sql` → regenerate drift manifest → register `required_indexes`. **Stop:** the RAG-path index is canary-gated, and ordering `fetchDocumentTitleAliasRows`'s unordered `.limit(12)` does not lift that — an imposed order can select a different twelve, so it is a second canary-gated change, not a way out of the first. The byte-identical claim was retracted. | -| 29 | `#099` | A3 | Specialist — answer path | After `#098` | Half a day per sub-item | Remaining fixed per-request round trips: the 8 `setCachedSearch` deferrals (abort semantics + mutation window), the anonymous subject+global limiter pair (needs a new atomic RPC first), and proxy→route identity duplication. Stop before hand-authoring locking SQL. | -| 30 | `#162` | A3 | High — frontend/UI | When starting the mode search redesign package | 0.5–1.5 days | Redesign `/tools?q=` as Compact Results Instrument (direction A): query-as-H1, one composer, dense tool rows, demote cross-mode cards, remove success-green filter banner and home hero on results. Comps in `public/mockups/mode-page-redesign-2026-07/tools-search/`. Verify phone+desktop chrome ownership and `verify:phone-chrome` / focused UI. Stop if scope expands into Tools home redesign without an explicit ask. | -| 31 | `#163` | A3 | High — frontend/UI | After or with `#162` | 0.5–1.5 days | Redesign `/services?q=` as Progressive Referral Workflow (direction B): H1 = query (not match count), progressive shortlist/compare (no always-on decision panel or giant step rail). Comps in `public/mockups/mode-page-redesign-2026-07/services-search/`. Verify referral shortlist still works; stop before changing Services home ModeHome. | -| 32 | `#164` | A3 | High — frontend/UI | Product confirmed Favourites is hybrid dashboard+search (no ModeHome) | 1–2 days | Redesign Favourites as one dashboard + search page: recommended Search-Led Workspace (direction B) — persistent search, sets as chips, Continue + recent + table on empty query, in-place filter on typed query. Comps in `public/mockups/mode-page-redesign-2026-07/favourites-hybrid/`. Do not reintroduce ModeHome for Favourites. Verify desktop+phone; stop before splitting into separate ModeHome routes. | -| 33 | `#202` | A3 | High — agent process | Next issues-skill touch | 30–60 min | Require recommendation/`/issues` answers to revalidate against `origin/main`'s ledger (or state checkout lag). Prevents re-proposing closed work from stale worktrees. | -| 34 | `#186` | A3 | Specialist — RAG ledger accuracy | Before any `#101` canary | 30–60 min | Rewrite open `#101` to credit PR 1474 hydration parallelisation and list only remaining canary-gated candidates. | -| 35 | `#187` | Optional | High — ledger hygiene | When writing the durable notes | 30–60 min | After one-line notes land for `#151`/`#154`, archive those process-lesson rows so the open table stays actionable. | -| 36 | `#090` | A3 | High — eslint toolchain | When ESLint 10 plugin peers are compatible | blocked; revisit monthly | Upgrade the eslint ecosystem to clear remaining dev-scoped high advisories — full `npm audit` reports zero high advisories from the eslint toolchain. | -| 37 | `#100` | A3 | Specialist — answer streaming | After offline Phase 0/1 design proof | provider-gated rollout | Buffered answer generation has no incremental verified delivery — [`verified-answer-incremental-delivery-design.md`](verified-answer-incremental-delivery-design.md) records the clinical-governance decision and staged co… | -| 38 | `#150` | Optional | Operator — review tooling | Next CodeRabbit billing/policy decision | 30–60 min decision | CodeRabbit reviewed none of a full day's PRs; spending cap reached — the repo's second automated reviewer is either funded or acknowledged as absent, rather than appearing to review while skipping. | -| 39 | `#152` | A2 | High — worktree hygiene | Next cleanup batch with #079 | 1–2 hours | Uncommitted work sits in worktrees whose branches are already merged — work that exists in no branch and no PR is either committed or knowingly discarded, not lost to a disk reclaim. | -| 40 | `#155` | A2 | High — agent process | Standing rule; next multi-agent session | process change | Several agent sessions edit the same branch and ledger concurrently — concurrent sessions stop silently undoing each other on shared `claude/*` branches and on this file. | -| 41 | `#159` | A3 | High — test hygiene | Next test-infra pass | 1–2 hours | Lists naming test files are duplicated, and the stale copy fails by running nothing — no gate, plan or config names a set of test files in a second place without being derived from the filesystem or asserted against it. | -| 42 | `#165` | A2 | High — clinical UI | Next answer-home UX pass | 0.5–1 day | Adopt a consolidated answer-home notice block — the studies exist, nothing adopts them — the answer hero states its safety obligation, its scope, and its verification requirement as one block in one voice. | -| 43 | `#166` | A2 | High — clinical safety UI | With #165 or next clinical chrome pass | 2–4 hours | Answer mode ships no verify-before-use caveat; every other clinical mode does — the surface that actually generates prose from retrieved sources says so, and says it must be checked. | -| 44 | `#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. | -| 45 | `#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. | -| 46 | `#170` | A2 | High — phone UI | Next documents/filter phone pass | 0.5–1 day | Documents and therapy already have page-owned phone filter sheets; remaining modes still use inline controls — shared-band Filter+Sheet adoption without regressing those two or sheetless Sort. | -| 48 | `#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. | -| 49 | `#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… | -| 50 | `#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… | -| 51 | `#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… | -| 52 | `#101` | A3 | Specialist — RAG/retrieval | After #186 update + canary approval | canary-gated | Canary-gated retrieval parallelisation candidates — independent retrieval stages stop running serially, proven by a live canary pair. Candidates: metadata/memory/visual hydration triples repeated on four branches (`rag.… | -| 53 | `#142` | Optional | High — docs hygiene | Next docs filing pass | 1–2 hours | Four loose dated docs need source and migration edits before they can be filed — every dated point-in-time doc lives in `docs/audit/` or `docs/archive/` as `docs/README.md` requires, not loose at the `docs/` top level. | -| 54 | `#151` | Optional | Operator — GitHub PAT | When writing the durable note (#187) | 15–30 min | `gh pr checks` cannot read CI, but the Actions API can — nobody concludes CI is unverifiable when it is merely reached through a different endpoint. | -| 55 | `#154` | Optional | High — agent process | When writing the durable note (#187) | 15–30 min | Row ids are not stable identifiers for "did my change land" — an agent confirms work reached `main` by content, never by id, title or PR state. | -| 56 | `#156` | A3 | High — ledger architecture | With #168 id-scheme work | design first | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge — two branches cannot silently claim the same outstanding-issues id, and no merge path can commit a file where they have. | -| 59 | `#177` | A3 | High — therapy catalogue build | Next therapy-index build change | 1–2 hours | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file — the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. | -| 60 | `#179` | A3 | High — therapy catalogue build | With #177/#180 | 1–2 hours | The full therapy catalogue silently switched from minified to pretty-printed — the full catalogue's on-disk format is a decision someone made, not a side effect. | -| 61 | `#180` | A3 | High — therapy catalogue build | With #177/#179 | 1–2 hours | build-therapies-index now overwrites its own source input — the therapy catalogue generator has a source it does not also destroy. | -| 63 | `#188` | A3 | Operator — DR/SRE | After any schema restore drill, or next DR review | checklist-owned | Document and track disaster-recovery re-creation checklist as ledger work — the five DR items that do not survive a schema restore are tracked with owners and verify steps, not only in `docs/operator-backlog.md`. | -| 64 | `#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. | -| 65 | `#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. | -| 66 | `#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. | -| 67 | `#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. | -| 68 | `#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. | -| 69 | `#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. | -| 70 | `#196` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-create pg_cron schedules after schema restore — ingestion/retention and related pg_cron schedules exist on the target DB after any schema restore. | -| 71 | `#197` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-add Vault secrets including cron_ingestion_jwt — required Vault secrets (at least `cron_ingestion_jwt`) are present after schema restore. | -| 72 | `#198` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-set custom database GUCs after schema restore — custom `app.*` GUCs required by the app/worker are set on the restored database. | -| 73 | `#199` | A3 | Operator — DR/SRE | After schema restore; Deno v2 available | 1–2 hours | DR: Redeploy Supabase edge functions (Deno v2.x) — required edge functions are deployed to the target project with Deno v2.x. | -| 74 | `#200` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-enter dashboard config after schema restore — auth providers/SSO redirect URLs, connection-pool caps, per-project keys, and `E2E_USER_*` are re-entered in the Supabase/Railway dashboards after restore. | -| 75 | `#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. | -| 76 | `#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. | -| 77 | `#208` | A2 | Specialist — clinical copy | With answer clipboard / PR-13 work | 1–2 hours | `answerClipboardText` must not replace `formatAnswerRenderCopyText` — compose render-policy warnings. **Gate:** focused clipboard/copy tests. **Stop:** do not drop render-policy caveats. | -| 78 | `#216` | A2 | High — design-system answer shell | After `#207` and clinical surface decision | 0.5–1 day | Adopt AnswerCard on the answer surface (deferred from PR-J). Own PR, own `verify:ui`. **Stop:** not before `#207`; show both surface treatments before choosing. | -| 79 | `#230` | A2 | High — PR policy / CI | Next ci.yml / pr-policy change | 1–2 hours | PR-policy body sync must no-op unless `PR_POLICY_BODY.md` is new in that PR's own diff (or move body out of repo). **Gate:** `check:github-actions` / workflow self-test. **Stop:** do not re-commit scratch bodies to main. | -| 80 | `#232` | A2 | High — review ledger hygiene | Next ledger touch for PR-J | 30–60 min | Supersede the PR-J clinical-governance ledger row so it describes the merged head (`ledger:append --supersede`). **Stop:** append-only — never edit/delete the old row. | -| 81 | `#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. | -| 82 | `#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. | -| 83 | `#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. | -| 84 | `#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. | -| 85 | `#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. | -| 86 | `#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. | -| 87 | `#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. | -| 88 | `#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. | -| 89 | `#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. | -| 90 | `#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. | -| 91 | `#236` | Optional | High — branch hygiene | Next cleanup batch with `#079` | 30–60 min | Dispose orphan DS V2 builder branches and leftover wave-5 dev servers. **Stop:** content-verify before delete; no force-clean. | -| 92 | `#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. | -| 93 | `#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. | -| 94 | `#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. | -| 95 | `#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. | -| 96 | `#241` | A3 | High — therapy catalogue | Standing; with any therapy-home change | 15–30 min | Therapy home summary count/slugs remain build-time; keep `build-therapies-index --check` load-bearing. **Stop:** do not bypass the check. | -| 97 | `#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. | -| 98 | `#244` | A3 | High — design tokens / forced-colors | With any ckb-v2 forced-colours edit | 15–30 min | Keep grouped dark selectors in the forced-colours media block so specificity matches dark rules. **Stop:** do not trim to a single `.ckb-v2.ckb-v2` selector. | -| 99 | `#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. | -| 100 | `#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. | -| 101 | `#249` | A3 | High — agent process | Next issues-skill / plan touch | 1–2 hours | Extend issues/plan with an agent-safe wins classifier (optional filter; no new skill unless reused thrice). **Stop:** do not outrank A1 operator work. | -| 102 | `#250` | A2 | High — multi-agent execution | After Wave 0 queue repair on main (done in this capture); run remaining Wave 0/#202 process gates next on the engineering track | multi-wave | Execute the fastest-wins multi-wave plan (Waves 0–4 + operator track) with parallel agents and per-PR gates. Waves do not outrank A1 acuity. **Stop:** provider/RAG approvals still required where flagged. | -| 103 | `#251` | Optional | High — agent process | Next handoff/gates doc touch | 15–30 min | Handoff checklist pairs gates skill with verification-router; paste decisive proof line. **Stop:** do not stack broad gates by default. | -| 104 | `#253` | A2 | High — phone results UI | Next open-PR sweep | 15–30 min | Decide #1606's fate: the `MobileResultFilterControl` it rewrites was deleted by #247, so there is nothing left to hand-merge. Verify keyboard parity of the replacement sheet on a real device, then close #1606 as superseded. **Stop:** the decision is a human's; do not close #1606 automatically. | -| 105 | `#254` | A2 | Operator — Codex Cloud | Before #1617 leaves draft | 1–2 hours | Re-run Codex Cloud acceptance at the exact current head or mark head-independent evidence. **Stop:** do not treat stale pins as coverage. | -| 106 | `#256` | A2 | High — mode section nav | Next information-page / mode-nav pass | 2–4 hours | Declared information-page section sets whose target ids nothing renders — verify each set against the rendered DOM per route; render anchors or delete the set. **Stop:** do not audit by grepping for `id=` alone (sectionId props exist). | -| 107 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | -| 108 | `#286` | A3 | High — in-page nav + frontend | After owner go-ahead for the information-page series | 1–2 days | Convert the six pill-rail information pages onto `InPageNavHeader`, widen Server Component–safe actions, then delete `informationPageSectionDefinitions`. **Gate:** focused DOM/contract tests + `verify:phone-chrome` for touched owners. **Stop:** do not convert DocumentViewer here; do not verify anchors by grepping `id=` alone. | -| 109 | `#287` | A3 | High — in-page nav + clinical owner | After `#286`; medications needs an owner product call | 0.5–1 day design + convert | Decide medications tab model, presentations MobileTabs vs `InPageNavHeader`, and factsheets heading→id scheme; convert or record lasting exceptions. **Stop:** do not port medications mechanically. | -| 110 | `#288` | Optional | High — document chrome | After `#286`/`#287`, or when declaring the series complete | 30–60 min | Confirm DocumentViewer non-adoption (already noted in `docs/search-chrome-behaviour.md`) as the final end state, or schedule a separate convergence PR that leaves pinned `--document-*` CSS names untouched. | -| 111 | `#289` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | +| 25 | `#086` | A3 | High — repository structure + Specialist | On explicit go-ahead for X3; later packages own their gates | 1 PR per work order | Ship remaining maturity backlog (X3 rag.ts; X7 src/lib reorg; X6 coverage floors; X5 ACL consolidation; L1 one-shot archive; M1 host hardening) as verified draft PRs from `docs/maturity-backlog-workorders.md`. L4 ledger rotation shipped in #1418. Start with X3 after go-ahead; stop before RAG edits without the flag or X5 without live-DB approval. | +| 26 | `#098` | A3 | High — test infrastructure | Before `#099` or `#101`; it is their enabler | 2–4 hours | Generalise the answer-route preamble guard into a counting-proxy round-trip budget harness over the existing offline fixtures. Must enforce admission-before-scope, never the reverse. No providers, no DB. Stop if it would require live credentials. | +| 27 | `#102` | A3 | Operator — Supabase + Specialist | Next approved index window, after the ordering question is settled | 1–2 hours plus apply | Author the migration (operator SQL alone never reaches staging/DR/local replay), then apply → mirror `schema.sql` → regenerate drift manifest → register `required_indexes`. **Stop:** the RAG-path index is canary-gated, and ordering `fetchDocumentTitleAliasRows`'s unordered `.limit(12)` does not lift that — an imposed order can select a different twelve, so it is a second canary-gated change, not a way out of the first. The byte-identical claim was retracted. | +| 28 | `#099` | A3 | Specialist — answer path | After `#098` | Half a day per sub-item | Remaining fixed per-request round trips: the 8 `setCachedSearch` deferrals (abort semantics + mutation window), the anonymous subject+global limiter pair (needs a new atomic RPC first), and proxy→route identity duplication. Stop before hand-authoring locking SQL. | +| 29 | `#162` | A3 | High — frontend/UI | When starting the mode search redesign package | 0.5–1.5 days | Redesign `/tools?q=` as Compact Results Instrument (direction A): query-as-H1, one composer, dense tool rows, demote cross-mode cards, remove success-green filter banner and home hero on results. Comps in `public/mockups/mode-page-redesign-2026-07/tools-search/`. Verify phone+desktop chrome ownership and `verify:phone-chrome` / focused UI. Stop if scope expands into Tools home redesign without an explicit ask. | +| 30 | `#163` | A3 | High — frontend/UI | After or with `#162` | 0.5–1.5 days | Redesign `/services?q=` as Progressive Referral Workflow (direction B): H1 = query (not match count), progressive shortlist/compare (no always-on decision panel or giant step rail). Comps in `public/mockups/mode-page-redesign-2026-07/services-search/`. Verify referral shortlist still works; stop before changing Services home ModeHome. | +| 31 | `#164` | A3 | High — frontend/UI | Product confirmed Favourites is hybrid dashboard+search (no ModeHome) | 1–2 days | Redesign Favourites as one dashboard + search page: recommended Search-Led Workspace (direction B) — persistent search, sets as chips, Continue + recent + table on empty query, in-place filter on typed query. Comps in `public/mockups/mode-page-redesign-2026-07/favourites-hybrid/`. Do not reintroduce ModeHome for Favourites. Verify desktop+phone; stop before splitting into separate ModeHome routes. | +| 32 | `#202` | A3 | High — agent process | Next issues-skill touch | 30–60 min | Require recommendation/`/issues` answers to revalidate against `origin/main`'s ledger (or state checkout lag). Prevents re-proposing closed work from stale worktrees. | +| 33 | `#186` | A3 | Specialist — RAG ledger accuracy | Before any `#101` canary | 30–60 min | Rewrite open `#101` to credit PR 1474 hydration parallelisation and list only remaining canary-gated candidates. | +| 34 | `#187` | Optional | High — ledger hygiene | When writing the durable notes | 30–60 min | After one-line notes land for `#151`/`#154`, archive those process-lesson rows so the open table stays actionable. | +| 35 | `#090` | A3 | High — eslint toolchain | When ESLint 10 plugin peers are compatible | blocked; revisit monthly | Upgrade the eslint ecosystem to clear remaining dev-scoped high advisories — full `npm audit` reports zero high advisories from the eslint toolchain. | +| 36 | `#100` | A3 | Specialist — answer streaming | After offline Phase 0/1 design proof | provider-gated rollout | Buffered answer generation has no incremental verified delivery — [`verified-answer-incremental-delivery-design.md`](verified-answer-incremental-delivery-design.md) records the clinical-governance decision and staged co… | +| 37 | `#150` | Optional | Operator — review tooling | Next CodeRabbit billing/policy decision | 30–60 min decision | CodeRabbit reviewed none of a full day's PRs; spending cap reached — the repo's second automated reviewer is either funded or acknowledged as absent, rather than appearing to review while skipping. | +| 38 | `#152` | A2 | High — worktree hygiene | Next cleanup batch with #079 | 1–2 hours | Uncommitted work sits in worktrees whose branches are already merged — work that exists in no branch and no PR is either committed or knowingly discarded, not lost to a disk reclaim. | +| 39 | `#155` | A2 | High — agent process | Standing rule; next multi-agent session | process change | Several agent sessions edit the same branch and ledger concurrently — concurrent sessions stop silently undoing each other on shared `claude/*` branches and on this file. | +| 40 | `#165` | A2 | High — clinical UI | Next answer-home UX pass | 0.5–1 day | Adopt a consolidated answer-home notice block — the studies exist, nothing adopts them — the answer hero states its safety obligation, its scope, and its verification requirement as one block in one voice. | +| 41 | `#166` | A2 | High — clinical safety UI | With #165 or next clinical chrome pass | 2–4 hours | Answer mode ships no verify-before-use caveat; every other clinical mode does — the surface that actually generates prose from retrieved sources says so, and says it must be checked. | +| 42 | `#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. | +| 43 | `#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. | +| 44 | `#170` | A2 | High — phone UI | Next documents/filter phone pass | 0.5–1 day | Documents and therapy already have page-owned phone filter sheets; remaining modes still use inline controls — shared-band Filter+Sheet adoption without regressing those two or sheetless Sort. | +| 45 | `#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. | +| 46 | `#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… | +| 47 | `#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… | +| 48 | `#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… | +| 49 | `#101` | A3 | Specialist — RAG/retrieval | After #186 update + canary approval | canary-gated | Canary-gated retrieval parallelisation candidates — independent retrieval stages stop running serially, proven by a live canary pair. Candidates: metadata/memory/visual hydration triples repeated on four branches (`rag.… | +| 50 | `#142` | Optional | High — docs hygiene | Next docs filing pass | 1–2 hours | Four loose dated docs need source and migration edits before they can be filed — every dated point-in-time doc lives in `docs/audit/` or `docs/archive/` as `docs/README.md` requires, not loose at the `docs/` top level. | +| 51 | `#151` | Optional | Operator — GitHub PAT | When writing the durable note (#187) | 15–30 min | `gh pr checks` cannot read CI, but the Actions API can — nobody concludes CI is unverifiable when it is merely reached through a different endpoint. | +| 52 | `#154` | Optional | High — agent process | When writing the durable note (#187) | 15–30 min | Row ids are not stable identifiers for "did my change land" — an agent confirms work reached `main` by content, never by id, title or PR state. | +| 53 | `#156` | A3 | High — ledger architecture | With #168 id-scheme work | design first | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge — two branches cannot silently claim the same outstanding-issues id, and no merge path can commit a file where they have. | +| 54 | `#177` | A3 | High — therapy catalogue build | Next therapy-index build change | 1–2 hours | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file — the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. | +| 55 | `#179` | A3 | High — therapy catalogue build | With #177/#180 | 1–2 hours | The full therapy catalogue silently switched from minified to pretty-printed — the full catalogue's on-disk format is a decision someone made, not a side effect. | +| 56 | `#180` | A3 | High — therapy catalogue build | With #177/#179 | 1–2 hours | build-therapies-index now overwrites its own source input — the therapy catalogue generator has a source it does not also destroy. | +| 57 | `#188` | A3 | Operator — DR/SRE | After any schema restore drill, or next DR review | checklist-owned | Document and track disaster-recovery re-creation checklist as ledger work — the five DR items that do not survive a schema restore are tracked with owners and verify steps, not only in `docs/operator-backlog.md`. | +| 58 | `#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. | +| 59 | `#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. | +| 60 | `#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. | +| 61 | `#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. | +| 62 | `#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. | +| 63 | `#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. | +| 64 | `#196` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-create pg_cron schedules after schema restore — ingestion/retention and related pg_cron schedules exist on the target DB after any schema restore. | +| 65 | `#197` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-add Vault secrets including cron_ingestion_jwt — required Vault secrets (at least `cron_ingestion_jwt`) are present after schema restore. | +| 66 | `#198` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-set custom database GUCs after schema restore — custom `app.*` GUCs required by the app/worker are set on the restored database. | +| 67 | `#199` | A3 | Operator — DR/SRE | After schema restore; Deno v2 available | 1–2 hours | DR: Redeploy Supabase edge functions (Deno v2.x) — required edge functions are deployed to the target project with Deno v2.x. | +| 68 | `#200` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-enter dashboard config after schema restore — auth providers/SSO redirect URLs, connection-pool caps, per-project keys, and `E2E_USER_*` are re-entered in the Supabase/Railway dashboards after restore. | +| 69 | `#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. | +| 70 | `#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. | +| 71 | `#208` | A2 | Specialist — clinical copy | With answer clipboard / PR-13 work | 1–2 hours | `answerClipboardText` must not replace `formatAnswerRenderCopyText` — compose render-policy warnings. **Gate:** focused clipboard/copy tests. **Stop:** do not drop render-policy caveats. | +| 72 | `#216` | A2 | High — design-system answer shell | After `#207` and clinical surface decision | 0.5–1 day | Adopt AnswerCard on the answer surface (deferred from PR-J). Own PR, own `verify:ui`. **Stop:** not before `#207`; show both surface treatments before choosing. | +| 73 | `#230` | A2 | High — PR policy / CI | Next ci.yml / pr-policy change | 1–2 hours | PR-policy body sync must no-op unless `PR_POLICY_BODY.md` is new in that PR's own diff (or move body out of repo). **Gate:** `check:github-actions` / workflow self-test. **Stop:** do not re-commit scratch bodies to main. | +| 74 | `#232` | A2 | High — review ledger hygiene | Next ledger touch for PR-J | 30–60 min | Supersede the PR-J clinical-governance ledger row so it describes the merged head (`ledger:append --supersede`). **Stop:** append-only — never edit/delete the old row. | +| 75 | `#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. | +| 76 | `#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. | +| 77 | `#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. | +| 78 | `#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. | +| 79 | `#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. | +| 80 | `#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. | +| 81 | `#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. | +| 82 | `#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. | +| 83 | `#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. | +| 84 | `#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. | +| 85 | `#236` | Optional | High — branch hygiene | Next cleanup batch with `#079` | 30–60 min | Dispose orphan DS V2 builder branches and leftover wave-5 dev servers. **Stop:** content-verify before delete; no force-clean. | +| 86 | `#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. | +| 87 | `#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. | +| 88 | `#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. | +| 89 | `#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. | +| 90 | `#241` | A3 | High — therapy catalogue | Standing; with any therapy-home change | 15–30 min | Therapy home summary count/slugs remain build-time; keep `build-therapies-index --check` load-bearing. **Stop:** do not bypass the check. | +| 91 | `#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. | +| 92 | `#244` | A3 | High — design tokens / forced-colors | With any ckb-v2 forced-colours edit | 15–30 min | Keep grouped dark selectors in the forced-colours media block so specificity matches dark rules. **Stop:** do not trim to a single `.ckb-v2.ckb-v2` selector. | +| 93 | `#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. | +| 94 | `#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. | +| 95 | `#249` | A3 | High — agent process | Next issues-skill / plan touch | 1–2 hours | Extend issues/plan with an agent-safe wins classifier (optional filter; no new skill unless reused thrice). **Stop:** do not outrank A1 operator work. | +| 96 | `#250` | A2 | High — multi-agent execution | After Wave 0 queue repair on main (done in this capture); run remaining Wave 0/#202 process gates next on the engineering track | multi-wave | Execute the fastest-wins multi-wave plan (Waves 0–4 + operator track) with parallel agents and per-PR gates. Waves do not outrank A1 acuity. **Stop:** provider/RAG approvals still required where flagged. | +| 97 | `#251` | Optional | High — agent process | Next handoff/gates doc touch | 15–30 min | Handoff checklist pairs gates skill with verification-router; paste decisive proof line. **Stop:** do not stack broad gates by default. | +| 98 | `#253` | A2 | High — phone results UI | Next open-PR sweep | 15–30 min | Decide #1606's fate: the `MobileResultFilterControl` it rewrites was deleted by #247, so there is nothing left to hand-merge. Verify keyboard parity of the replacement sheet on a real device, then close #1606 as superseded. **Stop:** the decision is a human's; do not close #1606 automatically. | +| 99 | `#254` | A2 | Operator — Codex Cloud | Before #1617 leaves draft | 1–2 hours | Re-run Codex Cloud acceptance at the exact current head or mark head-independent evidence. **Stop:** do not treat stale pins as coverage. | +| 100 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | +| 101 | `#289` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | @@ -211,14 +205,12 @@ removed after current-main verification; it is not missing recommended work. | #118 | P2 | task | Adopt the visual and Lighthouse baselines so the two new gates actually gate | OWNER DECISION 2026-08-09 — visual-baseline is NOT to be promoted to pr-required, and no longer runs on pull_request or merge_group (PR #1755). Supersedes step (3) of this row for the visual half: do not re-add pull_request or add the job to pr-required without the owner saying so. Reason: a pixel gate that reports on every UI PR taxes the work it exists to support — during a redesign the honest result is red on every push, and baselines cannot be refreshed until a run has produced the new images, so the red is unavoidable rather than actionable. It was never merge-blocking (continue-on-error, absent from pr-required's needs list) but the notification churn on PR #1743 was real friction. Drift is still caught on pushes to main/release, the weekly schedule, and workflow_dispatch, where a refresh costs one command: npm run design-system:baselines:adopt (added PR #1743). Accepted trade-off: a PR can now move a surface and land before anything notices; the red appears on the next main push. Narrower alternative if pre-merge coverage is ever wanted back: run it only on PRs carrying an opt-in label. The Lighthouse half of this row is unaffected and still fails repo-wide for the Chromium 150-vs-151 baseline reason recorded above. | session 2026-07-30; PR #1404 | 2026-07-30 | | #142 | P3 | task | Four loose dated docs need source and migration edits before they can be filed | **Outcome:** every dated point-in-time doc lives in `docs/audit/` or `docs/archive/` as `docs/README.md` requires, not loose at the `docs/` top level. **Detail:** PR #1436 filed the five that were docs-only moves. These four are referenced from outside `docs/`, so relocating them means editing source, tests and migration SQL comments — a different risk class than a docs tidy, and not worth bundling into one: `capacity-review.md` (`scripts/soak-test.ts`), `tenancy-defense-in-depth-review.md` (`src/lib/owner-scope.ts`, `tests/owner-scope-guard.test.ts`, two migrations, `SECURITY.md`, `.claude/agents/clinical-governance-reviewer.md`), `operator-apply-july8-batch.md` (three migrations plus `supabase/schema.sql`), `scale-readiness-review.md` (one migration). Also note `forward-codify-retrieval-rpcs-workorder.md` is indexed as a completed workorder but live is still ahead of the repo on those RPCs, so archiving it would misrepresent open operator work. **Next:** treat as low priority — the docs are correctly indexed and reachable where they are; only file them if a pass is already editing those migrations. Editing applied migration SQL is subject to `npm run check:migration-role`'s immutability pin. | PR #1436; session 2026-07-30 | 2026-07-30 | | #147 | P2 | rec | Mobile CLS breaches four routes, and it reproduces exactly offline | **Outcome:** mobile CLS is under 0.1 on every measured route, or each remaining breach has a recorded reason. **Measured 2026-07-30 with the LOCAL OFFLINE production harness** (`CHROME_PATH=/opt/pw-browsers/chromium npm run verify:lighthouse -- --keep`; Lighthouse 12.8.2, Chromium 141.0.7390.37, demo corpus, inert loopback Supabase). Mobile CLS: `/dsm` 0.363, `/documents/search` 0.220, `/therapy-compass` 0.142, `/` 0.023 — `/forms` did not measure locally (Lighthouse `NO_NAVSTART`, its own "run again" transient), but the live dispatch put it at 0.212. Desktop passes everywhere: 0.016–0.097. **The finding that matters is that these reproduce the live production dispatch EXACTLY.** Against run `30548662649` on `psychiatry.tools`, all four locally measurable mobile routes match to three decimals — 0.023 / 0.142 / 0.220 / 0.363 on both — and the desktop range matches at both endpoints, 0.016–0.097. So CLS on these routes is deterministic layout structure, not network, latency, or production data. Three consequences: (a) the CLS half of `#017` is debuggable and gate-able locally at zero provider cost; (b) `#017`'s recorded blocker "prod server hard-requires Supabase secrets" is stale — `scripts/run-lighthouse-budget.mjs` builds and serves an offline production app precisely to sidestep that, and its earlier note that dev-mode CLS looked "excellent at 0.00–0.04" was measuring DEV, which does not reproduce production; (c) re-dispatching the live workflow to re-measure CLS buys nothing. **LCP does NOT reproduce and must not be read from local runs:** local mobile LCP is 2014–2051 ms against 3639–4716 ms live, because the loopback server has no network latency. The LCP _ranking_ does survive — `/therapy-compass` is the outlier both locally (4978 ms, ~2.4x every other route) and live — which corroborates `#117` rather than replacing it. Mobile TBT, local: `/documents/search` 581 ms, `/dsm` 448, `/therapy-compass` 379, `/` 338. **Ranking by measured contribution, which is what `#017` asked for:** 1. `/dsm` CLS 0.363 (3.6x the threshold, the worst single number anywhere); 2. `/documents/search` 0.220; 3. `/forms` 0.212 (live only); 4. `/therapy-compass` 0.142, and separately the sole LCP outlier via `#117`; 5. `/` 0.023, which passes and needs nothing. **ATTRIBUTED 2026-07-30 — one cause dominates, and it is a self-inflicted round trip.** Driving Chromium directly against the same offline production build (Lighthouse mobile emulation: 412x823, DPR 1.75, 4x CPU throttle) with a `PerformanceObserver` on `layout-shift` reading `entry.sources[].node`: on `/dsm`, `/documents/search`, `/forms` and `/therapy-compass` the entire main content region moves **down 128px and straight back up 128px** within 15-60ms. Both moves score, so the round trip is pure cost with zero net movement. It is **100% of `/documents/search`** (0.110 down + 0.110 up = 0.220) and about 75% of `/dsm`. The shifting element is the `max-sm:pt-[var(--phone-overlay-chrome-h)]` wrapper around `
`, i.e. the phone overlay chrome reserve. **Mechanism, pinned by a MutationObserver timeline on the root style attribute rather than inferred:** the property goes CSS seed -> `200px` -> `72px`, and the decisive line is that the 200px is written when the header stack **already measures 72px** — `t=1552ms reserve=200px stack=72`, corrected to `72px` at `t=1612ms`. So `usePhoneOverlayChromeReserve` (`src/components/clinical-dashboard/use-phone-overlay-chrome-reserve.ts`) publishes a **stale** measurement: its `useLayoutEffect` reads `stack.offsetHeight` while the stack is still transiently 200px, and the write lands after the stack has already collapsed to 72px; its `ResizeObserver` then corrects it. The CSS seed at `globals.css:375` (`calc(max(0.5rem, var(--safe-area-top)) + var(--shell-header-h))` = 72px on a zero-inset profile) is **correct for the settled stack** — the seed is not the problem. **This corrects the mechanism recorded on `#130`** (now archived), which framed the defect as the seed under-reserving by `max(0, 0.5rem - inset)`, i.e. 0px on a notched iPhone and 8px elsewhere. Measured, the CLS driver is not an 8px seed shortfall but a 128px transient over-reserve written by the hook. Do not reopen `#130` on this; it is recorded here. **Control that makes the attribution trustworthy:** `/` never writes the property at all (no overlay stack) and is the one clean route at 0.023. **Variance, stated rather than smoothed over:** `/dsm` measured 0.363 (3 shift entries) and 0.219 (2 entries) on two runs — the round trip is the stable core and the third entry is intermittent. This harness has no network throttling, so `/forms` (0.310 local vs 0.212 live) and `/therapy-compass` (0.240 vs 0.142) run high locally; only `/dsm` 0.363, `/documents/search` 0.220 and `/` 0.023 reproduced the live dispatch exactly, and only those three should be treated as production-equivalent. **Next:** stop the hook publishing a measurement it is about to revise — defer the first publish until the stack has settled, or let the `ResizeObserver` be the only writer and trust the seed until it fires. That is a phone-chrome change, so it is governed by `docs/search-chrome-behaviour.md` and needs `npm run verify:phone-chrome` plus a before/after CLS pair from this harness. **Harness trap worth keeping:** attaching a `MutationObserver` to `document.documentElement` inside a Playwright `addInitScript` throws when the document element does not exist yet, which silently takes the CLS observer down with it and reports a uniform `CLS=0.000` across every route — a false clean bill, not a pass. Guard the attach. **Original next step, now done:** attribute the shifts to elements. Lighthouse's `layout-shift-elements` audit returned **zero items on every route** — the `cumulative-layout-shift` audit carries only `debugdata` — so attribution needs a driven Chromium session with a `PerformanceObserver` on `layout-shift`, reading `entry.sources[].node` and `entry.value`, against the same offline production build. Start with `/dsm`. **Gate — `#118` owns it, and this row supplies a constraint it was missing.** `#118` already tracks flipping `lighthouse-budget.json` to `enforce: true` and says not to do it "before `#117` or the baseline pins a known-slow route". These numbers make that concrete: pinning a baseline today would bake CLS 0.363 on `/dsm` in as the accepted value, so the CLS fixes must land first or the gate ratifies the breach. Separately, no baseline could have been committed from this run anyway — the grader correctly refused it as incomplete evidence because `/forms` produced no report, and that refusal was not overridden. **Reproduction trap:** without `CHROME_PATH`, `chrome-launcher` cannot find a browser in this container and every route fails; the harness reads `CHROME_PATH`/`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` (`run-lighthouse-budget.mjs:291`) but nothing sets either, even though `PLAYWRIGHT_BROWSERS_PATH` is set. Related to `#121`. Also seen: a truncated `progress` package in the npx cache broke Lighthouse startup entirely; clearing the npx cache entry fixed it. **Stop:** do not re-dispatch the live Web-Vitals workflow to re-measure CLS — it is reproducible offline for free. Do not commit a budget baseline from a run with a failed route. Do not treat local LCP numbers as production LCP. | `#017` live run `30548662649`; local `verify:lighthouse` 2026-07-30; `lighthouse-budget.json` | 2026-07-30 | -| #149 | P2 | issue | `check:installed-lock-parity` only compares seven top-level packages, so transitive drift passes | **Outcome:** a session cannot run local gates against a `node_modules` that silently disagrees with the lockfile. **Evidence 2026-07-30:** the `SessionStart` hook reported "node_modules matches the lockfile, skipping install" while `brace-expansion` was installed at **1.1.16** and the lockfile pinned **1.1.18** — the CVE-2026-14257 patch. Every local gate run in that window was weaker than it appeared, including several `verify:cheap` runs reported as green during PR #1430/#1444 work. It surfaced only because `tests/installed-lock-parity.test.ts` asserts the patched behaviour directly (`maxLength` honoured); its failure was initially mistaken for a pre-existing repo defect, since it also failed on clean `main` **in the same stale container**. `npm ci --include=dev` fixed it. **CORRECTION 2026-07-30 (PR #1496 review, Codex):** an earlier version of this row said the parity check lives only in `verify:ui` and proposed adding it to `verify:cheap`. **Both were wrong** — `verify:cheap:internal` already runs `check:installed-lock-parity`, so that follow-up was a no-op, and it ran green throughout the stale window. The real gap is its **scope**: `scripts/check-installed-lock-parity.mjs` compares only seven top-level packages (`criticalInstalledPackages` = next, react, react-dom, eslint, playwright, typescript, vitest), so a stale **transitive** dependency is invisible to it. Proved by faking `node_modules/brace-expansion` back to `1.1.16` against the `1.1.18` lockfile pin: `npm run check:installed-lock-parity` **exits 0**. That also explains why the hook and the gate both reported parity while the tree was stale — neither was looking at the package that drifted. **Next:** broaden the check beyond the seven names — validate the full installed tree against the lockfile, or stamp the install with the lockfile hash and compare that — so transitive drift is caught rather than assumed absent. **Stop:** do not treat a green `check:installed-lock-parity` as proof the install is lockfile-current; today it means seven packages match. **Do not archive on the audit-worktree disposition (checked 2026-07-31):** several `comprehensive-audit-*` worktrees carry an uncommitted row archiving this as “Resolved 2026-07-31 — `check:installed-lock-parity` now validates every concrete package location in `package-lock.json`, including nested transitives”. That fix exists nowhere: not on `main`, not in any of the 42 worktrees, and not in any open PR. `main` still reads `criticalInstalledPackages = ["next", "react", "react-dom", "eslint", "playwright", "typescript", "vitest"]`. The sibling `#103` disposition from the same worktrees WAS verified true and landed in PR #1528; this one was not. **Second, worse instance measured 2026-08-08 — the gap is not only transitive version drift, it is structural completeness.** Two `npm install` runs were killed mid-extraction (a concurrent session ran `npm cache clean`; both exited **127 with an empty log**, no npm error). What survived looked complete: every package directory present at its locked version, and `check:installed-lock-parity` **passed**. But `@testing-library/dom/types/index.d.ts` was **0 bytes** and `@types/node` was **six files short**, missing `web-globals/importmeta.d.ts`. The result was **163 phantom `tsc` errors** — 135 x TS2305 'no exported member screen', plus `ImportMeta.dirname`, `Timeout` and `unref` failures — that read exactly like real code breakage, and `@rolldown/binding-win32-x64-msvc` was absent so Vitest died with `ERR_DLOPEN_FAILED`. A version comparison of any width cannot catch this; the installed files were the right version and simply incomplete. Cheap detection that worked: run the gate against pristine `origin/main` content as well — an identical error count proves the tree innocent and the install guilty. Repair that worked: `rm -rf node_modules` then ONE `npm install `, which reifies the whole tree from the lockfile without touching `package.json` or `package-lock.json`. Worth folding into this row's fix: a completeness check alongside the version comparison — but zero-byte tracked `.d.ts` plus each package's `main`/`types` entry point is NOT enough on its own. The measured miss was `@types/node/web-globals/importmeta.d.ts`, a non-entry file; `@types/node/index.d.ts` stayed present and non-empty, so that shallow check would still pass. Require a completeness method that rejects structurally incomplete installs when arbitrary package files are missing (for example a lockfile/package file inventory, an extract-integrity stamp, or an equivalent that fails closed on a deleted non-entry path), and prove it with a fixture that deletes a non-entry file while leaving entry points intact. | `scripts/check-installed-lock-parity.mjs`; `tests/installed-lock-parity.test.ts`; `package.json` | 2026-07-30 | | #150 | P2 | issue | CodeRabbit reviewed none of a full day's PRs; spending cap reached | **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 | | #151 | P3 | issue | `gh pr checks` cannot read CI, but the Actions API can | **Outcome:** nobody concludes CI is unverifiable when it is merely reached through a different endpoint. **Detail:** the session `gh` credential is a fine-grained PAT with **Actions: read** but **not Checks: read**. `gh pr checks` fails per context with `Resource not accessible by personal access token` and `GET /repos/:o/:r/commits/:sha/check-runs` returns 403, so the obvious routes look like a hard wall. `GET .../commits/:sha/status` succeeds but returns `total: 0`, because this repo reports through Actions rather than legacy commit statuses — a silent empty result that reads like an absence of checks instead of an absence of permission. **The working route** is `gh api "repos/BigSimmo/Database/actions/runs?head_sha="` with a jq filter over `.workflow_runs[]`, which returned CI, PR Policy, PR mergeability, SAST and Secret Scan all `completed/success` for PR #1490. **Cost of not knowing this:** two PRs were handed off as green-locally-but-unverifiable and armed for auto-merge instead of merged; #1490 was then closed unmerged and its unique content lost (re-landed as #152). Separately, `scripts/guard-push.mjs` logs `auto-merge: gh not available` only when the CLI cannot execute; that is a distinct #132 prerequisite, not a Checks-permission symptom. **Next:** either add Checks: read to the PAT so `gh pr checks` works, or document the Actions query as the supported way to read CI here. **Stop:** do not report CI as unreadable without trying the Actions endpoint. | session 2026-07-30; PRs #1490, #1494 | 2026-07-30 | | #152 | P2 | issue | Uncommitted work sits in worktrees whose branches are already merged | **Outcome:** work that exists in no branch and no PR is either committed or knowingly discarded, not lost to a disk reclaim. **Inventory 2026-07-30**, all from worktrees whose pre-snapshot branch tips were fully merged into `origin/main`, so the uncommitted changes existed nowhere else: `codex/reconcile-immediate-20260730` — 21 files, +395/-200 across 19 tracked, including `.github/workflows/ci.yml`, `package.json` and `docs/scripts-index.md`; `codex/document-results-mockup-20260730` — 8 files (+13/-3 tracked) plus an untracked `document-search-results/page.tsx` under `src/app/mockups/` (named without a full path here because it does not resolve in this repo); `codex/chat-ledger-triage-d344` — `docs/outstanding-issues.md` +59/-61; `claude/section-spy-browser-coverage` — `tests/ui-smoke.spec.ts` +51. A fifth (`claude/frosty-mayer-2c6167`) self-resolved to clean during the session. **Preserved 2026-07-30, not reviewed:** each was committed on its own branch as an unpushed `wip: preserve uncommitted work before worktree cleanup` snapshot, so the work now survives a worktree reclaim — `codex/reconcile-immediate-20260730` `748ef018f`, `codex/document-results-mockup-20260730` `5dbd9f965`, `codex/chat-ledger-triage-d344` `b7eae51a4`, `claude/section-spy-browser-coverage` `d949859c3`. All four worktrees are now clean. None is pushed and none is reviewed or verified; the snapshots exist to stop silent loss, not to endorse the content. First captured in PR #1490, which was closed unmerged; re-landed on `main` by PR #1508. **Next:** per snapshot, review and either promote it to a real branch/PR or `git reset --hard HEAD~1` to drop it. Do not bulk-delete worktrees without this check — it is why only one of the two "safe" candidates was removed in the 2026-07-30 cleanup. | session 2026-07-30 worktree cleanup; PR #1490 (closed); PR #1508 | 2026-07-30 | | #154 | P3 | rec | Row ids are not stable identifiers for "did my change land" | **Outcome:** an agent confirms work reached `main` by content, never by id, title or PR state. **Detail:** three separate checks gave false answers during the 2026-07-30 session. (1) Grepping row **titles** reported rows missing that were present — a concurrent session had reworded them. (2) Grepping row **ids** reported rows present that were absent — `main`'s `#149`/`#150` were unrelated rows (installed-lock parity, CodeRabbit budget) that took those ids during a parallel renumber, because ids are allocated by read-modify-write against a marker and two branches can allocate the same number. (3) `gh pr view` reported `state=MERGED` for a PR whose branch head was not an ancestor of `main` — normal for squash merges, and separately a PR showed `MERGED` while its content genuinely had not landed yet at the ref that had been fetched. A fourth variant: `git show :` silently mangles under MSYS, so a grep over its empty output returns 0 and reads as absence. **What works:** resolve the blob (`git ls-tree `) and `git cat-file -p` it, then grep for **distinctive prose** from the change. **Cost:** PR #1490 was believed landed, was closed unmerged, and took the only record of four preservation snapshots with it; it had to be reconstructed as #152. **Next:** consider a short note in `AGENTS.md` next to the existing squash-verification guidance. **Stop:** never report work as landed on the strength of an id match, a title match, or a PR state field. | session 2026-07-30; PRs #1490, #1508 | 2026-07-31 | | #156 | P3 | issue | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge | **Outcome:** two branches cannot silently claim the same outstanding-issues id, and no merge path can commit a file where they have. **Detail:** Residual of archived `#112` (gate landed; underlying read-modify-write race explicitly left open) plus post-`#133` evidence. `#133` fixed the two causes of *conflict frequency* — `#1444` removed `merge=union` and `#1479` excluded the ledger from Prettier so a maximum-width row stops re-padding the whole table. Neither touches **id allocation**, which is still read-modify-write against the `issues:next-id` marker, so two branches open at the same time still pick the same number. Measured on PR #1451 (2026-07-30): one P3 row was renumbered `#135` -> `#141` -> `#145` -> `#147` -> `#149` across four sync cycles, because `main` had taken each id in turn — every renumber was manual. This capture itself demonstrates the same hazard: first written as `#151` on PR #1506, then `#154` / `#155` after earlier main syncs, then `#156` here because `main` had already taken `#154` and `#155` for unrelated rows. Both renumbers happened *after* `#133` was closed, which is the point: the fixes in `#1444` and `#1479` were real, but they were fixes to conflict frequency, and allocation was never the same problem. The sharper finding is the resolution path: the GitHub **Update branch** button pushed a sync to that PR head (`df3f3aeed`) whose auto-merge produced **two rows numbered `#141` and two `next-id` markers**, leaving the marker at `142` — below `main`'s highest id, so the next allocation would have reused a live number. `git merge` reported success; only `npm run check:outstanding-issues` caught it. That guard runs in `verify:cheap` and `static-pr`, so such a head cannot merge — but the corruption is produced by a one-click path that runs no guard, and the cost lands on whoever notices. A second session on the same branch later dropped an entire appended evidence block while resolving this file, which the guard cannot detect at all: it validates ids and structure, not whether a merge kept both sides' prose. **Next:** cheapest first — document that Update branch must not be used on PRs touching this file (prefer `npm run sync:pr-branches:apply`, which the repo already prefers for other reasons), then consider allocating ids from a source that cannot collide (per-row files, or a date-plus-slug id) so concurrent branches never contend. **Stop:** do not reintroduce a merge driver here — `#133` settled that; this is about allocation and about merges that silently drop rows, not about the driver. Do not reopen archived `#112` — keep the gate outcome there and track the residual race here. | PR #1451 sync cycles; `df3f3aeed`; archived `#112`; session 2026-07-30; PR #1506 | 2026-07-31 | | #155 | P2 | rec | Several agent sessions edit the same branch and ledger concurrently | **Outcome:** concurrent sessions stop silently undoing each other on shared `claude/*` branches and on this file. **Observed across one task on 2026-07-30/31:** (a) PR #1490 was **closed unmerged by another actor while auto-merge was armed**, and because arming had been treated as "done", the only record of four preservation snapshots went with it and had to be reconstructed as #152. (b) Three branches (`claude/organize-local-worktree-d22bc3`, `claude/root-dir-coverage-gate-v2`, `claude/capture-session-followups`) received pushes from a Cursor Agent and a Codex session mid-task, producing repeated non-fast-forward rejections; one rejection was masked because the push was piped to `tail`, so the reported exit code was `tail`'s and the push looked successful. (c) `scripts/guard-push.mjs` correctly refused a push with `Pushing now races the squash-merge and can orphan this commit`, requiring disarm to push then re-arm. (d) Ledger ids were renumbered underneath in-flight work (#135 to #141 to #144), which is the mechanism behind #154. **New evidence 2026-08-07 (Track A1, PR #1678) — the sharpest form yet:** a concurrent session in the same worktree ran a commit BETWEEN this session's `git add -A` and its `git commit`, sweeping this session's staged index into a foreign commit `e422624f4` "Consolidate tailwind and UI fixes" mixed with unrelated mode-nav work, then created `claude/consolidated-ui-fixes`, pointed the in-use branch `claude/tailwind-merge-cn` at it too, and checked the new branch out — moving the worktree's HEAD off the branch being worked on. `git commit` reported "nothing to commit, working tree clean" and exit 1; only reading `git log -1` revealed the commit was not this session's. That foreign commit was built on the stale base `dfcbe2039`, so relative to `origin/main` it would revert PRs #1674 and #1675. Recovery was non-destructive: a fresh worktree off current `origin/main`, `git checkout e422624f4 -- `, re-verify, commit. **Next:** for a green ledger-only PR prefer merging it immediately over arming auto-merge; confirm a push landed with `git ls-remote` rather than the command's exit code; expect ids and row wording to move between reading and writing; after any `git commit`, verify `git log -1` is YOUR message and `git rev-parse --abbrev-ref HEAD` is still YOUR branch. **Stop:** do not treat auto-merge as completion, do not assume a branch you pushed an hour ago still has your commit at its tip, and never resolve a collision by resetting or re-pointing another session's branch. | session 2026-07-30/31; PRs #1490, #1508, #1511 | 2026-07-31 | -| #159 | P2 | rec | Lists naming test files are duplicated, and the stale copy fails by running nothing | **Outcome:** no gate, plan or config names a set of test files in a second place without being derived from the filesystem or asserted against it. **Detail:** four instances found on 2026-07-30, each failing the same way — not red, but silently running nothing: (1) `mockupPatterns` in `scripts/ci-change-scope.mjs` matched `-mockups.tsx` only, missing eight singular `-mockup.tsx` components and three of the five `@mockup` specs, so the advisory lane never started for them (PR #1453); (2) `scripts/phone-chrome-plan.mjs` grepped four ownership journeys out of a filename they had moved out of, selecting zero tests; (3) nothing under `tests/helpers/**` was in `uiPatterns`, so editing a shared Playwright fixture reported `ui_changed=false` and `Production UI` skipped entirely — `zero-touch.ts` alone backs six required specs, and this one predated the PR that found it; (4) a helper-only change ran four grepped cases in one of that helper's three consumers (Codex, PR #1466). Each is now guarded individually — `assertMockupSpecParity`, `tests/playwright-project-isolation.test.ts`, and the consumer-list parity test in `tests/verify-phone-chrome.test.ts` — but the general rule is enforced nowhere, so a fifth instance is a matter of time. **Next:** decide whether one gate can assert the class (any array or regex naming `tests/` paths is checked against `readdirSync`), or whether per-site guards are the honest ceiling. **Stop:** do not treat "CI would still have caught it" as a defence — in (3) CI did not, and in (2) and (4) the local gate the repo tells you to run reported green. | PR #1453; PR #1466; session 2026-07-30 | 2026-07-30 | | #162 | P2 | task | Redesign Tools search results state (Compact Results Instrument) | **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 | | #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 | @@ -253,7 +245,7 @@ removed after current-main verification; it is not missing recommended work. | #207 | P1 | task | DS V2 PR 13 blocker: AnswerState has no ungrounded-answer channel | answerStateFromRetrieval() maps a grounded:false / confidence:'unsupported' answer over current sources to 'ready'. The live product already gates on grounded/confidence/unverifiedNumericTokens (evidence-panels.tsx, answer-thread-turn.tsx) to show 'Review source match', so adopting AnswerCard as-is would silently retire a warning shipped today. Needs a fifth state or companion flag; wording is a clinical-owner decision. | clinical-governance-reviewer P1-2 on PR 6 (claude/ds-v2-answer-safety); recorded in docs/design-system/SPEC.md PR 6 clinical review note and COMPONENTS.md 9.13 | 2026-08-02 | | #208 | P2 | task | DS V2: answerClipboardText must not replace formatAnswerRenderCopyText in PR 13 | PR 6 strengthened answerClipboardText (unconditional attribution + verify line, enumerated sources, provenance suppressed where it would contradict the caveat). It is still narrower than formatAnswerRenderCopyText (src/lib/answer-render-policy.ts), which carries the render policy's own warnings. PR 13 must compose the two or extend answerClipboardText with clinical-owner review, never swap it in. | clinical-governance-reviewer P1-1 on PR 6; recorded in docs/design-system/SPEC.md and COMPONENTS.md 9.13 | 2026-08-02 | | #209 | P3 | task | DS V2 Gate 1: add contrast pair for --warning used as body text | 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 | Restore the npm run typecheck gate | Clear stale .next generated types or update validator references so npm run typecheck reflects source health. Evidence: 14 errors in .next/dev/types/validator.ts referencing removed mockup pages; source-only typecheck is clean. See docs/review-findings-2026-08-02.md sections 2.1 and 9. WIDER THAN TYPECHECK (session 2026-08-06, PR #1647): the trigger is running npm run ensure, which makes the dev server generate .next/dev/types/validator.ts; tsconfig.json includes .next/dev/types/**/*.ts, so that file breaks BOTH repo-wide npm run typecheck AND every Playwright production build, because scripts/run-playwright.mjs writes an isolated tsconfig that extends the root one. Observed as 'Type error: Cannot find name __IsExpected' aborting the isolated build; rm -rf .next/dev restored both. This bites anyone following the documented 'run npm run ensure before browser work' instruction. Next: give the generated Playwright tsconfig its own include rather than inheriting the root include, or stop the dev server writing types into a path the production typecheck reads. | session 2026-08-02 /ledger sweep; docs/review-findings-2026-08-02.md | 2026-08-02 | +| #210 | P2 | task | npm run ensure generates .next/dev types that break typecheck and every Playwright build | RETITLED AND RE-SCOPED 2026-08-12 — the original title 'Restore the npm run typecheck gate' is wrong and cost this row its clarity: the gate was never missing. `npm run typecheck` IS in `verify:cheap:internal` (package.json:71) and runs today. The live defect is the generated-types include: tsconfig.json:28 still lists `.next/dev/types/**/*.ts` in `include`, so running `npm run ensure` — which the repo's own docs tell you to do before any browser work — makes the dev server write .next/dev/types/validator.ts, and that file then breaks BOTH repo-wide `npm run typecheck` AND every Playwright production build, because scripts/run-playwright.mjs writes an isolated tsconfig extending the root one ('Type error: Cannot find name __IsExpected'). `rm -rf .next/dev` restores both. Next: stop including dev-generated types in the checked project (drop `.next/dev/types/**/*.ts` from include, or give the Playwright isolated tsconfig its own include list), then confirm typecheck stays clean after `npm run ensure`. Stop: do not 'fix' this by removing typecheck from the gate — the gate is not the problem. | session 2026-08-02 /ledger sweep; docs/review-findings-2026-08-02.md | 2026-08-02 | | #211 | P2 | task | Plan and start the noUncheckedIndexedAccess migration | 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 | | #212 | P2 | task | Replace as unknown as casts and unvalidated JSON.parse with Zod or runtime guards | 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 | 51 call sites use response.json().catch(() => ({})) or void fetch(...).catch(() => undefined), hiding API, JSON, auth, telemetry and stream failures. Add explicit response.ok checks and a typed safeFetch wrapper that preserves the failure signal for telemetry and user feedback. See docs/review-findings-2026-08-02.md sections 3.1-3.4 and 8. | session 2026-08-02 /ledger sweep — docs/review-findings-2026-08-02.md | 2026-08-02 | @@ -283,32 +275,25 @@ removed after current-main verification; it is not missing recommended work. | #251 | P3 | rec | Handoff checklist should pair gates skill with verification-router | **Outcome:** every PR handoff picks the smallest correct gate and pastes the decisive proof line, using verification-router when scope is unclear. **Next:** add one line to handoff/gates productivity defaults: after flightplan, run verification-router (or gates) before claiming green; never report exit 0 alone. **Stop:** do not stack verify:cheap + verify:ui + verify:release by default. | session 2026-08-05 fastest-wins plan | 2026-08-05 | | #253 | P3 | task | #1606 needs a hand-merge against merged PR #1615, not a rebase | SUPERSEDED IN PART 2026-08-07: the component both PRs rewrite no longer exists. `MobileResultFilterControl` — the native `` whose system-blue selected-value highlight #1606 set out to fix — was deleted when every mode moved to `ResultFilterTrigger` + `ResultFilterSheet` (#247). The blue-highlight defect is therefore gone by removal rather than by #1606's portal menu, and #1615's iOS 16px anti-zoom rule no longer applies to any phone results filter (it still governs other native selects; see search-chrome-behaviour rule 5). What is left of this row is a decision, not a merge: #1606 is still open and cannot be rebased onto a function that was deleted. Recommend closing it as superseded rather than hand-merging — but that is a human call, and this row stays open until someone makes it. On keyboard parity, correcting an earlier version of this row that claimed otherwise: `ResultFilterSheet` does NOT use native radio inputs. It is `role="radiogroup"` over buttons with a bespoke roving-tabIndex handler — one tab stop per group, Arrow/Home/End moving focus, selection following except onto a dead end — modelled on `SegmentedControl`. That is the same shape #1606 hand-rolled, so parity is plausible but not free; the DOM tests in `tests/search-results-header-band.dom.test.tsx` pin it, and a real-device pass is still worth doing before closing #1606. The Codex P2 about ArrowDown focusing a disabled placeholder is answered by construction: arrowing onto a dead end moves focus so its note is announced but never commits it, asserted by "puts a dead end on the arrow path without ever selecting it". | session 2026-08-05 open-PR review sweep; PR #1606 (open, dirty, verified live); Codex review thread on search-results-header-band.tsx:696 | 2026-08-05 | | #254 | P2 | issue | PR #1617's Codex Cloud acceptance evidence is pinned to a head the branch has moved past | PR #1617 is the surviving side of the Codex Cloud MCP dispute (PR #1613 was closed against it on 2026-08-05), and its whole case rests on one first-hand measurement: a fresh Codex Cloud task reporting 'no Railway tool exposed', 'no Supabase tool exposed', 'no GitHub tool exposed' — zero provider tools from repo-written MCP config — while the hosted OAuth path passed in ChatGPT. That evidence is recorded in docs/audit/cloud-connection-acceptance-2026-08-05.md against task head 76dfe85fa93787b3845d0bd460aa18ff753ca2ca, and the PR body states 'The final branch head is 7d485f88db391cc7e8e73c57ddbde61f532375fc'. The branch head is now bf7684a77ef5fa5d53a1e7891cb7f446d4741e4a, so both pins are stale and the acceptance no longer describes what would merge. This matters more than usual because the acceptance IS the argument: #1613 was closed on the strength of it, and its own merge gate is the kind of exact-head check that a moved head invalidates. Next action: before #1617 leaves draft, re-run the fresh Codex Cloud acceptance at the exact current head and update both the report and the body pin, or state explicitly which parts of the evidence are head-independent (the connector-page absence of Railway/Supabase is a product-surface fact and does not depend on the head; the zero-tool discovery result does). Stop: do not treat the 76dfe85f/7d485f88 evidence as covering the current head, and do not reopen #1613 on the basis that #1617's evidence is stale — staleness is not refutation. Renumbered from this PR's original #251 → #254 because main claimed #249–#251 via PR #1624. | session 2026-08-05 open-PR review sweep; PR #1617 body and docs/audit/cloud-connection-acceptance-2026-08-05.md | 2026-08-05 | | #257 | P3 | issue | Single unreproduced ui-formulation flake: keeps specifier and formulation route families clinically separate | Observed once on 2026-08-06 at PR #1647 head f5833acc, running tests/ui-formulation.spec.ts + tests/ui-specifiers.spec.ts together against local Chromium (1 failed, 11 passed). Did NOT reproduce: passed in isolation with --grep, and passed again on a full-file re-run (7/7). Recorded only so a second sighting is recognisable as a second rather than looking like a first. Per docs/testing.md this is one reproduction of three — do NOT quarantine, and do not weaken the assertion. Next: no action unless it recurs; if a second reproduction lands on the same SHA, note it here, and only on a third open a tests/flake-ledger.json entry with @quarantine and a <=30-day expiry. | session 2026-08-06; PR #1647 | 2026-08-06 | From 85b154cf30d1f61661f454b1d72b92f15fa030c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 09:54:49 +0000 Subject: [PATCH 08/13] =?UTF-8?q?docs(issues):=20round=202=20=E2=80=94=20a?= =?UTF-8?q?rchive=20three=20delivered=20answer-surface=20rows,=20re-scope?= =?UTF-8?q?=20the=20wave=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Archived with source evidence: - #166 the verify-before-use caveat ships (verification-notice.tsx:52, rendered unconditionally by AnswerCard at answer-card.tsx:108). I reported this row as open twice before reading the code; the archive says so. - #208 buildAnswerClipboardText composes renderCopyText rather than replacing formatAnswerRenderCopyText (answer-copy-payload.ts:121-138). - #216 AnswerCard is adopted on the answer surface and the manifest agrees. Re-scoped #250: its wave plan names A1 rows that are now closed and hygiene rows that are in flight, so as written it misdirects. Recorded what actually remains (#147, #117, #118, #098, #189). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017paT42ZVMf8jaLtkjFxdy5 --- docs/outstanding-issues.md | 123 ++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 63 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index f6308162f2..e6c0956cd8 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -91,65 +91,62 @@ removed after current-main verification; it is not missing recommended work. | 36 | `#152` | A2 | High — worktree hygiene | Next cleanup batch with #079 | 1–2 hours | Uncommitted work sits in worktrees whose branches are already merged — work that exists in no branch and no PR is either committed or knowingly discarded, not lost to a disk reclaim. | | 37 | `#155` | A2 | High — agent process | Standing rule; next multi-agent session | process change | Several agent sessions edit the same branch and ledger concurrently — concurrent sessions stop silently undoing each other on shared `claude/*` branches and on this file. | | 38 | `#165` | A2 | High — clinical UI | Next answer-home UX pass | 0.5–1 day | Adopt a consolidated answer-home notice block — the studies exist, nothing adopts them — the answer hero states its safety obligation, its scope, and its verification requirement as one block in one voice. | -| 39 | `#166` | A2 | High — clinical safety UI | With #165 or next clinical chrome pass | 2–4 hours | Answer mode ships no verify-before-use caveat; every other clinical mode does — the surface that actually generates prose from retrieved sources says so, and says it must be checked. | -| 40 | `#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. | -| 41 | `#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. | -| 42 | `#170` | A2 | High — phone UI | Next documents/filter phone pass | 0.5–1 day | Documents and therapy already have page-owned phone filter sheets; remaining modes still use inline controls — shared-band Filter+Sheet adoption without regressing those two or sheetless Sort. | -| 43 | `#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. | -| 44 | `#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… | -| 45 | `#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… | -| 46 | `#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… | -| 47 | `#101` | A3 | Specialist — RAG/retrieval | After #186 update + canary approval | canary-gated | Canary-gated retrieval parallelisation candidates — independent retrieval stages stop running serially, proven by a live canary pair. Candidates: metadata/memory/visual hydration triples repeated on four branches (`rag.… | -| 48 | `#142` | Optional | High — docs hygiene | Next docs filing pass | 1–2 hours | Four loose dated docs need source and migration edits before they can be filed — every dated point-in-time doc lives in `docs/audit/` or `docs/archive/` as `docs/README.md` requires, not loose at the `docs/` top level. | -| 49 | `#151` | Optional | Operator — GitHub PAT | When writing the durable note (#187) | 15–30 min | `gh pr checks` cannot read CI, but the Actions API can — nobody concludes CI is unverifiable when it is merely reached through a different endpoint. | -| 50 | `#154` | Optional | High — agent process | When writing the durable note (#187) | 15–30 min | Row ids are not stable identifiers for "did my change land" — an agent confirms work reached `main` by content, never by id, title or PR state. | -| 51 | `#156` | A3 | High — ledger architecture | With #168 id-scheme work | design first | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge — two branches cannot silently claim the same outstanding-issues id, and no merge path can commit a file where they have. | -| 52 | `#177` | A3 | High — therapy catalogue build | Next therapy-index build change | 1–2 hours | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file — the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. | -| 53 | `#180` | A3 | High — therapy catalogue build | With #177/#179 | 1–2 hours | build-therapies-index now overwrites its own source input — the therapy catalogue generator has a source it does not also destroy. | -| 54 | `#188` | A3 | Operator — DR/SRE | After any schema restore drill, or next DR review | checklist-owned | Document and track disaster-recovery re-creation checklist as ledger work — the five DR items that do not survive a schema restore are tracked with owners and verify steps, not only in `docs/operator-backlog.md`. | -| 55 | `#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. | -| 56 | `#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. | -| 57 | `#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. | -| 58 | `#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. | -| 59 | `#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. | -| 60 | `#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. | -| 61 | `#196` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-create pg_cron schedules after schema restore — ingestion/retention and related pg_cron schedules exist on the target DB after any schema restore. | -| 62 | `#197` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-add Vault secrets including cron_ingestion_jwt — required Vault secrets (at least `cron_ingestion_jwt`) are present after schema restore. | -| 63 | `#198` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-set custom database GUCs after schema restore — custom `app.*` GUCs required by the app/worker are set on the restored database. | -| 64 | `#199` | A3 | Operator — DR/SRE | After schema restore; Deno v2 available | 1–2 hours | DR: Redeploy Supabase edge functions (Deno v2.x) — required edge functions are deployed to the target project with Deno v2.x. | -| 65 | `#200` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-enter dashboard config after schema restore — auth providers/SSO redirect URLs, connection-pool caps, per-project keys, and `E2E_USER_*` are re-entered in the Supabase/Railway dashboards after restore. | -| 66 | `#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. | -| 67 | `#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. | -| 68 | `#208` | A2 | Specialist — clinical copy | With answer clipboard / PR-13 work | 1–2 hours | `answerClipboardText` must not replace `formatAnswerRenderCopyText` — compose render-policy warnings. **Gate:** focused clipboard/copy tests. **Stop:** do not drop render-policy caveats. | -| 69 | `#216` | A2 | High — design-system answer shell | After `#207` and clinical surface decision | 0.5–1 day | Adopt AnswerCard on the answer surface (deferred from PR-J). Own PR, own `verify:ui`. **Stop:** not before `#207`; show both surface treatments before choosing. | -| 70 | `#232` | A2 | High — review ledger hygiene | Next ledger touch for PR-J | 30–60 min | Supersede the PR-J clinical-governance ledger row so it describes the merged head (`ledger:append --supersede`). **Stop:** append-only — never edit/delete the old row. | -| 71 | `#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. | -| 72 | `#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. | -| 73 | `#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. | -| 74 | `#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. | -| 75 | `#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. | -| 76 | `#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. | -| 77 | `#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. | -| 78 | `#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. | -| 79 | `#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. | -| 80 | `#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. | -| 81 | `#236` | Optional | High — branch hygiene | Next cleanup batch with `#079` | 30–60 min | Dispose orphan DS V2 builder branches and leftover wave-5 dev servers. **Stop:** content-verify before delete; no force-clean. | -| 82 | `#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. | -| 83 | `#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. | -| 84 | `#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. | -| 85 | `#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. | -| 86 | `#241` | A3 | High — therapy catalogue | Standing; with any therapy-home change | 15–30 min | Therapy home summary count/slugs remain build-time; keep `build-therapies-index --check` load-bearing. **Stop:** do not bypass the check. | -| 87 | `#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. | -| 88 | `#244` | A3 | High — design tokens / forced-colors | With any ckb-v2 forced-colours edit | 15–30 min | Keep grouped dark selectors in the forced-colours media block so specificity matches dark rules. **Stop:** do not trim to a single `.ckb-v2.ckb-v2` selector. | -| 89 | `#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. | -| 90 | `#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. | -| 91 | `#249` | A3 | High — agent process | Next issues-skill / plan touch | 1–2 hours | Extend issues/plan with an agent-safe wins classifier (optional filter; no new skill unless reused thrice). **Stop:** do not outrank A1 operator work. | -| 92 | `#250` | A2 | High — multi-agent execution | After Wave 0 queue repair on main (done in this capture); run remaining Wave 0/#202 process gates next on the engineering track | multi-wave | Execute the fastest-wins multi-wave plan (Waves 0–4 + operator track) with parallel agents and per-PR gates. Waves do not outrank A1 acuity. **Stop:** provider/RAG approvals still required where flagged. | -| 93 | `#251` | Optional | High — agent process | Next handoff/gates doc touch | 15–30 min | Handoff checklist pairs gates skill with verification-router; paste decisive proof line. **Stop:** do not stack broad gates by default. | -| 94 | `#253` | A2 | High — phone results UI | Next open-PR sweep | 15–30 min | Decide #1606's fate: the `MobileResultFilterControl` it rewrites was deleted by #247, so there is nothing left to hand-merge. Verify keyboard parity of the replacement sheet on a real device, then close #1606 as superseded. **Stop:** the decision is a human's; do not close #1606 automatically. | -| 95 | `#254` | A2 | Operator — Codex Cloud | Before #1617 leaves draft | 1–2 hours | Re-run Codex Cloud acceptance at the exact current head or mark head-independent evidence. **Stop:** do not treat stale pins as coverage. | -| 96 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | -| 97 | `#289` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | +| 39 | `#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. | +| 40 | `#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. | +| 41 | `#170` | A2 | High — phone UI | Next documents/filter phone pass | 0.5–1 day | Documents and therapy already have page-owned phone filter sheets; remaining modes still use inline controls — shared-band Filter+Sheet adoption without regressing those two or sheetless Sort. | +| 42 | `#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. | +| 43 | `#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… | +| 44 | `#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… | +| 45 | `#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… | +| 46 | `#101` | A3 | Specialist — RAG/retrieval | After #186 update + canary approval | canary-gated | Canary-gated retrieval parallelisation candidates — independent retrieval stages stop running serially, proven by a live canary pair. Candidates: metadata/memory/visual hydration triples repeated on four branches (`rag.… | +| 47 | `#142` | Optional | High — docs hygiene | Next docs filing pass | 1–2 hours | Four loose dated docs need source and migration edits before they can be filed — every dated point-in-time doc lives in `docs/audit/` or `docs/archive/` as `docs/README.md` requires, not loose at the `docs/` top level. | +| 48 | `#151` | Optional | Operator — GitHub PAT | When writing the durable note (#187) | 15–30 min | `gh pr checks` cannot read CI, but the Actions API can — nobody concludes CI is unverifiable when it is merely reached through a different endpoint. | +| 49 | `#154` | Optional | High — agent process | When writing the durable note (#187) | 15–30 min | Row ids are not stable identifiers for "did my change land" — an agent confirms work reached `main` by content, never by id, title or PR state. | +| 50 | `#156` | A3 | High — ledger architecture | With #168 id-scheme work | design first | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge — two branches cannot silently claim the same outstanding-issues id, and no merge path can commit a file where they have. | +| 51 | `#177` | A3 | High — therapy catalogue build | Next therapy-index build change | 1–2 hours | Therapy catalogue aliases duplicate 2.53 MB of bytes instead of pointing at the hashed file — the unversioned catalogue aliases stop costing a second copy of every payload in the repo and the image. | +| 52 | `#180` | A3 | High — therapy catalogue build | With #177/#179 | 1–2 hours | build-therapies-index now overwrites its own source input — the therapy catalogue generator has a source it does not also destroy. | +| 53 | `#188` | A3 | Operator — DR/SRE | After any schema restore drill, or next DR review | checklist-owned | Document and track disaster-recovery re-creation checklist as ledger work — the five DR items that do not survive a schema restore are tracked with owners and verify steps, not only in `docs/operator-backlog.md`. | +| 54 | `#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. | +| 55 | `#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. | +| 56 | `#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. | +| 57 | `#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. | +| 58 | `#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. | +| 59 | `#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. | +| 60 | `#196` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-create pg_cron schedules after schema restore — ingestion/retention and related pg_cron schedules exist on the target DB after any schema restore. | +| 61 | `#197` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-add Vault secrets including cron_ingestion_jwt — required Vault secrets (at least `cron_ingestion_jwt`) are present after schema restore. | +| 62 | `#198` | A3 | Operator — DR/SRE | After schema restore drill | 30–60 min | DR: Re-set custom database GUCs after schema restore — custom `app.*` GUCs required by the app/worker are set on the restored database. | +| 63 | `#199` | A3 | Operator — DR/SRE | After schema restore; Deno v2 available | 1–2 hours | DR: Redeploy Supabase edge functions (Deno v2.x) — required edge functions are deployed to the target project with Deno v2.x. | +| 64 | `#200` | A3 | Operator — DR/SRE | After schema restore drill | 1–2 hours | DR: Re-enter dashboard config after schema restore — auth providers/SSO redirect URLs, connection-pool caps, per-project keys, and `E2E_USER_*` are re-entered in the Supabase/Railway dashboards after restore. | +| 65 | `#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. | +| 66 | `#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. | +| 67 | `#232` | A2 | High — review ledger hygiene | Next ledger touch for PR-J | 30–60 min | Supersede the PR-J clinical-governance ledger row so it describes the merged head (`ledger:append --supersede`). **Stop:** append-only — never edit/delete the old row. | +| 68 | `#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. | +| 69 | `#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. | +| 70 | `#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. | +| 71 | `#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. | +| 72 | `#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. | +| 73 | `#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. | +| 74 | `#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. | +| 75 | `#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. | +| 76 | `#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. | +| 77 | `#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. | +| 78 | `#236` | Optional | High — branch hygiene | Next cleanup batch with `#079` | 30–60 min | Dispose orphan DS V2 builder branches and leftover wave-5 dev servers. **Stop:** content-verify before delete; no force-clean. | +| 79 | `#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. | +| 80 | `#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. | +| 81 | `#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. | +| 82 | `#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. | +| 83 | `#241` | A3 | High — therapy catalogue | Standing; with any therapy-home change | 15–30 min | Therapy home summary count/slugs remain build-time; keep `build-therapies-index --check` load-bearing. **Stop:** do not bypass the check. | +| 84 | `#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. | +| 85 | `#244` | A3 | High — design tokens / forced-colors | With any ckb-v2 forced-colours edit | 15–30 min | Keep grouped dark selectors in the forced-colours media block so specificity matches dark rules. **Stop:** do not trim to a single `.ckb-v2.ckb-v2` selector. | +| 86 | `#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. | +| 87 | `#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. | +| 88 | `#249` | A3 | High — agent process | Next issues-skill / plan touch | 1–2 hours | Extend issues/plan with an agent-safe wins classifier (optional filter; no new skill unless reused thrice). **Stop:** do not outrank A1 operator work. | +| 89 | `#250` | A2 | High — multi-agent execution | After Wave 0 queue repair on main (done in this capture); run remaining Wave 0/#202 process gates next on the engineering track | multi-wave | Execute the fastest-wins multi-wave plan (Waves 0–4 + operator track) with parallel agents and per-PR gates. Waves do not outrank A1 acuity. **Stop:** provider/RAG approvals still required where flagged. | +| 90 | `#251` | Optional | High — agent process | Next handoff/gates doc touch | 15–30 min | Handoff checklist pairs gates skill with verification-router; paste decisive proof line. **Stop:** do not stack broad gates by default. | +| 91 | `#253` | A2 | High — phone results UI | Next open-PR sweep | 15–30 min | Decide #1606's fate: the `MobileResultFilterControl` it rewrites was deleted by #247, so there is nothing left to hand-merge. Verify keyboard parity of the replacement sheet on a real device, then close #1606 as superseded. **Stop:** the decision is a human's; do not close #1606 automatically. | +| 92 | `#254` | A2 | Operator — Codex Cloud | Before #1617 leaves draft | 1–2 hours | Re-run Codex Cloud acceptance at the exact current head or mark head-independent evidence. **Stop:** do not treat stale pins as coverage. | +| 93 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | +| 94 | `#289` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | @@ -211,7 +208,6 @@ removed after current-main verification; it is not missing recommended work. | #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 | -| #166 | P2 | issue | Answer mode ships no verify-before-use caveat; every other clinical mode does | **Outcome:** the surface that actually generates prose from retrieved sources says so, and says it must be checked. **Detail:** differentials carry "Clinical decision support only. Review before use.", prescribing carries "Confirm against source", specifiers carry a confirm-the-manual line, and calculators carry "Scores support clinical judgement — they never replace a full assessment." The answer hero carries neither an equivalent nor anything about generation: only the APP-5 privacy line and "Searches indexed clinical sources", which reads as assurance rather than caution. `CLAUDE.md` calls this repo a clinical reference prototype and explicitly **not** validated clinical decision support, so the one mode that synthesises text is the one most needing the caveat. Proposed wording, matching the registers above rather than opening a new one: "Answers are AI-generated — verify against the cited source before clinical use." **Independent of `#165`:** even keeping today's three-notice layout, the missing sentence is the gap. **Next:** clinical-governance sign-off on the exact wording, then add it to the answer hero (bundled with `#165` if that lands first). | session 2026-07-30; PR #1437; `src/components/clinical-dashboard/answer-status.tsx` | 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 | | #169 | P2 | issue | Local branches carry work that exists on no remote | **Outcome:** committed work is not lost when a machine or worktree is reclaimed. **Detail 2026-07-31:** six `claude/*` branches in this checkout have commits and no `origin/` counterpart. Verified real for `claude/clinical-kb-design-system-333a69` — 57 files / +4069, tip `feat(design-system): v2 token layer, 26 components, browser-crash fix` dated 2026-07-31 17:40, whose added `.design-sync/previews/*.tsx` files are absent from main. Others unverified: `design-sync-db0a54`, `fable-implementation-fc937c`, `frosty-mayer-2c6167`, `issues-133-evidence`. **How to check, because the obvious measure lies:** `git rev-list --count origin/main..` and a three-dot diff both report landed work as unmerged, since this repo squash-merges and the original commits never become ancestors — my own merged branch reported 1 commit and +476 by that measure. Test instead whether files the branch adds exist on main (`git ls-tree origin/main `). **Next:** per branch, push it for review or confirm it is superseded and delete it; do not bulk-delete on the commit count. Sibling of #152, which covers uncommitted work in worktrees rather than unpushed commits on branches. NOT VERIFIABLE FROM A CLOUD SESSION (checked 2026-08-12): this row describes machine-local git or process state. Claude Code on the web runs in a container cloned fresh at session start, so it reports 1 worktree, 0 upstream-less branches and no running dev servers no matter what is true on the machine that raised this. Do NOT close this row from a cloud session — that reading is an artifact of the container, not evidence. Verify from the originating machine. | session 2026-07-31; local branch audit | 2026-07-31 | | #170 | P2 | task | Phone filter sheets exist for documents and therapy; shared-band adoption remains | **Outcome:** phone filtering opens the repo's own bottom sheet instead of competing for width in the utilities rail. **Current state (2026-08-04):** documents already mounts a Filter documents Sheet, and therapy-compass mounts TherapyFilterSheet; both also pass appliedFilters into SearchResultsHeaderBand. The remaining modes still keep filter controls inline (or page-owned chips/nav), and the shared band has not yet adopted a Filter trigger + Sheet that wraps mobileControls for every mode. **Detail:** SearchResultsHeaderBand still renders mobileControls inline below sm for pages that supply them that way. src/components/ui/sheet.tsx already provides the primitive — bottom sheet on mobile via sheet-up, centred dialog at sm+, safe-area aware, focus-trapped, Escape and backdrop dismiss — so shared-band adoption is the remaining work, not new UI. Design settled in the round-7 study: one Filter control at both widths, badge counting applied filters, sheet titled Filter and sort because a phone bar cannot fit a labelled Sort beside Filter at 390 px. **Next:** replace the inline mobileControls render with a Filter trigger + Sheet containing mobileControls ?? filterControls for the modes that still lack a page-owned sheet, without regressing documents/therapy sheets or removing Sort from sheetless Sort consumers (differentials, forms, services). **Stop:** at least four Playwright specs drive those inline controls directly (document-source-type-select, search-query-ribbon-mobile-control-pair); budget for updating them, and do not ship without verify:ui — PR #1523 showed Production UI critical does gate this surface properly. | Round-7 design study; PR #1523 notes; `src/components/ui/sheet.tsx` | 2026-07-31 | @@ -237,14 +233,12 @@ removed after current-main verification; it is not missing recommended work. | #199 | P3 | task | DR: Redeploy Supabase edge functions (Deno v2.x) | **Outcome:** required edge functions are deployed to the target project with Deno v2.x. **Next:** operator deploy after restore; confirm function list/health. Parent `#188`. **Stop:** needs Deno toolchain and explicit approval for hosted deploy. | docs/operator-backlog.md; #188 | 2026-07-31 | | #200 | P3 | task | DR: Re-enter dashboard config after schema restore | **Outcome:** auth providers/SSO redirect URLs, connection-pool caps, per-project keys, and `E2E_USER_*` are re-entered in the Supabase/Railway dashboards after restore. **Next:** operator checklist in `docs/operator-backlog.md`. Parent `#188`. **Stop:** do not commit dashboard secrets. | docs/operator-backlog.md; #188 | 2026-07-31 | | #206 | P2 | task | AnswerState partial_retrieval has no app-facing producer | 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 | -| #208 | P2 | task | DS V2: answerClipboardText must not replace formatAnswerRenderCopyText in PR 13 | PR 6 strengthened answerClipboardText (unconditional attribution + verify line, enumerated sources, provenance suppressed where it would contradict the caveat). It is still narrower than formatAnswerRenderCopyText (src/lib/answer-render-policy.ts), which carries the render policy's own warnings. PR 13 must compose the two or extend answerClipboardText with clinical-owner review, never swap it in. | clinical-governance-reviewer P1-1 on PR 6; recorded in docs/design-system/SPEC.md and COMPONENTS.md 9.13 | 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 | RETITLED AND RE-SCOPED 2026-08-12 — the original title 'Restore the npm run typecheck gate' is wrong and cost this row its clarity: the gate was never missing. `npm run typecheck` IS in `verify:cheap:internal` (package.json:71) and runs today. The live defect is the generated-types include: tsconfig.json:28 still lists `.next/dev/types/**/*.ts` in `include`, so running `npm run ensure` — which the repo's own docs tell you to do before any browser work — makes the dev server write .next/dev/types/validator.ts, and that file then breaks BOTH repo-wide `npm run typecheck` AND every Playwright production build, because scripts/run-playwright.mjs writes an isolated tsconfig extending the root one ('Type error: Cannot find name __IsExpected'). `rm -rf .next/dev` restores both. Next: stop including dev-generated types in the checked project (drop `.next/dev/types/**/*.ts` from include, or give the Playwright isolated tsconfig its own include list), then confirm typecheck stays clean after `npm run ensure`. Stop: do not 'fix' this by removing typecheck from the gate — the gate is not the problem. | session 2026-08-02 /ledger sweep; docs/review-findings-2026-08-02.md | 2026-08-02 | | #211 | P2 | task | Plan and start the noUncheckedIndexedAccess migration | 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 | | #212 | P2 | task | Replace as unknown as casts and unvalidated JSON.parse with Zod or runtime guards | 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 | image-lightbox.tsx and pwa-lifecycle.tsx lack decoding=async; public/demo-documents/*.png are ~80 KB each and not served in WebP/AVIF; SignedImage lacks a priority prop for above-fold evidence images. Apply decoding=async, add an optional priority prop, and convert demo PNGs with a PNG fallback. Most performance image findings are already tracked under #016, #013, #117 and #147; this row captures the new image-only observations from the 2026-08-02 audit. | session 2026-08-02 /ledger sweep — docs/audit/performance-image-cwv-audit-2026-08-02.md | 2026-08-02 | -| #216 | P2 | task | Adopt the AnswerCard container on the answer surface (deferred from PR-J) | PR-J adopted the answer safety components (VerificationNotice, RetrievalStateBanner, AnswerState projection, composed clipboard) but NOT AnswerCard itself, so it stays at zero product imports and PR 13's answer surface is adopted in substance, not in shell. The swap replaces the answerSurface wrapper with AnswerCard's article, its query echo with UserQuestionBubble, wraps NaturalLanguageAnswer in its --measure-clamped prose div, maps copy/feedback/follow-up onto its structured actions array, and adds AnswerFooter (needs publisher/version/reviewDate/generatedAt threaded). Deferred deliberately: two competing surface treatments must be resolved by the design owner (SPEC 2.4 border-or-ring), the measure clamp reflows every answer and moves the phone scroll-runway pins that cost PR-V two CI cycles (finding L), and bundling it would make a red verify:ui unattributable across PR-J's other five surfaces. Reasons recorded in docs/design-system/ADOPTION.md 2.6. Next action: own PR after PR-J lands and soaks, own verify:ui pass, own glance; show the user both surface treatments before choosing. | session 2026-08-03 (PR-J Wave 5 controller) | 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 | 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 | @@ -263,7 +257,7 @@ removed after current-main verification; it is not missing recommended work. | #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 | PR #1614 repairs the symptom with a mark-applied guard only. Confirm out-of-band whether the earlier reconcile migration partially applied, indexes were manually dropped, or schema_migrations was repaired — and whether check:drift should have caught this class before runtime search_schema_health. Renumbered from this PR's original #237 → #246 because main already used #237–#247 (PR #1616 findings plus the results-bar rows from PR #1615). | PR #1614 review / session 2026-08-05 (renumbered on main merge) | 2026-08-05 | | #249 | P2 | rec | Extend issues/plan with an agent-safe wins classifier | IN FLIGHT 2026-08-12 in PR #1840 (wins classifier). 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:** /issues and recommendation answers can filter agent-safe short wins (estimate ≤4h, Capability not Operator-only, no RAG/provider, not blocked on a human decision) without inventing a 35th skill. **Next:** add an optional filter/section to the issues skill (and plan/flightplan when useful) that lists those wins from the open table + queue; keep it as procedure on the existing skills unless the classifier is reused thrice — only then mint a thin wins alias. **Stop:** do not weaken acuity ordering; operator A1s still outrank engineering wins. | session 2026-08-05 fastest-wins plan | 2026-08-05 | -| #250 | P2 | task | Execute the fastest-wins multi-wave plan (Wave 0–4) | **Outcome:** the 2026-08-04/05 fastest-wins plan is executed with parallel agents, correct gates, and no regression. **Acuity first:** the recommended queue remains acuity-ordered — A1 rows (#059/#053/#231/#207/#226) are not demoted by wave numbers. Operator-track items (#022/#183) retain their A2 priority and approval gates. **A1 track (parallel, not a wave):** run approved A1 work (#059/#053/#231/#207/#226) whenever capability/approvals allow — do not wait for Waves 0–3. **Engineering waves only:** 0 = ledger/process gates (#201 resolved 2026-08-06; continue with remaining #202 work); 1A = #147+#176 phone CLS; 1B = #149+#167+#204+#210 gate integrity; 1C = hygiene/docs #232–#236+#223+#157+#151/#154/#187; 2 = #117 then #118; 3 = #098 then #189 (defer #099 body); 4 = remaining non-A1 clinical/UI packaging once approvals exist (never a holding pen for A1 items). **Next:** continue Wave 0/#202 on a fresh branch off origin/main in parallel with any approved A1 work; use gates + verification-router per PR; respect #155 concurrency. **Stop:** no RAG behaviour without flag+canary; no provider gates without approval; do not mix operationalRisk with clinical/UI in one squash. | session 2026-08-05 fastest-wins plan | 2026-08-05 | +| #250 | P2 | task | Execute the fastest-wins multi-wave plan (Wave 0–4) | SUPERSEDED IN LARGE PART 2026-08-12 — this row is a wave plan whose contents have been overtaken, and as written it now misdirects. Rows it names as live A1 work are CLOSED: #207 and #226 were archived on main, and #166 (its sibling in that cluster) archived in the 2026-08-12 sweep. Of its engineering waves: 1B's gate-integrity set is done (#149, #210 re-scoped, #204/#167 covered by in-flight PR #1837); 1C's hygiene set is largely done or in flight (#232, #151, #154, #187, #142, #156, #186 all now carry IN FLIGHT notes against PR #1835/#1836); Wave 0/#202 is in flight in PR #1840. What genuinely remains of the plan is Wave 1A (#147 phone CLS, still open and still reproducible offline at zero provider cost), Wave 2 (#117 then #118), and Wave 3 (#098 then #189). The A1 track is now just #059, #053 and #231 — two operator rows and one live investigation. Next: either re-cut this row against that much smaller remainder, or close it and let #147/#117/#118/#098/#189 stand on their own, which is probably the honest move now that the multi-agent framing has served its purpose. Stop unchanged: no RAG behaviour change without flag plus canary, no provider gates without approval, and do not mix operationalRisk with clinical or UI in one squash. | session 2026-08-05 fastest-wins plan | 2026-08-05 | | #251 | P3 | rec | Handoff checklist should pair gates skill with verification-router | IN FLIGHT 2026-08-12 in PR #1840 (handoff checklist). 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:** every PR handoff picks the smallest correct gate and pastes the decisive proof line, using verification-router when scope is unclear. **Next:** add one line to handoff/gates productivity defaults: after flightplan, run verification-router (or gates) before claiming green; never report exit 0 alone. **Stop:** do not stack verify:cheap + verify:ui + verify:release by default. | session 2026-08-05 fastest-wins plan | 2026-08-05 | | #253 | P3 | task | #1606 needs a hand-merge against merged PR #1615, not a rebase | SUPERSEDED IN PART 2026-08-07: the component both PRs rewrite no longer exists. `MobileResultFilterControl` — the native `