diff --git a/docs/audit-handover-2026-07-14.md b/docs/audit-handover-2026-07-14.md new file mode 100644 index 000000000..42579a98a --- /dev/null +++ b/docs/audit-handover-2026-07-14.md @@ -0,0 +1,257 @@ +# Handover — repository audit findings (2026-07-14) + +**Purpose:** Single handoff artifact for the multi-skill repository audit. Use this to continue +remediation without re-running discovery. + +| Field | Value | +| ---------------- | -------------------------------------------------------------------------------------------- | +| Audit HEAD | `570e6ba56ae60bea56a32801b9cc96c5a8dfde4f` (`main` at audit time) | +| Audit date | 2026-07-14 | +| Mode | Pure review (no app-code fixes applied as part of the audit) | +| Remediation plan | [`audit-remediation-plan-2026-07-14.md`](audit-remediation-plan-2026-07-14.md) | +| Ledger rows | [`branch-review-ledger.md`](branch-review-ledger.md) (multi-skill + consolidated rows) | +| Plan PR | [#673](https://github.com/BigSimmo/Database/pull/673) (`cursor/audit-remediation-plan-0411`) | +| Ledger-only PR | [#652](https://github.com/BigSimmo/Database/pull/652) (`cursor/repo-wide-audit-ledger-0411`) | + +--- + +## 1. Verdict + +- **No confirmed P0/P1 cross-tenant-breach or clinical-governance-bypass defects** were found in + application source. This scoping is deliberate: other P1-severity issues exist outside those two + defect classes. +- **Active majors** include six items: five **operator / legal / launch-confidence** blockers (M1-M3, + M5-M6) plus one **code P1 availability defect** (M4: anonymous catalog rate-limit bypass). The code + P1 must not be under-prioritized relative to the operator/legal items. +- Offline structure gate was green at audit time (`verify:cheap` ~2,290 tests / 2 skipped). +- Provider-backed gates (`check:drift`, golden evals, `verify:release`, live advisors) were **not** + run (API confirmation boundary). + +**Highest residual risks if nothing else is fixed:** APP 8 overseas processing, unproven release +evals, service-role tenancy regression class, upstream OCR quality labels driving danger refusal. + +--- + +## 2. Skills run + +| Skill / pass | Outcome summary | +| ---------------------- | ---------------------------------------------------------------------- | +| Repo auditor | No P0/P1 structural defects; P2 env/orphan/bundle | +| Security review | No confirmed IDOR P0; P1 privacy (PIA-1); P2 DTO/tenancy/auth UI | +| Clinical governance | No P0/P1; fail-closed answer path looks solid; P3 ranking notes | +| RAG retrieval | D4/D5 default-OFF confirmed safe; P3 demotion floor | +| Ingestion worker | No P0/P1; P2 unreachable commit fallback, recovery crash, unwired gate | +| API review | P1 anon catalog rate-limit bypass; P2 envelopes/limits/summarize | +| Frontend UI / a11y | No P0; P2 describedby / icons / Tab / live region | +| Release readiness | Launch blocked on OPS/LEGAL; backlog drift vs runbooks | +| Testing / code quality | CI scope holes; soft critical safety assert; flake debt | + +--- + +## 3. Active major issues + +### M1 — PIA-1 overseas processing (OPERATOR / LEGAL — P0 for real-patient use) + +| | | +| ---------------------- | ---------------------------------------------------------------------------------- | +| **What** | Queries (and excerpts when synthesizing) process via Railway Singapore + OpenAI US | +| **Where** | `docs/privacy-impact-assessment.md`, `docs/openai-cross-border-basis.md` | +| **Risk** | Sensitive health info overseas without closed APP 8 / contractual basis | +| **Address** | Remediation Wave B1 — DPA / ZDR / processor record + counsel | +| **Status at handover** | Open (DPA rows still `_no_`) | + +### M2 — PIA-2 `RAG_QUERY_HASH_SECRET` on Railway (OPERATOR — P1) + +| | | +| ---------------------- | ---------------------------------------------------------------------------------- | +| **What** | Code fails closed without secret; GitHub has it; Railway runtime still `🔎 verify` | +| **Where** | `docs/operator-backlog.md`, `src/lib/env.ts` / instrumentation boot guard | +| **Risk** | Boot fail or weak query hashing in prod | +| **Address** | Remediation Wave B2 — confirm same ≥16-char value in Railway | +| **Status at handover** | Verify pending | + +### M3 — Release gate / evals / canary / staging incomplete (OPERATOR — P1) + +| | | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| **What** | `verify:release`, golden retrieval 36/36, rag-only quality, staging soak, Eval Canary two greens not closed; prior OpenAI embedding quota exhaustion | +| **Where** | `docs/operator-backlog.md`, `docs/launch-operator-runbook.md` | +| **Risk** | Cannot claim clinical release confidence | +| **Address** | Remediation Waves B3–B4 + J | +| **Status at handover** | Pending | + +### M4 — Anonymous public catalogs skip rate limits (CODE — P1) + +| | | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **What** | No-auth catalog GETs short-circuit before rate limiting; large JSON possible | +| **Where** | `src/lib/public-api-access.ts:73–76` (`shouldResolvePublicCatalogAccess`); e.g. `src/app/api/medications/route.ts:108+`; same pattern registry/differentials | +| **Risk** | Egress/CPU DoS by omitting auth | +| **Address** | Remediation Wave C1 — always rate-limit anon + prefer `fields=index` default | +| **Prove** | Unauth GET still hits limiter; payload size capped | +| **Status at handover** | Unfixed | + +### M5 — Operator backlog vs runbook drift (PROCESS — P1 for ops error) + +| | | +| ---------------------- | ------------------------------------------------------------------------------------------------------ | +| **What** | Backlog still `⏳` for items runbook/other docs treat as done/verify | +| **Where** | `docs/operator-backlog.md` vs `docs/launch-operator-runbook.md`, empty `supabase/drift-allowlist.json` | +| **Risk** | Wrong re-apply or false unfinished state | +| **Address** | Remediation Wave A1 — reconcile with live migration list / drift | +| **Status at handover** | Open | + +### 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 | + +--- + +## 4. Sub-issues (P2) + +### 4.1 Security / privacy / API + +| ID | Finding | Evidence | Remediation wave | +| --- | ----------------------------------------------------------- | -------------------------------------------------------- | ---------------- | +| S1 | Authed public-doc DTOs leak `storage_path` / `content_hash` | `documents/route.ts`, `documents/[id]/route.ts` | Wave D1 | +| S2 | Auth UI trusts `getSession()`; APIs use `getUser` | `src/lib/supabase/client.tsx` | Wave D3 | +| S3 | Public-upload quarantine pool high blast radius if enabled | `upload/route.ts` + pool owner | Wave D4 | +| S4 | Auth 401 envelope `{ error }` only | `supabase/auth.ts` vs `http.ts` | Wave H1 | +| S5 | Many hand-rolled `{ error }` omit `code`/`message` | upload, doc 404s, feedback, demo | Wave H1 | +| S6 | Auth write/admin routes lack rate limits | bulk, labels, table-facts, ingestion/*, eval-cases, jobs | Wave H2 | +| S7 | Document list offset max `1_000_000` | `documents/route.ts` | Wave H3 | +| S8 | Upload reserves max body when Content-Length absent | `upload/route.ts` | Wave H4 | +| S9 | Authed stream summarize excludes public docs | `answer/stream` → `summarizeDocument` owner-only | Wave H5 | +| S10 | Bulk returns raw DB `error.message` | `documents/bulk/route.ts` | Wave D5 | +| S11 | `/api/jobs` can return demo jobs when env missing | `jobs/route.ts` | Wave H6 | + +### 4.2 Ingestion / indexing + +| ID | Finding | Evidence | Remediation wave | +| --- | -------------------------------------------------------------- | ----------------------------------------------------- | ---------------- | +| I1 | Unreachable commit fallback after `throw` | `worker/main.ts:544–547` | Wave E1 | +| I2 | Recovery `pending`+`failed` → two retries → unique index crash | `ingestion-recovery.ts`, `recover-ingestion-queue.ts` | Wave E2 | +| I3 | `decideReindexGate` never wired into `scripts/reindex.ts` | grep shows no production driver | Wave E3 | + +### 4.3 Structure / config / CI / testing + +| ID | Finding | Evidence | Remediation wave | +| ---------- | ------------------------------------------------------------- | ----------------------------------- | ------------------- | +| Finding C1 | `.env.example` weak-OR flag `true` vs code default `false` | env example vs `env.ts` | Wave F5 | +| C2 | Bundle budget unenforced (`enforce: false`, null baseline) | `bundle-budget.json` | Wave F6 | +| C3 | CI `ui_changed` skips `app-modes` / `ui-copy` / route helpers | probed `ui_changed: false` | Wave F1 | +| C4 | CI `rag_eval` skips `clinical-safety` / privacy libs | probed `rag_eval_changed: false` | Wave F2 | +| C5 | Known flakes in blocking suite; `@quarantine` unused | `flake-ledger.json` (4) | Wave F4 | +| C6 | Soft `@critical` safety UI assert | `ui-smoke.spec.ts` optional trigger | Wave F3 | +| C7 | `waitForTimeout` / swallowed `networkidle` in UI smoke | `ui-smoke.spec.ts` | Wave F4 | +| C8 | `reindex-eval-gate.ts` production orphan (~488 lines) | tests only | Wave E3 / Wave I P3 | + +### 4.4 Frontend / a11y + +| ID | Finding | Evidence | Remediation wave | +| --- | ----------------------------------------------------- | -------------------------- | ---------------- | +| U1 | `aria-describedby` → `aria-hidden` description (dead) | `mode-action-popup.tsx` | Wave G1 | +| U2 | ~25 dynamic Lucide `` missing `aria-hidden` | ESLint rule gap | Wave G2 | +| U3 | Mode menu does not close on Tab | `master-search-header.tsx` | Wave G3 | +| U4 | No live region for answer ready / stream complete | answer surfaces | Wave G4 | + +--- + +## 5. Sub-issues (P3) + +| ID | Finding | Notes | +| -------- | -------------------------------------------------------------------------------------- | --------------------------------- | +| P3-R1 | Demotion floor `Math.max(hybrid_score, boosted)` at `rag.ts:663` | Weakens feature D4 when activated | +| P3-R2 | Feature D4 hits corpus-wide `document_status: "unknown"` default | Activation needs golden eval | +| P3-R3 | `outdatedPenalty` ON vs "no governance weighting" wording | Demotion-only, documented tension | +| P3-I1 | Bare `updateJob` without lease filter | Cosmetic race | +| P3-I2 | Cleanup RPC still uses JSONB path vs typed column | Forward migrate | +| P3-I3 | NULL `p_worker_id` bypasses lease in RPCs | Footgun | +| P3-S1 | Signed URL 10‑min bearer lifetime | Product threat model | +| P3-S2 | Setup-status detail for any authed user | Info disclosure | +| P3-S3 | Auth callback reflects provider errors into URL | Minor leak/UX | +| P3-U1–U4 | Forced-colors glass/gloss; Sheet overlay token; reduced-motion scale; brand hex adjust | UI polish | +| P3-C1 | Duplicate `test:e2e` / `test:e2e:all` | Alias cleanup | +| P3-C2 | Spend/price env vars missing from `.env.example` | Docs | +| P3-PERF | Hybrid RPC latency tail | Capacity workstream | +| P3-SIZE | Large modules (`rag.ts`, dashboard, document viewer) | Continue decomp when touching | + +Full address sketches: Remediation Wave I in the remediation plan. + +--- + +## 6. What looks solid (do not “fix” casually) + +- Fail-closed `buildGovernedAnswerClientResponse` on `/api/answer` and stream; no provisional + clinical prose before governance +- Numeric / quote / citation verification; prompt-injection sanitization +- Owner-scope fail-closed helpers; query hash redaction defaults +- D4/D5 (#649) default-OFF and fail-safe +- Architecture-boundaries: no import cycles; server modules isolated from client graph +- July-8 ingestion batch marked live-verified in backlog (`✅`) +- Offline cheap gate green at audit HEAD + +--- + +## 7. Checks run / not run + +| Ran | Not run (need confirmation) | +| -------------------------------------------------- | --------------------------------------------------- | +| Specialist static audits | Live Supabase / OpenAI / Railway | +| Structure `verify:cheap` (reported green) | `check:drift`, `check:supabase-project` | +| Focused Vitest (governance, RAG, ingestion suites) | `eval:retrieval:quality`, `eval:quality --rag-only` | +| `ci-change-scope` probes for scope holes | `verify:release`, `verify:ui` | +| | `check:production-readiness` with live secrets | +| | Security advisors / live A/B tenancy | + +--- + +## 8. Suggested next actions for the receiving agent + +1. Read this file + [`audit-remediation-plan-2026-07-14.md`](audit-remediation-plan-2026-07-14.md). +2. **Do not** re-audit the whole repo unless HEAD changed materially — check ledger first. +3. Prefer first CODE PR: **Remediation Wave C1 + Wave D1** (catalog rate limits + public DTO redaction). +4. Parallel OPS track: Wave A reconcile backlog, then Wave B2 secret verify, then Wave B1 legal. +5. Keep PRs one theme each (see remediation plan "Suggested PR granularity"). +6. After each wave: update this handover's status notes, flip backlog rows, and update the existing + ledger row for the current branch/ref. Only append a new ledger row when the branch/ref, HEAD SHA, + or review scope changes, or when a fresh review is explicitly requested. + +### Suggested first CODE PR checklist + +- [ ] Remediation Wave C1: anonymous catalog rate limits (finding M4) (+ tests) +- [ ] Remediation Wave D1: redact `storage_path` / `content_hash` for non-owner public docs (finding S1) +- [ ] `npm run verify:cheap` + focused route tests +- [ ] `npm run verify:pr-local` before handoff +- [ ] Link PR body to finding IDs (M4, S1) + +--- + +## 9. Related references + +| Doc | Role | +| ------------------------------------------------------------------------------ | -------------------------------- | +| [`audit-remediation-plan-2026-07-14.md`](audit-remediation-plan-2026-07-14.md) | How to fix, sequenced | +| [`operator-backlog.md`](operator-backlog.md) | Human/provider actions index | +| [`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 | +| [`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 | + +--- + +## 10. Explicit non-actions at handover + +- No application source was patched for these findings in the audit session. +- No live Supabase/OpenAI/Railway mutations were performed. +- No merge to `main`, force-push, or branch cleanup. +- Remediations require separate feature PRs per wave/theme. diff --git a/docs/audit-remediation-plan-2026-07-14.md b/docs/audit-remediation-plan-2026-07-14.md new file mode 100644 index 000000000..4013edf0c --- /dev/null +++ b/docs/audit-remediation-plan-2026-07-14.md @@ -0,0 +1,462 @@ +# Audit remediation plan — 2026-07-14 + +Plan derived from the multi-skill repository audit of `main` @ `570e6ba` (ledger rows in +[`branch-review-ledger.md`](branch-review-ledger.md)). This document is the sequenced address plan for +every finding: major blockers first, then P2 sub-issues, then P3 cleanup. + +**Findings handover (inventory + status):** [`audit-handover-2026-07-14.md`](audit-handover-2026-07-14.md). + +**Rules for executing this plan** + +- Prefer the smallest safe change per finding; do not bundle unrelated domains in one PR. +- Code fixes use `npm run verify:cheap` first, then the smallest domain check, then + `npm run verify:pr-local` before handoff. +- Provider/live actions are **operator-gated** (`⏸`). Do not run them without explicit confirmation. +- Reconcile [`operator-backlog.md`](operator-backlog.md) against + [`launch-operator-runbook.md`](launch-operator-runbook.md) before repeating any historical apply. + +**Legend** + +| Tag | Meaning | +| --------------- | ----------------------------------------------------- | +| **OWNER:CODE** | Implementable in-repo | +| **OWNER:OPS** | Railway / Supabase / GitHub / OpenAI dashboard or CLI | +| **OWNER:LEGAL** | Privacy officer / counsel | +| **⏸** | Provider or legal confirmation required | +| **Prove** | Smallest acceptance check | + +--- + +## 0. Recommended delivery waves + +Work top-down. Later waves assume earlier majors are either done or consciously deferred. + +```text +Wave A Confirm live/ops truth (doc sync) OWNER:OPS ⏸ +Wave B Launch / privacy blockers OWNER:LEGAL+OPS ⏸ +Wave C Code P1 availability (catalog rate limits) OWNER:CODE +Wave D Security & tenancy P2 OWNER:CODE (+OPS for live A/B) +Wave E Ingestion / recovery P2 OWNER:CODE +Wave F CI / testing / config P2 OWNER:CODE +Wave G Frontend a11y P2 OWNER:CODE +Wave H API contract hygiene P2 OWNER:CODE +Wave I P3 backlog + structure debt OWNER:CODE/OPS +Wave J Release confidence close-out OWNER:OPS ⏸ +``` + +Suggested PR granularity (one theme per PR): + +1. Catalog rate-limit + public DTO redaction +2. Ingestion recovery + commit fallback +3. CI scope + critical safety assert + flake hygiene +4. A11y (describedby / icons / Tab / live region) +5. JSON error taxonomy + admin rate limits +6. Env example / bundle budget / summarize public scope +7. Operator backlog status reconciliation (docs-only) + +--- + +## Wave A — Confirm live truth before acting + +### A1. Reconcile operator backlog vs launch runbook + +| | | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | M5 — backlog still `⏳` for drift-codify / prod deploy / worker while runbook says more is already live | +| **Owner** | OWNER:OPS (+ docs edit) | +| **Address** | Diff each backlog row against `launch-operator-runbook.md`, July-8 apply notes, empty `supabase/drift-allowlist.json`, and linked migration list. Flip rows to `✅` / `🔎 verify` / keep `⏳` with evidence date. | +| **Files** | `docs/operator-backlog.md`, optionally a short note in `docs/launch-operator-runbook.md` | +| **Prove** | `⏸ npx supabase migration list --linked`; `⏸ npm run check:drift`; human sign-off that backlog matches live | + +### A2. Confirm July-13 scrub / lexical migration posture + +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Process-hardening still lists July-13 PHI-scrub / lexical items as remaining in places | +| **Owner** | OWNER:OPS | +| **Address** | Confirm whether `20260713103000_scrub_legacy_rag_query_text` (and related) are applied; if not, schedule under Wave B; if yes, mark docs done. | +| **Prove** | `⏸` all four process-hardening scrub checks expect `0` (unless `RAG_PERSIST_RAW_QUERY_TEXT` is deliberately enabled): `select count(*) from rag_queries where query not like 'redacted-query:%';`; `select count(*) from rag_query_misses where query not like 'redacted-query:%' or normalized_query not like 'redacted-query:%';`; `select count(*) from rag_retrieval_logs where query not like 'redacted-query:%' or (normalized_query is not null and normalized_query not like 'redacted-query:%');`; `select count(*) from rag_response_cache where normalized_query not like 'redacted-cache:%';`; plus lexical probe from process-hardening | + +--- + +## Wave B — Launch / privacy majors (block real-patient use) + +### B1. Close PIA-1 APP 8 / overseas processing basis + +| | | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | M1 — Railway Singapore + OpenAI US process incidental PHI queries | +| **Owner** | OWNER:LEGAL + OWNER:OPS | +| **Address** | Execute checklist in [`openai-cross-border-basis.md`](openai-cross-border-basis.md): OpenAI DPA, ZDR eligibility enablement where chosen, Railway DPA/processor record, update PIA status tables and `/privacy` copy only after counsel approval. | +| **Prove** | Status record rows move from `_no_` to dated `_yes_` / approved alternative; PIA-1 no longer High-open | + +### B2. Verify Railway `RAG_QUERY_HASH_SECRET` (PIA-2) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | M2 — secret verified in GitHub CI, Railway runtime still `🔎 verify` | +| **Owner** | OWNER:OPS | +| **Address** | Set/confirm a ≥16-char value in Railway **production** that matches the GitHub Actions secret used for CI smoke. For staging, set a **separate** staging-only `RAG_QUERY_HASH_SECRET` per [`staging-setup.md`](staging-setup.md) — do not reuse the production HMAC key. Confirm boot smoke and deep readiness. | +| **Prove** | `⏸ npm run check:deployment-readiness` / production health boot; backlog → `✅` | + +### B3. Restore OpenAI quota and run release gates + +| | | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | M3 — release gate + golden evals incomplete; prior quota exhaustion | +| **Owner** | OWNER:OPS | +| **Address** | Complete the runbook §0 identity preflight with `npm run check:supabase-project`, then restore embedding/completions quota → run `npm run eval:retrieval:quality` (36/36) → `npm run eval:quality -- --rag-only` → `npm run verify:release` per [`launch-operator-runbook.md`](launch-operator-runbook.md) §2. | +| **Prove** | Paste summaries into release notes / backlog; canary path in B4 | + +### B4. Eval Canary trust + staging soak + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | M3 leftovers — canary not yet “two greens”; staging soak pending | +| **Owner** | OWNER:OPS | +| **Address** | Provision staging if absent ([`staging-setup.md`](staging-setup.md)); soak (`scripts/soak-test.ts --confirm-staging`, answer p95 ≤ 25 s); run two consecutive Eval Canary greens from `main`. | +| **Prove** | Soak log + two green workflow runs recorded in backlog | + +### B5. Worker image / secret / seed post-deploy confirm + +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Worker redeploy, registry seed, `HEALTH_DEEP_PROBE_SECRET`, auth connection cap | +| **Owner** | OWNER:OPS | +| **Address** | Follow runbook §6 after B3: `reindex:health`, seed registry/differentials, wire ops-digest secrets if desired, flip auth connection allocation **before** vertical scale ([`auth-connection-cap-runbook.md`](auth-connection-cap-runbook.md)). | +| **Prove** | Non-empty Services/Forms; reindex health clear; optional ops-digest cron enabled | + +--- + +## Wave C — Code P1: anonymous catalog availability + +### Wave C1. Rate-limit anonymous public catalogs (finding M4) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Catalog routes short-circuit before rate limits when no auth signal | +| **Owner** | OWNER:CODE | +| **Address** | Always resolve a rate-limit subject (anonymous IP/fingerprint) for medications/registry/differentials catalog GETs; or force lightweight `fields=index` as default for anon and hard-cap full payloads. Prefer both: limiter + default index shape. | +| **Files** | `src/lib/public-api-access.ts`, `src/app/api/medications/route.ts`, `src/app/api/registry/**`, `src/app/api/differentials/**`, rate-limit tests | +| **Prove** | Unit/route tests: unauth GET still calls limiter; optional size cap; `npm run verify:cheap` | + +--- + +## Wave D — Security & tenancy P2 + +### Wave D1. Redact internal fields on authenticated public-document DTOs (findings S1 / S10 detail) + +| | | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Authed viewers of `owner_id IS NULL` docs receive `storage_path`, `content_hash`, etc. | +| **Owner** | OWNER:CODE | +| **Address** | Apply the same omit-list used for anonymous responses whenever `document.owner_id !== access.ownerId` (or whenever public overlay rows are returned). Keep full fields for the owning user / operators only if product requires. | +| **Files** | `src/app/api/documents/route.ts`, `src/app/api/documents/[id]/route.ts`, focused API tests | +| **Prove** | Authed non-owner GET public doc omits `storage_path` / `content_hash` / `import_batch_id` | + +### D2. Defense-in-depth tenancy CI guard (M6) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **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` | +| **Prove** | Guard fails on a synthetic unscope fixture; live A/B green | + +### D3. Auth UI: prefer validated user for privilege display (S2) + +| | | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Browser `getSession()` can show signed-in until API 401 | +| **Owner** | OWNER:CODE | +| **Address** | Initialize `AuthProvider` via `getUser()` (or revalidate session with `getUser` before elevating UI), keep optimistic cache only for non-privileged chrome. | +| **Files** | `src/lib/supabase/client.tsx`, auth UI tests | +| **Prove** | Tampered/stale local session → UI not elevated / recovers to signed-out | + +### D4. Public-upload pool principal hygiene (S3) + +| | | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Finding** | Shared quarantine owner is high blast radius when uploads enabled | +| **Owner** | OWNER:OPS (+ CODE docs) | +| **Address** | Ensure pool UUID has no human password/OAuth sessions; document as non-interactive principal; keep `NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED` off in prod unless moderation workflow is staffed. | +| **Prove** | Auth user list check + prod env flag audit | + +### D5. Bulk / internal error redaction (S10) + +| | | +| ----------- | ------------------------------------------------------------- | +| **Finding** | Bulk edit returns raw PostgREST/`error.message` | +| **Owner** | OWNER:CODE | +| **Address** | Map to `PublicApiError` / stable codes; log server-side only. | +| **Files** | `src/app/api/documents/bulk/route.ts` | +| **Prove** | Injected update failure → no raw Postgres text in JSON | + +--- + +## Wave E — Ingestion / recovery P2 + +### E1. Fix unreachable commit fallback (I1) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | `throw` before `upsertIndexQuality` / `deleteStaleIndexGenerationRows` | +| **Owner** | OWNER:CODE | +| **Address** | Restructure `commitDocumentIndexGeneration`: on RPC error, if error is not `lease_lost`, run the documented client fallback then rethrow or return structured failure; never leave unreachable statements. Enable unreachable-code lint/`allowUnreachableCode: false` for worker if feasible. | +| **Files** | `worker/main.ts`, worker/unit tests if present | +| **Prove** | Simulated commit RPC failure invokes cleanup once; typecheck/lint clean | + +### E2. Deduplicate recovery retries per document (I2) + +| | | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | `pending` + `failed` → two retry actions → unique index violation | +| **Owner** | OWNER:CODE | +| **Address** | In `buildIngestionRecoveryPlan`, emit at most one retry per `document_id` (prefer existing `pending`, else promote one `failed`). Align `recover-ingestion-queue.ts` with `reindex.ts` status query policy. | +| **Files** | `src/lib/ingestion-recovery.ts`, `scripts/recover-ingestion-queue.ts`, `tests/ingestion-recovery.test.ts` | +| **Prove** | New unit test: pending+failed → `retryCount === 1`; script dry-run does not throw `23505` | + +### E3. Wire reindex eval gate (I3) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | `decideReindexGate` unused by `scripts/reindex.ts` | +| **Owner** | OWNER:CODE | +| **Address** | After worker settles each generation (or at script end), load eval summary JSON and call `decideReindexGate`; exit non-zero on `NO_GO`. Keep opt-out flag for emergency ops (`--skip-eval-gate`) documented and logged. Optionally relocate module next to the driver under `scripts/`. | +| **Files** | `scripts/reindex.ts`, `src/lib/reindex-eval-gate.ts`, docs in reindex runbook | +| **Prove** | Fixture NO_GO summary → non-zero exit; GO passes | + +--- + +## Wave F — CI / testing / config P2 + +### F1. Expand CI UI scope patterns (C3) + +| | | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | `src/lib/app-modes.ts`, `ui-copy.ts`, document-flow / search-navigation helpers do not set `ui_changed` | +| **Owner** | OWNER:CODE | +| **Address** | Extend `uiPatterns` in `scripts/ci-change-scope.mjs` to include those libs (and any other shell-imported copy/routing modules). Add self-tests. | +| **Prove** | `node scripts/ci-change-scope.mjs --json --files src/lib/app-modes.ts` → `ui_changed: true` | + +### F2. Expand CI RAG-eval scope patterns (C4) + +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | `clinical-safety.ts`, `privacy.ts`, `public-api-access.ts`, `private-search-scope.ts` miss `rag_eval` | +| **Owner** | OWNER:CODE | +| **Address** | Add those modules to `ragEvalPatterns`; keep test pattern coverage; add self-tests. Fix stale “advisory regression” sentence in `docs/process-hardening.md` L11. | +| **Prove** | Classify `src/lib/clinical-safety.ts` → `rag_eval_changed: true` | + +### F3. Harden `@critical` safety UI assertion (C6) + +| | | +| ----------- | ------------------------------------------------------------------------------------------------------- | +| **Finding** | Clozapine demo smoke treats safety findings as optional | +| **Owner** | OWNER:CODE | +| **Address** | For that fixture, require `answer-safety-findings-trigger` and ≥1 `safety-finding-row` unconditionally. | +| **Files** | `tests/ui-smoke.spec.ts` | +| **Prove** | Focused Chromium critical title; intentional findings removal fails the test | + +### F4. Flake ledger → quarantine or fix (C5 / C7) + +| | | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Four known flakes in blocking suite; `@quarantine` unused; sleeps/`networkidle` swallows | +| **Owner** | OWNER:CODE | +| **Address** | For each `flake-ledger.json` entry: fix root cause **or** tag `@quarantine` with owner/date. Replace `waitForTimeout` with condition waits; stop relying on swallowed `networkidle` for readiness. | +| **Prove** | Either ledger empty or every ledger title tagged/fixed; critical/regression green | + +### F5. Align `.env.example` with safe defaults (Finding C1) + +| | | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Example enables `RAG_TEXT_WEAK_OR_RELAXATION=true` | +| **Owner** | OWNER:CODE | +| **Address** | Set example to `false` or comment as opt-in experiment requiring golden eval. Document spend/price env vars (P3) in the same pass if touching the file. | +| **Prove** | Example matches `env.ts` defaults for risky flags | + +### F6. Capture and enforce bundle budget (C2) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------- | +| **Finding** | `enforce: false`, no baseline | +| **Owner** | OWNER:CODE | +| **Address** | On a known-good `npm run build`, run `npm run check:bundle-budget -- --update`, then set `enforce: true`. | +| **Prove** | CI fails on synthetic budget break; passes on baseline | + +--- + +## Wave G — Frontend a11y P2 + +### G1. Fix dead `aria-describedby` (U1) + +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Description span is `aria-hidden="true"` while referenced by `aria-describedby` | +| **Owner** | OWNER:CODE | +| **Address** | Remove `aria-hidden` from description span in `mode-action-popup.tsx` (keep description out of the accessible _name_). | +| **Prove** | Chromium a11y check / axe on action menu; unit DOM assertion | + +### G2. Dynamic Lucide icons `aria-hidden` (U2) + +| | | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | ~25 variable `` sites; ESLint rule gap | +| **Owner** | OWNER:CODE | +| **Address** | Add `aria-hidden="true"` at all decorative dynamic icon sites; extend `eslint-rules/require-lucide-icon-aria.mjs` to track LucideIcon variables when feasible. | +| **Prove** | Lint clean; spot-check mode menu / send button | + +### G3. Close mode menu on Tab (U3) + +| | | +| ----------- | -------------------------------------------------------------------------------------------- | +| **Finding** | Tab leaves `role="menu"` open | +| **Owner** | OWNER:CODE | +| **Address** | In `handleModeOptionKeyDown`, on Tab: `setModeMenuOpen(false)` and allow default focus move. | +| **Files** | `master-search-header.tsx` | +| **Prove** | Keyboard Playwright: open menu → Tab → menu gone | + +### G4. Announce answer readiness (U4) + +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | No live region for streaming answer completion | +| **Owner** | OWNER:CODE | +| **Address** | Keep/extend `role="status" aria-live="polite"` through loading → complete; announce start once and “Answer ready” (or equivalent) on completion — not per token. | +| **Files** | `answer-status.tsx` / `ClinicalDashboard.tsx` / `answer-content.tsx` | +| **Prove** | Accessibility test asserts status text transitions | + +--- + +## Wave H — API contract hygiene P2 + +### H1. Unify JSON error envelopes (S4 / S5) + +| | | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Finding** | Auth and many routes return `{ error }` without `code`/`message` | +| **Owner** | OWNER:CODE | +| **Address** | Route all failures through `jsonError` / `PublicApiError`. Update auth helper first, then upload/document/feedback/demo paths in follow-up commits if too large. | +| **Prove** | Contract tests for 401/400/404 sample paths | + +### H2. Rate-limit authenticated write/admin routes (S6) + +| | | +| ----------- | --------------------------------------------------------------------------------------------------------------- | +| **Finding** | bulk / labels / table-facts / ingestion quality / eval-cases / jobs lack buckets | +| **Owner** | OWNER:CODE | +| **Address** | Assign buckets (reuse `document_read` or add `document_admin` / `ingestion_admin`); return 429 + `Retry-After`. | +| **Prove** | Mocked exhaustion → 429 | + +### H3. Cap document list offset (S7) + +| | | +| ----------- | --------------------------------------------------------------- | +| **Finding** | Offset max 1_000_000 | +| **Owner** | OWNER:CODE | +| **Address** | Cap to e.g. 10_000 (match jobs) or introduce cursor pagination. | +| **Prove** | Schema rejects high offset | + +### H4. Upload Content-Length admission (S8) + +| | | +| ----------- | ------------------------------------------------------------------------------ | +| **Finding** | Missing length reserves max body bytes | +| **Owner** | OWNER:CODE | +| **Address** | Require Content-Length (411/400) or reserve after bounded read of actual size. | +| **Prove** | Length-less small uploads do not exhaust global budget | + +### H5. Authed summarize includes public docs (S9) + +| | | +| ----------- | ------------------------------------------------------------------------------------------- | +| **Finding** | Stream summarize uses owner-only equality, excluding public corpus | +| **Owner** | OWNER:CODE | +| **Address** | Use `withOwnerReadScope` / public-OR semantics in `summarizeDocument` when ownerId present. | +| **Files** | `src/lib/rag.ts`, `src/app/api/answer/stream/route.ts`, summarize route | +| **Prove** | Authed summary of public UUID succeeds | + +### H6. Harden `/api/jobs` misconfig path (S11) + +| | | +| ----------- | ------------------------------------------------------------------------------------------ | +| **Finding** | Missing server env returns demo jobs without auth | +| **Owner** | OWNER:CODE | +| **Address** | Authenticate first; on missing env return 503/`internal_error` outside explicit demo mode. | +| **Prove** | Unauthed + broken env → non-200 without demo payload | + +--- + +## Wave I — P3 backlog (schedule, don’t block launch) + +Address when touching the same files, or as a cleanup epic after Waves C–H. + +| ID | Issue | Address sketch | Prove | +| ---------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| P3-R1 | Demotion floor `Math.max(hybrid_score, boosted)` | Clamp below hybrid only for explicit demotion delta; or subtract after floor; golden-eval gate any change | `npm run eval:retrieval:quality` 36/36 | +| P3-R2 | D4 unknown blast radius | Docs + activation runbook: require governance enrichment % + golden eval before enabling | Operator checklist | +| P3-R3 | Governance-weighting principle wording | Reconcile process-hardening / ranking docs with demotion-only policy | Doc consistency review | +| P3-I1 | Bare `updateJob` without lease | Route stage updates through leased helper | Worker unit/integration | +| P3-I2 | Cleanup RPC JSONB vs typed column | Forward migration to typed compare | Schema tests + drift | +| P3-I3 | NULL `p_worker_id` lease bypass | Fail closed when worker id null for service callers; allowlist migrations | SQL test | +| P3-S1 | 10-min signed URLs | Product decision: shorten TTL or bind | Threat-model note | +| P3-S2 | Setup-status detail breadth | Gate on deep probe / admin role | Authed vs deep response shape | +| P3-S3 | Auth callback error reflection | Generic client message; server log detail | Callback harness | +| P3-U1 | Forced-colors glass/gloss tokens | Map to `Canvas` in `globals.css` | `verify:ui` a11y media | +| P3-U2 | Sheet `bg-black/45` | Use `--overlay-backdrop` | Visual parity | +| P3-U3 | `active:scale` without `motion-safe` | Prefix `motion-safe:` | Reduced-motion check | +| P3-U4 | Brand squares forced-color-adjust | `forced-color-adjust: none` on brand spans | Forced-colors screenshot | +| P3-C1 | Duplicate `test:e2e` aliases | Keep one; alias documented or remove | `package.json` | +| P3-C2 | Spend/price env missing from example | Document with defaults | Env parity script | +| P3-C3 | Orphan `reindex-eval-gate` location | Move beside driver after E3 | Import graph | +| P3-STRUCT1 | `client-env.ts` duplicates `isLocalNoAuthMode` / `publicUploadsEnabled` from `env.ts` via raw `process.env` | Keep intentional client/server split but share one normalized helper or document the allowed divergence; add a parity regression if values can drift | `npm run check:env-parity`; focused env unit that fails if client/server helpers disagree | +| P3-STRUCT2 | `mockup`-named component files retained under `src/components/` | Keep while gated by `mockupsEnabled()`, or rename/remove once production call sites no longer need the mockup path; record disposition in ledger | Import/grep shows every production import goes through `mockupsEnabled()` or files are renamed/removed | +| P3-PERF | Hybrid RPC latency tail | Capacity/RPC workstream (existing RAG findings #25) — not a drive-by | p90 metrics | +| P3-SIZE | Large modules | Continue extractive decomp when editing `rag.ts` / dashboard | LOC ratchet optional | + +--- + +## Wave J — Release confidence close-out + +After Waves B–H land (or are deferred with dated waiver): + +1. `⏸ npm run check:supabase-project` +2. `⏸ npm run check:drift` (allowlist empty → any surprise is a stop) +3. `⏸ npm run check:production-readiness` (configured prod context) +4. `⏸ npm run eval:retrieval:quality` + `npm run eval:quality -- --rag-only` +5. `⏸ npm run verify:release` +6. Update PIA / backlog / this plan’s status column (add a “Status” column locally as work completes) +7. Record outcomes in [`branch-review-ledger.md`](branch-review-ledger.md) + +--- + +## Priority matrix (quick view) + +| Priority | IDs | Launch impact | +| --------------------------------------- | -------------------- | ----------------------------- | +| Block real patients | B1, B2 | Legal + secret placement | +| Block next release claim | B3, B4, J | Evidence missing | +| Fix before wide anon traffic | Wave C1 | DoS / cost | +| Fix before multi-tenant trust expansion | Waves D1, D2, D5 | Disclosure / regression class | +| Fix before next reindex / recovery | E1–E3 | Index correctness | +| Fix before next UI merge wave | F1–F4, G1–G4 | Silent CI holes / a11y | +| Hygiene | H1–H6, F5–F6, Wave I | Contracts & debt | + +--- + +## Out of scope for this plan + +- Broad product redesign or RAG Phase-N architecture beyond the listed P3 ranking floor +- Forced dependency upgrades (`dependency` shortcut) +- Destructive branch cleanup +- Live mutations without explicit operator confirmation + +--- + +## Tracking + +Append progress to the review ledger when a wave completes, and flip matching rows in +[`operator-backlog.md`](operator-backlog.md). Prefer linking PRs back to issue IDs in this document +(`Wave C1`, `Wave E2`, …) in the PR body. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 485dbee3f..b4a094c44 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -490,6 +490,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-14 | PR #655 / codex/release-blocker-remediation | 3ed3a7a2df37d7d15143ab7606e5748ac7ecca09 + reviewed follow-up diff | offline dose-route latency follow-up | The next isolated timeout was a short IM/PO agitation question receiving the same blanket ten-term AND expansion. Agitation dose/route retrieval now keeps only the dose and route signals present in the question; the exact case retrieves its expected source and five citations in 1.54 seconds without a model. All remaining RAG cases 23–44 passed individually, so no further deadline crash remains. | Focused clinical-search/retrieval Vitest 111/111; scoped ESLint; Prettier; `git diff --check`; live provider-free case 22 passed; live provider-free cases 23–44 passed individually. | | 2026-07-14 | PR #655 / codex/release-blocker-remediation | a3f3a89676015cd5f018c07e8c3ad9483f91cef6 + reviewed follow-up diff | offline adversarial-latency follow-up | The final blocking offline-quality failure was an adversarial secret-exfiltration query that correctly refused but first spent about 25 seconds in lexical retrieval. Adversarial manipulation now short-circuits at the search boundary before provider-client creation, cache access, classification, aliases, or Supabase work, and is never cached. | Focused Vitest 2/2; scoped ESLint; Prettier; full TypeScript; `git diff --check`; live provider-free adversarial case completed in 100 ms with 0 ms RPC time; `eval:quality:release:offline` passed with zero blocking failures and zero model, request-ID, token, cost, or generation-latency evidence. Flaky local browser/composite suites intentionally not repeated; hosted CI remains authoritative. | | 2026-07-14 | PR #666 / codex/release-blocker-remediation | 9b56eebe4b23ab783207445fb827c317c8d59be8 + reviewed follow-up diff | review-followup | One late P2 retrieval-contract gap was confirmed: the optimized agitation query retained IM/PO but could drop other already-supported amount, route, and frequency aliases. Medication evidence intent is now shared with retrieval selection, and focused agitation queries preserve requested numeric units, SC, SL, PRN, and frequency signals without restoring the broad ten-term expansion. | GitHub review-thread inspection; focused clinical-search/retrieval Vitest 112/112; scoped ESLint; Prettier; `git diff --check`. Hosted final-head TypeScript/build/CI and exact-head staging evidence remain required after push. | +| 2026-07-15 | PR #673 / cursor/audit-remediation-plan-0411 | 03fe72683d83606b73c951914278eabe897fc395 + reviewed follow-up diff | docs audit remediation plan review + CI | Fixed the hosted Prettier failure and all actionable review comments: preserved the real branch ref for the generic ledger parser, corrected `npm run` commands, scoped the no-P0/P1 verdict without obscuring the M4 code P1, disambiguated finding IDs from wave IDs, aligned wave-to-ledger update instructions with the review protocol, and made B3 require the Supabase project-identity preflight before provider-backed release gates. No additional high-confidence defect remains in the changed docs. | GitHub unresolved-thread inventory; focused Prettier on all changed docs; docs link/script reference checks; `git diff --check`. Docs-only; no Supabase, OpenAI, or other live-service checks. | | 2026-07-14 | codex/formulation-specifier-separation-current | 25d7f733f + uncommitted fix | Specifiers/Formulation boundary bug hunt and remediation | Confirmed two P1 clinical-navigation/content boundary defects: the completed Formulation change deleted the distinct psychiatric Specifiers workspace and redirected every `/specifiers/*` route into Formulation; a later universal-search contract also labelled Formulation mechanism results as `specifiers`. Restored the independent Specifiers catalogue, detail, builder, compare, and map surfaces; registered both app modes and action sets; kept submitted searches in their owning standalone workspace; and split universal search into real `specifiers` and `formulation` domains with correct records, headings, mode ownership, and destinations. | Offline only: full Vitest 2,404 passed/1 skipped, TypeScript, full ESLint, focused Vitest 56/56, sitemap check, `git diff --check`, production-readiness READY with expected missing-env warnings, identity-checked server at `http://localhost:3384`, and repaired Chromium journeys for Specifiers desktop/mobile, Formulation mobile, and explicit route-family separation all passed. No provider-backed or live API checks run. | | 2026-07-14 | claude/specifiers-v2-design-r55baf | 343f4ee4e89844e6a668910958ce1e3f119c128e | branch-cleanup | Retained: open PR #656 (full DSM-5-TR specifier catalog; +19k, novel data/loaders not on main). | Provider-backed GitHub PR inventory via MCP (400 PRs #264-#674, all merged=false); local read-only git: rewritten-main-aware cherry-pick + two-dot tree + branch-only-file novelty sweep vs origin/main e75fad90; adversarial re-check. No remote mutation attempted. No OpenAI/Supabase/live-eval checks. | | 2026-07-14 | codex/formulation-specifier-separation-current | 17c9ea7383f5b463ec648317cd170c24b8e7c584 | branch-cleanup | Retained: open PR #672 (Specifiers/Formulation separation residual; core already landed on main via #674). | Provider-backed GitHub PR inventory via MCP (400 PRs #264-#674, all merged=false); local read-only git: rewritten-main-aware cherry-pick + two-dot tree + branch-only-file novelty sweep vs origin/main e75fad90; adversarial re-check. No remote mutation attempted. No OpenAI/Supabase/live-eval checks. | diff --git a/docs/operator-backlog.md b/docs/operator-backlog.md index 50a50e206..2ac3a23b9 100644 --- a/docs/operator-backlog.md +++ b/docs/operator-backlog.md @@ -8,6 +8,9 @@ This exists so that launch-blocking state lives in the repo instead of chat memo `⏳ pending`, `🔎 verify` (may already be done — confirm before repeating), `✅ done`, `—` (n/a). Update the row (and its runbook) when an action lands. The sequenced flow with exact commands and approval gates is [launch-operator-runbook.md](launch-operator-runbook.md); this table is the index. +Code + ops remediation waves from the 2026-07-14 multi-skill audit live in +[audit-remediation-plan-2026-07-14.md](audit-remediation-plan-2026-07-14.md). +Findings inventory for handover: [audit-handover-2026-07-14.md](audit-handover-2026-07-14.md). > Status column is seeded from repo runbooks + session memory and **must be confirmed against live > state** before acting — do not treat a `🔎 verify` row as authoritative.