From a2258eb9be748763524c7331d3bdb13d9cc8b833 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:02:04 +0800 Subject: [PATCH 1/3] chore(issues): clean ledger docs and canonical references --- .../agents/clinical-governance-reviewer.md | 2 +- AGENTS.md | 9 + SECURITY.md | 2 +- docs/README.md | 4 +- .../operator-apply-july8-batch.md | 2 +- docs/audit/audit-handover-2026-07-14.md | 16 +- .../audit-remediation-plan-2026-07-14.md | 2 +- docs/{ => audit}/capacity-review.md | 0 docs/audit/latency-audit-2026-07-28.md | 6 +- docs/{ => audit}/scale-readiness-review.md | 0 .../tenancy-defense-in-depth-review.md | 4 +- docs/auth-connection-cap-runbook.md | 8 +- docs/branch-review-ledger.md | 1 + docs/codebase-index.md | 24 +-- docs/deploy-corrector-public-titles.md | 4 +- docs/deployment-architecture.md | 8 +- docs/ingestion-concurrency-fix-workorder.md | 6 +- docs/ingestion-state-machine.md | 2 +- docs/launch-operator-runbook.md | 16 +- docs/operator-backlog.md | 16 +- docs/outstanding-issues.md | 167 +++++++++--------- docs/privacy-impact-assessment.md | 2 +- docs/process-hardening.md | 2 +- ...codex-data-database-safety-ultra-review.md | 2 +- ...ex-performance-reliability-ultra-review.md | 4 +- docs/staging-setup.md | 6 +- docs/supabase-migration-reconciliation.md | 2 +- docs/worker-deploy-runbook.md | 2 +- scripts/archive/check-july8-live-batch.ts | 2 +- scripts/check-owner-scope-api.mjs | 10 +- scripts/check-retrieval-owner-migration.ts | 2 +- scripts/soak-test.ts | 4 +- src/lib/owner-scope.ts | 2 +- ...0_ingestion_jobs_one_open_per_document.sql | 2 +- ...01_retrieval_owner_matches_fail_closed.sql | 2 +- ...0_ingestion_jobs_one_open_per_document.sql | 2 +- ...717120000_corrector_public_titles_only.sql | 2 +- ...0724120000_table_facts_plpgsql_execute.sql | 2 +- supabase/schema.sql | 2 +- tests/owner-scope-guard.test.ts | 2 +- 40 files changed, 178 insertions(+), 175 deletions(-) rename docs/{ => archive}/operator-apply-july8-batch.md (98%) rename docs/{ => audit}/capacity-review.md (100%) rename docs/{ => audit}/scale-readiness-review.md (100%) rename docs/{ => audit}/tenancy-defense-in-depth-review.md (99%) diff --git a/.claude/agents/clinical-governance-reviewer.md b/.claude/agents/clinical-governance-reviewer.md index 7b28d1a9e9..9b18134612 100644 --- a/.claude/agents/clinical-governance-reviewer.md +++ b/.claude/agents/clinical-governance-reviewer.md @@ -18,7 +18,7 @@ Follow `AGENTS.md` review throttling and `docs/codex-review-protocol.md` before - `src/lib/{source-governance,source-metadata,document-label-governance,indexed-source-formatting,source-spans,source-text-sanitizer,rag-source-block}.ts` - `src/lib/{citations,answer-verification,rag-quote-verification,rag-answer-support,answer-render-policy,clinical-safety}.ts` - `src/lib/{privacy,query-privacy}.ts`, `src/components/privacy-input-notice.tsx` -- `docs/{clinical-*,privacy-*,source-governance-*,rag-injection-*}.md`, `docs/openai-cross-border-basis.md`, `docs/tenancy-defense-in-depth-review.md` +- `docs/{clinical-*,privacy-*,source-governance-*,rag-injection-*}.md`, `docs/openai-cross-border-basis.md`, `docs/audit/tenancy-defense-in-depth-review.md` ## Provider boundary diff --git a/AGENTS.md b/AGENTS.md index 88ec727f80..99766fe27a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -810,6 +810,15 @@ named PR). Future process only. pushes; it fails explicitly on `mergeable_state: dirty`. Behind-but-clean heads still use `npm run sync:pr-branches` / `:apply` with a human `gh` identity — never bot `update-branch`. +- When `gh pr checks` cannot read check runs with the current token, query the Actions runs for the + exact head SHA instead; do not report CI as unverifiable until that read-only fallback has also + failed. +- Treat outstanding-issue IDs as display locators, not proof that work landed. Verify completion + from distinctive content and its recorded check on the exact target ref; PR state, row ID, and row + title are insufficient, especially after squash merges or concurrent renumbering. Allocate and + mutate rows only through `npm run issues:add|update|done`, never use GitHub's Update branch button + on a PR touching `docs/outstanding-issues.md`, and let the structural gate force any allocation + collision to be resolved explicitly. - Keep Playwright blocking tests at zero retries. Quarantine only after three reproductions on the same SHA via `tests/flake-ledger.json` (`@quarantine`, not `@critical`, ≤30-day expiry). Do not weaken tap targets to `min-h-11` to chase generic a11y guidance — that diff --git a/SECURITY.md b/SECURITY.md index ee64823485..ae2316b131 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -51,7 +51,7 @@ not validated clinical decision support (see `README.md` → "Clinical Safety St ## Related security documentation - `docs/rag-injection-threat-model.md` — prompt-injection threat model. -- `docs/tenancy-defense-in-depth-review.md` — multi-tenant isolation review. +- `docs/audit/tenancy-defense-in-depth-review.md` — multi-tenant isolation review. - `docs/privacy-impact-assessment.md` — privacy impact assessment and launch blockers. - `docs/clinical-hazard-analysis.md` — clinical hazard register. - `docs/openai-cross-border-basis.md` — cross-border data-processing basis. diff --git a/docs/README.md b/docs/README.md index 63bb45c5d4..39c6804c10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -132,8 +132,8 @@ as it was on that date; supersede with a new dated document rather than editing. - `release-source-metadata-debt-2026-06-30.json` — captured source-metadata debt policy, consumed by `npm run audit:source-governance:release` and `npm run eval:quality:release` - [forward-codify-retrieval-rpcs-workorder.md](forward-codify-retrieval-rpcs-workorder.md) — completed retrieval RPC codification workorder - [project-alignment-cleanup.md](archive/project-alignment-cleanup.md) — completed June 2026 repo-alignment record -- [capacity-review.md](capacity-review.md), [scale-readiness-review.md](scale-readiness-review.md), [tenancy-defense-in-depth-review.md](tenancy-defense-in-depth-review.md) -- `*-2026-*` findings and status docs, e.g. [chunking-ocr-reindex-lever-finding-2026-07-08.md](chunking-ocr-reindex-lever-finding-2026-07-08.md), [source-governance-status-2026-07-08.md](archive/source-governance-status-2026-07-08.md), [source-governance-priorities-2026-07-02.md](archive/source-governance-priorities-2026-07-02.md), [source-review-priority-2026-07-02.md](source-review-priority-2026-07-02.md), [operator-apply-july8-batch.md](operator-apply-july8-batch.md) +- [capacity-review.md](audit/capacity-review.md), [scale-readiness-review.md](audit/scale-readiness-review.md), [tenancy-defense-in-depth-review.md](audit/tenancy-defense-in-depth-review.md) +- `*-2026-*` findings and status docs, e.g. [chunking-ocr-reindex-lever-finding-2026-07-08.md](chunking-ocr-reindex-lever-finding-2026-07-08.md), [source-governance-status-2026-07-08.md](archive/source-governance-status-2026-07-08.md), [source-governance-priorities-2026-07-02.md](archive/source-governance-priorities-2026-07-02.md), [source-review-priority-2026-07-02.md](source-review-priority-2026-07-02.md), [operator-apply-july8-batch.md](archive/operator-apply-july8-batch.md) ## Archive diff --git a/docs/operator-apply-july8-batch.md b/docs/archive/operator-apply-july8-batch.md similarity index 98% rename from docs/operator-apply-july8-batch.md rename to docs/archive/operator-apply-july8-batch.md index 05ee37f721..5f4b5368b5 100644 --- a/docs/operator-apply-july8-batch.md +++ b/docs/archive/operator-apply-july8-batch.md @@ -91,5 +91,5 @@ active end-to-end. - Forward-codify live-ahead retrieval RPC bodies (`docs/database-drift-detection.md` backlog #0) - deep-memory delete scoping design (`docs/ingestion-concurrency-fix-workorder.md`) -- Staging soak (`docs/staging-setup.md`, `docs/capacity-review.md`) +- Staging soak (`docs/staging-setup.md`, `docs/audit/capacity-review.md`) - `registry:seed` per owner (`docs/process-hardening.md`) diff --git a/docs/audit/audit-handover-2026-07-14.md b/docs/audit/audit-handover-2026-07-14.md index a969d672ca..5ae5fc3542 100644 --- a/docs/audit/audit-handover-2026-07-14.md +++ b/docs/audit/audit-handover-2026-07-14.md @@ -103,13 +103,13 @@ evals, service-role tenancy regression class, upstream OCR quality labels drivin ### M6 — Single-layer tenancy (ARCHITECTURE — P1 regression class) -| | | -| ---------------------- | ----------------------------------------------------------------------------------------------------------------- | -| **What** | APIs use service role; ownership enforced in app helpers/RPCs, not full second-layer RLS for public-overlay model | -| **Where** | `createAdminClient()`, `withOwnerReadScope`, `retrieval_owner_matches`; `docs/tenancy-defense-in-depth-review.md` | -| **Risk** | Future route forgetting owner scope → private corpus leak | -| **Address** | Remediation Wave D2 — CI unscoped-query guard + live A/B | -| **Status at handover** | Mitigated in current routes; guard not yet coded | +| | | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **What** | APIs use service role; ownership enforced in app helpers/RPCs, not full second-layer RLS for public-overlay model | +| **Where** | `createAdminClient()`, `withOwnerReadScope`, `retrieval_owner_matches`; `docs/audit/tenancy-defense-in-depth-review.md` | +| **Risk** | Future route forgetting owner scope → private corpus leak | +| **Address** | Remediation Wave D2 — CI unscoped-query guard + live A/B | +| **Status at handover** | Mitigated in current routes; guard not yet coded | --- @@ -242,7 +242,7 @@ Full address sketches: Remediation Wave I in the remediation plan. | [`launch-operator-runbook.md`](../launch-operator-runbook.md) | Sequenced launch commands | | [`privacy-impact-assessment.md`](../privacy-impact-assessment.md) | PIA register | | [`openai-cross-border-basis.md`](../openai-cross-border-basis.md) | APP 8 checklist | -| [`tenancy-defense-in-depth-review.md`](../tenancy-defense-in-depth-review.md) | Tenancy defense notes | +| [`tenancy-defense-in-depth-review.md`](tenancy-defense-in-depth-review.md) | Tenancy defense notes | | [`process-hardening.md`](../process-hardening.md) | Process & known debts | | [`codex-review-protocol.md`](../codex-review-protocol.md) | Review severity / mutation rules | | [`branch-review-ledger.md`](../branch-review-ledger.md) | Prevent repeat audits | diff --git a/docs/audit/audit-remediation-plan-2026-07-14.md b/docs/audit/audit-remediation-plan-2026-07-14.md index a89dd32d1e..a22e13eb36 100644 --- a/docs/audit/audit-remediation-plan-2026-07-14.md +++ b/docs/audit/audit-remediation-plan-2026-07-14.md @@ -180,7 +180,7 @@ Suggested PR granularity (one theme per PR): | **Finding** | Service-role single layer; forgotten owner filter is a full leak class | | **Owner** | OWNER:CODE (+ OWNER:OPS for live A/B) | | **Address** | Add static CI guard grepping `src/app/api/**` for admin queries on owner-scoped tables without known helpers (`withOwnerReadScope`, `requireOwnerScope`, `.eq("owner_id"` patterns allowlisted). Document remaining intentional exceptions. Schedule `⏸` user A vs B smoke on documents + signed URLs + search. | -| **Files** | new `scripts/check-owner-scope-api.mjs` (or extend existing), `package.json` / `verify:cheap`, tests; update `docs/tenancy-defense-in-depth-review.md` | +| **Files** | new `scripts/check-owner-scope-api.mjs` (or extend existing), `package.json` / `verify:cheap`, tests; update `docs/audit/tenancy-defense-in-depth-review.md` | | **Prove** | Guard fails on a synthetic unscope fixture; live A/B green | ### D3. Auth UI: prefer validated user for privilege display (S2) diff --git a/docs/capacity-review.md b/docs/audit/capacity-review.md similarity index 100% rename from docs/capacity-review.md rename to docs/audit/capacity-review.md diff --git a/docs/audit/latency-audit-2026-07-28.md b/docs/audit/latency-audit-2026-07-28.md index 3466865e0a..1159c3611b 100644 --- a/docs/audit/latency-audit-2026-07-28.md +++ b/docs/audit/latency-audit-2026-07-28.md @@ -4,7 +4,7 @@ **Branch:** `claude/latency-audit-f1cbcd` (worktree `prompt-skill-improvements-7d5f80`), base 1 commit behind `origin/main` (`ea6d2d954`, mockups-only — no latency surface) **Method:** Three read-only reconnaissance sweeps (server request path / client-browser path / database + prior-work), then line-level verification of every load-bearing claim by the primary author. Four planned remediations were **retired during verification** because the evidence did not support them — recorded under "Retired during verification" rather than silently dropped. **Scope:** Latency only. Full server request path, client first-paint path, database/RPC surface, and the ingestion/worker path where it bounds a user-visible wait. Excludes correctness, security, and clinical-governance findings except where they _gate_ a latency fix. -**Guardrail posture:** Obeys (1) the `src/lib/rag/**` FLAG + `RAG impact:` rule and its live-canary requirement for behaviour change; (2) ledger `#017`, which gates client payload work behind measured Web-Vitals evidence; (3) `docs/capacity-review.md:123-125` explicit non-actions; (4) the provider-confirmation boundary — no OpenAI/Supabase/hosted-CI call was made. +**Guardrail posture:** Obeys (1) the `src/lib/rag/**` FLAG + `RAG impact:` rule and its live-canary requirement for behaviour change; (2) ledger `#017`, which gates client payload work behind measured Web-Vitals evidence; (3) `docs/audit/capacity-review.md:123-125` explicit non-actions; (4) the provider-confirmation boundary — no OpenAI/Supabase/hosted-CI call was made. --- @@ -124,7 +124,7 @@ measured-and-cleared · **L6** deliberate. - **Evidence.** `proxy.ts:125` awaits `supabase.auth.getClaims()` on every matched request — the matcher excludes only static assets, so **every `/api/*` call** is included. Each public API route then independently resolves identity via `getOptionalAuthenticatedUser`, and `auth.ts:161` constructs a **fresh `createServerClient` per request**, with no caching of the resolved user. - **Correction to the first-pass finding.** `proxy.ts:102-105` short-circuits when no `sb-` cookie is present, so **anonymous traffic pays nothing here**, and `getClaims()` may be local JWKS verification rather than a network call depending on the project's JWT signing algorithm. The accurate claim is _two independent identity resolutions per authenticated request, at least one of which (`getUser`) always contacts the Auth server_ — not "always two network round trips". - **Why no memo was added.** Verified: every route resolves identity **exactly once per HTTP method handler** — the multi-call files have one call per `GET`/`POST`/`PATCH`/`DELETE`, never two per request. The real duplication spans the proxy and the route handler, which are separate invocations holding **different `Request` objects**, so no in-process memo can bridge them. A memo would dedupe nothing. -- **Capacity relevance.** `docs/capacity-review.md:106-113` names the Auth tier's ~10 absolute DB connections the **first hard failure**. Halving per-request auth resolutions is a capacity lever, not only a latency one — which is why `#099` cross-references `#011`. +- **Capacity relevance.** `docs/audit/capacity-review.md:106-113` names the Auth tier's ~10 absolute DB connections the **first hard failure**. Halving per-request auth resolutions is a capacity lever, not only a latency one — which is why `#099` cross-references `#011`. - **Fix shape (report-only).** Have the proxy forward its already-verified claims to the route handler through a request header it controls, so the route trusts the proxy's resolution instead of repeating it. Requires care: the header must be proxy-set and unspoofable from outside. ### L1-4 · Anonymous `answer`/`document_upload` consume two sequential rate-limit RPCs @@ -340,7 +340,7 @@ Recorded so the next audit cannot re-file these. | **`#017`** | L3-1, L3-2, L3-3, L3-6, L3-7, L3-8, L4-1 | Live Lighthouse/Web-Vitals evidence first | | **operator** | L1-4, L2-3, L2-5, + `#011` connection allocation | Hosted apply / dashboard change | -**Explicit non-actions**, per `docs/capacity-review.md:123-125`: no read replicas, no horizontal app scaling, **no retrieval concurrency semaphore** until soak data shows queueing. Its verdict — _Postgres CPU under hybrid-RPC concurrency is the first soft failure; "answer p95 inflates well before errors appear"_ — is precisely why L1/L2 round-trip reduction has capacity value beyond latency, and why a semaphore remains the wrong lever. +**Explicit non-actions**, per `docs/audit/capacity-review.md:123-125`: no read replicas, no horizontal app scaling, **no retrieval concurrency semaphore** until soak data shows queueing. Its verdict — _Postgres CPU under hybrid-RPC concurrency is the first soft failure; "answer p95 inflates well before errors appear"_ — is precisely why L1/L2 round-trip reduction has capacity value beyond latency, and why a semaphore remains the wrong lever. --- diff --git a/docs/scale-readiness-review.md b/docs/audit/scale-readiness-review.md similarity index 100% rename from docs/scale-readiness-review.md rename to docs/audit/scale-readiness-review.md diff --git a/docs/tenancy-defense-in-depth-review.md b/docs/audit/tenancy-defense-in-depth-review.md similarity index 99% rename from docs/tenancy-defense-in-depth-review.md rename to docs/audit/tenancy-defense-in-depth-review.md index 45176f1ff4..a89b547454 100644 --- a/docs/tenancy-defense-in-depth-review.md +++ b/docs/audit/tenancy-defense-in-depth-review.md @@ -93,7 +93,7 @@ $$; Legitimate public/demo paths pass the sentinel, not `NULL`. Verify live with `npm run check:july8-live-batch` after applying the July 8 batch -([operator runbook](operator-apply-july8-batch.md)). +([operator runbook](../archive/operator-apply-july8-batch.md)). --- @@ -298,7 +298,7 @@ small, largely-cooperative user set with a public shared corpus. **Bottom line:** the current single-layer enforcement is correct today (0/33 gaps). Item 1 (fail-closed RPC) is live in the repo (#409); **apply to production** per -[`docs/operator-apply-july8-batch.md`](operator-apply-july8-batch.md). Item 2 (CI owner-scope guard) is +[`docs/archive/operator-apply-july8-batch.md`](../archive/operator-apply-july8-batch.md). Item 2 (CI owner-scope guard) is now landed and blocks the regression class in CI. Item 3 (live cross-tenant integration test) closes the remaining app-layer regression exposure; full RLS (item 4) is justified before multi-tenant scale. diff --git a/docs/auth-connection-cap-runbook.md b/docs/auth-connection-cap-runbook.md index 9598ec120f..22b0c37a01 100644 --- a/docs/auth-connection-cap-runbook.md +++ b/docs/auth-connection-cap-runbook.md @@ -3,7 +3,7 @@ **Owner:** operator (dashboard action; **not settable via SQL/MCP**). **When:** complete this _before_ the first compute (vertical) scale-up and before any horizontal replica add. See the ordering in `docs/deployment-architecture.md` -§2 and the bottleneck analysis in `docs/capacity-review.md` §2–§3. +§2 and the bottleneck analysis in `docs/audit/capacity-review.md` §2–§3. **Project:** `Clinical KB Database` (`sjrfecxgysukkwxsowpy`), region ap-southeast-2 (Sydney). @@ -40,7 +40,7 @@ Concretely, the desired end state is: Max Connections if you lean on the PostgREST API heavily, up to **80 %** otherwise), which **leaves adequate room for the Auth server and other utilities** ([connection-management][connmgmt]). This app is PostgREST-heavy - (the answer path fans out to ~6 hybrid RPCs — `docs/capacity-review.md` §2), + (the answer path fans out to ~6 hybrid RPCs — `docs/audit/capacity-review.md` §2), so stay near the 40 % end. 3. When you scale compute, the auth service pool moves **with** the new `max_connections` instead of staying pinned at the old absolute number. @@ -93,7 +93,7 @@ Concretely, the desired end state is: settings screenshot. - [ ] **Soak at higher load passes with zero auth failures.** Re-run the ward-round soak against **staging** at the higher user count and confirm - the "Auth failures during ramp = 0" row of the `docs/capacity-review.md` §4 + the "Auth failures during ramp = 0" row of the `docs/audit/capacity-review.md` §4 success table holds (command below). Higher-load soak (authenticated, staging only — so the sign-in burst actually @@ -127,7 +127,7 @@ npx tsx scripts/soak-test.ts \ ## References -- `docs/capacity-review.md` §2 (Auth: 10 absolute DB connections) and §3 +- `docs/audit/capacity-review.md` §2 (Auth: 10 absolute DB connections) and §3 (first-bottleneck verdict) and §4 (soak test + success criteria). - `docs/deployment-architecture.md` §2 (single-instance scale-out plan) and §5 (why staging must not share the production auth cap). diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b087c87a17..1a36029e4f 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -897,3 +897,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-11 | claude/spacing-icon-design-review-rxwh28 | 5b96281ee7da817d5ce7f1102004ebe6f861b920 | pr-1815 heavy review-and-fix | remote already merged main (shadow-tight Switch kept); cherry-picked privacy -mb-4 reclaim + calculators dock cancel; removed duplicate UniversalSearchAlsoMatches; rail-aware section-sheet focus restore; dispositioned CodeRabbit docs/ledger/gates nits and outdated Sentry skeleton gap | verify:cheap PASS prior tip; verify:pr-local PASS prior tip; vitest privacy+in-page-nav 28 passed on cherry-pick; merge-tree clean vs origin/main | | 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 | PR #1595 / claude/ds-v2-adopt | 590eb6cfb229c5ae0f7a5025352fa871d8321521 | Supersedes 2026-08-03 PR-J clinical-governance review at f9f73c707d9b6b6226fc04d172fef8e426513055; accepted delta through merged PR head | SUPERSEDES the earlier PR-J clinical-governance row for merge evidence. The final delta added the answer-state projection, the two scoped review fixes, and the clinically approved #228 attribution wording. The user accepted that delta without a second clinical-governance review; this record preserves that explicit limitation rather than implying the earlier review covered the final tree. | Final PR head 590eb6cfb229c5ae0f7a5025352fa871d8321521; squashed to main as f4448f8c1 (historical mapping recorded in #232); no new provider or clinical review performed | diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 06a6bec462..6666900467 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -358,18 +358,18 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement: ## Related docs -| Topic | Doc | -| -------------------------- | ---------------------------------------------------------------- | -| Full documentation index | `docs/README.md` | -| Routes and modes | `docs/site-map.md` | -| Search/RAG roadmap | `docs/search-rag-master-plan.md` | -| Universal task ledger | `docs/outstanding-issues.md` | -| Reindex operations | `docs/reindex-runbook.md` | -| Production readiness | `docs/production-readiness-checklist.md` | -| Capacity / scale-up | `docs/capacity-review.md`, `docs/auth-connection-cap-runbook.md` | -| Frontend architecture | `docs/frontend-architecture.md` | -| Repo audit (2026-07-01) | `docs/audit/repo-audit-2026-07-01.md` | -| Latency audit (2026-07-28) | `docs/audit/latency-audit-2026-07-28.md` | +| Topic | Doc | +| -------------------------- | ---------------------------------------------------------------------- | +| Full documentation index | `docs/README.md` | +| Routes and modes | `docs/site-map.md` | +| Search/RAG roadmap | `docs/search-rag-master-plan.md` | +| Universal task ledger | `docs/outstanding-issues.md` | +| Reindex operations | `docs/reindex-runbook.md` | +| Production readiness | `docs/production-readiness-checklist.md` | +| Capacity / scale-up | `docs/audit/capacity-review.md`, `docs/auth-connection-cap-runbook.md` | +| Frontend architecture | `docs/frontend-architecture.md` | +| Repo audit (2026-07-01) | `docs/audit/repo-audit-2026-07-01.md` | +| Latency audit (2026-07-28) | `docs/audit/latency-audit-2026-07-28.md` | --- diff --git a/docs/deploy-corrector-public-titles.md b/docs/deploy-corrector-public-titles.md index 38eee5c12f..af76b5000d 100644 --- a/docs/deploy-corrector-public-titles.md +++ b/docs/deploy-corrector-public-titles.md @@ -227,6 +227,6 @@ manifest and report clean. A clean result does not replace the migration-history ## Related - `docs/supabase-migration-reconciliation.md` — migration drift/repair policy -- `docs/operator-apply-july8-batch.md` — example operator apply runbook +- `docs/archive/operator-apply-july8-batch.md` — example operator apply runbook - `docs/database-drift-detection.md` — how `check:drift` / the manifest work -- `docs/tenancy-defense-in-depth-review.md` — the owner-scoping model this fix aligns with +- `docs/audit/tenancy-defense-in-depth-review.md` — the owner-scoping model this fix aligns with diff --git a/docs/deployment-architecture.md b/docs/deployment-architecture.md index c1c6a4ba16..6a6671ef46 100644 --- a/docs/deployment-architecture.md +++ b/docs/deployment-architecture.md @@ -2,7 +2,7 @@ Decision record for the production topology of Clinical KB. Written 2026-07-06, revised 2026-07-12 when the app went live on Railway. Companion documents: -`docs/observability-slos.md` (SLOs + eval canary) and `docs/capacity-review.md` +`docs/observability-slos.md` (SLOs + eval canary) and `docs/audit/capacity-review.md` (load model, first bottleneck, soak test). Status of this document: **decided and live in production.** The app tier and @@ -124,7 +124,7 @@ one. longer. That is hostile to per-request serverless billing/limits. - **Connection amplification.** Many cold instances multiply concurrent PostgREST/auth traffic against a database whose auth server is capped at 10 - absolute connections (see `docs/capacity-review.md`). + absolute connections (see `docs/audit/capacity-review.md`). Scale-out plan: stay at 1 replica (vertical scaling first) until sustained load demands more; replicas are safe but dilute in-memory coalescing, so add them only @@ -133,7 +133,7 @@ prefer a single-region replica bump (`railway scale southeast-asia=N`) over spreading replicas across regions, which would multiply the cross-region DB hop. **Before the first vertical scale-up**, clear the auth 10-connection cap so the auth pool scales with compute instead of staying pinned — operator runbook: -`docs/auth-connection-cap-runbook.md` (`docs/capacity-review.md` §2–§3). +`docs/auth-connection-cap-runbook.md` (`docs/audit/capacity-review.md` §2–§3). ### 2.1 The Railway↔Supabase connection (Singapore → Sydney) @@ -424,7 +424,7 @@ Rules: The declared staging ref must differ from production and every stale project; otherwise `check:supabase-project` fails closed. - The soak test (`scripts/soak-test.ts`) targets staging **only** — see - `docs/capacity-review.md`. + `docs/audit/capacity-review.md`. ## 6. Rollout and rollback diff --git a/docs/ingestion-concurrency-fix-workorder.md b/docs/ingestion-concurrency-fix-workorder.md index 8a7da69fb6..f490ea60cb 100644 --- a/docs/ingestion-concurrency-fix-workorder.md +++ b/docs/ingestion-concurrency-fix-workorder.md @@ -22,11 +22,11 @@ provenance and must not be read as current live state. Sequenced, operator-applied plan for the state-machine violations that could **not** be safely landed from a chat session. Companion to `docs/ingestion-state-machine.md` (§6 findings, §8 backlog) and -`docs/scale-readiness-review.md`. +`docs/audit/scale-readiness-review.md`. Author date: 2026-07-08. All facts below were read from the **live** project `Clinical KB Database` (`sjrfecxgysukkwxsowpy`) via read-only `execute_sql`; -nothing here was applied to live at author time. **Historical status (2026-07-09):** July 8 migrations were merged to `main` but pending live apply — see [`docs/operator-apply-july8-batch.md`](operator-apply-july8-batch.md). Function bodies are quoted from +nothing here was applied to live at author time. **Historical status (2026-07-09):** July 8 migrations were merged to `main` but pending live apply — see [`docs/archive/operator-apply-july8-batch.md`](archive/operator-apply-july8-batch.md). Function bodies are quoted from `pg_get_functiondef` so migrations are derived from live truth, not `schema.sql` (which is known-drifted — see R24e and `docs/database-drift-detection.md`). @@ -327,7 +327,7 @@ naive scope — it can crash live enrichment.** ## Suggested landing order -**Superseded for merged items** — use [`docs/operator-apply-july8-batch.md`](operator-apply-july8-batch.md) +**Superseded for merged items** — use [`docs/archive/operator-apply-july8-batch.md`](archive/operator-apply-july8-batch.md) for live apply of R24e → RPC hardening → fail-closed → R5 → R17 (`20260708170000`). Remaining repo work: diff --git a/docs/ingestion-state-machine.md b/docs/ingestion-state-machine.md index 7ed665ab34..8fc193fbdd 100644 --- a/docs/ingestion-state-machine.md +++ b/docs/ingestion-state-machine.md @@ -18,7 +18,7 @@ confirmed (5 narrowed, 0 refuted). Seven violations are **deterministic** — no concurrency required at all. Companion docs: `docs/audit/repo-audit-2026-07-01.md` (M9/M11/M13 proved this -bug class live), `docs/scale-readiness-review.md` (phase 2), +bug class live), `docs/audit/scale-readiness-review.md` (phase 2), `docs/reindex-runbook.md`. ## Lifecycle at a glance diff --git a/docs/launch-operator-runbook.md b/docs/launch-operator-runbook.md index 3cb3436b12..9d7abc14ec 100644 --- a/docs/launch-operator-runbook.md +++ b/docs/launch-operator-runbook.md @@ -47,12 +47,12 @@ verified on 2026-07-13. Their detailed procedures remain for staging/disaster re them merely because they appear below. First compare linked migration history and verify the remaining PIA-4 retention migrations. **Do not redeploy the worker until `20260708130000` is confirmed live.** -| Group | Migration/control | Recorded status | Operator action | -| ----- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| a–f | July-8 ingestion/tenancy batch in [operator-apply-july8-batch.md](operator-apply-july8-batch.md) | **Verified live 2026-07-13** | Verify only; redeploy the worker if that recorded remaining action is still open. | -| g | `20260708120000_rag_query_misses_retention` | **Applied and verified live 2026-07-14** | Job 13 is active with the 90-day retention window. | -| h | `20260713201542_consolidate_rag_response_cache_retention` | **Applied and verified live 2026-07-14** | Job 16 is active and bounded; obsolete duplicate job confirmed absent. | -| i | Retrieval RPC forward-codification (`20260713062107`…`20260713062139`) | **Applied and drift/readiness verified 2026-07-13** | Verify only; see [forward-codify-retrieval-rpcs-workorder.md](forward-codify-retrieval-rpcs-workorder.md). | +| Group | Migration/control | Recorded status | Operator action | +| ----- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| a–f | July-8 ingestion/tenancy batch in [operator-apply-july8-batch.md](archive/operator-apply-july8-batch.md) | **Verified live 2026-07-13** | Verify only; redeploy the worker if that recorded remaining action is still open. | +| g | `20260708120000_rag_query_misses_retention` | **Applied and verified live 2026-07-14** | Job 13 is active with the 90-day retention window. | +| h | `20260713201542_consolidate_rag_response_cache_retention` | **Applied and verified live 2026-07-14** | Job 16 is active and bounded; obsolete duplicate job confirmed absent. | +| i | Retrieval RPC forward-codification (`20260713062107`…`20260713062139`) | **Applied and drift/readiness verified 2026-07-13** | Verify only; see [forward-codify-retrieval-rpcs-workorder.md](forward-codify-retrieval-rpcs-workorder.md). | **⏸ PAUSE:** if and only if linked history shows migration `20260708120000` or `20260713201542` absent, apply the reviewed committed migration through the normal guarded workflow. Do not use this status @@ -129,7 +129,7 @@ Detailed: [staging-setup.md](staging-setup.md). No code change — the identity npx tsx scripts/soak-test.ts --target https:// --confirm-staging \ --users 30 --duration-s 600 --ramp-s 120 ``` - Targets ([capacity-review.md](capacity-review.md) §4): search p95 ≤ 3 s, **answer p95 ≤ 25 s** + Targets ([capacity-review.md](audit/capacity-review.md) §4): search p95 ≤ 3 s, **answer p95 ≤ 25 s** (watch this given the Railway↔Sydney hop), non-429 error rate < 1 %. 4. Rehearse rollback = redeploy the previous Railway image tag; confirm health returns. @@ -154,7 +154,7 @@ no scale-to-zero, Railway health `/api/health/ready`. I'll prep the Railway serv - **Registry seed (prod)** 🧑 — `npm run registry:seed -- --owner-id --write --confirm` (+ `differentials:seed` for the slug-retitle prune). Until seeded, Services/Forms show empty. - **Auth connection cap** 🧑 — before the first vertical scale-up, switch Supabase auth from the 10-absolute - cap to **percentage-based** allocation in the dashboard ([capacity-review.md](capacity-review.md) §3). + cap to **percentage-based** allocation in the dashboard ([capacity-review.md](audit/capacity-review.md) §3). Not settable via SQL/MCP. - **Observability wiring** 🧑 — once host metrics exist, wire the warn/page SLO thresholds ([observability-slos.md](observability-slos.md) §2) into a real alert channel; confirm the nightly eval diff --git a/docs/operator-backlog.md b/docs/operator-backlog.md index ef3dfc165a..090b7e277b 100644 --- a/docs/operator-backlog.md +++ b/docs/operator-backlog.md @@ -20,22 +20,22 @@ Findings inventory for handover: [audit/audit-handover-2026-07-14.md](audit/audi | Action | Status | Blocked by | Verify command | Runbook | | ----------------------------------------------------- | ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Apply July-8 migration batch (a–g) to live | ✅ done | — | `SUPABASE_ENVIRONMENT=production npm run check:july8-live-batch` (2026-07-13: 6 live, apply=no-op) | [operator-apply-july8-batch.md](operator-apply-july8-batch.md) | +| Apply July-8 migration batch (a–g) to live | ✅ done | — | `SUPABASE_ENVIRONMENT=production npm run check:july8-live-batch` (2026-07-13: 6 live, apply=no-op) | [operator-apply-july8-batch.md](archive/operator-apply-july8-batch.md) | | Apply drift-codify forward migration (step 1h) | ✅ done | — | Applied and drift/readiness verified 2026-07-13; verify only unless new reviewed drift is found | [database-drift-detection.md](database-drift-detection.md) | | Apply repo-ahead migrations to live (post-2026-07-13) | ✅ done | — | Zero unsafe title-word rows; `npm run check:drift`; then `eval:retrieval:quality` (36/36) for the corrector | [deploy-corrector-public-titles.md](deploy-corrector-public-titles.md) · [operator-apply-performance-latency-remediation.md](operator-apply-performance-latency-remediation.md) | | Full release gate (bounded OpenAI spend) | ⏳ pending | hosted audit/browser | Let Dependabot PRs #1268/#1269 clear the production audit, then run one exact-SHA release/browser gate | [launch-operator-runbook.md §2](launch-operator-runbook.md) | | Reconcile existing staging Supabase schema | ⏳ pending | reviewed full-chain scope | 2026-07-30 live list shows 24 local-only versions, including ten earlier history holes and fourteen after `20260719055623`; apply only the complete reviewed chain, then `npm run check:indexing` | [staging-setup.md](staging-setup.md) | -| Staging soak + rollback rehearsal on Railway | ⏳ pending | staging schema parity | Re-run tenancy proof, then `scripts/soak-test.ts --confirm-staging` (answer p95 ≤ 25 s) | [launch-operator-runbook.md §4](launch-operator-runbook.md) · [capacity-review.md](capacity-review.md) | +| Staging soak + rollback rehearsal on Railway | ⏳ pending | staging schema parity | Re-run tenancy proof, then `scripts/soak-test.ts --confirm-staging` (answer p95 ≤ 25 s) | [launch-operator-runbook.md §4](launch-operator-runbook.md) · [capacity-review.md](audit/capacity-review.md) | | Production deploy to Railway | ✅ done | — | App deployment recorded live 2026-07-14; re-verify with `GET /api/health` and deployment readiness | [deployment-architecture.md](deployment-architecture.md) | ## Post-deploy actions -| Action | Status | Blocked by | Verify command | Runbook | -| --------------------------------------------------------------------- | ---------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| Redeploy worker (one always-on instance) | ✅ done | — | Worker deployment recorded live 2026-07-14; re-verify with `npm run reindex:health` | [worker-deploy-runbook.md](worker-deploy-runbook.md) | -| Seed registry / differentials / medications (prod) | ✅ done | — | Read-only 2026-07-27 counts: registry 276, medications 328, differentials 232; no write needed | [launch-operator-runbook.md §6](launch-operator-runbook.md) | -| Switch auth connection cap 10-absolute → percentage-based (dashboard) | ⏳ pending | before first vertical scale-up | dashboard — not SQL/MCP settable | [auth-connection-cap-runbook.md](auth-connection-cap-runbook.md) · [capacity-review.md](capacity-review.md) | -| Wire SLO warn/page thresholds into a real alert channel | ⏳ pending | accountable responder/channel | Scheduled canary `30216191889` is stable; prove one owned non-PHI alert event before activation | [observability-slos.md](observability-slos.md) | +| Action | Status | Blocked by | Verify command | Runbook | +| --------------------------------------------------------------------- | ---------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Redeploy worker (one always-on instance) | ✅ done | — | Worker deployment recorded live 2026-07-14; re-verify with `npm run reindex:health` | [worker-deploy-runbook.md](worker-deploy-runbook.md) | +| Seed registry / differentials / medications (prod) | ✅ done | — | Read-only 2026-07-27 counts: registry 276, medications 328, differentials 232; no write needed | [launch-operator-runbook.md §6](launch-operator-runbook.md) | +| Switch auth connection cap 10-absolute → percentage-based (dashboard) | ⏳ pending | before first vertical scale-up | dashboard — not SQL/MCP settable | [auth-connection-cap-runbook.md](auth-connection-cap-runbook.md) · [capacity-review.md](audit/capacity-review.md) | +| Wire SLO warn/page thresholds into a real alert channel | ⏳ pending | accountable responder/channel | Scheduled canary `30216191889` is stable; prove one owned non-PHI alert event before activation | [observability-slos.md](observability-slos.md) | ## Standing secret / config placement (per environment) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 4cadf4ea42..3a68886de5 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -86,84 +86,77 @@ removed after current-main verification; it is not missing recommended work. | 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. | -| 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… | -| 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. | -| 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. | -| 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. | +| 34 | `#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. | +| 35 | `#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… | +| 36 | `#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. | +| 37 | `#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. | +| 38 | `#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. | +| 39 | `#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. | +| 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 | `#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. | +| 46 | `#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. | +| 47 | `#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… | +| 48 | `#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… | +| 49 | `#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… | +| 50 | `#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.… | +| 51 | `#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. | +| 52 | `#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. | +| 53 | `#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. | +| 54 | `#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. | +| 55 | `#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. | +| 56 | `#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 … | +| 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 | `#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. | +| 75 | `#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. | +| 76 | `#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. | +| 77 | `#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. | +| 78 | `#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. | +| 79 | `#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. | +| 80 | `#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. | +| 81 | `#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. | +| 82 | `#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. | +| 83 | `#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. | +| 84 | `#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. | +| 85 | `#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. | +| 86 | `#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. | +| 87 | `#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. | +| 88 | `#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. | +| 89 | `#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. | +| 90 | `#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. | +| 91 | `#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. | +| 92 | `#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. | +| 93 | `#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. | +| 94 | `#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. | +| 95 | `#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. | +| 96 | `#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. | +| 97 | `#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. | +| 98 | `#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. | +| 99 | `#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). | +| 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 | `#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. | +| 102 | `#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. | +| 103 | `#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. | +| 104 | `#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. | @@ -189,7 +182,7 @@ removed after current-main verification; it is not missing recommended work. | #053 | P1 | task | Execute cross-border privacy/legal package | Execute OpenAI and Railway DPAs; decide ZDR and Australian data residency; obtain prompt-cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not represent the release as privacy-approved or alter final public privacy wording before sign-off. | `docs/openai-cross-border-basis.md`; `docs/privacy-impact-assessment.md` | 2026-07-24 | | #055 | P2 | task | Run one exact-SHA full release and PR gate | Before the next full-confidence release/handoff, record the candidate/PR SHA and run the local/provider release gates, Firefox/WebKit, required hosted CI, and actionable GitHub review-thread closure once. Stop at the first actionable failure and rerun only the repaired smallest gate. | `docs/launch-operator-runbook.md`; `docs/codex-review-protocol.md` | 2026-07-24 | | #056 | P2 | task | Reconcile the existing staging migration history | `Clinical KB Staging` already exists as a healthy, empty Supabase/Railway tier with distinct secrets and no production clinical data, but it is 24 repository migrations behind (ten earlier history holes plus fourteen after `20260719055623`). In the next approved staging schema window, apply the exact missing migration chain, then re-run indexing, health, identity and data-boundary proof. Do not recreate the environment or copy production clinical documents. | current-main staging verification; `docs/staging-setup.md`; `docs/operator-backlog.md` | 2026-07-27 | -| #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/capacity-review.md` | 2026-07-24 | +| #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/audit/capacity-review.md` | 2026-07-24 | | #011 | P3 | task | Auth DB-connection allocation is operator-only | Supabase Auth (GoTrue) is capped at ~10 absolute DB connections (Supabase perf advisor). Switch to **percentage-based** allocation in the Supabase **dashboard** before the first compute scale-up — **not settable via SQL/MCP** (operator-owned). Verify via a staging soak + an approval-gated read-only advisor re-check. | `docs/auth-connection-cap-runbook.md`; `docs/process-hardening.md` (Known follow-up debts) | 2026-07-21 | | #013 | P3 | rec | Route-chunk + mockup catalogue JSON weight | Keep this recommendation open and measurement-gated. `build:analyze` still finds route-scoped catalogue modules: `/specifiers` ships `specifiers-search-index.json` (~180 KB parsed), `/forms` ships `forms-catalog.json` (~132 KB), and `/formulation` ships `formulation-content.json` (~52 KB; client-side local search needs an index/full split or a search endpoint). The approved Lighthouse sample covered `/forms`, but `/specifiers` and `/formulation` remain unmeasured, so the precommitted `#017` rule does not permit archiving their payload work. **Next:** collect route-specific LCP/CLS and CrUX INP evidence for `/specifiers` and `/formulation`, then close only the routes that meet every `#017` threshold. **Mockup half settled 2026-08-09 (#252):** this row's position that development-only `*-mockups.tsx` chunks are not an initial production bundle is now enforced rather than merely asserted — `check:bundle-budget` weighs `/mockups/**`-exclusive chunks against a separate `mockups` baseline (273,873 gzip bytes, 25% tolerance) and production against its own (1,309,772, 10%), so mockup growth no longer consumes production headroom and the mockup share is reported on every run. That also supplies the separate-reporting measurement this row asked for before any prune: 267.5 KiB across 76 chunks over 66 mockup routes, 17.29% of the repo-wide total. The 'do not restructure them without deploy-artifact or cold-start evidence' guidance stands unchanged — the split removed the false pressure to prune, it did not authorise pruning. | session 2026-07-21 (`build:analyze`); PR #1470 review | 2026-07-21 | | #016 | P3 | rec | "Big but not easy" structural + motion perf | Deferred larger levers: (a) nonce-CSP forces every product route to `╞Æ Dynamic` (zero static generation) — evaluate Partial Prerendering / static shells for the static clinical catalogues (DSM/differentials/therapy/specifiers/formulation); (b) sidebar expand/collapse animates `grid-template-columns` (biggest smoothness cost, motion-gated — needs a transform-overlay rethink); (c) Therapy Compass fetches 692 KB / 2.5 MB JSON client-side (defer until interaction + confirm brotli); (d) settings/setup/admin dialogs static-imported into the home chunk (`next/dynamic` them); (e) **DONE 2026-08-01 in PR-T (ds-v2 therapy teardown):** deleted `therapy-compass.css` and removed its route-group layout import — no longer render-blocking on `/`, `/documents`, `/forms`, `/dsm` and every mode home; (f) `shared-search-app-shell.tsx:8` statically imports the `therapy-compass` barrel, pulling `workspace.tsx` + `bindings.tsx` + `nav.tsx` into every `(search-app)` route; (g) three client waterfalls (`use-app-preferences.ts:156-182`, `ClinicalDashboard.tsx:977-1069`, `signed-image.tsx:60-84` + `use-signed-image-url.ts:39`) and the paint offenders in `globals.css` beyond the sidebar grid — three stacked `backdrop-filter` passes on an always-mounted translating element (`:709-748`), `box-shadow` inside a `transition` list (`:677-684`), and `@keyframes shimmer` animating `background-position` on the shared `Skeleton` (`:2289-2296`). **CORRECTED 2026-07-29 on (c):** the Therapy Compass filenames are unversioned and Next serves `/public` with an ETag, so only the FIRST visit pays 690.6 KB / 2,470 KB — repeat visits pay ~4 revalidation round trips. The fix is content-hashed filenames + `immutable` (touching `scripts/build-therapies-index.mjs` and `check:therapy-data-index`), NOT a bare `Cache-Control` line. See `docs/audit/latency-audit-2026-07-28.md` L3-1/L3-2/L3-3/L3-6/L3-7. | session 2026-07-21 (build route table + design audit) | 2026-07-21 | @@ -209,18 +202,14 @@ removed after current-main verification; it is not missing recommended work. | #098 | P2 | task | Offline round-trip budget harness for the hot routes | **Outcome:** per-scenario Supabase round-trip counts are pinned by a test, so an extra round trip on a hot path is a red gate rather than an inference. **Done 2026-07-29:** the measurement gap is closed — `Server-Timing` now covers `auth`/`ratelimit`/`scope` on `/api/answer`, `auth`/`ratelimit`/`search`/`total` on `/api/search`, and `auth`/`ratelimit` on `/api/answer/stream` (previously the route the UI actually calls emitted no header at all). Headers flush before the first SSE frame, so in-stream stages cannot reach a header and must NOT be routed through the governed `progress`/`final` contract. `tests/answer-route-preamble.test.ts` pins admission-before-scope (no scope call while the limiter is pending or after a deny) and the client-disconnect abort signal. **Done 2026-07-30 (PR #1450, `1bff4c78`):** the counting proxy exists and the answer path is budgeted. `tests/helpers/supabase-round-trip-counter.ts` counts on **execution, not construction** — a builder that is never awaited costs zero, one awaited twice costs two — which is the distinction that makes the count mean "requests issued". `tests/rag-round-trip-budget.test.ts` pins two offline answer-path scenarios (a single-source source-only answer, and that trips do not scale with the number of retrieved sources) plus three self-tests of the counter, and is registered in `scripts/fixtures/rag-offline-contract-tests.json` so it runs inside the offline contract rather than only on demand. Verified locally, provider-free: `Test Files 1 passed (1)`, `Tests 5 passed (5)`. Its documented blind spot is worth repeating before anyone cites a budget as total cost: it sees only traffic through the wrapped client, so a trip issued via another client instance, a direct `fetch`, or a provider SDK is invisible to it. **Done 2026-07-30 (search *retrieval core*, not the endpoint):** `tests/search-round-trip-budget.test.ts` pins `searchChunksWithTelemetry` — what `/api/search` calls to retrieve — registered in both the contract fixture and `scripts/rag-offline-contract.mjs`. **Corrected after Codex review on PR #1464:** an earlier version of this row and the test itself claimed to pin `/api/search`. They do not. The route's auth, rate limiting, scope resolution, related-document enrichment and telemetry write are all invisible to this suite, so a round trip added to any of them leaves it green — and the refusal budget below is about *retrieval*, not about an adversarial HTTP request, which still pays the route preamble. **The measured shape is itself the finding:** one search costs **11 round trips** — `rag_aliases` 1, `match_document_chunks_text_v2` **3**, `match_document_table_facts_text_v2` **3**, `get_related_document_metadata_v2` 1, `document_index_quality` 1, `document_images` 2 — so the two text RPCs are each issued three times per search. Pinned by total *and* breakdown, because a refactor swapping one probe for an unrelated query would keep the total at 11 while changing the traffic. Deterministic across three consecutive runs. The refusal budget asserts **zero** Supabase traffic, matching `rag.ts`'s claim that prompt-injection intent is refused before any query issues, and was proven against the broken shape: with a non-refused query it fails on the round-trip assertion (`expected 11 to be +0`), which is why that assertion is ordered ahead of the results assertion. **Note 2026-07-30 (corrected):** a work-branch experiment on `origin/work` (`1f52c704`, reverted in `a0cd00ba`) collapsed both text surfaces offline (budget 11→7) but never reached `main` and was never canaried on those SHAs. Live canaries `30579804611` and `30580564419` ran on unrelated `main` docs commits (`fde68ed4` / `4312a214`) and must not be cited as probe-collapse evidence. Next (b) remains open and still needs a real canary pair before any collapse. **Next:** (a) add the route-level budget this suite does not provide — drive `POST` from `src/app/api/search/route.ts` with counted clients, following the `tests/answer-route-preamble.test.ts` pattern, so a round trip added to the route preamble or post-processing is a red gate; (b) do **not** wholesale-collapse the ×3 sibling lexical variants as the next step — the offline collapse on `origin/work` never landed on `main` with a canary on those SHAs (see Note above), so that avenue is not an approved follow-up; any later latency work must use a materially different approach (for example overlap/parallelism that preserves all three variants) or a real canary pair on the changed tip under the usual RAG gate. (c) `scripts/eval-rag-offline.mjs` and `scripts/test-rag-offline.mjs` remain unwired; the offline contract runner is now the de-facto single home for budgets, so either adopt that explicitly here or wire them. | `docs/audit/latency-audit-2026-07-28.md` measurement plan; `src/lib/server-timing.ts`; `src/lib/answer-stream-contract.ts:18-21` | 2026-07-29 | | #099 | P2 | task | Remove the remaining fixed per-request round trips | **Outcome:** the answer path stops paying avoidable per-request Supabase round trips. **Done 2026-07-29:** shared-cache-hit promotion deferred off the response path with its mid-request staleness guard intact and documented (`rag.ts:3234`, `rag-cache.ts`); scope resolution overlapped with the rate-limit RPC, signal threaded so a client disconnect finally cancels its paginated queries (`answer/route.ts`). **REFUTED on PR #1377 review — do not retry:** the same pass also overlapped scope with the rate-limit RPC and aborted it on deny, claiming the limiter could "deny for free". It cannot. With caller-supplied `filters` or explicit ids, scope passes its zero-query early returns (`search-scope.ts:242,253`) into the paginated `documents` loop at `:269`, and an `AbortSignal` cancels the client request without un-executing a statement Postgres already began — so throttled traffic kept burning database capacity while collecting 429s, against `capacity-review.md:106-113`'s first-soft-failure warning. Scope is behind admission again, pinned by `tests/answer-route-preamble.test.ts`. Re-attempting the overlap requires a non-database admission gate ahead of the durable limiter first. **Remaining:** (a) the 8 `setCachedSearch` awaits — deferring changes `throwIfAborted` semantics and widens a real mutation window because the clone happens after an `await`, so each branch needs discharging individually; (b) batch the anonymous subject+global rate-limit pair, which needs a NEW atomic RPC modelled on `consume_summary_rate_limits_atomic` and cannot be called until the operator applies it — `Promise.all` is the WRONG fix because it consumes the global bucket even when the subject bucket already denied; (c) stop the proxy and route handler resolving identity twice per authenticated request — no in-process memo can do this (different `Request` objects), so the proxy must forward unspoofable verified claims via a header it controls. Cross-references #011: halving auth resolutions eases the ~10-connection Auth cap that `capacity-review.md:106-113` calls the first hard failure. | `docs/audit/latency-audit-2026-07-28.md` L1-1/L1-3/L1-4; `src/lib/api-rate-limit.ts:276-282`; `src/proxy.ts:125` | 2026-07-29 | | #100 | P2 | rec | Buffered answer generation has no incremental verified delivery | **Design complete; runtime work remains provider-gated.** [`verified-answer-incremental-delivery-design.md`](verified-answer-incremental-delivery-design.md) records the clinical-governance decision and staged contract: keep the `progress`/`final`/`error` allowlist; disclose bounded, owner-scoped evidence only after the canonical danger-level source-governance refusal permits it, then emit complete answer sections only after each reuses the full production verification boundary; reconcile every preview byte-for-byte with the authoritative `final`; discard all previews on error/cancel/retry; deploy behind separate parse/emission/render flags. Phase 0 contract proof and Phase 1 evidence preview can be developed offline, but visible rollout still needs clinical/browser proof. Phase 2 changes generation architecture and requires explicit approval for answer-quality evals plus a baseline/post live canary pair. **Naive token streaming remains REFUTED:** never re-land `token`, `revising`, provisional prose, or a weaker stream-only verifier. Cross-references #021. | `docs/verified-answer-incremental-delivery-design.md`; `docs/audit/latency-audit-2026-07-28.md` L0-1; `src/lib/answer-stream-contract.ts:18-21` | 2026-07-30 | -| #101 | P3 | rec | Canary-gated retrieval parallelisation candidates | **Outcome:** independent retrieval stages stop running serially, proven by a live canary pair. Candidates: metadata/memory/visual hydration triples repeated on four branches (`rag.ts:2460,2493,2521` and three more) while `rag.ts:2751-2804` already parallelises three RPCs in one `Promise.all`, so the omission is inconsistency rather than intent; the nested `await`-in-loop scope enumeration (`search-scope.ts:202,328`); typeahead results never cached (`rag.ts:2698-2711`); universal-search coalescing (`/api/search` has it, `/api/search/universal` does not). Each changes candidate assembly, truncation, or what the next keystroke returns, so each needs 36/36 retrieval plus recall 1.0 and zero per-case rr regressions. Distinct from #001 (semantic rerank). Resolved #075 and #083 are the precedents for why these are gated rather than free. **Stop:** needs the #098 harness and explicit canary approval first. | `docs/audit/latency-audit-2026-07-28.md` L2-1/L2-2/L2-8/L1-5 | 2026-07-29 | +| #101 | P3 | rec | Canary-gated retrieval parallelisation candidates | **Outcome:** remaining retrieval parallelisation candidates are explicit after PR #1474 shipped the metadata, memory, and visual hydration triples. **Remaining:** nested `await`-in-loop scope enumeration (`search-scope.ts:202,328`), uncached typeahead results (`rag.ts:2698-2711`), and universal-search coalescing (`/api/search` has it; `/api/search/universal` does not). Each changes candidate assembly, truncation, or what the next keystroke returns, so each requires the #098 harness, the RAG flag, explicit live-canary approval, 36/36 retrieval, recall 1.0, and zero per-case reciprocal-rank regressions. Distinct from #001 (semantic rerank). **Completed:** PR #1474 parallelised the repeated hydration triples; do not propose that work again. **Stop:** no remaining candidate proceeds without its canary gate. | `docs/audit/latency-audit-2026-07-28.md` L2-1/L2-2/L2-8/L1-5; PR #1474 | 2026-07-29 | | #102 | P3 | task | Apply the additive `documents` index debt (operator) | **Outcome:** bare-column `ILIKE` and the paged status scan on `documents` are index-served on hosted. `documents_title_trgm_idx` indexes a CONCATENATED expression, so the bare-column predicates in `api/documents/route.ts:193` and `rag-candidate-sources.ts:477` (RAG path) cannot use it and fall back to scanning; `search-scope.ts:271-277` sorts per page against the single-column `documents_status_idx`. **Runbook prepared 2026-07-29 — NOT applied, item stays open:** three `CREATE INDEX CONCURRENTLY` statements authored and reviewed in `docs/operator-apply-performance-latency-remediation.md` — additive, though **the "recall is byte-identical" claim was RETRACTED on 2026-07-29 review**: `fetchDocumentTitleAliasRows` (`rag-candidate-sources.ts:482`) applies `.limit(12)` with no `ORDER BY`, so a new index can change which title-alias documents feed candidate assembly. Only the documents-list use stays ordering-safe; `(status,id)` is canary-gated too — see runbook, and making that `.limit(12)` deterministic first does **not** lift the gate — an unordered `LIMIT` has no stable selection to preserve, so imposing an order can pick a different twelve and is itself an ordering behaviour change on a retrieval surface, which AGENTS.md requires a canary pair for. Sequencing the ordering fix first is worthwhile (unordered `LIMIT` on a retrieval input is latent nondeterminism regardless) but yields two canary-gated changes, not one (PR #1377 review). **Deliberately NO migration file:** an additive-index migration without a synchronized `schema.sql` mirror and regenerated drift manifest is exactly what closed PR #1312, and the mirror cannot come first because `required_indexes` in `search_schema_health()` (`schema.sql:3178`) runs against live. **Next (operator):** **author the migration first** — `supabase/migrations/` is the source of truth and `schema.sql` only a mirror, so hand-run operator SQL never reaches staging, disaster-recovery replay, or a local `supabase db reset`, and a `required_indexes` registration would fail there (PR #1377 review); follow the `20260717170000_registry_projection_cleanup.sql` idempotent pattern. **That migration must also carry the health-function change** — `required_indexes` lives inside `search_schema_health()`, which is redefined by `create or replace function` in eleven migrations (copy `20260705180000_reconcile_search_health_indexes.sql:62`); editing `schema.sql:3177` alone moves only the mirror and leaves the indexes unmonitored on hosted (PR #1377 review). Then apply concurrently, confirm `indisvalid`, mirror both the index statements and the identical function body into `schema.sql`, run `npm run drift:manifest` (Docker), and deploy the migration LAST — in that order, in one change. Expect `check:drift` to report them as unexpected between steps 1 and 2. **Rollback is three deployed phases, not the reverse of one:** retract `required_indexes` via its own `create or replace function` migration and deploy → drop concurrently live → only then deploy the `schema.sql` removal plus an idempotent forward `drop index if exists` migration, because Supabase wraps migrations in a transaction and a plain `DROP INDEX` there takes the lock the concurrent procedure exists to avoid (PR #1377 review). | `docs/audit/latency-audit-2026-07-28.md` L2-3/L2-5; `docs/operator-apply-performance-latency-remediation.md` | 2026-07-29 | | #117 | P2 | rec | Therapy Compass catalogue payload is the mobile LCP outlier | **Outcome:** `/therapy-compass` mobile LCP lands near the other mobile routes instead of double them. **Measured 2026-07-30** by the new pre-merge Lighthouse budget: mobile LCP 5229 ms, TBT 612 ms, CLS 0.142, against 2123-2460 ms on every other mobile route and 826 ms on desktop — so it is client-side work under mobile CPU/network throttling, not server latency. **Cause before this PR:** `useTherapyData` fetched `public/therapy-compass-data/therapies-index.json` (the thin browse index; 205 records) for the home/search/pathways screens, so the download plus JSON parse sat on the critical path before content painted. **Current split:** home now fetches `public/therapy-compass-data/therapies-home.211dab554c4ec62d.json` (136,288 bytes raw), pathways use the thin browse index, and search loads the full prose corpus (#1471). 90% of the index weight is long-form clinical prose — indications 159 KB (26%), contraindicationsOrCautions 139 KB (23%), bestUsedFor 73 KB (12%), clinicalSummary 67 KB (11%), patientPopulation 59 KB (10%), targetSymptoms 48 KB (8%) — while name, slug, category, tags and setting together are 54 KB (7%). **Remaining decision for search/pathways: rendered on the card, matched by search, or neither.** `therapy-card.tsx` references five of those prose fields and the same index feeds the search screen, so stripping fields could silently change clinical display or search recall. **Next:** settle that per-field question, then either pre-truncate prose that only feeds card display, or move search matching server-side / load prose on first keystroke. **Gate:** `check:therapy-data-index` plus the therapy Playwright journeys; re-measure with `npm run verify:lighthouse`. **Stop:** do not drop a field from the catalogue payload without confirming no card renders it and no search path matches on it. Same class as #013 (route-chunk / catalogue JSON weight), different route and now measured. | session 2026-07-30 Lighthouse budget first run; PR #1404; PR #1489 | 2026-07-30 | | #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 | @@ -242,8 +231,6 @@ removed after current-main verification; it is not missing recommended work. | #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 | -| #187 | P3 | rec | Archive process-lesson rows #151 and #154 once a durable one-line note exists outside the ledger | **Outcome:** `#151` (`gh pr checks` vs Actions API) and `#154` (row ids are not stable land proof) move to Resolved/archive after a durable note lands in `AGENTS.md` or `docs/codex-review-protocol.md`, so the open table is not a graveyard of already-learned lessons. **Next:** add the one-line notes, then `/issues done` each with the doc anchor. **Stop:** do not delete history; archive only. | session 2026-07-31 ledger hygiene review | 2026-07-31 | | #188 | P3 | task | Document and track disaster-recovery re-creation checklist as ledger work | **Outcome:** the five DR items that do not survive a schema restore are tracked with owners and verify steps. **Split into concrete tasks 2026-07-31:** `#196` pg_cron; `#197` Vault secrets; `#198` custom GUCs; `#199` edge functions; `#200` dashboard config. Keep this umbrella as the index; execute the child tasks. Distinct from `#056` (staging schema) and resolved `#054` (secrets reconciliation). **Stop:** do not treat a schema restore as complete until children are green. | docs/operator-backlog.md Disaster-recovery re-creation; session 2026-07-31 | 2026-07-31 | | #189 | P2 | task | Pin /api/search route-level round trips and disposition the x3 text RPC probes | **Outcome:** a counting-proxy budget drives `POST` `/api/search` (auth/ratelimit/scope/enrichment/telemetry), and the retrieval-core finding that `match_document_chunks_text_v2` and `match_document_table_facts_text_v2` each issue three times per search is either documented as intentional or collapsed under the RAG canary gate. **Source:** residual next actions on `#098` after answer-path and retrieval-core budgets landed. **Next:** (a) route-level budget following `tests/answer-route-preamble.test.ts`; (b) decide probe vs collapse — behaviour change needs RAG flag + canary. **Stop:** do not change retrieval assembly without approval. | session 2026-07-31; #098 residual; tests/search-round-trip-budget.test.ts | 2026-07-31 | | #190 | P3 | task | X3: Finish rag.ts monolith decomposition | **Outcome:** `src/lib/rag/rag.ts` is decomposed into focused modules per `docs/maturity-backlog-workorders.md` X3, with existing offline RAG contracts green. **Status:** IN PROGRESS (DocumentViewer/Dashboard extractions done; rag.ts remains). **Next:** continue safe extractions only with the RAG flag before editing protected surfaces; one verified draft PR per unit. **Stop:** no behaviour change without canary when retrieval/answer paths move. | docs/maturity-backlog-workorders.md X3; #086 | 2026-07-31 | @@ -272,7 +259,6 @@ removed after current-main verification; it is not missing recommended work. | #226 | P1 | task | PR-J: VerificationNotice pushes the phone short-answer runway past the in-flow activation band | CI run 30820496984 reported the real numbers: ui-smoke phantom-scroll 97 against an 8px budget, short-runway maxOffset 251 against a 200px ceiling. Local readings for the same tree were 29 and passing, confirming finding L's 41-81px offset and the rule never to pin from this machine. Re-pinned from CI per the user decision: bare phantom budget 8 to 112, maxOffset ceiling 200 to 280, postCollapseMaxOffset ceiling 72 to 160. State plainly what that means: the 72px in-flow activation band was a real contract and the post-collapse runway no longer fits inside it, because every answer now carries an unconditional verification notice above the prose. The collapse budget is untouched - chrome height did not change, only the content below it. OPEN QUESTION for the clinical owner, separate from the pin: a one-sentence answer now carries roughly 97px of notice above it on a phone, which is what the phantom-scroll guard was originally written to prevent. Widening the pin accepts that as intended product behaviour. | session 2026-08-03 (PR-J Wave 5, gate window, verify:phone-chrome) | 2026-08-03 | | #230 | P2 | issue | CI's PR-policy body sync can overwrite PR descriptions from a committed scratch file | ci.yml's Sync PR policy body job runs only when the PR head contains PR_POLICY_BODY.md (pr_policy_body_present) and then replaces that PR's description with the file. #1546 committed the scratch file to main; any open PR whose head still contained the file (including those that merged or rebased onto that tip) inherited #1546's body — and pr-policy.mjs parses the body as merge-gating input, so governance checklists and verification claims appeared on PRs they did not belong to. Heads without the file are not rewritten. #1548 deleted the file, which removes today's symptom; nothing stops the next branch committing one. Next action: make the sync job no-op unless the file is new in that PR's own diff, or move the handoff body out of the repo entirely. | Observed during the search-results-bar work (#1555); recorded in that branch's handoff doc rather than the ledger at the time | 2026-08-04 | | #231 | P1 | issue | Live answers degrade to source-only: fast-route answerRouteBudgetMs.fast (25000) binds while retrieval is healthy | Measured live 2026-08-04 on psychiatry.tools, container d6c2b9526. POST /api/answer for 'Lithium dosing?' took 24.7s and returned degradedMode {active:true, reason:'generation_fallback:provider_timeout'}, routingReason 'clinical_fast_grounded_synthesis; generation_fallback:provider_timeout; source_backed_extractive_fallback', modelUsed null, answerQualityTier 'source_only', openAIUsage 6349 input / 586 output tokens (billed then discarded). Retrieval is NOT the fault: retrievalDiagnostics gateStatus 'passed', candidateCount 12, distinctDocumentCount 5, topScore 0.7125, fallbackReason null; POST /api/search returned 8 correct results for both 'lithium dosing' and 'clozapine monitoring'; the top lithium document is document_status 'current', review_date 2029-11-30, clinical_validation_status 'locally_reviewed'; and the dose path sets embedding_skipped true (dose_evidence_text_match) so it never calls OpenAI for retrieval. This corrects the 2026-08-04 production-triage handover, whose four ranked hypotheses (query embedding, Supabase RPC, source governance, corpus) are all ruled out. THE BINDING BUDGET IS NOT OPENAI_ANSWER_TIMEOUT_MS - do not raise it, and note production does not even set it. The query routed fast (routeMode 'fast', smart_api_response_mode 'fast_grounded_answer'), so answerRouteBudgetMs.fast = 25000 is the ceiling and generationRequestTimeoutMs returns min(30000, remaining - generationRecoveryReserveMs 2000); after ~2s retrieval that leaves roughly 21s for generation, and the observed 24.7s is the route budget being spent, not the 30s env knob. Production sets OPENAI_MAX_OUTPUT_TOKENS=16000 and RAG_PROVIDER_MODE=auto with no timeout or reasoning-effort override. Second finding: answerRouteResultCanBeCached returns !deadlineExceeded, which correctly excludes a route-deadline-exceeded answer but NOT one degraded by the OpenAI request timing out - deadlineExceeded stays false there - so a generation_fallback answer is cacheable in rag_response_cache and keeps being served after the provider recovers, which is what user-observed ~2s 'No current source ...' responses were. Also live only since this window: 47c512e07 bumped openai 6.49.0 to 7.3.0 (major). Next action: own PR, because every candidate fix is inside behaviour-protected `src/lib/rag/**` - flag it first, carry a RAG impact line, and pair it with a live eval canary. Decide (a) whether medication_dose_risk belongs on the fast route when it consistently overruns 25s, and (b) whether answerRouteResultCanBeCached should also exclude generation_fallback results, which is the half with clinical consequence. Stop: do not edit `src/lib/rag/**` without flagging, and do not raise a timeout that cannot bind. Renumbered from this PR branch original #230 after main claimed #230 for the PR-policy body-sync issue (PR #1609). | session 2026-08-04 (production triage, live /api/search + /api/answer) | 2026-08-04 | -| #232 | P2 | issue | The PR-J clinical governance review record describes a head that was never merged | docs/branch-review-ledger.md records the clinical-governance review of PR #1595 (PR-J) at head f9f73c707. The head that actually merged was 590eb6cfb (squashed to f4448f8c1 on main). Three things landed after the reviewed head: the bot's answer-state projection changes, the two Codex review fixes, and the #228 attribution wording. The user passed the review knowing this, so the review decision itself is not in question - the record simply does not describe the merged tree, which defeats the ledger's purpose of proving what was reviewed. Next action: append a superseding record (never edit the existing row) with npm run ledger:append -- --supersede against the merged head, scoped to the delta between f9f73c707 and 590eb6cfb, or state explicitly that the delta was accepted unreviewed. Stop: the ledger is append-only - do not rewrite or delete the original row. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | | #233 | P3 | task | COMPONENTS.md section 0 describes the pre-adoption world, and the optionality-marker contract change is undocumented | Two documentation debts left by PR-J, both in docs/design-system/COMPONENTS.md, naturally one PR. First: section 0's maturity matrix is stale. FormField, TextField, SearchField, Select, Checkbox, RadioGroup, PageHeader and Breadcrumb now have real product mounts, so 0.1 and 0.2 misdescribe what is registered versus built-but-unregistered, and 0.4's field-shell defects are closed by the five-control fold. A reader deciding whether a component is safe to adopt is reading the wrong answer. Second: FormField now marks only the requirement and leaves optional fields unmarked - (optional) was removed app-wide by design decision and is pinned by tests/ui-v2-form-field.dom.test.tsx - which is a design-system contract change that appears in no document. It belongs in COMPONENTS.md section 4 and probably DECISIONS.md. Next action: one docs PR updating section 0 from the actual mount list and recording the optionality rule with its rationale. Stop: do not re-add (optional) markers to satisfy a generic form-accessibility rule - the removal was deliberate and is test-pinned. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | | #234 | P3 | task | answer-copy-payload.ts is the single clipboard payload builder for three surfaces and has no documentation | src/lib/answer-copy-payload.ts arrived in PR-J exporting answerStateForAnswer, buildAnswerClipboardText, resolveAnswerSources, citedSourcesOnly and singleDocumentClipboardMetadata. It is now the one place three product surfaces build a clipboard payload, which makes it a contract rather than a helper: a future caller that bypasses it can reintroduce the false-attribution defect the module exists to prevent (see #228). Nothing in docs/design-system mentions it. Next action: document the module and its five exports where the answer surface's copy contract is described, and state that new copy paths route through it rather than composing their own text. Found during PR-J close-out, 2026-08-04. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | | #235 | P3 | task | ADOPTION.md section 7 proof shots exist for only four of the adopted surfaces | The adoption contract asks for a proof shot per adopted surface. The Wave 5 adoption captured four - DSM header, settings rows, patient panel, answer surface - and none for the forms fold, the catalogue and docs surfaces, the headers convergence, or the empty states adopted since. Section 7 therefore reads as complete while most of the adoption is unevidenced, which matters because the proof shot is what a later reader uses to tell an intended restyle from a regression (the #229 DSM eyebrow was almost rediscovered as a defect for exactly this reason). Next action: capture the missing shots against a warmed local server and attach them to section 7. Cheap and mechanical - no gate, no provider access. Stop: this is not the visual-baseline harness (#118) - do not commit Playwright snapshot PNGs or flip that job to blocking. | session 2026-08-04 (DS V2 Wave 5 close-out capture) | 2026-08-04 | @@ -499,3 +485,10 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #167 | issue | `verify:pr-local` exits 0 when its own build step refuses to run | Resolved 2026-08-09: guard-next-build now exits 76 when it refuses a build, verify:pr-local propagates the failed selected step, and its self-test plus focused contracts prove the aggregate cannot report green when the build never ran. | 2026-08-09 | | #204 | issue | npm 11.6.2 regenerates a lockfile its own `npm ci` rejects, reddening every CI job | Resolved 2026-08-09: verify:pr-local now selects npm ci --dry-run --ignore-scripts before broad checks whenever package.json or package-lock.json changes; plan self-tests and focused CLI contracts cover both paths. | 2026-08-09 | | #255 | issue | Remote/Cloud containers cannot run any browser gate: Playwright lock drift plus a missing Chromium build | Resolved 2026-08-09: the actual Playwright launch preflight checks the locked browser revision before an explicit executable override or production build in the download-disabled container, while docs/testing.md preserves CI delegation and the verified image-recovery recipe. | 2026-08-09 | +| #142 | task | Four loose dated docs need source and migration edits before they can be filed | Resolved 2026-08-12. Moved the three point-in-time reviews to `docs/audit/` and the completed July-8 operator record to `docs/archive/`; updated every source, test, schema/migration comment, runbook, and index reference, with link and migration guards retained. | 2026-08-12 | +| #151 | issue | `gh pr checks` cannot read CI, but the Actions API can | Resolved 2026-08-12. The Actions-run fallback for tokens without Checks permission is now a durable one-line rule in `AGENTS.md` under the CI-speed procedure. | 2026-08-12 | +| #154 | rec | Row ids are not stable identifiers for "did my change land" | Resolved 2026-08-12. `AGENTS.md` now requires completion proof from distinctive content and its recorded verification on the exact target ref, and explicitly rejects PR state, row ID, or row title as sufficient land proof. | 2026-08-12 | +| #156 | issue | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge | Resolved 2026-08-12 as a conservative allocation architecture decision. Sequential IDs remain display locators rather than canonical identity; all mutations use the guarded writer, GitHub Update branch is forbidden for this ledger, content plus verification proves completion, and the structural gate forces allocation collisions to fail loudly. No unsafe merge driver or history rewrite was introduced. | 2026-08-12 | +| #186 | task | Update #101 for shipped hydration parallelisation before any further canary work | Resolved 2026-08-12. Open row #101 now records PR #1474 hydration parallelisation as completed and lists only scope enumeration, typeahead caching, and universal-search coalescing, each behind the existing RAG flag and canary criteria. | 2026-08-12 | +| #187 | rec | Archive process-lesson rows #151 and #154 once a durable one-line note exists outside the ledger | Resolved 2026-08-12. Durable one-line rules now cover the Actions API fallback and content-based land proof in `AGENTS.md`; process-lesson rows #151 and #154 were archived through the guarded writer without deleting history. | 2026-08-12 | +| #232 | issue | The PR-J clinical governance review record describes a head that was never merged | Resolved 2026-08-12. Appended (without editing the prior row) a superseding branch-review-ledger record for final PR #1595 head `590eb6cfb229c5ae0f7a5025352fa871d8321521`, explicitly recording that the post-review delta was accepted without a second clinical-governance review. | 2026-08-12 | diff --git a/docs/privacy-impact-assessment.md b/docs/privacy-impact-assessment.md index bc2bfdc0db..0d34ce53d3 100644 --- a/docs/privacy-impact-assessment.md +++ b/docs/privacy-impact-assessment.md @@ -491,5 +491,5 @@ PIA-3 is mitigated by default-null answer logging. If exceptional non-production ever enabled, it remains governance-gated. The historical cleanup migration is prepared but unexecuted; this assessment does not claim live cleanup or legal approval. -See the companion **[tenancy defense-in-depth review](docs/tenancy-defense-in-depth-review.md)** for the +See the companion **[tenancy defense-in-depth review](docs/audit/tenancy-defense-in-depth-review.md)** for the cross-tenant isolation analysis referenced above. diff --git a/docs/process-hardening.md b/docs/process-hardening.md index a82bf2a304..d826f1a3e3 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -433,7 +433,7 @@ one-open-job index, R5 metadata deep-merge, and `retrieval_owner_matches` fail-c in `supabase/migrations/` but **not yet verified on live** as of 2026-07-09. **Do not redeploy the ingestion worker from current `main` until `20260708130000` is live** — `worker/main.ts` already passes `p_worker_id`. Ordered apply steps, R17 manual `CONCURRENTLY` index, and post-apply probes: -[`docs/operator-apply-july8-batch.md`](operator-apply-july8-batch.md) · `npm run check:july8-live-batch`. +[`docs/archive/operator-apply-july8-batch.md`](archive/operator-apply-july8-batch.md) · `npm run check:july8-live-batch`. ## PR merge gate: risk-scoped CI + required aggregate (2026-07-10) diff --git a/docs/prompts/codex-data-database-safety-ultra-review.md b/docs/prompts/codex-data-database-safety-ultra-review.md index 38b06cfebe..029a1c53d5 100644 --- a/docs/prompts/codex-data-database-safety-ultra-review.md +++ b/docs/prompts/codex-data-database-safety-ultra-review.md @@ -64,7 +64,7 @@ Locate and read these when present. Do not invent missing documents. Priority set: - `docs/codex-review-protocol.md` -- `docs/tenancy-defense-in-depth-review.md` +- `docs/audit/tenancy-defense-in-depth-review.md` - `docs/supabase-migration-reconciliation.md` - `docs/deployment-architecture.md` - `docs/process-hardening.md` diff --git a/docs/prompts/codex-performance-reliability-ultra-review.md b/docs/prompts/codex-performance-reliability-ultra-review.md index b0c47b79f7..9386e0376a 100644 --- a/docs/prompts/codex-performance-reliability-ultra-review.md +++ b/docs/prompts/codex-performance-reliability-ultra-review.md @@ -47,8 +47,8 @@ Priority set: - `docs/codex-review-protocol.md` - `docs/deployment-architecture.md` -- `docs/capacity-review.md` -- `docs/scale-readiness-review.md` +- `docs/audit/capacity-review.md` +- `docs/audit/scale-readiness-review.md` - `docs/operator-apply-performance-latency-remediation.md` - `docs/process-hardening.md` - `docs/search-chrome-behaviour.md` diff --git a/docs/staging-setup.md b/docs/staging-setup.md index 9516481f59..a1ed416e1a 100644 --- a/docs/staging-setup.md +++ b/docs/staging-setup.md @@ -2,7 +2,7 @@ Turnkey runbook for standing up a staging environment. Companion to `docs/deployment-architecture.md` §5 (why staging is a dedicated project, not a -prod branch) and `docs/capacity-review.md` §4 (the soak test that validates it). +prod branch) and `docs/audit/capacity-review.md` §4 (the soak test that validates it). Staging is two independent tiers: a **staging Supabase project** (data) and a **staging app host** (compute). Do the data tier first — the app needs it. @@ -132,7 +132,7 @@ Reuse the app image; only the environment variables differ. --confirm-staging --users 30 --duration-s 600 --ramp-s 120 ``` - Success targets are in `docs/capacity-review.md` §4 (search p95 ≤ 3 s, + Success targets are in `docs/audit/capacity-review.md` §4 (search p95 ≤ 3 s, answer p95 ≤ 25 s, non-429 error rate < 1 %). ## What is operator-only (cannot be scripted here) @@ -142,4 +142,4 @@ Reuse the app image; only the environment variables differ. variables). Writing admin credentials to Railway is an operator/authorized action. - Any change to the **production** project's settings (e.g. auth - percentage-based connection allocation — see `docs/capacity-review.md` §3). + percentage-based connection allocation — see `docs/audit/capacity-review.md` §3). diff --git a/docs/supabase-migration-reconciliation.md b/docs/supabase-migration-reconciliation.md index 67366a0607..7d1c94cb2b 100644 --- a/docs/supabase-migration-reconciliation.md +++ b/docs/supabase-migration-reconciliation.md @@ -145,5 +145,5 @@ Manual key rotation and live migration apply decisions are recorded in [`docs/archive/operator-decisions-2026-07-04.md`](archive/operator-decisions-2026-07-04.md) and [`docs/archive/operator-decisions-2026-07-06.md`](archive/operator-decisions-2026-07-06.md). The **July 8 ingestion & tenancy batch** (merged to `main`, pending live apply as of -2026-07-09) is in [`docs/operator-apply-july8-batch.md`](operator-apply-july8-batch.md). +2026-07-09) is in [`docs/archive/operator-apply-july8-batch.md`](archive/operator-apply-july8-batch.md). Do not execute live applies from CI or agent automation without explicit operator approval. diff --git a/docs/worker-deploy-runbook.md b/docs/worker-deploy-runbook.md index 1c36e296cd..3e223e6460 100644 --- a/docs/worker-deploy-runbook.md +++ b/docs/worker-deploy-runbook.md @@ -30,7 +30,7 @@ applied to the live project _before_ the worker is (re)deployed from current `complete_strict_enrichment_job`). Deploying the new worker against a database that has not yet taken this migration means the R1/R2 lease fences the worker relies on are not present. See -[`operator-apply-july8-batch.md`](operator-apply-july8-batch.md) (step 2: +[`operator-apply-july8-batch.md`](archive/operator-apply-july8-batch.md) (step 2: _"apply before worker redeploy"_) for the ordered apply plan. Confirm the gate before continuing: diff --git a/scripts/archive/check-july8-live-batch.ts b/scripts/archive/check-july8-live-batch.ts index 233b11168e..9472afdd7e 100644 --- a/scripts/archive/check-july8-live-batch.ts +++ b/scripts/archive/check-july8-live-batch.ts @@ -369,7 +369,7 @@ async function main() { if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((error) => { console.error("[July8 Live Batch] FAIL:", error instanceof Error ? error.message : error); - console.error("[July8 Live Batch] Runbook: docs/operator-apply-july8-batch.md"); + console.error("[July8 Live Batch] Runbook: docs/archive/operator-apply-july8-batch.md"); process.exit(1); }); } diff --git a/scripts/check-owner-scope-api.mjs b/scripts/check-owner-scope-api.mjs index 8ef3c806ee..966b23c306 100644 --- a/scripts/check-owner-scope-api.mjs +++ b/scripts/check-owner-scope-api.mjs @@ -3,7 +3,7 @@ // // The app is a deliberately single-layer tenancy design: every API route uses the // service-role Supabase client (RLS bypassed) and enforces ownership in application -// code via an `owner_id` filter. `docs/tenancy-defense-in-depth-review.md` verified +// code via an `owner_id` filter. `docs/audit/tenancy-defense-in-depth-review.md` verified // 0/33 route gaps, but flagged (§6 item 2) that a *future* handler dropping the owner // filter is the single regression class this design is exposed to. // @@ -44,26 +44,26 @@ const SCOPE_TOKENS = [ // Intentional exceptions: a handler that queries an owner-scoped table where ownership // is enforced indirectly (e.g. the query filters by document ids that were themselves // fetched under an owner scope). Each entry needs a reason and a reviewer sign-off in -// docs/tenancy-defense-in-depth-review.md. Keep this list empty unless a real, reviewed +// docs/audit/tenancy-defense-in-depth-review.md. Keep this list empty unless a real, reviewed // indirect-scope pattern exists — a forgotten filter must NOT be silenced here. export const OWNER_SCOPE_ALLOWLIST = [ { file: "src/app/api/setup-status/route.ts", table: "documents", reason: - "Global setup/health diagnostic: a `.limit(1)` existence probe (is any document indexed?), not an owner-data read. The route is gated to local origin and returns only status booleans — see docs/tenancy-defense-in-depth-review.md §3 (setup-status row / TEN-N1).", + "Global setup/health diagnostic: a `.limit(1)` existence probe (is any document indexed?), not an owner-data read. The route is gated to local origin and returns only status booleans — see docs/audit/tenancy-defense-in-depth-review.md §3 (setup-status row / TEN-N1).", }, { file: "src/app/api/setup-status/route.ts", table: "import_batches", reason: - "Global setup/health diagnostic: a `.limit(1)` existence probe for schema provisioning, not an owner-data read. Same local-origin-gated status route — see docs/tenancy-defense-in-depth-review.md §3 (TEN-N1).", + "Global setup/health diagnostic: a `.limit(1)` existence probe for schema provisioning, not an owner-data read. Same local-origin-gated status route — see docs/audit/tenancy-defense-in-depth-review.md §3 (TEN-N1).", }, { file: "src/app/api/setup-status/route.ts", table: "storage_cleanup_jobs", reason: - "Global setup/health diagnostic: a head-count existence probe for pending cleanup rows (schema/ops posture), not an owner-data read. Same local-origin-gated status route — see docs/tenancy-defense-in-depth-review.md §3 (TEN-N1).", + "Global setup/health diagnostic: a head-count existence probe for pending cleanup rows (schema/ops posture), not an owner-data read. Same local-origin-gated status route — see docs/audit/tenancy-defense-in-depth-review.md §3 (TEN-N1).", }, ]; diff --git a/scripts/check-retrieval-owner-migration.ts b/scripts/check-retrieval-owner-migration.ts index 72b25bee8f..875ebe56e3 100644 --- a/scripts/check-retrieval-owner-migration.ts +++ b/scripts/check-retrieval-owner-migration.ts @@ -50,7 +50,7 @@ async function main() { if (nullFilterCheck === true) { console.error("[Retrieval Owner Migration] FAIL: retrieval_owner_matches is still fail-OPEN on NULL owner_filter."); console.error( - "Apply 20260708160001_retrieval_owner_matches_fail_closed.sql — see docs/operator-apply-july8-batch.md", + "Apply 20260708160001_retrieval_owner_matches_fail_closed.sql — see docs/archive/operator-apply-july8-batch.md", ); process.exit(1); } diff --git a/scripts/soak-test.ts b/scripts/soak-test.ts index 37df6589d9..792ae2f325 100644 --- a/scripts/soak-test.ts +++ b/scripts/soak-test.ts @@ -2,7 +2,7 @@ * Ward-round soak test for the Clinical KB app tier. * * STAGING ONLY. This script drives sustained answer/search load and must never - * point at production. See docs/capacity-review.md §4 for the load model, + * point at production. See docs/audit/capacity-review.md §4 for the load model, * usage examples, and success criteria. * * Safety rails: @@ -267,7 +267,7 @@ async function main() { console.error("FAIL: non-429 failure rate exceeded 5%."); process.exit(1); } - console.log("PASS: failure rate within budget. Compare percentiles against docs/capacity-review.md §4."); + console.log("PASS: failure rate within budget. Compare percentiles against docs/audit/capacity-review.md §4."); } main().catch((error) => { diff --git a/src/lib/owner-scope.ts b/src/lib/owner-scope.ts index a6233eecad..1ecb525b4e 100644 --- a/src/lib/owner-scope.ts +++ b/src/lib/owner-scope.ts @@ -45,7 +45,7 @@ export function retrievalRpcScopeArgs(scope: RetrievalAccessScope) { // rows). Return the public sentinel instead, so these modes see only the shared // public (null-owner) corpus. Combined with the DB fail-closed change (migration // 20260708160001_retrieval_owner_matches_fail_closed), no legitimate caller ever -// passes NULL. See docs/tenancy-defense-in-depth-review.md §6. +// passes NULL. See docs/audit/tenancy-defense-in-depth-review.md §6. export function requireOwnerScope(ownerId: string | null | undefined): string { if (ownerId) return ownerId; if (isDemoMode() || isLocalNoAuthMode() || process.env.NODE_ENV === "test") { diff --git a/supabase/migrations/20260708160000_ingestion_jobs_one_open_per_document.sql b/supabase/migrations/20260708160000_ingestion_jobs_one_open_per_document.sql index c12dbbca0a..abc50a06cc 100644 --- a/supabase/migrations/20260708160000_ingestion_jobs_one_open_per_document.sql +++ b/supabase/migrations/20260708160000_ingestion_jobs_one_open_per_document.sql @@ -5,7 +5,7 @@ -- preview sync with "Remote migration versions not found in local migrations -- directory." The transactional index creation lives in -- 20260708170000_ingestion_jobs_one_open_per_document.sql; on live, prefer the --- lock-free CONCURRENTLY path in docs/operator-apply-july8-batch.md when the +-- lock-free CONCURRENTLY path in docs/archive/operator-apply-july8-batch.md when the -- queue is idle, then repair history to match effect. select 1 where false; diff --git a/supabase/migrations/20260708160001_retrieval_owner_matches_fail_closed.sql b/supabase/migrations/20260708160001_retrieval_owner_matches_fail_closed.sql index 5851ebccdd..6749a5ff40 100644 --- a/supabase/migrations/20260708160001_retrieval_owner_matches_fail_closed.sql +++ b/supabase/migrations/20260708160001_retrieval_owner_matches_fail_closed.sql @@ -1,4 +1,4 @@ --- Tenancy defense-in-depth (docs/tenancy-defense-in-depth-review.md §6, item 1). +-- Tenancy defense-in-depth (docs/audit/tenancy-defense-in-depth-review.md §6, item 1). -- -- retrieval_owner_matches(owner_filter, row_owner_id) previously returned TRUE for -- EVERY row when owner_filter IS NULL (fail-open). Because RLS is service-role-only, diff --git a/supabase/migrations/20260708170000_ingestion_jobs_one_open_per_document.sql b/supabase/migrations/20260708170000_ingestion_jobs_one_open_per_document.sql index 3ad5933509..3c08701958 100644 --- a/supabase/migrations/20260708170000_ingestion_jobs_one_open_per_document.sql +++ b/supabase/migrations/20260708170000_ingestion_jobs_one_open_per_document.sql @@ -12,7 +12,7 @@ -- Before applying on live, confirm `npm run reindex:health` reports -- jobs_pending = 0 and jobs_processing = 0 (live had 0/0 as of 2026-07-08). -- On a busy queue, prefer the lock-free form from --- docs/operator-apply-july8-batch.md (CREATE INDEX CONCURRENTLY outside a +-- docs/archive/operator-apply-july8-batch.md (CREATE INDEX CONCURRENTLY outside a -- transaction), then `supabase migration repair --linked --status applied -- 20260708170000`. -- diff --git a/supabase/migrations/20260717120000_corrector_public_titles_only.sql b/supabase/migrations/20260717120000_corrector_public_titles_only.sql index 46d0629202..f29b61c6dc 100644 --- a/supabase/migrations/20260717120000_corrector_public_titles_only.sql +++ b/supabase/migrations/20260717120000_corrector_public_titles_only.sql @@ -4,7 +4,7 @@ -- It previously built its spell-correction vocabulary from EVERY indexed document -- title AND every enabled rag_aliases row regardless of owner, while the rest of -- retrieval is strictly owner-scoped and fail-closed (see --- docs/tenancy-defense-in-depth-review.md and retrieval_owner_matches). Both +-- docs/audit/tenancy-defense-in-depth-review.md and retrieval_owner_matches). Both -- rag_aliases (owner_id column; deep-memory.ts persists owner-scoped aliases and -- canonicals for private documents) and documents carry private rows, so the unscoped -- reads let a private tenant's terms bias — and, via observable query rewriting, leak diff --git a/supabase/migrations/20260724120000_table_facts_plpgsql_execute.sql b/supabase/migrations/20260724120000_table_facts_plpgsql_execute.sql index 1168cb50cb..33e36acf25 100644 --- a/supabase/migrations/20260724120000_table_facts_plpgsql_execute.sql +++ b/supabase/migrations/20260724120000_table_facts_plpgsql_execute.sql @@ -2,7 +2,7 @@ -- the actual bound parameter values each call. LANGUAGE sql + SET search_path -- made the body non-inlinable, so Postgres cached a generic plan that could not -- estimate trigram/tsv selectivity for unknown $1 and paid ~1–5s instead of --- ~70ms (see docs/scale-readiness-review.md F1). Dynamic EXECUTE forces a +-- ~70ms (see docs/audit/scale-readiness-review.md F1). Dynamic EXECUTE forces a -- one-shot custom plan. RAG impact: no retrieval behaviour change — result rows -- and ordering predicates are unchanged. diff --git a/supabase/schema.sql b/supabase/schema.sql index f6d913b029..0b6a2844f4 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -817,7 +817,7 @@ create index if not exists ingestion_jobs_document_status_idx -- more than one open job per document. Migration -- 20260708170000_ingestion_jobs_one_open_per_document.sql applies the same -- index transactionally via `db push`; operators on a busy queue may use the --- CONCURRENTLY variant documented in docs/operator-apply-july8-batch.md instead. +-- CONCURRENTLY variant documented in docs/archive/operator-apply-july8-batch.md instead. create unique index if not exists ingestion_jobs_one_open_per_document_uidx on public.ingestion_jobs(document_id) where status in ('pending', 'processing'); diff --git a/tests/owner-scope-guard.test.ts b/tests/owner-scope-guard.test.ts index ba2bf22545..cb56e30bf1 100644 --- a/tests/owner-scope-guard.test.ts +++ b/tests/owner-scope-guard.test.ts @@ -134,7 +134,7 @@ describe("owner-scope tenancy guard", () => { it("documents every allowlist exception explicitly in the tenancy review", () => { // The review doc must list each exact file/table pair (not just a reason string mentioning // the doc), so a new bypass entry cannot pass without the reviewer actually recording it. - const review = readFileSync("docs/tenancy-defense-in-depth-review.md", "utf8"); + const review = readFileSync("docs/audit/tenancy-defense-in-depth-review.md", "utf8"); const lines = review.split("\n"); for (const entry of OWNER_SCOPE_ALLOWLIST) { expect(entry.file).toMatch(/^src\/app\/api\//); From fb69cdb52b62da23970735b04b9a73bea4b4e17e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Aug 2026 07:48:58 +0000 Subject: [PATCH 2/3] fix(issues): restore #156 as open, correct #101 visual hydration claim, fix #186 archive - Restore #156 (read-modify-write race + Update-branch corruption) from archive back to the open issues section with its original full text; the underlying allocation race is not resolved by the conservative architecture decision described in the previous commit. - Re-add #156 to the recommendations priority table as row 51 (between #101 and #172, consistent with its original ordering). - Correct #101 to credit only metadata and memory parallelisation (hydrateCandidatesWithMetadataAndMemory) from PR #1474; visual hydration (attachPageVisualEvidence) is still called serially after hydration on all six call sites and is explicitly listed as a remaining candidate. - Update #186 archive text to reflect that only metadata+memory were updated in #101, not visual hydration. Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com> --- docs/outstanding-issues.md | 115 +++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 3a68886de5..4c440725c1 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -103,60 +103,61 @@ removed after current-main verification; it is not missing recommended work. | 48 | `#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… | | 49 | `#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… | | 50 | `#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.… | -| 51 | `#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. | -| 52 | `#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. | -| 53 | `#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. | -| 54 | `#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. | -| 55 | `#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. | -| 56 | `#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 … | -| 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 | `#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. | -| 75 | `#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. | -| 76 | `#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. | -| 77 | `#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. | -| 78 | `#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. | -| 79 | `#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. | -| 80 | `#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. | -| 81 | `#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. | -| 82 | `#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. | -| 83 | `#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. | -| 84 | `#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. | -| 85 | `#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. | -| 86 | `#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. | -| 87 | `#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. | -| 88 | `#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. | -| 89 | `#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. | -| 90 | `#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. | -| 91 | `#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. | -| 92 | `#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. | -| 93 | `#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. | -| 94 | `#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. | -| 95 | `#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. | -| 96 | `#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. | -| 97 | `#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. | -| 98 | `#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. | -| 99 | `#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). | -| 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 | `#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. | -| 102 | `#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. | -| 103 | `#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. | -| 104 | `#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. | +| 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 | `#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. | +| 53 | `#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. | +| 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 | `#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 … | +| 58 | `#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`. | +| 59 | `#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. | +| 60 | `#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. | +| 61 | `#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. | +| 62 | `#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. | +| 63 | `#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. | +| 64 | `#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. | +| 65 | `#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. | +| 66 | `#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. | +| 67 | `#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. | +| 68 | `#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. | +| 69 | `#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. | +| 70 | `#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. | +| 71 | `#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. | +| 72 | `#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. | +| 73 | `#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. | +| 74 | `#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. | +| 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 | `#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). | +| 101 | `#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. | +| 102 | `#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. | +| 103 | `#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. | +| 104 | `#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. | +| 105 | `#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. | @@ -202,7 +203,7 @@ removed after current-main verification; it is not missing recommended work. | #098 | P2 | task | Offline round-trip budget harness for the hot routes | **Outcome:** per-scenario Supabase round-trip counts are pinned by a test, so an extra round trip on a hot path is a red gate rather than an inference. **Done 2026-07-29:** the measurement gap is closed — `Server-Timing` now covers `auth`/`ratelimit`/`scope` on `/api/answer`, `auth`/`ratelimit`/`search`/`total` on `/api/search`, and `auth`/`ratelimit` on `/api/answer/stream` (previously the route the UI actually calls emitted no header at all). Headers flush before the first SSE frame, so in-stream stages cannot reach a header and must NOT be routed through the governed `progress`/`final` contract. `tests/answer-route-preamble.test.ts` pins admission-before-scope (no scope call while the limiter is pending or after a deny) and the client-disconnect abort signal. **Done 2026-07-30 (PR #1450, `1bff4c78`):** the counting proxy exists and the answer path is budgeted. `tests/helpers/supabase-round-trip-counter.ts` counts on **execution, not construction** — a builder that is never awaited costs zero, one awaited twice costs two — which is the distinction that makes the count mean "requests issued". `tests/rag-round-trip-budget.test.ts` pins two offline answer-path scenarios (a single-source source-only answer, and that trips do not scale with the number of retrieved sources) plus three self-tests of the counter, and is registered in `scripts/fixtures/rag-offline-contract-tests.json` so it runs inside the offline contract rather than only on demand. Verified locally, provider-free: `Test Files 1 passed (1)`, `Tests 5 passed (5)`. Its documented blind spot is worth repeating before anyone cites a budget as total cost: it sees only traffic through the wrapped client, so a trip issued via another client instance, a direct `fetch`, or a provider SDK is invisible to it. **Done 2026-07-30 (search *retrieval core*, not the endpoint):** `tests/search-round-trip-budget.test.ts` pins `searchChunksWithTelemetry` — what `/api/search` calls to retrieve — registered in both the contract fixture and `scripts/rag-offline-contract.mjs`. **Corrected after Codex review on PR #1464:** an earlier version of this row and the test itself claimed to pin `/api/search`. They do not. The route's auth, rate limiting, scope resolution, related-document enrichment and telemetry write are all invisible to this suite, so a round trip added to any of them leaves it green — and the refusal budget below is about *retrieval*, not about an adversarial HTTP request, which still pays the route preamble. **The measured shape is itself the finding:** one search costs **11 round trips** — `rag_aliases` 1, `match_document_chunks_text_v2` **3**, `match_document_table_facts_text_v2` **3**, `get_related_document_metadata_v2` 1, `document_index_quality` 1, `document_images` 2 — so the two text RPCs are each issued three times per search. Pinned by total *and* breakdown, because a refactor swapping one probe for an unrelated query would keep the total at 11 while changing the traffic. Deterministic across three consecutive runs. The refusal budget asserts **zero** Supabase traffic, matching `rag.ts`'s claim that prompt-injection intent is refused before any query issues, and was proven against the broken shape: with a non-refused query it fails on the round-trip assertion (`expected 11 to be +0`), which is why that assertion is ordered ahead of the results assertion. **Note 2026-07-30 (corrected):** a work-branch experiment on `origin/work` (`1f52c704`, reverted in `a0cd00ba`) collapsed both text surfaces offline (budget 11→7) but never reached `main` and was never canaried on those SHAs. Live canaries `30579804611` and `30580564419` ran on unrelated `main` docs commits (`fde68ed4` / `4312a214`) and must not be cited as probe-collapse evidence. Next (b) remains open and still needs a real canary pair before any collapse. **Next:** (a) add the route-level budget this suite does not provide — drive `POST` from `src/app/api/search/route.ts` with counted clients, following the `tests/answer-route-preamble.test.ts` pattern, so a round trip added to the route preamble or post-processing is a red gate; (b) do **not** wholesale-collapse the ×3 sibling lexical variants as the next step — the offline collapse on `origin/work` never landed on `main` with a canary on those SHAs (see Note above), so that avenue is not an approved follow-up; any later latency work must use a materially different approach (for example overlap/parallelism that preserves all three variants) or a real canary pair on the changed tip under the usual RAG gate. (c) `scripts/eval-rag-offline.mjs` and `scripts/test-rag-offline.mjs` remain unwired; the offline contract runner is now the de-facto single home for budgets, so either adopt that explicitly here or wire them. | `docs/audit/latency-audit-2026-07-28.md` measurement plan; `src/lib/server-timing.ts`; `src/lib/answer-stream-contract.ts:18-21` | 2026-07-29 | | #099 | P2 | task | Remove the remaining fixed per-request round trips | **Outcome:** the answer path stops paying avoidable per-request Supabase round trips. **Done 2026-07-29:** shared-cache-hit promotion deferred off the response path with its mid-request staleness guard intact and documented (`rag.ts:3234`, `rag-cache.ts`); scope resolution overlapped with the rate-limit RPC, signal threaded so a client disconnect finally cancels its paginated queries (`answer/route.ts`). **REFUTED on PR #1377 review — do not retry:** the same pass also overlapped scope with the rate-limit RPC and aborted it on deny, claiming the limiter could "deny for free". It cannot. With caller-supplied `filters` or explicit ids, scope passes its zero-query early returns (`search-scope.ts:242,253`) into the paginated `documents` loop at `:269`, and an `AbortSignal` cancels the client request without un-executing a statement Postgres already began — so throttled traffic kept burning database capacity while collecting 429s, against `capacity-review.md:106-113`'s first-soft-failure warning. Scope is behind admission again, pinned by `tests/answer-route-preamble.test.ts`. Re-attempting the overlap requires a non-database admission gate ahead of the durable limiter first. **Remaining:** (a) the 8 `setCachedSearch` awaits — deferring changes `throwIfAborted` semantics and widens a real mutation window because the clone happens after an `await`, so each branch needs discharging individually; (b) batch the anonymous subject+global rate-limit pair, which needs a NEW atomic RPC modelled on `consume_summary_rate_limits_atomic` and cannot be called until the operator applies it — `Promise.all` is the WRONG fix because it consumes the global bucket even when the subject bucket already denied; (c) stop the proxy and route handler resolving identity twice per authenticated request — no in-process memo can do this (different `Request` objects), so the proxy must forward unspoofable verified claims via a header it controls. Cross-references #011: halving auth resolutions eases the ~10-connection Auth cap that `capacity-review.md:106-113` calls the first hard failure. | `docs/audit/latency-audit-2026-07-28.md` L1-1/L1-3/L1-4; `src/lib/api-rate-limit.ts:276-282`; `src/proxy.ts:125` | 2026-07-29 | | #100 | P2 | rec | Buffered answer generation has no incremental verified delivery | **Design complete; runtime work remains provider-gated.** [`verified-answer-incremental-delivery-design.md`](verified-answer-incremental-delivery-design.md) records the clinical-governance decision and staged contract: keep the `progress`/`final`/`error` allowlist; disclose bounded, owner-scoped evidence only after the canonical danger-level source-governance refusal permits it, then emit complete answer sections only after each reuses the full production verification boundary; reconcile every preview byte-for-byte with the authoritative `final`; discard all previews on error/cancel/retry; deploy behind separate parse/emission/render flags. Phase 0 contract proof and Phase 1 evidence preview can be developed offline, but visible rollout still needs clinical/browser proof. Phase 2 changes generation architecture and requires explicit approval for answer-quality evals plus a baseline/post live canary pair. **Naive token streaming remains REFUTED:** never re-land `token`, `revising`, provisional prose, or a weaker stream-only verifier. Cross-references #021. | `docs/verified-answer-incremental-delivery-design.md`; `docs/audit/latency-audit-2026-07-28.md` L0-1; `src/lib/answer-stream-contract.ts:18-21` | 2026-07-30 | -| #101 | P3 | rec | Canary-gated retrieval parallelisation candidates | **Outcome:** remaining retrieval parallelisation candidates are explicit after PR #1474 shipped the metadata, memory, and visual hydration triples. **Remaining:** nested `await`-in-loop scope enumeration (`search-scope.ts:202,328`), uncached typeahead results (`rag.ts:2698-2711`), and universal-search coalescing (`/api/search` has it; `/api/search/universal` does not). Each changes candidate assembly, truncation, or what the next keystroke returns, so each requires the #098 harness, the RAG flag, explicit live-canary approval, 36/36 retrieval, recall 1.0, and zero per-case reciprocal-rank regressions. Distinct from #001 (semantic rerank). **Completed:** PR #1474 parallelised the repeated hydration triples; do not propose that work again. **Stop:** no remaining candidate proceeds without its canary gate. | `docs/audit/latency-audit-2026-07-28.md` L2-1/L2-2/L2-8/L1-5; PR #1474 | 2026-07-29 | +| #101 | P3 | rec | Canary-gated retrieval parallelisation candidates | **Outcome:** remaining retrieval parallelisation candidates are explicit after PR #1474 shipped the metadata and memory parallelisation. **Remaining:** visual hydration triples (each migrated path still calls `attachPageVisualEvidence` after hydration — not yet parallelised, see `rag.ts:1442,1811,1857,1959,2194,2281`); nested `await`-in-loop scope enumeration (`search-scope.ts:202,328`); uncached typeahead results (`rag.ts:2698-2711`); and universal-search coalescing (`/api/search` has it; `/api/search/universal` does not). Each changes candidate assembly, truncation, or what the next keystroke returns, so each requires the #098 harness, the RAG flag, explicit live-canary approval, 36/36 retrieval, recall 1.0, and zero per-case reciprocal-rank regressions. Distinct from #001 (semantic rerank). **Completed:** PR #1474 parallelised metadata and memory (`hydrateCandidatesWithMetadataAndMemory`); do not propose that specific change again. **Stop:** no remaining candidate proceeds without its canary gate. | `docs/audit/latency-audit-2026-07-28.md` L2-1/L2-2/L2-8/L1-5; PR #1474 | 2026-07-29 | | #102 | P3 | task | Apply the additive `documents` index debt (operator) | **Outcome:** bare-column `ILIKE` and the paged status scan on `documents` are index-served on hosted. `documents_title_trgm_idx` indexes a CONCATENATED expression, so the bare-column predicates in `api/documents/route.ts:193` and `rag-candidate-sources.ts:477` (RAG path) cannot use it and fall back to scanning; `search-scope.ts:271-277` sorts per page against the single-column `documents_status_idx`. **Runbook prepared 2026-07-29 — NOT applied, item stays open:** three `CREATE INDEX CONCURRENTLY` statements authored and reviewed in `docs/operator-apply-performance-latency-remediation.md` — additive, though **the "recall is byte-identical" claim was RETRACTED on 2026-07-29 review**: `fetchDocumentTitleAliasRows` (`rag-candidate-sources.ts:482`) applies `.limit(12)` with no `ORDER BY`, so a new index can change which title-alias documents feed candidate assembly. Only the documents-list use stays ordering-safe; `(status,id)` is canary-gated too — see runbook, and making that `.limit(12)` deterministic first does **not** lift the gate — an unordered `LIMIT` has no stable selection to preserve, so imposing an order can pick a different twelve and is itself an ordering behaviour change on a retrieval surface, which AGENTS.md requires a canary pair for. Sequencing the ordering fix first is worthwhile (unordered `LIMIT` on a retrieval input is latent nondeterminism regardless) but yields two canary-gated changes, not one (PR #1377 review). **Deliberately NO migration file:** an additive-index migration without a synchronized `schema.sql` mirror and regenerated drift manifest is exactly what closed PR #1312, and the mirror cannot come first because `required_indexes` in `search_schema_health()` (`schema.sql:3178`) runs against live. **Next (operator):** **author the migration first** — `supabase/migrations/` is the source of truth and `schema.sql` only a mirror, so hand-run operator SQL never reaches staging, disaster-recovery replay, or a local `supabase db reset`, and a `required_indexes` registration would fail there (PR #1377 review); follow the `20260717170000_registry_projection_cleanup.sql` idempotent pattern. **That migration must also carry the health-function change** — `required_indexes` lives inside `search_schema_health()`, which is redefined by `create or replace function` in eleven migrations (copy `20260705180000_reconcile_search_health_indexes.sql:62`); editing `schema.sql:3177` alone moves only the mirror and leaves the indexes unmonitored on hosted (PR #1377 review). Then apply concurrently, confirm `indisvalid`, mirror both the index statements and the identical function body into `schema.sql`, run `npm run drift:manifest` (Docker), and deploy the migration LAST — in that order, in one change. Expect `check:drift` to report them as unexpected between steps 1 and 2. **Rollback is three deployed phases, not the reverse of one:** retract `required_indexes` via its own `create or replace function` migration and deploy → drop concurrently live → only then deploy the `schema.sql` removal plus an idempotent forward `drop index if exists` migration, because Supabase wraps migrations in a transaction and a plain `DROP INDEX` there takes the lock the concurrent procedure exists to avoid (PR #1377 review). | `docs/audit/latency-audit-2026-07-28.md` L2-3/L2-5; `docs/operator-apply-performance-latency-remediation.md` | 2026-07-29 | | #117 | P2 | rec | Therapy Compass catalogue payload is the mobile LCP outlier | **Outcome:** `/therapy-compass` mobile LCP lands near the other mobile routes instead of double them. **Measured 2026-07-30** by the new pre-merge Lighthouse budget: mobile LCP 5229 ms, TBT 612 ms, CLS 0.142, against 2123-2460 ms on every other mobile route and 826 ms on desktop — so it is client-side work under mobile CPU/network throttling, not server latency. **Cause before this PR:** `useTherapyData` fetched `public/therapy-compass-data/therapies-index.json` (the thin browse index; 205 records) for the home/search/pathways screens, so the download plus JSON parse sat on the critical path before content painted. **Current split:** home now fetches `public/therapy-compass-data/therapies-home.211dab554c4ec62d.json` (136,288 bytes raw), pathways use the thin browse index, and search loads the full prose corpus (#1471). 90% of the index weight is long-form clinical prose — indications 159 KB (26%), contraindicationsOrCautions 139 KB (23%), bestUsedFor 73 KB (12%), clinicalSummary 67 KB (11%), patientPopulation 59 KB (10%), targetSymptoms 48 KB (8%) — while name, slug, category, tags and setting together are 54 KB (7%). **Remaining decision for search/pathways: rendered on the card, matched by search, or neither.** `therapy-card.tsx` references five of those prose fields and the same index feeds the search screen, so stripping fields could silently change clinical display or search recall. **Next:** settle that per-field question, then either pre-truncate prose that only feeds card display, or move search matching server-side / load prose on first keystroke. **Gate:** `check:therapy-data-index` plus the therapy Playwright journeys; re-measure with `npm run verify:lighthouse`. **Stop:** do not drop a field from the catalogue payload without confirming no card renders it and no search path matches on it. Same class as #013 (route-chunk / catalogue JSON weight), different route and now measured. | session 2026-07-30 Lighthouse budget first run; PR #1404; PR #1489 | 2026-07-30 | | #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 | @@ -211,6 +212,7 @@ removed after current-main verification; it is not missing recommended work. | #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 | | #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 | | #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 | +| #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 | | #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 | @@ -488,7 +490,6 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #142 | task | Four loose dated docs need source and migration edits before they can be filed | Resolved 2026-08-12. Moved the three point-in-time reviews to `docs/audit/` and the completed July-8 operator record to `docs/archive/`; updated every source, test, schema/migration comment, runbook, and index reference, with link and migration guards retained. | 2026-08-12 | | #151 | issue | `gh pr checks` cannot read CI, but the Actions API can | Resolved 2026-08-12. The Actions-run fallback for tokens without Checks permission is now a durable one-line rule in `AGENTS.md` under the CI-speed procedure. | 2026-08-12 | | #154 | rec | Row ids are not stable identifiers for "did my change land" | Resolved 2026-08-12. `AGENTS.md` now requires completion proof from distinctive content and its recorded verification on the exact target ref, and explicitly rejects PR state, row ID, or row title as sufficient land proof. | 2026-08-12 | -| #156 | issue | Outstanding-issues ids are still allocated read-modify-write, and Update-branch corrupts the merge | Resolved 2026-08-12 as a conservative allocation architecture decision. Sequential IDs remain display locators rather than canonical identity; all mutations use the guarded writer, GitHub Update branch is forbidden for this ledger, content plus verification proves completion, and the structural gate forces allocation collisions to fail loudly. No unsafe merge driver or history rewrite was introduced. | 2026-08-12 | -| #186 | task | Update #101 for shipped hydration parallelisation before any further canary work | Resolved 2026-08-12. Open row #101 now records PR #1474 hydration parallelisation as completed and lists only scope enumeration, typeahead caching, and universal-search coalescing, each behind the existing RAG flag and canary criteria. | 2026-08-12 | +| #186 | task | Update #101 for shipped hydration parallelisation before any further canary work | Resolved 2026-08-12. Open row #101 now correctly credits PR #1474 metadata and memory parallelisation (`hydrateCandidatesWithMetadataAndMemory`) as completed; visual hydration (`attachPageVisualEvidence`) is not yet parallelised and remains a candidate alongside scope enumeration, typeahead caching, and universal-search coalescing, each behind the existing RAG flag and canary criteria. | 2026-08-12 | | #187 | rec | Archive process-lesson rows #151 and #154 once a durable one-line note exists outside the ledger | Resolved 2026-08-12. Durable one-line rules now cover the Actions API fallback and content-based land proof in `AGENTS.md`; process-lesson rows #151 and #154 were archived through the guarded writer without deleting history. | 2026-08-12 | | #232 | issue | The PR-J clinical governance review record describes a head that was never merged | Resolved 2026-08-12. Appended (without editing the prior row) a superseding branch-review-ledger record for final PR #1595 head `590eb6cfb229c5ae0f7a5025352fa871d8321521`, explicitly recording that the post-review delta was accepted without a second clinical-governance review. | 2026-08-12 | From fe46e5ade8018d21cf15d149711579b1b43c7fb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:05:20 +0000 Subject: [PATCH 3/3] chore: add PR_POLICY_BODY.md for clinical governance preflight sync --- PR_POLICY_BODY.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 PR_POLICY_BODY.md diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md new file mode 100644 index 0000000000..d5df12d617 --- /dev/null +++ b/PR_POLICY_BODY.md @@ -0,0 +1,30 @@ +### Motivation + +- Consolidate and file dated point-in-time reviews into the audit/archive areas so `docs/README.md` conventions are honoured and loose top-level dated docs do not mislead source/reference consumers. +- Make ledger outcomes and process lessons durable and verifiable (archive rows instead of deleting history) so agents and humans can rely on content+verification rather than fragile row ids or titles. +- Prevent repeated proposals of already-landed work by updating the `#101` retrieval-parallelisation row to reflect shipped hydration parallelisation and keep only true canary-gated candidates. +- Preserve append-only ledger invariants by adding a superseding branch-review entry for the PR-J clinical-governance review so the ledger records what was actually merged without editing prior rows. + +### Description + +- Moved several dated docs into their canonical locations under `docs/audit/` or `docs/archive/` and updated inbound references to those paths (notably the capacity/scale and tenancy audit docs). +- Added durable guidance to `AGENTS.md` requiring the Actions-run fallback when `gh pr checks` lacks Checks permission and clarifying that outstanding-issue IDs are display locators only and completion must be proven by distinctive content + verification on the exact ref. +- Updated the universal ledger with guarded writer scripts: archived rows `#142`, `#154`, `#156`, `#186`, `#187`, and `#232` in `docs/outstanding-issues.md` through the repository `issues` writer, and updated `#101` to list only remaining canary-gated candidates after PR #1474. +- Appended a superseding, non-destructive branch-review record for PR-J (final head `590eb6cfb229c5ae0f7a5025352fa871d8321521`) into `docs/branch-review-ledger.md` using the ledger tool rather than editing the existing row, and updated script references/docs to point at the moved audit/archive files where appropriate. + +### Testing + +- Ran formatting and verification commands and observed green results: `npm run format` (Prettier write), `npm run check:outstanding-issues`, and `npm run check:branch-review-ledger` all passed. +- Ran docs and inventory checks: `npm run docs:check-links` and `npm run docs:check-inventory` passed and `npm run docs:check-index` confirmed documented mappings; `npm run design-system:adoption:update` also ran successfully to refresh adoption manifests. +- Ran gateway/verifier scripts and repository checks: `npm run check:migration-role` passed and `git diff --check` / `git diff --cached --check` reported no whitespace or merge-marker problems; no unrelated file churn was introduced and the working tree was left consistent with the scoped edits. + +------ +[Codex Task](https://chatgpt.com/codex/cloud/tasks/task_e_6a7c06b0f8608322bccdef26197fb9d8) + +## Clinical Governance Preflight + + + +_Note: The only `supabase/` change is `20260724120000_table_facts_plpgsql_execute.sql`, a performance-only migration adding `force_custom_plan` to avoid generic cached plans — no result-set, RLS, schema, or clinical-logic changes._ + +RAG impact: no retrieval behaviour change — this PR is ledger/docs hygiene and a performance-only SQL plan hint only.