From 3473e17e7781eabbbfaeff6117ca8b3390d057ec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 09:44:42 +0000 Subject: [PATCH] docs(issues): reconcile 17 queued inbox requests into the canonical ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The immutable inbox had accumulated 17 unapplied requests across several sessions, six of them from the snapshot-conflict work in #2530. They are applied here in the single fresh-base reconciliation operation the ledger discipline requires, rather than by editing table rows directly. Composition: eight `done` closures, five `add` findings, two `update` amendments, one `cancel` decision, and one request whose closure another session had already queued independently. Open rows go 70 -> 67. Each request moves to `docs/outstanding-issues-inbox/applied/` as its own immutable audit record, so the canonical diff can be checked against the recorded transaction — which is what `check:ledger-write-discipline` compares, and why the requests are moved rather than deleted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014Mn8yfo1bQzk4kDEiG1TEn --- data/outstanding-issues-snapshot.json | 133 +++++++----------- .../1eb6df92-f59b-46bc-9cf4-8c50482c25c6.json | 0 .../247ff620-b5b1-436b-9435-430f5dedbc3e.json | 0 .../250fb5a6-dd95-47d8-ba69-a8f3e9efbe53.json | 0 .../39f1842e-9780-4c73-8082-28cf9f271104.json | 0 .../3c5d6d42-0952-4bc8-a1f7-a4858789d0ed.json | 0 .../52120ceb-7c70-4c40-b401-c4bbd051903d.json | 0 .../73cb84d3-27ff-4554-bda1-01e1d53a5c6d.json | 0 .../7418d9b5-ebb9-4cf2-9cde-ae405e923063.json | 0 .../81f20b9d-c84b-4929-8528-91e2fc8b4a01.json | 0 .../9a57cec3-e154-4523-bde4-9ddfa8e0c0dd.json | 0 .../a24114b2-feb8-4588-94f8-82a88ea3fc53.json | 0 .../b11bdfdd-70eb-4bf9-9384-4d30af2a6ad2.json | 0 .../b9a2d8d3-bf12-4961-b1c5-2424eca1fbd4.json | 0 .../bcbdf769-62cf-4d67-be04-9065a603f439.json | 0 .../d1a71c3e-a4ce-4c64-b31d-aab39ed3c79c.json | 0 .../da308925-8094-4c05-88fd-f6dc36ad4361.json | 0 .../f5078359-d4ea-4a91-b376-d5bbecb452a6.json | 0 docs/outstanding-issues.md | 25 ++-- 19 files changed, 68 insertions(+), 90 deletions(-) rename docs/outstanding-issues-inbox/{ => applied}/1eb6df92-f59b-46bc-9cf4-8c50482c25c6.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/247ff620-b5b1-436b-9435-430f5dedbc3e.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/250fb5a6-dd95-47d8-ba69-a8f3e9efbe53.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/39f1842e-9780-4c73-8082-28cf9f271104.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/3c5d6d42-0952-4bc8-a1f7-a4858789d0ed.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/52120ceb-7c70-4c40-b401-c4bbd051903d.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/73cb84d3-27ff-4554-bda1-01e1d53a5c6d.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/7418d9b5-ebb9-4cf2-9cde-ae405e923063.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/81f20b9d-c84b-4929-8528-91e2fc8b4a01.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/9a57cec3-e154-4523-bde4-9ddfa8e0c0dd.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/a24114b2-feb8-4588-94f8-82a88ea3fc53.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/b11bdfdd-70eb-4bf9-9384-4d30af2a6ad2.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/b9a2d8d3-bf12-4961-b1c5-2424eca1fbd4.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/bcbdf769-62cf-4d67-be04-9065a603f439.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/d1a71c3e-a4ce-4c64-b31d-aab39ed3c79c.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/da308925-8094-4c05-88fd-f6dc36ad4361.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/f5078359-d4ea-4a91-b376-d5bbecb452a6.json (100%) diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index 76e93999ce..4cbd886517 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -5,13 +5,13 @@ "committed_at": "2026-09-02T14:22:01+08:00" }, "counts": { - "open": 70, + "open": 67, "p1": 2, - "p2": 49, - "p3": 19, + "p2": 47, + "p3": 18, "queued": 7, "pending": 0, - "resolved": 459 + "resolved": 467 }, "queue": [ { @@ -237,15 +237,6 @@ "source": "Codex review of PR #2250 (P2, comment 3833062803 and 3833062807); docs/audit/live-drift-forensics-2026-08.md Phase 5 close-out 5.1(b); supabase/schema.sql:8033-8054", "added": "2026-08-21" }, - { - "id": "#Y090R5", - "priority": "P2", - "type": "issue", - "summary": "data/outstanding-issues-snapshot.json is a generated file every inbox PR must regenerate, so any two concurrent ledger PRs conflict on it and the loser must re-resolve after every main merge", - "detail": "Observed 2026-08-22 on PR #2284, which conflicted twice within about an hour and was closed rather than untangled. check:outstanding-issues-snapshot requires the committed data/outstanding-issues-snapshot.json to be in step with docs/outstanding-issues.md plus every pending inbox request, so a PR that queues a request MUST commit a regenerated snapshot or CI fails with 'counts.pending: committed N vs regenerated M'. But the regenerated content depends on every OTHER pending request too, so the file differs between any two concurrent ledger PRs and conflicts as soon as either lands. Recent main history shows the collision surface is real, not theoretical: 93af96cf8, 4cbac0ceb, 2ca31d6d8 and 639108f07 all touch that one file. The immutable-request design deliberately removed this class of conflict for the requests themselves; the snapshot reintroduces it in a single generated artifact, which is the same serial-only bottleneck #EH9VA6 and the ledger write-discipline work were meant to eliminate. Resolution is mechanical but must be done exactly once per main merge - never hand-merge it: take main's version then re-run node scripts/generate-outstanding-issues-snapshot.mjs. Options worth weighing: regenerate the snapshot during npm run issues:reconcile (the already-serialized step) instead of in every request-adding PR, so ordinary branches never touch the file; or have the check tolerate a snapshot that is in step with the canonical ledger while ignoring pending-request counts; or add a union/regenerate merge strategy. Whichever is chosen, this bites every future issues PR that does not land within the gap between other ledger merges. SECOND SYMPTOM, same root cause, measured 2026-08-22 on PR #2299: because the file lives under data/ - the generated CLINICAL snapshot export directory - scripts/pr-policy.mjs classifies it clinicalRisk:true. Confirmed by calling classifyPullRequestFiles directly: ['data/outstanding-issues-snapshot.json'] alone returns clinicalRisk:true, while the inbox JSONs alone return false. So every ledger PR that regenerates the snapshot is forced to carry a complete ## Clinical Governance Preflight in its body for a file holding no clinical data at all, and fails PR policy with 'Clinical-risk paths require the ## Clinical Governance Preflight section' until it does. That is ceremony with no safety value, and it trains reviewers to tick clinical governance boxes reflexively on changes that have nothing to do with clinical output - which is the failure mode that section exists to prevent. Moving regeneration into issues:reconcile fixes both symptoms at once; relocating the artefact out of data/ would fix this second one on its own.", - "source": "session 2026-08-22, closing PR #2284", - "added": "2026-08-22" - }, { "id": "#J8SJQ9", "priority": "P2", @@ -255,15 +246,6 @@ "source": "canary run 32589154243; src/lib/rag/rag-eval-cases.ts quality-antipsychotic-metabolic-monitoring; ledger #NPQJKP", "added": "2026-08-22" }, - { - "id": "#EFETZT", - "priority": "P2", - "type": "issue", - "summary": "The repo-awareness snapshot goes stale on any PR that waits, and it already reddened CI once", - "detail": "UPDATE 2026-08-27 (PR #2413, second and third occurrences): this is not only a staleness problem, it also produces hard git merge conflicts that block auto-merge. data/repo-awareness-snapshot.json conflicted twice in fifteen minutes on one PR head — first when main advanced to be65b8a, again when main advanced to fb88b47 — each time setting mergeable_state=dirty, which suppresses refs/pull//merge so pull_request CI (CI, Gitleaks, Semgrep) does not run at all and the check list reads empty rather than red. Both were resolved identically: git merge origin/main, git checkout --theirs the snapshot, npm run snapshot:repo-awareness, commit, push. The staleness half also reproduced: check:repo-awareness-snapshot failed Static PR checks with 'review_state differs from the repository'. Cost per occurrence is one full CI round trip plus a push, and it scales with how many PRs are open, since every merge to main regenerates the file for all of them. Reinforces the fix direction already proposed on this row: exclude docs/branch-review-records/ from the compared content the way captured_revision already is, or compare a narrower key set. Adding .gitattributes merge guidance for this path would blunt the conflict half but not the CI-staleness half.", - "source": "PR #2359 CI failure, 2026-08-25", - "added": "2026-08-25" - }, { "id": "#JZ8B36", "priority": "P2", @@ -287,7 +269,7 @@ "priority": "P2", "type": "task", "summary": "Retrieval RPC still treats a null document owner as public, so the owner_id republication hole is only half closed", - "detail": "RE-VERIFIED against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 on 2026-09-01. CLOSED: the owner-deletion path is no longer outstanding. public.documents, document_labels, document_summaries and document_table_facts now use ON DELETE RESTRICT, with the live migration and a schema proof that pins the four visibility tables and their exact restrict action. CODE DEFECT (unresolved): public.retrieval_owner_matches treats the public sentinel as row_owner_id IS NULL, and retrieval_owner_matches_v2 treats include_public as row_owner_id IS NULL; neither predicate requires metadata.public_corpus = true. An ownerless row without the publication marker could therefore still enter a public retrieval result. EXPOSURE (measured 2026-09-01, owner-approved read-only GET against ref sjrfecxgysukkwxsowpy): total documents 2851; owner_id NOT NULL = 0; owner_id IS NULL = 2851; of those, metadata.public_corpus = true = 2851. EXPOSED COUNT = 0. The production corpus is deliberately published and has no privately owned or unmarked ownerless document. PRIORITY: P2, not P1. NEXT: change the retrieval contract so its public branch requires both owner_id IS NULL and metadata.public_corpus = true, preserving owned-document retrieval; this is a protected RAG behaviour change and requires the RAG flag, an offline regression proof, and a live eval-canary pair.", + "detail": "RE-VERIFIED against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 on 2026-09-01. CLOSED: the owner-deletion path is no longer outstanding. public.documents, document_labels, document_summaries and document_table_facts now use ON DELETE RESTRICT, with the live migration and a schema proof that pins the four visibility tables and their exact restrict action. CODE DEFECT (unresolved): public.retrieval_owner_matches treats the public sentinel as row_owner_id IS NULL, and retrieval_owner_matches_v2 treats include_public as row_owner_id IS NULL; neither predicate requires metadata.public_corpus = true. An ownerless row without the publication marker could therefore still enter a public retrieval result. EXPOSURE (measured 2026-09-01, owner-approved read-only GET against ref sjrfecxgysukkwxsowpy): total documents 2851; owner_id NOT NULL = 0; owner_id IS NULL = 2851; of those, metadata.public_corpus = true = 2851. EXPOSED COUNT = 0. The production corpus is deliberately published and has no privately owned or unmarked ownerless document. PRIORITY: P2, not P1. NEXT: change the retrieval contract so its public branch requires both owner_id IS NULL and metadata.public_corpus = true, preserving owned-document retrieval; this is a protected RAG behaviour change and requires the RAG flag, an offline regression proof, and a live eval-canary pair. RE-MEASURED 2026-09-02 (independent user-authorised read-only query, same ref): unchanged at public_marked=2851, ownerless_unmarked=0, owned=0, total=2851, so EXPOSED COUNT remains 0 across two consecutive days. That read also answered a question this row never recorded: public.document_corpus_access_state.mode = 'public'. set_document_corpus_access_mode('public') has therefore been run against live at some point, even though no migration or script in the repo invokes it and the table seeds to 'private' (20260825025032:43-45). That operator back-stamp is what closed the legacy ownerless-but-unmarked population described in the 20260825025032 header, and it is why this defect currently has zero blast radius. The exposure is zero by data state, not by predicate, so any future ownerless insert that skips the marker re-opens it silently and the NEXT step above still stands.", "source": "repo-wide audit at 3ed1932 (six domain reviewers); re-verified against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 and an owner-approved read-only production count, database coordination chat 2026-09-01", "added": "2026-08-23" }, @@ -318,15 +300,6 @@ "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", "added": "2026-08-23" }, - { - "id": "#JFRCZ4", - "priority": "P3", - "type": "issue", - "summary": "check:repo-awareness-snapshot needs git, and nobody has established what it does without it", - "detail": "The gate shells out to git ls-files and git log through the generator, and it now sits in verify:cheap, verify:pr-local and CI. What it does in a git-less checkout - a git archive export, which is the very technique the Phase 2 spec section 8.2 names as a required proof - has never been tested. The generator itself is proven to fail loudly there, which is correct under ruling R5 because it only ever runs from docs:update. The gate is different: it runs in three gate chains, so a git-less environment turns a safety check into an unexplained failure. Establish the behaviour and document it, or make the gate skip explicitly with a stated reason rather than erroring.", - "source": "Phase 2 whole-branch review, PR #2359", - "added": "2026-08-25" - }, { "id": "#9P4XAE", "priority": "P2", @@ -345,15 +318,6 @@ "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", "added": "2026-08-23" }, - { - "id": "#Z61JRT", - "priority": "P2", - "type": "issue", - "summary": "The batch image endpoint calls getPublicUrl against a bucket pinned private, so public-corpus figures silently fail to load", - "detail": "api/images/signed-urls/route.ts:109 uses getPublicUrl for every image whose document isPublicDocument, but clinical-images is inserted and re-asserted with public = false. The URL 404s while the route still returns HTTP 200, so nothing surfaces as an error. The singular route (images/[id]/signed-url) always signs correctly. Latent privacy edge: if the bucket is ever flipped public to fix this, the same branch hands out permanent unauthenticated URLs, and isPublicDocument checks metadata markers BEFORE owner_id, so a document with a non-null owner carrying public_corpus metadata would take that branch too. FIX: delete the public branch and sign uniformly.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", - "added": "2026-08-23" - }, { "id": "#NCAWAF", "priority": "P2", @@ -390,15 +354,6 @@ "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", "added": "2026-08-23" }, - { - "id": "#ZM8902", - "priority": "P3", - "type": "issue", - "summary": "docs:check-links passes over a broken relative markdown link in a spec, so a binding document went missing unnoticed", - "detail": "scripts/check-docs-links.mjs reported 1905 references resolving while docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md line 180 linked to ../../caring-contacts/design-handoff.md, a file that has never existed, and line 131 described it as binding. Found by hand on 2026-08-19 and repointed to interaction-matrix.md, which actually holds the 24-row modality matrix. The gate is a check that cannot fail for this class of link: verify whether relative markdown links outside a recognised prefix are skipped, and close the gap. Same family as the mutation-testing concerns already recorded for stderr hooks and tail-masked exit codes.", - "source": "Caring Contacts doc repair 2026-08-19", - "added": "2026-08-18" - }, { "id": "#CJCH2E", "priority": "P3", @@ -471,15 +426,6 @@ "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", "added": "2026-08-23" }, - { - "id": "#N8B176", - "priority": "P3", - "type": "issue", - "summary": "isPublicDocument ranks caller-adjacent metadata above owner_id", - "detail": "src/lib/documents/is-public-document.ts:49-61 returns true for metadata.public_corpus, is_public, public, visibility === 'public' or source_kind === 'registry_record' BEFORE consulting owner_id at :64 - a second, weaker definition of public than the one the DB and publish_approved_documents use. Not exploitable today: upload metadata is server-constructed and bulk edit accepts only a fixed key schema. But the function is exported as a standardized predicate and documented for reuse, so the next access decision that adopts it inherits a metadata-driven publication bypass. FIX: make owner_id === null the first and authoritative check, or scope the helper to rendering only.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", - "added": "2026-08-23" - }, { "id": "#J43Z6B", "priority": "P2", @@ -525,22 +471,13 @@ "source": "session 2026-08-22", "added": "2026-08-22" }, - { - "id": "#CTA8CR", - "priority": "P2", - "type": "issue", - "summary": "A stale 'D4 is OFF' production-deploy claim survives uncorrected in two places in the live-drift forensics doc", - "detail": "docs/audit/live-drift-forensics-2026-08.md:1243-1244 and :1720 both assert flatly, with no forward pointer, that D4 is OFF and nothing reaches production on merge. A later section at :2056-2072 corrects this to 'treat D4 as UNRESOLVED and assume merging a migration PR deploys it to production'. A third instance at :2024 DOES carry a superseded pointer, so the doc's own convention exists and simply was not applied to the earlier two. A future session jumping to the Phase 4 or #Q5JHBJ sections would reasonably read the stale claim as settled fact about a live clinical database. FIX: add the same one-line superseded pointer at both sites. Also check docs/database-remediation-coordination.md:159, which references the same ambiguity.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", - "added": "2026-08-23" - }, { "id": "#4STSM1", "priority": "P2", "type": "task", - "summary": "Caring Contacts synthetic production build: spec approved and committed, implementation plan not yet written", - "detail": "docs/superpowers/specs/2026-08-19-caring-contact-production-build-design.md is the binding spec: ten decision-lock revisions, the sealed domain rules layer, a dedicated Supabase project hard-separated from the Clinical KB project, seven screens required by existing decisions but never designed, four recommended screens, the design non-regression contract and the elevation brief. Delivery is two pull requests with subagent-driven development: (1) rules plus datastore plus doc repairs, (2) screens plus demo clock, synthetic caseload and training mode. Next step is the writing-plans skill to produce the implementation plan for part one. Design phase itself is complete and merged (PR #2095, #2133).", - "source": "Caring Contacts design session 2026-08-19", + "summary": "Caring Contacts synthetic production build: Phases 1 and 2 built and merged; Phase 3 planned but not built", + "detail": "SUPERSEDES the original row, which said the implementation plan was unwritten and named part one as the next step. Verified against main at 45a3dca on 2026-09-02: part one's plan is docs/superpowers/plans/2026-08-19-caring-contact-domain-and-datastore.md with its eleven test-first tasks, and it landed - the sealed domain layer (36 modules under src/lib/caring-contacts/), eight migrations under caring-contacts/supabase/migrations/, and 73 caring-contacts test files are all on main. Phase 2 landed too, under docs/superpowers/plans/2026-08-24-caring-contact-phase-2b-screens.md, with the production workspace routes under src/app/caring-contacts/. WHAT REMAINS is Phase 3, 'make it demonstrable' (spec §10, §2.9 and the rehearsed demonstration path), which spec §13 said folds into the Phase 2 pull request 'unless it grows' - Phase 2B merged without it, so it grew and now needs its own PR. Its plan was written in PR #2520 as docs/superpowers/plans/2026-09-02-caring-contact-phase-3-demonstrable.md: eleven test-first tasks in five groups covering the advanceable demo clock and its production-absence proof, the seed extended from five patients to twelve across all nine required states, the §2.9 bounded clinical-record plan summary with its exclusions asserted as absences, training mode isolated at the store seam, and the five-minute path as a tracked document plus an executable journey. The plan is DRAFT, not approved for execution, and carries four questions for the owner - none blocking its first task: (1) is the §2.9 summary a print view or a saveable file (the plan assumes print, the safer half); (2) contact detail and system states exist as mockup routes with no production equivalent - Phase 2 gap or deliberate; (3) are the five existing seeded patient names kept and seven added; (4) spec §14's four open decisions remain open, of which the patient-visible reply wording is spoken aloud during the demonstration. NEXT STEP is owner approval of the plan, then subagent-driven execution of its Task 1.", + "source": "Verified repo read at main 45a3dca; plan written in PR #2520, 2026-09-02", "added": "2026-08-18" }, { @@ -552,15 +489,6 @@ "source": "data/forms-catalog.json; data/forms-pdf-manifest.json; public/forms-pdf; direct measurement 2026-08-24", "added": "2026-08-24" }, - { - "id": "#875H6T", - "priority": "P3", - "type": "rec", - "summary": "Ward Flow: six agreed enhancements not yet assigned to a phase", - "detail": "Ward prediction track record; 'why not here' across the whole state for one patient; a sixty-second self-driving guided tour; out-of-area ledger; 'waiting since' promoted in the priority queue; named moments on the demo clock. All accepted by the product owner 2026-08-26 and described in docs/ward-flow-roadmap.md.", - "source": "Product-owner direction, 2026-08-26", - "added": "2026-08-26" - }, { "id": "#9GPWT3", "priority": "P2", @@ -722,6 +650,51 @@ "detail": "Task 5b made the Patients directory search match patient names, and the filter is a GET form (server-side filtering was mandated by the brief; Ruling 13 forbids client state; the approved design requires name search). So a search reaches the address bar, browser history, any Referer header and any server access log as ?q=. Before Task 5b only synthetic identifiers went there. Judged NOT a defect by both the implementer's reviewer and the controller -- there was no compliant alternative under the standing constraints -- and recorded deliberately rather than fixed quietly, because it is exactly the kind of residual that gets carried into a real build unnoticed. Decide before any deployment carrying real patient names: POST-and-redirect, a client boundary accepted as an exception to Ruling 13, or an accepted risk with logging scrubbed.", "source": "Task 5b review finding I-4, 2026-08-25", "added": "2026-08-24" + }, + { + "id": "#707F09", + "priority": "P3", + "type": "issue", + "summary": "The developer hub's pending-requests panel reads empty during local dev, because no lifecycle regenerates the snapshot for the dev server", + "detail": "Raised by Codex review on PR #2530 (scripts/generate-outstanding-issues-snapshot.mjs) and verified rather than accepted on trust. THE FINDING IS CORRECT: npm run dev and npm run ensure invoke no prebuild, so ledger-snapshot.ts imports the committed JSON and the ledger page's 'requests not yet applied' count and list render empty while requests exist in docs/outstanding-issues-inbox/. That is the #338 class of failure the developer hub exists to prevent - a panel silently under-reporting. IT IS NOT A REGRESSION FROM PR #2530, and this matters for how it gets fixed: measured on main at 45a3dcacb before that branch existed, the committed snapshot already carried counts.pending 0 while 8 inbox requests were tracked in the same commit. The panel was already blank in dev. PR #2530 changed it from accidentally blank to blank by design, and added a gate that keeps it that way, so the fix now has to be deliberate. THE OBVIOUS FIX IS WRONG: adding a predev that generates the pending-inclusive form would write a populated pending into the tracked working tree on every dev-server start, which is precisely the churn #Y090R5 records and PR #2530 removed - and check:outstanding-issues-snapshot now fails on a committed non-empty pending, so a developer would be left with a permanently dirty tracked file that the gate refuses. Two shapes are worth weighing instead. (a) A development-only reader: the ledger page merges docs/outstanding-issues-inbox/*.json from disk when NODE_ENV is not production, leaving the committed artefact untouched. Cheap, but it adds an environment-conditional code path to a module whose docstring records that reading docs/ live was deliberately rejected because the production image never copies docs/ - the divergence would be benign here (production gets the true list from prebuild) but the reasoning must be written down rather than quietly reversed. (b) Generate the pending-inclusive form to a SEPARATE gitignored sidecar that the page reads when present, so nothing tracked is ever dirtied; costs a second artefact and a tolerant reader. Neither is urgent: the panel is administrator-gated, 404s for non-admins in production, and is correct in the deployed image because prebuild regenerates it there.", + "source": "Codex review on PR #2530, 2026-09-02", + "added": "2026-09-02" + }, + { + "id": "#5ECZQA", + "priority": "P3", + "type": "issue", + "summary": "Batch image signed-url route swallows per-item createSignedUrls errors and still returns 200", + "detail": "src/app/api/images/signed-urls/route.ts:105 checks only the top-level signed.error returned by createSignedUrls. supabase-js returns a per-path result array of { error, path, signedUrl }, so a single path that fails to sign yields an entry with no usable signedUrl while the top-level error stays null. The loop at :112-122 then skips that image because of the if (signedUrl) guard, and the route returns HTTP 200 with the image silently absent from the urls map. The client (src/lib/batch-signed-urls.ts) treats a missing key as \"not returned\" rather than \"failed\", so a figure disappears from the document view with nothing logged and no error surfaced anywhere. This is a milder instance of exactly the silent-failure class that #Z61JRT was about, and it survived that fix because the fix replaced getPublicUrl with createSignedUrls without adding per-item error handling. It is not a privacy or tenancy defect: the owner-scope gate at :70-88 has already run, so only images the caller is entitled to reach ever get to the signing step. The impact is diagnosability and a confusing partial render, not exposure. FIX: inspect each per-path result, and either surface a per-image error field in the response so the client can distinguish \"failed\" from \"not found\", or log the failing paths through the existing observability layer so a systematic storage problem is visible rather than presenting as scattered missing figures. Prefer the first: the response shape is already a per-id record, so an error discriminant fits without a breaking change. Note the singular route src/app/api/images/[id]/signed-url/route.ts:71 has the mirror-image gap, dereferencing signed.data.signedUrl without a null guard where the batch route has an explicit !signed.data check. Worth aligning both in the same change.", + "source": "Found while verifying #Z61JRT on main 45a3dca, 2026-09-02; supabase-schema-guardian review of src/app/api/images/signed-urls/route.ts", + "added": "2026-09-02" + }, + { + "id": "#9ZGNW7", + "priority": "P3", + "type": "issue", + "summary": "Developer-hub CODE still flips perf_changed, so an admin-only mockup route pulls a full Lighthouse run", + "detail": "Found while fixing #EFETZT on 2026-09-02 and deliberately NOT fixed in that PR, because it means editing a fail-closed CI classification surface. PR #2530 added data/repo-awareness-snapshot.json to perfExclusionPatterns in scripts/ci-change-scope.mjs, mirroring the carve-out data/outstanding-issues-snapshot.json already had for the same reason (PR #2302). That closes the common case - a handoff PR that only regenerates the snapshot no longer pays a ~7-minute Lighthouse budget run against a budget the change cannot move. It does NOT close the code case. src/components/developer-area/hub/** and src/lib/developer-area/** match the generic 'src' entry in perfPatterns (ci-change-scope.mjs:226) and are not excluded, because only the ROUTE WRAPPER lives under the excluded src/app/mockups prefix - the panel components live one directory hop away under src/components. So a PR touching the developer hub's own code still triggers lighthouse-budget for /mockups/development/**, which 404s for non-admins in production (src/app/mockups/layout.tsx and src/proxy.ts gate it behind DEVELOPER_AREA_HEADER) and cannot appear in either budgeted journey. WHY IT WAS LEFT: the exclusion list is a fail-closed safety surface, and widening it by directory prefix risks exempting a future component that IS reachable from a budgeted route. The safe shape is probably an explicit list of the developer-hub component and lib paths rather than a prefix, pinned by an assertScope self-test beside the two that already exist (ci-change-scope.mjs:1022), plus a test proving a non-hub file under src/components still flips perf_changed. Cost of leaving it is bounded and only paid by developer-hub PRs, which are rare.", + "source": "session 2026-09-02, PR #2530; verification-router review", + "added": "2026-09-02" + }, + { + "id": "#1M0J6D", + "priority": "P2", + "type": "issue", + "summary": "Web-container sessions start on a shallow clone, so two committed specs fail with messages that read as content errors", + "detail": "Measured 2026-09-02 in a Claude Code web container on origin/main 45a3dcacb. The session's checkout is a SHALLOW CLONE (depth 102). Two specs in the committed suite then fail, stably across two full runs, and neither message says anything about history depth: tests/clinical-hazard-controls.test.ts fails with 'CLINICAL-TRUTH-AUTHORITY: reviewedCommit does not exist 883f1007a85cd4e02198f39c12c4a4e467d4b89e' plus 'path is absent from reviewedCommit', and tests/rag-plan-package-parity.test.ts fails with 'manifest reconciledBase is unavailable locally: f3d1a3cce2c943ad3083425ed9c7c46dbef23087' from scripts/build-rag-plan-packages.mjs --check --require-origin-main. Both read as governance-manifest corruption, which is exactly the wrong conclusion: the commits are real and simply absent from a truncated history. PROOF: git fetch --deepen=2000 takes the clone from 102 to 5462 commits, both commits then resolve under git cat-file -e, and the two specs pass 27/27 with no code change. AGENTS.md already prescribes this remedy under 'Deleting code you believe is dead' for check:dead-code-candidate ('assessed on a shallow clone, where nothing can be dated - run git fetch --deepen=2000 first'), so the fix is known; it is simply not applied at session start and these two specs are not covered by that note. COST: a full npm run test is about six minutes, so a session can burn two of them before recognising the pattern, and the plausible-looking failure text invites someone to 'fix' a hazard manifest that is not broken. OPTIONS, cheapest first: have .claude/hooks/session-start.sh deepen the clone when git rev-parse --is-shallow-repository reports true (it already provisions Node and runs npm ci, so it is the natural place and the cost is one fetch); or make the two checks detect a shallow repository and fail with that as the stated reason rather than naming the commit; or document it in docs/testing.md. The first is preferred because it fixes the dead-code gate's known problem at the same time.", + "source": "session 2026-09-02, PR #2530 verification", + "added": "2026-09-02" + }, + { + "id": "#6APN03", + "priority": "P2", + "type": "task", + "summary": "Corpus health panel and the hub document count have never been seen against the real library", + "detail": "Both merged (#2504, #2512) and both were built in a cloud container with no live database and no browser, so every test uses stand-in data. Confirmation needs a machine with live Supabase config and a signed-in administrator: run npm run ensure, open /mockups/development and check the environment strip shows a real document count rather than 'document count unavailable', then open /mockups/development/corpus-health and check the four status tiles show numbers rather than 'Not read'. Then record which of the five spread cases resolveQualitySpread reports for extraction quality. An unverified report says every document may carry an identical placeholder quality_score. A uniform reading is a prompt to investigate and NOT a confirmed fault: assessDocumentIndexQuality starts the score at 1 and only subtracts penalties before rounding to three decimals, so a cleanly extracted corpus legitimately scores 1.000 for every document. Treat a uniform 0.00 as the suspicious case, since 0 is the column default and extraction_quality defaults to unknown, and corroborate against the issues array and metrics JSON on the same rows before recording anything against the scoring pipeline. Full context in docs/corpus-health-panel-handover.md.", + "source": "docs/corpus-health-panel-handover.md", + "added": "2026-09-02" } ], "pending": [] diff --git a/docs/outstanding-issues-inbox/1eb6df92-f59b-46bc-9cf4-8c50482c25c6.json b/docs/outstanding-issues-inbox/applied/1eb6df92-f59b-46bc-9cf4-8c50482c25c6.json similarity index 100% rename from docs/outstanding-issues-inbox/1eb6df92-f59b-46bc-9cf4-8c50482c25c6.json rename to docs/outstanding-issues-inbox/applied/1eb6df92-f59b-46bc-9cf4-8c50482c25c6.json diff --git a/docs/outstanding-issues-inbox/247ff620-b5b1-436b-9435-430f5dedbc3e.json b/docs/outstanding-issues-inbox/applied/247ff620-b5b1-436b-9435-430f5dedbc3e.json similarity index 100% rename from docs/outstanding-issues-inbox/247ff620-b5b1-436b-9435-430f5dedbc3e.json rename to docs/outstanding-issues-inbox/applied/247ff620-b5b1-436b-9435-430f5dedbc3e.json diff --git a/docs/outstanding-issues-inbox/250fb5a6-dd95-47d8-ba69-a8f3e9efbe53.json b/docs/outstanding-issues-inbox/applied/250fb5a6-dd95-47d8-ba69-a8f3e9efbe53.json similarity index 100% rename from docs/outstanding-issues-inbox/250fb5a6-dd95-47d8-ba69-a8f3e9efbe53.json rename to docs/outstanding-issues-inbox/applied/250fb5a6-dd95-47d8-ba69-a8f3e9efbe53.json diff --git a/docs/outstanding-issues-inbox/39f1842e-9780-4c73-8082-28cf9f271104.json b/docs/outstanding-issues-inbox/applied/39f1842e-9780-4c73-8082-28cf9f271104.json similarity index 100% rename from docs/outstanding-issues-inbox/39f1842e-9780-4c73-8082-28cf9f271104.json rename to docs/outstanding-issues-inbox/applied/39f1842e-9780-4c73-8082-28cf9f271104.json diff --git a/docs/outstanding-issues-inbox/3c5d6d42-0952-4bc8-a1f7-a4858789d0ed.json b/docs/outstanding-issues-inbox/applied/3c5d6d42-0952-4bc8-a1f7-a4858789d0ed.json similarity index 100% rename from docs/outstanding-issues-inbox/3c5d6d42-0952-4bc8-a1f7-a4858789d0ed.json rename to docs/outstanding-issues-inbox/applied/3c5d6d42-0952-4bc8-a1f7-a4858789d0ed.json diff --git a/docs/outstanding-issues-inbox/52120ceb-7c70-4c40-b401-c4bbd051903d.json b/docs/outstanding-issues-inbox/applied/52120ceb-7c70-4c40-b401-c4bbd051903d.json similarity index 100% rename from docs/outstanding-issues-inbox/52120ceb-7c70-4c40-b401-c4bbd051903d.json rename to docs/outstanding-issues-inbox/applied/52120ceb-7c70-4c40-b401-c4bbd051903d.json diff --git a/docs/outstanding-issues-inbox/73cb84d3-27ff-4554-bda1-01e1d53a5c6d.json b/docs/outstanding-issues-inbox/applied/73cb84d3-27ff-4554-bda1-01e1d53a5c6d.json similarity index 100% rename from docs/outstanding-issues-inbox/73cb84d3-27ff-4554-bda1-01e1d53a5c6d.json rename to docs/outstanding-issues-inbox/applied/73cb84d3-27ff-4554-bda1-01e1d53a5c6d.json diff --git a/docs/outstanding-issues-inbox/7418d9b5-ebb9-4cf2-9cde-ae405e923063.json b/docs/outstanding-issues-inbox/applied/7418d9b5-ebb9-4cf2-9cde-ae405e923063.json similarity index 100% rename from docs/outstanding-issues-inbox/7418d9b5-ebb9-4cf2-9cde-ae405e923063.json rename to docs/outstanding-issues-inbox/applied/7418d9b5-ebb9-4cf2-9cde-ae405e923063.json diff --git a/docs/outstanding-issues-inbox/81f20b9d-c84b-4929-8528-91e2fc8b4a01.json b/docs/outstanding-issues-inbox/applied/81f20b9d-c84b-4929-8528-91e2fc8b4a01.json similarity index 100% rename from docs/outstanding-issues-inbox/81f20b9d-c84b-4929-8528-91e2fc8b4a01.json rename to docs/outstanding-issues-inbox/applied/81f20b9d-c84b-4929-8528-91e2fc8b4a01.json diff --git a/docs/outstanding-issues-inbox/9a57cec3-e154-4523-bde4-9ddfa8e0c0dd.json b/docs/outstanding-issues-inbox/applied/9a57cec3-e154-4523-bde4-9ddfa8e0c0dd.json similarity index 100% rename from docs/outstanding-issues-inbox/9a57cec3-e154-4523-bde4-9ddfa8e0c0dd.json rename to docs/outstanding-issues-inbox/applied/9a57cec3-e154-4523-bde4-9ddfa8e0c0dd.json diff --git a/docs/outstanding-issues-inbox/a24114b2-feb8-4588-94f8-82a88ea3fc53.json b/docs/outstanding-issues-inbox/applied/a24114b2-feb8-4588-94f8-82a88ea3fc53.json similarity index 100% rename from docs/outstanding-issues-inbox/a24114b2-feb8-4588-94f8-82a88ea3fc53.json rename to docs/outstanding-issues-inbox/applied/a24114b2-feb8-4588-94f8-82a88ea3fc53.json diff --git a/docs/outstanding-issues-inbox/b11bdfdd-70eb-4bf9-9384-4d30af2a6ad2.json b/docs/outstanding-issues-inbox/applied/b11bdfdd-70eb-4bf9-9384-4d30af2a6ad2.json similarity index 100% rename from docs/outstanding-issues-inbox/b11bdfdd-70eb-4bf9-9384-4d30af2a6ad2.json rename to docs/outstanding-issues-inbox/applied/b11bdfdd-70eb-4bf9-9384-4d30af2a6ad2.json diff --git a/docs/outstanding-issues-inbox/b9a2d8d3-bf12-4961-b1c5-2424eca1fbd4.json b/docs/outstanding-issues-inbox/applied/b9a2d8d3-bf12-4961-b1c5-2424eca1fbd4.json similarity index 100% rename from docs/outstanding-issues-inbox/b9a2d8d3-bf12-4961-b1c5-2424eca1fbd4.json rename to docs/outstanding-issues-inbox/applied/b9a2d8d3-bf12-4961-b1c5-2424eca1fbd4.json diff --git a/docs/outstanding-issues-inbox/bcbdf769-62cf-4d67-be04-9065a603f439.json b/docs/outstanding-issues-inbox/applied/bcbdf769-62cf-4d67-be04-9065a603f439.json similarity index 100% rename from docs/outstanding-issues-inbox/bcbdf769-62cf-4d67-be04-9065a603f439.json rename to docs/outstanding-issues-inbox/applied/bcbdf769-62cf-4d67-be04-9065a603f439.json diff --git a/docs/outstanding-issues-inbox/d1a71c3e-a4ce-4c64-b31d-aab39ed3c79c.json b/docs/outstanding-issues-inbox/applied/d1a71c3e-a4ce-4c64-b31d-aab39ed3c79c.json similarity index 100% rename from docs/outstanding-issues-inbox/d1a71c3e-a4ce-4c64-b31d-aab39ed3c79c.json rename to docs/outstanding-issues-inbox/applied/d1a71c3e-a4ce-4c64-b31d-aab39ed3c79c.json diff --git a/docs/outstanding-issues-inbox/da308925-8094-4c05-88fd-f6dc36ad4361.json b/docs/outstanding-issues-inbox/applied/da308925-8094-4c05-88fd-f6dc36ad4361.json similarity index 100% rename from docs/outstanding-issues-inbox/da308925-8094-4c05-88fd-f6dc36ad4361.json rename to docs/outstanding-issues-inbox/applied/da308925-8094-4c05-88fd-f6dc36ad4361.json diff --git a/docs/outstanding-issues-inbox/f5078359-d4ea-4a91-b376-d5bbecb452a6.json b/docs/outstanding-issues-inbox/applied/f5078359-d4ea-4a91-b376-d5bbecb452a6.json similarity index 100% rename from docs/outstanding-issues-inbox/f5078359-d4ea-4a91-b376-d5bbecb452a6.json rename to docs/outstanding-issues-inbox/applied/f5078359-d4ea-4a91-b376-d5bbecb452a6.json diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 11171ca826..ee970b4be5 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -96,24 +96,19 @@ removed after current-main verification; it is not missing recommended work. | #VKH7N1 | P3 | rec | eval-canary neuroleptic-side-effect-escalation exceeded its 20 s latency SLO once | Run 32111839806 (canary pair 32100681177 -> 32111839806, otherwise green): strong generation took 20.2 s on neuroleptic-side-effect-escalation, flagged as a non-blocking latency advisory. Answer was still grounded via the source-backed extractive fallback. Watch on subsequent canaries; escalate only if it repeats or worsens. | docs/rag-improvement/HANDOVER.md packet table row S2, canary pair 32100681177 -> 32111839806 | 2026-08-18 | | #SBKXZ7 | P2 | task | Therapy clinician sign-off remains outstanding for 205 records; governed local workflow is implemented | Final replacement implementation update 2026-08-23 — ROW REMAINS OPEN. The repository-side tooling gap is implemented: the generator is guarded by a central contract requiring exactly seven explicit boolean checks, all true for reviewed; display-approved public reviewer attribution that rejects the full central trivial placeholder set and role-qualified variants, email and account handles, phone and obvious private identifiers; a real non-future UTC review time; a content-bound hash that invalidates stale sign-off; reviewCompleteness 100; and removal of pending-review warnings. The public-role vocabulary now covers common clinician titles, abbreviations, professions and role modifiers. A centralized token-sequence adjacency rule rejects trivial labels next to those roles even when identity-like words follow, including Fake Dr Jane, Dr Fake Jane, Anonymous GP Smith, N/A GP Smith, Fake senior doctor Jane and Anonymous registered nurse Smith. The single name-like token Na is excluded only from adjacency rejection so legitimate Dr Na Li remains valid; Test Valley Clinical Governance Committee and Dr Maria Testa also remain valid. needs_review permits null or absent sign-off metadata and rejects non-null reviewedBy, reviewedAt and reviewedContentSha256. ReviewStatus is exactly reviewed or needs_review. npm run therapy:review remains report-only by default; its only write flow is one exact slug in an interactive TTY, shows every governed field, collects all seven answers, requires byte-exact REVIEW confirmation, and has no batch, yes, answer, provider or production path. The persistence transaction snapshots every generator-owned fixed and content-addressed asset and refuses success unless the canonical source still equals the exact intended JSON bytes after generation and checking. A generator or check failure observed while the source remains unchanged restores the exact pre-review source and generated bytes; deterministic injected cases preserve concurrent source bytes and restore generated assets. Current-head focused evidence for the final role-only hardening: Therapy workflow tests passed 87 of 87; focused ESLint, Prettier, JavaScript syntax and diff checks passed. Broader gates were intentionally not rerun for this narrow vocabulary follow-up. Canonical source and generated catalogue bytes remain unchanged. Clinical state remains 205 total, 205 needs_review, 0 reviewed and 0 attributed. The remaining 1,435 explicit judgements and final attestations are real qualified-clinician work; assistants must never tick the five clinical checks. Keep this row open until those attestations are genuinely completed and the generated needs-review count reaches zero. No provider or production write was run. | session 2026-08-18 | 2026-08-18 | | #8VAY97 | P2 | task | The document_index_units retrieval path has no EXPLAIN baseline, and Phase 5 has no query-specific plan-flip evidence | Two Phase 5.1 deliverables are explicitly OPEN, not discharged. Re-graded P3 -> P2 versus the withdrawn request 2040d1fb, because that request understated the gap by claiming substitute coverage that does not exist. (A) NO EXPLAIN BASELINE FOR THE INDEX-UNITS PATH. public.explain_retrieval_rpc accepts exactly four names -- match_documents_for_query, match_document_chunks_text, match_document_lookup_chunks_text, match_document_table_facts_text -- and raises 22023 Unsupported retrieval RPC for anything else, proven against production for both match_document_chunks_text_v2 and match_document_index_units_hybrid_v2. For the first of those the v1 sibling match_document_chunks_text shares the owning table document_chunks and is a usable stand-in. For the second there is none: match_document_index_units_hybrid_v2 delegates to match_document_index_units_hybrid_scoped over document_index_units (supabase/schema.sql:8033-8054), and no supported RPC touches that table. document_index_units is one of the two section 1.2 outliers, so the outlier that most needed a baseline is the one that has none. (B) NO QUERY-SPECIFIC PLAN-FLIP EVIDENCE. explain_retrieval_rpc EXPLAINs `select * from public.(...)`, so a PL/pgSQL body's inner plan is never exposed and every sample reports a single Function Scan with no index names. Plan section 5.1's 'record plan flips (seq scan -> index scan)' is therefore unanswerable through this instrument. The pg_stat_user_indexes read captured in Phase 5.1(c) is a WEAKER and DIFFERENT signal, not a substitute: idx_scan is cumulative across every workload touching the table and no before/after delta was captured around the samples, so it can prove an index is never chosen by anything but cannot prove that a given query changed plan. NEXT: one migration extending the explain_retrieval_rpc p_rpc branch list to the _v2 family (at minimum match_document_index_units_hybrid_v2 and match_document_chunks_text_v2), shipped in an approved window -- with D4 ON, merging it to main deploys it, so it needs the window and a green post-merge live-drift run. Then re-run npm run profile:retrieval --analyze to capture the missing baseline. For (B), consider whether an auto_explain-style capture is a better fit than widening the RPC. STOP: do not record the cumulative index-usage read as plan-flip evidence; that conflation is exactly what this row exists to prevent. | Codex review of PR #2250 (P2, comment 3833062803 and 3833062807); docs/audit/live-drift-forensics-2026-08.md Phase 5 close-out 5.1(b); supabase/schema.sql:8033-8054 | 2026-08-21 | -| #Y090R5 | P2 | issue | data/outstanding-issues-snapshot.json is a generated file every inbox PR must regenerate, so any two concurrent ledger PRs conflict on it and the loser must re-resolve after every main merge | Observed 2026-08-22 on PR #2284, which conflicted twice within about an hour and was closed rather than untangled. check:outstanding-issues-snapshot requires the committed data/outstanding-issues-snapshot.json to be in step with docs/outstanding-issues.md plus every pending inbox request, so a PR that queues a request MUST commit a regenerated snapshot or CI fails with 'counts.pending: committed N vs regenerated M'. But the regenerated content depends on every OTHER pending request too, so the file differs between any two concurrent ledger PRs and conflicts as soon as either lands. Recent main history shows the collision surface is real, not theoretical: 93af96cf8, 4cbac0ceb, 2ca31d6d8 and 639108f07 all touch that one file. The immutable-request design deliberately removed this class of conflict for the requests themselves; the snapshot reintroduces it in a single generated artifact, which is the same serial-only bottleneck #EH9VA6 and the ledger write-discipline work were meant to eliminate. Resolution is mechanical but must be done exactly once per main merge - never hand-merge it: take main's version then re-run node scripts/generate-outstanding-issues-snapshot.mjs. Options worth weighing: regenerate the snapshot during npm run issues:reconcile (the already-serialized step) instead of in every request-adding PR, so ordinary branches never touch the file; or have the check tolerate a snapshot that is in step with the canonical ledger while ignoring pending-request counts; or add a union/regenerate merge strategy. Whichever is chosen, this bites every future issues PR that does not land within the gap between other ledger merges. SECOND SYMPTOM, same root cause, measured 2026-08-22 on PR #2299: because the file lives under data/ - the generated CLINICAL snapshot export directory - scripts/pr-policy.mjs classifies it clinicalRisk:true. Confirmed by calling classifyPullRequestFiles directly: ['data/outstanding-issues-snapshot.json'] alone returns clinicalRisk:true, while the inbox JSONs alone return false. So every ledger PR that regenerates the snapshot is forced to carry a complete ## Clinical Governance Preflight in its body for a file holding no clinical data at all, and fails PR policy with 'Clinical-risk paths require the ## Clinical Governance Preflight section' until it does. That is ceremony with no safety value, and it trains reviewers to tick clinical governance boxes reflexively on changes that have nothing to do with clinical output - which is the failure mode that section exists to prevent. Moving regeneration into issues:reconcile fixes both symptoms at once; relocating the artefact out of data/ would fix this second one on its own. | session 2026-08-22, closing PR #2284 | 2026-08-22 | | #J8SJQ9 | P2 | issue | Antipsychotic metabolic monitoring returns a source-backed stub instead of a written answer, and the eval case must not be relaxed to hide it | FOUND BY THE PACKET 2 CANARY, run 32589154243 (2026-08-22). "What metabolic monitoring is required for antipsychotics?" now returns the source-backed review stub — "The uploaded documents contain relevant guidance on metabolic monitoring for antipsychotics, but a full written answer could not be completed just now. Relevant document passages are cited below" — because the extractive candidate behind it was one of the two incoherent guidance-wrapper answers that #NPQJKP shipped a predicate to reject. The degradation is correct behaviour; the underlying defect it exposes is that the answer path cannot produce a usable written answer for this query at all. THIS IS NOT AN EVAL-CASE BUG AND MUST NOT BE FIXED BY ADDING acceptSourceOnly. All four cases carrying that flag document the same rationale: the corpus has no single authoritative source, so a source pointer is a legitimate answer, and quality-discharge-documentation deliberately drops mustContainAny for exactly that reason. quality-antipsychotic-metabolic-monitoring is the opposite case — it names expectedFiles ["MHSP.MetabolicScreening.pdf"], an authoritative source exists, and antipsychotic metabolic monitoring is a routine question a psychiatrist should get answered in prose. Adding the flag would silence a true signal. The targeting eval already grades it correctly at score 0 with reason "source-backed review stub", even though its mustContainAny ["metabolic", "monitor"] is satisfied by the stub text, so the instrument is working and only the answer is missing. FIRST DIAGNOSTIC STEP, because it splits the problem in two: determine whether generation was attempted for this case at all. The same run recorded provider_attempted:false for 11 of 30 targeting cases. If OpenAI was never called for this one, the cause is upstream of answer quality entirely (routing, admission control, or the retry ladder — note Packet 1 covers deadline admission control and should be checked for overlap before starting). If it was called and returned nothing usable, the cause is in extraction or generation for the medication_dose_risk class against this document. Do not begin work without checking Packet 1 and Packet 3 (#S4R2W3) for overlap: all three touch rag.ts, and overlapping changes ruin canary attribution. | canary run 32589154243; src/lib/rag/rag-eval-cases.ts quality-antipsychotic-metabolic-monitoring; ledger #NPQJKP | 2026-08-22 | -| #EFETZT | P2 | issue | The repo-awareness snapshot goes stale on any PR that waits, and it already reddened CI once | UPDATE 2026-08-27 (PR #2413, second and third occurrences): this is not only a staleness problem, it also produces hard git merge conflicts that block auto-merge. data/repo-awareness-snapshot.json conflicted twice in fifteen minutes on one PR head — first when main advanced to be65b8a, again when main advanced to fb88b47 — each time setting mergeable_state=dirty, which suppresses refs/pull//merge so pull_request CI (CI, Gitleaks, Semgrep) does not run at all and the check list reads empty rather than red. Both were resolved identically: git merge origin/main, git checkout --theirs the snapshot, npm run snapshot:repo-awareness, commit, push. The staleness half also reproduced: check:repo-awareness-snapshot failed Static PR checks with 'review_state differs from the repository'. Cost per occurrence is one full CI round trip plus a push, and it scales with how many PRs are open, since every merge to main regenerates the file for all of them. Reinforces the fix direction already proposed on this row: exclude docs/branch-review-records/ from the compared content the way captured_revision already is, or compare a narrower key set. Adding .gitattributes merge guidance for this path would blunt the conflict half but not the CI-staleness half. | PR #2359 CI failure, 2026-08-25 | 2026-08-25 | | #JZ8B36 | P2 | issue | Caring Contacts: grow the safety-incident responder note into a lightweight patient case-note capability, and settle its retention disposition then | caring_contacts.service_stops.note is free text a responder writes mid-incident and the schema comments it as patient data. Today nothing can remove it: retention.ts covers episodes and audit events only and never reaches this table; UPDATE is blocked by the assert_service_stop_immutable trigger (Rulings 30 and 32); DELETE is the only remaining path and Ruling 34 deliberately left it unblocked. Owner decision 2026-08-21: keep patient case notes as an available capability and KEEP the patient record - build this part brief and lightweight now, to be extended later. So no purge or de-identification path is required at this stage and no code change is owed; what is owed is that when case notes are actually built in a later phase, the retention disposition of this note and of any patient case note is settled deliberately at that point rather than inherited by accident. Do NOT resolve this by blocking DELETE on service_stops without providing a removal path first, or the data becomes permanently unremovable. Synthetic prototype only - no real patient data is or has been involved. | Task 11a fix-round-2 review (Ruling 34) and owner decision 2026-08-21; docs/caring-contacts/phase-2a-build-record.md | 2026-08-20 | | #000GN4 | P2 | issue | A hardcoded topic denylist refuses in-corpus psychiatric queries with zero retrieval and caches the empty result | rag-query-guard.ts:6 short-circuits any query matching ssri, antibiotic, pneumonia, hyperkalaemia and others; every token was transcribed from the eval fixture questions. ssri is demonstrably in-corpus: the golden fixture case vector-gad-worry expects a Generalised Anxiety document whose expectedContentTerms include ssri. So 'Which SSRI is first line for generalised anxiety disorder?' is refused content-blind and the empty result is cached. Worse for eval integrity: classifyCorpusGrounding, the deterministic mechanism built to make exactly this call, is explicitly bypassed for these queries, so the unsupported-query controls pass by literal topic-word match on their own question text rather than by the grounding machinery they exist to validate. A second divergent copy of the same regex lives at clinical-search.ts:371 and additionally contains 'ketamine sedation', so the two guards already disagree. FIX: delete the denylist, let classifyCorpusGrounding decide, single source of truth. Protected surface: needs approval and a canary pair. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | -| #ZBAC9D | P2 | task | Retrieval RPC still treats a null document owner as public, so the owner_id republication hole is only half closed | RE-VERIFIED against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 on 2026-09-01. CLOSED: the owner-deletion path is no longer outstanding. public.documents, document_labels, document_summaries and document_table_facts now use ON DELETE RESTRICT, with the live migration and a schema proof that pins the four visibility tables and their exact restrict action. CODE DEFECT (unresolved): public.retrieval_owner_matches treats the public sentinel as row_owner_id IS NULL, and retrieval_owner_matches_v2 treats include_public as row_owner_id IS NULL; neither predicate requires metadata.public_corpus = true. An ownerless row without the publication marker could therefore still enter a public retrieval result. EXPOSURE (measured 2026-09-01, owner-approved read-only GET against ref sjrfecxgysukkwxsowpy): total documents 2851; owner_id NOT NULL = 0; owner_id IS NULL = 2851; of those, metadata.public_corpus = true = 2851. EXPOSED COUNT = 0. The production corpus is deliberately published and has no privately owned or unmarked ownerless document. PRIORITY: P2, not P1. NEXT: change the retrieval contract so its public branch requires both owner_id IS NULL and metadata.public_corpus = true, preserving owned-document retrieval; this is a protected RAG behaviour change and requires the RAG flag, an offline regression proof, and a live eval-canary pair. | repo-wide audit at 3ed1932 (six domain reviewers); re-verified against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 and an owner-approved read-only production count, database coordination chat 2026-09-01 | 2026-08-23 | +| #ZBAC9D | P2 | task | Retrieval RPC still treats a null document owner as public, so the owner_id republication hole is only half closed | RE-VERIFIED against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 on 2026-09-01. CLOSED: the owner-deletion path is no longer outstanding. public.documents, document_labels, document_summaries and document_table_facts now use ON DELETE RESTRICT, with the live migration and a schema proof that pins the four visibility tables and their exact restrict action. CODE DEFECT (unresolved): public.retrieval_owner_matches treats the public sentinel as row_owner_id IS NULL, and retrieval_owner_matches_v2 treats include_public as row_owner_id IS NULL; neither predicate requires metadata.public_corpus = true. An ownerless row without the publication marker could therefore still enter a public retrieval result. EXPOSURE (measured 2026-09-01, owner-approved read-only GET against ref sjrfecxgysukkwxsowpy): total documents 2851; owner_id NOT NULL = 0; owner_id IS NULL = 2851; of those, metadata.public_corpus = true = 2851. EXPOSED COUNT = 0. The production corpus is deliberately published and has no privately owned or unmarked ownerless document. PRIORITY: P2, not P1. NEXT: change the retrieval contract so its public branch requires both owner_id IS NULL and metadata.public_corpus = true, preserving owned-document retrieval; this is a protected RAG behaviour change and requires the RAG flag, an offline regression proof, and a live eval-canary pair. RE-MEASURED 2026-09-02 (independent user-authorised read-only query, same ref): unchanged at public_marked=2851, ownerless_unmarked=0, owned=0, total=2851, so EXPOSED COUNT remains 0 across two consecutive days. That read also answered a question this row never recorded: public.document_corpus_access_state.mode = 'public'. set_document_corpus_access_mode('public') has therefore been run against live at some point, even though no migration or script in the repo invokes it and the table seeds to 'private' (20260825025032:43-45). That operator back-stamp is what closed the legacy ownerless-but-unmarked population described in the 20260825025032 header, and it is why this defect currently has zero blast radius. The exposure is zero by data state, not by predicate, so any future ownerless insert that skips the marker re-opens it silently and the NEXT step above still stands. | repo-wide audit at 3ed1932 (six domain reviewers); re-verified against current main c944fcdad78e106a7fb7b9c99e5e3a829e022fc4 and an owner-approved read-only production count, database coordination chat 2026-09-01 | 2026-08-23 | | #A3AHCX | P2 | issue | Caring Contacts: the governed-message validator has zero production callers | validateGovernedMessage in src/lib/caring-contacts/message-policy.ts enforces the segment limit, the prohibited vocabulary, the required support fragments and (from 2026-08-24) the fictional-contact check, but grep across src/ and worker/ finds NO production caller -- it is exercised only by tests. That is consistent with there being no send path in this prototype, and the checks are the mechanism a future sender must use. Recorded so nobody mistakes 'the validator refuses this' for 'the system refuses this'. When a dispatch or preview path is built, wiring it to this validator is the first requirement. | Task C implementer concern 1, independently confirmed by grep 2026-08-24 | 2026-08-24 | | #5PQRSW | P2 | issue | Patient-profile alerts surface unassessed gates only for contraindication rows, so 266 caution, dose-adjust and monitor gates silently no-op | medication-patient-alerts.ts:344-358 collects missingGates for every gated row but the consumer discards them unless patient.action is contraindication. Counted from data/medications-snapshot.json: 256 caution, 145 dose-adjust and 62 monitor rows, 266 of them gated on a numeric or categorical input (eGFR, CrCl, QTc, age, hepatic). A clinician entering a partial profile without eGFR gets no considerations from a renal DOSE REDUCTION row and an empty panel indistinguishable from an all-clear; the UI sentence that would warn is scoped to contraindications only. FIX: include dose-adjust at minimum in the unassessed set and widen the UI copy. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | | #ZK460W | P2 | issue | The extractive review fallback flips grounded false to true, which is the shared root cause behind the three tracked adversarial divergences | rag.ts:3107-3122 enters the branch BECAUSE the answer failed its quality gate (!finalizedAnswer.grounded) and then sets grounded: true with confidence re-derived from retrieval similarity alone. Claim support force-classifies every claim on this route as routine, so the authority gate passes vacuously and trust resolves to high, unlocking quote cards and suppressing the source-gap warning. The prose restates the clinician's own query as though the corpus confirmed it. This is the mechanism behind #NTAV3D, #C2D9JF and #VXB8XA; none of those rows names it. FIX: keep grounded false and confidence unsupported, retaining citations as review-only provenance (that reason string already exists in answer-render-policy). Protected RAG surface: own PR, RAG impact line, canary pair; flips the three divergence pins together. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | -| #JFRCZ4 | P3 | issue | check:repo-awareness-snapshot needs git, and nobody has established what it does without it | The gate shells out to git ls-files and git log through the generator, and it now sits in verify:cheap, verify:pr-local and CI. What it does in a git-less checkout - a git archive export, which is the very technique the Phase 2 spec section 8.2 names as a required proof - has never been tested. The generator itself is proven to fail loudly there, which is correct under ruling R5 because it only ever runs from docs:update. The gate is different: it runs in three gate chains, so a git-less environment turns a safety check into an unexplained failure. Establish the behaviour and document it, or make the gate skip explicitly with a stated reason rather than erroring. | Phase 2 whole-branch review, PR #2359 | 2026-08-25 | | #9P4XAE | P2 | issue | Form 12A displays a false Password protected warning although its committed PDF is readable | Measured on 2026-08-24: public/forms-pdf/form-12a.pdf has no /Encrypt marker, while data/forms-pdf-manifest.json records passwordProtected true and the form page renders a warning from that flag. Before changing any data manifest, identify its canonical source and supported generation path; if none exists, introduce a measured manifest-generation or validation workflow that derives the flag from committed PDF bytes, then regenerate the manifest and make the form test assert the generated contract. This changes a clinician-facing statement and requires Clinical Governance Preflight. | Direct inspection of public/forms-pdf/form-12a.pdf; data/forms-pdf-manifest.json; src/lib/form-catalog.ts; src/components/forms/form-detail-page.tsx; tests/forms.test.ts | 2026-08-24 | | #VK8ZYY | P2 | issue | Medication source_status current is derived from a substring and never expires | medication-records.ts derives source_status from sourceText.includes('checked'). The snapshot Sources rows carry real dates (all 2026-05/06 today) but nothing parses or ages them, so these records will still report current in 2028; medication-badges only ever renders Review due or Outdated from an explicit sourceStatus this derivation cannot produce. The same substring also matches the negative forms 'not checked' and 'unchecked'. The sibling validation_status literal was fixed in the audit branch; this half remains. FIX: parse the ISO date already present in the source text and return review_due past a defined interval, unknown when no date parses. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | -| #Z61JRT | P2 | issue | The batch image endpoint calls getPublicUrl against a bucket pinned private, so public-corpus figures silently fail to load | api/images/signed-urls/route.ts:109 uses getPublicUrl for every image whose document isPublicDocument, but clinical-images is inserted and re-asserted with public = false. The URL 404s while the route still returns HTTP 200, so nothing surfaces as an error. The singular route (images/[id]/signed-url) always signs correctly. Latent privacy edge: if the bucket is ever flipped public to fix this, the same branch hands out permanent unauthenticated URLs, and isPublicDocument checks metadata markers BEFORE owner_id, so a document with a non-null owner carrying public_corpus metadata would take that branch too. FIX: delete the public branch and sign uniformly. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | | #NCAWAF | P2 | issue | Railway has no Australian region, so the current app tier cannot host a real-patient Caring Contacts deployment | docs/deployment-architecture.md records Railway regions as US West, US East, Amsterdam and Singapore; the app tier runs in Singapore against Supabase in ap-southeast-2 Sydney. The Caring Contacts decision lock requires identifiers, message content, application data, backups, logs and provider processing to remain in Australia. A real-patient pilot therefore needs a separately contracted Australian PHI-capable environment, not the current Clinical KB deployment. Does not block the synthetic build, which holds no real patient data. Hazard H-36. | docs/deployment-architecture.md; Caring Contacts decision lock hosting controls | 2026-08-18 | | #TDKW4W | P2 | task | Caring Contacts hospital referral feed feasibility is unconfirmed and is the largest programme risk | Every screen, rule and table assumes a structured referral arrives from a WA hospital system and a structured outcome is written back. No document names an actual system; nobody has confirmed the feed is possible, who owns it, or what it costs. The build is insulated by a provider-neutral referral interface with a synthetic adapter, so this does not block development. docs/caring-contacts/referral-feasibility.md holds the twelve questions to ask, who to ask, and the manual-entry fallback if a structured feed is not achievable. Hazard H-44. One conversation with a WA Health clinical informatics lead is worth more than a month of code. | Caring Contacts design session 2026-08-19; hazard log H-44 | 2026-08-18 | | #W98GR7 | P2 | issue | Enrichment artifact families can be permanently lost, the designed repair function is called by nothing, and no monitoring exists | supabase/functions/indexing-v3-agent deletes an artifact family (document_memory_cards, document_index_units, document_embedding_fields) BEFORE calling OpenAI and re-inserting, one family at a time, never staged-then-swapped. A provider outage spanning the retry and deferral budget leaves the family permanently empty, and both terminal states (failed, needs_enrichment_artifacts) are excluded from claim eligibility forever. repair_strict_enrichment_gate_batch (migration 20260625033425) is invoked by NOTHING in the codebase, and no script or query references needs_enrichment_artifacts for monitoring, so a stuck document reports as indexed with an empty artifact family. Silent corruption, not a crash. FIX: stage-then-swap per family, plus a re-queue path or wire the existing repair function into an ops script. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | | #DW3XK8 | P2 | issue | Four no-DDL migrations sit outside every history guard, and the drift allowlist was never reconciled against a live read | Sixteen migration files contain no executable DDL. tests/migration-history-placeholders.test.ts tracks only six of the ten select-1 files; untracked are 20260629100000, 20260702170000, 20260708160000 and 20260709150000. 20260702170000 is the sharpest: supabase/drift-allowlist.json allowlists every neighbour in its window (100000 through 180000) but not it, and every allowlisted neighbour carries real DDL while 20260702170000_fix_match_chunks_text_n1.sql is bare select 1. The allowlist header states it was seeded by name from the repo, NOT by a live read. NEXT (operator, read-only): select (public.schema_drift_snapshot() -> 'migration_history'); and check statement counts for those versions. Any zero-statement row without an allowlist entry needs a 20260804110240-pattern validation guard. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | -| #ZM8902 | P3 | issue | docs:check-links passes over a broken relative markdown link in a spec, so a binding document went missing unnoticed | scripts/check-docs-links.mjs reported 1905 references resolving while docs/superpowers/specs/2026-08-15-caring-contact-coordination-design.md line 180 linked to ../../caring-contacts/design-handoff.md, a file that has never existed, and line 131 described it as binding. Found by hand on 2026-08-19 and repointed to interaction-matrix.md, which actually holds the 24-row modality matrix. The gate is a check that cannot fail for this class of link: verify whether relative markdown links outside a recognised prefix are skipped, and close the gap. Same family as the mutation-testing concerns already recorded for stderr hooks and tail-masked exit codes. | Caring Contacts doc repair 2026-08-19 | 2026-08-18 | | #CJCH2E | P3 | issue | Ingestion panel reports 'could not reach' when it reached the endpoint but could not parse the body | A malformed JSON body on a 200 response falls into the generic catch in IngestionPanel and produces 'The panel could not reach the ingestion jobs endpoint.' It did reach it. The adjacent parseReadyPayload path gets this right and says 'returned an unexpected shape'. Small, but the panel's whole purpose is telling a reader precisely what is and is not known, so a message that misattributes the failure is off-key. Found during the ingestion panel review. | Ingestion panel review, 2026-08-25 | 2026-08-25 | | #EG4Q7W | P3 | rec | Caring Contacts: postgres-repository.ts is ~2,080 lines and holds five self-contained clusters | STILL OPEN AND LARGER THAN RECORDED; re-measured against origin/main d1bb2c197 on 2026-09-02. A done request for this row (97bbfd51) claimed the file had been modularized into five cohesive domain modules (core, plans, contacts, referrals-pathways, assignments). That claim is false against current main and the request was cancelled, so nothing was lost -- but the reason recorded on the cancellation ('target issue is no longer in Open items on current main') was also wrong, since the row was and remains open. Recording the measurement so a third attempt does not start from either error. MEASURED: src/lib/caring-contacts/db/postgres-repository.ts is a single file of 2,500 lines. It has not been split, and it is now about 420 lines LARGER than the ~2,080 this row recorded when it was filed. There are no sibling domain modules beside it under db/. The original recommendation is unchanged and still applies: the split is pure structure with no behaviour change, every method's set_config / set local role preamble must survive it intact because this file is the code half of row-level security, and the existing shared contract suite is the proof that it did. | docs/caring-contacts/phase-2a-build-record.md deferred list item 2 | 2026-08-24 | | #59JT7W | P2 | issue | Caring Contacts: the closing-message refusal is a guard that cannot fire | RE-VERIFIED STILL OPEN against origin/main d1bb2c197 on 2026-09-02. A done request (c862828e) claimed this was resolved because resolveClosingContactMessageBody and tests/caring-contacts-message-policy.test.ts exist. It was cancelled, and cancelling it was correct -- but for a different reason than the one recorded, so pinning the measurement here to stop a third attempt closing the row on the same mistake. The function existing was never what this row asked for. MEASURED: grep for resolveClosingContactMessageBody across src/**/*.ts and src/**/*.tsx returns exactly two hits -- its own definition at src/lib/caring-contacts/message-policy.ts:208, and a prose mention in a doc comment at src/lib/caring-contacts/message-copy.ts:121. There is still no production caller. The tests at tests/caring-contacts-message-policy.test.ts:477 exercise the function directly, which proves the refusal is correct but not that anything is obliged to ask for it, so the guard remains one a future sender can bypass by resolving a closing body some other way. The row's own stop rule is unchanged and still binding: do not record approved copy item A4 as closed. The blocker is also unchanged -- PlannedContact in schedule.ts carries messageType and no body, so there is still no seam to wire this to, and inventing one means deciding where an authored closing body comes from. | Task C review finding, 2026-08-24; confirmed by controller and reviewer independently | 2026-08-24 | @@ -122,16 +117,13 @@ removed after current-main verification; it is not missing recommended work. | #4VKAA1 | P2 | issue | Caring Contacts: four bare foreign keys onto plans/contacts predate the composite same-team rule | caring-contacts/supabase/migrations/0001_caring_contacts_foundation.sql lines 146, 172, 218 and 227 declare plan_id/contact_id references without the team_id composite that Rulings 25 and 27 later made mandatory, so a row written by one team can point at another team's plan or contact. Verified in the migration text 2026-08-24. Decide whether to add the composite keys by migration or record the exception; a bare key already caught one real cross-team defect. | docs/caring-contacts/phase-2a-build-record.md (Rulings 25/27); Phase 2A deferred list | 2026-08-24 | | #1S81R8 | P1 | task | Caring Contacts: three unmitigated hazards block any real-patient pilot (safety officer, lived-experience review, Aboriginal health review) | docs/caring-contacts/hazard-log.md records H-00 (no named clinical safety officer, so nobody owns clinical risk), H-04 (the message set has never been read by anyone with lived experience) and H-05 (no Aboriginal cultural safety review, in a WA suicide-aftercare service). All three are Open with no control. None blocks the synthetic build; every one blocks a pilot. H-04 is ready to run today: docs/caring-contacts/message-review-pack.md is a complete facilitation pack. Owner Josh for H-00 and H-04; Aboriginal health governance for H-05. | Caring Contacts design session 2026-08-19; hazard log H-00/H-04/H-05 | 2026-08-18 | | #Q4Y7TR | P2 | issue | No automated reaper for abandoned reindex generations, so crashed mid-builds leak rows and storage indefinitely | cleanup_abandoned_document_index_generations is called from exactly one place, scripts/cleanup-abandoned-reindex-generations.ts via npm run reindex:cleanup-staged. No cron.schedule, Railway cron, or CI job references it. Any crash mid-build, or the empty-generation guard at worker/main.ts throwing, leaves staged rows under a never-committed index_generation_id and storage objects under {owner}/images/{doc}/{generation}/ that nothing revisits. Unbounded Postgres row and Storage growth, dependent on an operator remembering to run a janitor. FIX: write storage_cleanup_jobs rows for superseded generations at commit time, where the commit RPC already knows both generation ids. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | -| #N8B176 | P3 | issue | isPublicDocument ranks caller-adjacent metadata above owner_id | src/lib/documents/is-public-document.ts:49-61 returns true for metadata.public_corpus, is_public, public, visibility === 'public' or source_kind === 'registry_record' BEFORE consulting owner_id at :64 - a second, weaker definition of public than the one the DB and publish_approved_documents use. Not exploitable today: upload metadata is server-constructed and bulk edit accepts only a fixed key schema. But the function is exported as a standardized predicate and documented for reuse, so the next access decision that adopts it inherits a metadata-driven publication bypass. FIX: make owner_id === null the first and authoritative check, or scope the helper to rendering only. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | | #J43Z6B | P2 | rec | The tenancy boundary is app-code only, so a single missing owner predicate has nothing behind it; make the property mechanical | 20260719070000_align_existing_acls revokes ALL on every public base table from public, anon and authenticated and re-grants only service_role, and roles.sql makes that the default for future objects. The roughly 30 policies written TO authenticated therefore can never be evaluated, and every read path uses the RLS-bypassing admin client across 37 API route files. This is deliberate and pinned by tests, not a bug - but it means RLS is a dead backstop and app code is the only tenancy boundary. The audit scanned all owner-bearing .from() calls under src/app/api and found no unscoped read, so the property holds today by review rather than by enforcement. FIX: a lint or contract test asserting that any admin-client query against an owner-bearing table in src/app/api is lexically accompanied by an owner predicate. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | | #QCNE6N | P2 | issue | Schema drift is gated against schema.sql only; the migration chain's semantics are never diffed against the mirror | generate-drift-manifest.ts replays supabase/schema.sql and embeds ITS sha256; CI's db-reset-verify runs supabase migration up --local, proving the chain APPLIES but never diffing the result against schema.sql. A migration whose function or policy body diverges from the mirror passes every gate. Object-NAME parity does hold today (all create targets across 211 migrations resolve into schema.sql) but it is enforced by hand-written per-object tests, not systematically. FIX: after migration up --local, replay schema.sql into a second scratch database and diff schema_drift_snapshot() between the two. Fully offline and containerised, no provider access. DEMONSTRATED 2026-09-01, no longer theoretical: live-drift run 33484535655 (red on main at d3074946a) reported its sole unexpected finding as public.correct_clinical_query_terms(text,real) def_hash manifest e2356565 vs live 2ebaf978. Cause was exactly this gap - migration 20260831100000 (PR #2477) redefined that function with a duplicated 'and length(canonical) between 4 and 40' predicate and supabase/schema.sql was never updated to match, so the manifest disagreed with live while every pre-merge gate stayed green. The db-reset-verify assertion that did run is committed.schema_sha256 === generated.schema_sha256 (.github/workflows/ci.yml ~L1082-1095), which only catches an unrefreshed manifest, not a chain/mirror divergence. Behaviour impact of that instance was nil (the duplicate predicate is a boolean no-op) but it cost a red daily alarm and a remediation PR, and it is the second occurrence of this failure class after the #316 root cause (SET work_mem absent from schema.sql). Strengthens the case for scheduling the containerised two-database diff above. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f; demonstrated by live-drift run 33484535655, classified by the database coordination chat 2026-09-01 | 2026-08-23 | | #AGRAKQ | P3 | issue | Caring Contacts: the interface prohibited-language helper has the same 'lead' job-title collision B2 just fixed for messages | NARROWED TO PLURALS; re-measured against origin/main d1bb2c197 on 2026-09-02 by executing the pattern, not by reading it. A done request (461a406d) claimed the plural case was fixed and was cancelled; the cancellation's stated reason was wrong (the row is open), but the outcome is right, because the plural case is NOT fixed. HALF DONE: tests/helpers/caring-contacts-prohibited-language.ts now carries the same negative lookbehind the message rules use -- (? | P3 | issue | run-playwright reported a production build failure with no compile error, once, unreproduced | Seen during Task 19 on 2026-08-22. node scripts/run-playwright.mjs tests/ui-caring-contacts-workspace.spec.ts --project=chromium printed 'Playwright production build failed (status 1)' with no TypeScript or bundler error anywhere in its output; the identical command run immediately afterwards, against an unchanged tree, built and ran cleanly. It did not recur across roughly a dozen further invocations that session, including six mutation runs. Recorded rather than explained. Why it matters: the wrapper's own contract is that a non-zero exit is either a genuine red or the distinguishable exit 75 admission-busy case, and a silent build failure is neither - a session that hit it once and stopped would report a red gate that does not exist, and a session that retried without noticing would never learn the run had been lost. The machine was under heavy concurrent load at the time (other agents held Vitest and lint leases in the same repository), so a resource or lock interaction during the Next build is the first place to look. Next step if it recurs: capture the full unfiltered output of the failing invocation before retrying, and check whether scripts/run-playwright.mjs is swallowing the child build's stderr rather than the build having produced none. | docs/caring-contacts/phase-2a-sdd-archive/task-19-report.md (Concerns 5) | 2026-08-22 | | #B0530F | P2 | task | Caring Contacts database and row-level-security suite runs in no automated gate | The caring-contacts-db Vitest project is conditional on CARING_CONTACTS_DATABASE_URL, and the two database files are excluded from the default project unconditionally, so roughly 1,330 lines of row-level-security and cross-team-isolation proof are collected by no automated gate. grep over .github/ returns no reference to the suite. NOT unexecuted: it was run repeatedly during Phase 2A, most recently at 174 passed, so the evidence exists for that branch - what is missing is automation, so nothing would catch a future regression. An absent Vitest project prints nothing at all, which is worse than a skip. The fix is a CI job with a postgres:17 service container running npm run caring-contacts:db:test, plus a static contract test asserting the script is reachable from a workflow. Deliberately not done inside the Phase 2A final fix wave: wiring it into an offline verify chain would break every offline run because the suite hard-fails without a container, and a dedicated alias would be a check that cannot fail. Needs a repository-wide CI change with its own review. | session 2026-08-22 | 2026-08-22 | -| #CTA8CR | P2 | issue | A stale 'D4 is OFF' production-deploy claim survives uncorrected in two places in the live-drift forensics doc | docs/audit/live-drift-forensics-2026-08.md:1243-1244 and :1720 both assert flatly, with no forward pointer, that D4 is OFF and nothing reaches production on merge. A later section at :2056-2072 corrects this to 'treat D4 as UNRESOLVED and assume merging a migration PR deploys it to production'. A third instance at :2024 DOES carry a superseded pointer, so the doc's own convention exists and simply was not applied to the earlier two. A future session jumping to the Phase 4 or #Q5JHBJ sections would reasonably read the stale claim as settled fact about a live clinical database. FIX: add the same one-line superseded pointer at both sites. Also check docs/database-remediation-coordination.md:159, which references the same ambiguity. | repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f | 2026-08-23 | -| #4STSM1 | P2 | task | Caring Contacts synthetic production build: spec approved and committed, implementation plan not yet written | docs/superpowers/specs/2026-08-19-caring-contact-production-build-design.md is the binding spec: ten decision-lock revisions, the sealed domain rules layer, a dedicated Supabase project hard-separated from the Clinical KB project, seven screens required by existing decisions but never designed, four recommended screens, the design non-regression contract and the elevation brief. Delivery is two pull requests with subagent-driven development: (1) rules plus datastore plus doc repairs, (2) screens plus demo clock, synthetic caseload and training mode. Next step is the writing-plans skill to produce the implementation plan for part one. Design phase itself is complete and merged (PR #2095, #2133). | Caring Contacts design session 2026-08-19 | 2026-08-18 | +| #4STSM1 | P2 | task | Caring Contacts synthetic production build: Phases 1 and 2 built and merged; Phase 3 planned but not built | SUPERSEDES the original row, which said the implementation plan was unwritten and named part one as the next step. Verified against main at 45a3dca on 2026-09-02: part one's plan is docs/superpowers/plans/2026-08-19-caring-contact-domain-and-datastore.md with its eleven test-first tasks, and it landed - the sealed domain layer (36 modules under src/lib/caring-contacts/), eight migrations under caring-contacts/supabase/migrations/, and 73 caring-contacts test files are all on main. Phase 2 landed too, under docs/superpowers/plans/2026-08-24-caring-contact-phase-2b-screens.md, with the production workspace routes under src/app/caring-contacts/. WHAT REMAINS is Phase 3, 'make it demonstrable' (spec §10, §2.9 and the rehearsed demonstration path), which spec §13 said folds into the Phase 2 pull request 'unless it grows' - Phase 2B merged without it, so it grew and now needs its own PR. Its plan was written in PR #2520 as docs/superpowers/plans/2026-09-02-caring-contact-phase-3-demonstrable.md: eleven test-first tasks in five groups covering the advanceable demo clock and its production-absence proof, the seed extended from five patients to twelve across all nine required states, the §2.9 bounded clinical-record plan summary with its exclusions asserted as absences, training mode isolated at the store seam, and the five-minute path as a tracked document plus an executable journey. The plan is DRAFT, not approved for execution, and carries four questions for the owner - none blocking its first task: (1) is the §2.9 summary a print view or a saveable file (the plan assumes print, the safer half); (2) contact detail and system states exist as mockup routes with no production equivalent - Phase 2 gap or deliberate; (3) are the five existing seeded patient names kept and seven added; (4) spec §14's four open decisions remain open, of which the patient-visible reply wording is spoken aloud during the demonstration. NEXT STEP is owner approval of the plan, then subagent-driven execution of its Task 1. | Verified repo read at main 45a3dca; plan written in PR #2520, 2026-09-02 | 2026-08-18 | | #W1B9RP | P2 | task | Forms mode: 33 password-protected forms retain generic Clock, Authority, and Criteria prose | The 33 forms other than Form 12A with generic maker and threshold prose remain blocked because their approved-form instruction text is unavailable in password-protected PDFs. Obtain readable approved-form instruction text or an equivalent authoritative extract before writing form-level prose. Do not infer these clinical assertions from Act sections alone. Form 12A is excluded because its PDF is readable and its clock is already form-specific. | data/forms-catalog.json; data/forms-pdf-manifest.json; public/forms-pdf; direct measurement 2026-08-24 | 2026-08-24 | -| #875H6T | P3 | rec | Ward Flow: six agreed enhancements not yet assigned to a phase | Ward prediction track record; 'why not here' across the whole state for one patient; a sixty-second self-driving guided tour; out-of-area ledger; 'waiting since' promoted in the priority queue; named moments on the demo clock. All accepted by the product owner 2026-08-26 and described in docs/ward-flow-roadmap.md. | Product-owner direction, 2026-08-26 | 2026-08-26 | | #9GPWT3 | P2 | issue | Ward Flow: bed-release state model is unvalidated by any ward clinician | predicted -> confirmed -> blocked -> released is a software model of how a bed comes free. A bed may be confirmed and blocked simultaneously in reality, and 'predicted' may compress several states a charge nurse would separate. Cheap to change while synthetic; recorded in the Phase 5 spec as D14 and as the assumption most likely to be wrong. Check before Phase 7 builds on it. | Ward Flow Phase 5 design, 2026-08-26 | 2026-08-26 | | #Z5P2BW | P2 | issue | Caring Contacts: the vocabulary scan rewards leaving developer identifiers on screen, and the sealed domain it pushes copy into is watched by no vocabulary scan at all | Found in Phase 2B Task 7, 2026-08-25, and sharpened by its re-review. Two joined defects. FIRST, the scan is inverted. `tests/caring-contacts-interface-vocabulary.test.ts` refuses "lead" as a whole word anywhere under `src/components/caring-contacts/workspace/**` and `src/app/caring-contacts/**`, with no notion of a job title. So the raw role identifiers `clinicalProgrammeLead` and `livedExperienceRepresentative` PASS — only because there is no word boundary inside `ProgrammeLead` — while the correct plain-words labels a clinician should read FAIL. The scan currently rewards leaving developer gibberish on a clinical screen. SECOND, and larger: the workaround pushes that copy into `src/lib/caring-contacts/**`, which is outside EVERY prohibited-language scan in this repository. Only the interface scan and the overlay definition tests use `CARING_CONTACTS_PROHIBITED_LANGUAGE`, and neither reaches `src/lib`. So role wording now lives where no vocabulary check watches it — not merely exempt from the "lead" rule, unwatched entirely. That hole pre-dates Task 7 (`service-state.ts` already had it) and correctly did not block the fix. THE FIX IS SMALLER THAN IT LOOKS: `src/lib/caring-contacts/message-rules.ts` line 81's `COMMERCIAL_LEAD_PATTERN` is an already-written, already-tested negative lookbehind for `incident\|programme\|clinical\|team\|service` that would exempt every one of these offences unchanged, and that file records why an allowlist of commercial modifiers was itself a defect. Reusing it in the interface scan is a one-line change; extending a scan to cover the sealed domain's user-facing string constants is the separable second half. Deliberately not done in Task 7: the implementer had already narrowed one safety guard that round, and widening a second to admit a word it had just introduced is the wrong direction to argue from. | session 2026-08-25 | 2026-08-25 | | #PAMATF | P2 | issue | Caring Contacts: a draft plan's contacts are already in state scheduled and listSendableContacts has no plan-state gate | Found in the Phase 2B Task 9 review, 2026-08-25, while checking a claim on the activation screen. `createPlan` writes every planned contact in state `scheduled` or `suppressed` AT CREATION — see `in-memory-repository.ts` around line 535 and `db/postgres-repository.ts` around line 1114 — and `listSendableContacts` filters on `contact.state === "scheduled"` with NO gate on the owning plan's state (`in-memory-repository.ts` ~1329-1335). Nothing in `model.ts`'s contact transitions checks `plan.state` either. So a plan that has been created and NOT activated already presents sendable contacts. NOT A LIVE RISK TODAY: there is no sender, no SMS provider and no dispatch path anywhere in the tree, so nothing reads that list and acts. It matters for two reasons. FIRST, it falsified a reassurance on the one screen where it counts: the created-but-not-started state told a coordinator 'no message is scheduled to go out yet', which the code does not support. That copy is being fixed in Task 9 fix round 2. SECOND, whoever builds the dispatcher will read `listSendableContacts` as the list of what to send, and unless they independently remember to filter on plan state, a draft plan would dispatch. The question to answer is whether the plan-state gate belongs in `listSendableContacts`, in the future dispatcher, or in the contact state machine — deliberately NOT decided inside Task 9, because it changes a retrieval surface and that needs its own review rather than riding on a copy fix. Whoever takes it should also check whether `suppressed` at creation and the absorbed-Week-1 case interact with the answer. | session 2026-08-25 | 2026-08-25 | @@ -150,6 +142,11 @@ removed after current-main verification; it is not missing recommended work. | #RZVMPD | P2 | issue | Caring Contacts: the plans list query fetches every patient's mobile number and identifiers on every caseload render | PLAN_COLUMNS in src/lib/caring-contacts/db/postgres-repository.ts includes patient_mobile_number and patient_identifiers, and listPlans selects it verbatim. toPlanRecord then discards both, so nothing is released -- PlanRecord excludes patientDetail structurally and that guarantee is intact. But every render of /caring-contacts/patients pulls both fields for the whole caseload into the application process, and the names projection added in Task 5b then issues a second query to obtain names narrowly. Giving listPlans its own column list (readPlanRecord still needs the detail columns for getEpisode; listPlans does not) would mean the sensitive fields are never fetched at all for a list read. Genuine privacy improvement on a hot path; deserves its own review rather than riding a comment fix. | Task 5b review finding I-1, 2026-08-25 | 2026-08-24 | | #Y30AXB | P2 | issue | An agent push rewrote tests/ui-smoke.spec.ts from 6,019 lines to 1,081, deleting 74 of 82 UI journey tests, and only a merge conflict stopped it auto-merging | Observed 2026-08-31 on PR #2481 (the answer-surface colour fix). Commit d1485d6e8, message 'test(ui): compare answer status surfaces', authored via the BigSimmo account, changed exactly one file with 4 insertions and 4,924 deletions: tests/ui-smoke.spec.ts fell from 6,019 lines / 82 tests to 1,081 lines / 8 tests. The commit message describes a narrow comparison change, so the diff is almost certainly an accidental whole-file rewrite by a tool that regenerated the file instead of editing a region of it — the surviving 8 tests are a contiguous slice, which is the signature of that failure mode. WHY IT MATTERS BEYOND THE ONE COMMIT: squash auto-merge was armed on the PR at the time. A gutted suite goes green trivially, so had the branch merged cleanly the deletion would have reached main automatically with no human reading the diff. The only thing that actually prevented it was an unrelated merge conflict against a main that had moved on; the subsequent merge of main into the branch restored the file, and the squash landed only the intended two-file colour change (verified by reading main at e16d5f66d). So no harm occurred, by luck rather than by any gate. NOTHING IN CI WOULD HAVE CAUGHT IT: no check asserts a floor on the number of Playwright tests or fails on a large deletion in a test file, and AGENTS.md's 'never skip, disable, or quarantine a test to get green' rule is agent policy, not an enforced gate. check:dead-code-candidate guards exported symbols, not test bodies. SUGGESTED REMEDY, cheapest first: a static check that fails when a committed diff removes more than a threshold share of test cases from tests/**, in the spirit of the existing bundle-budget ratchets — a count of test( occurrences per spec file, ratcheted per path, so a deliberate reduction is a reviewed baseline change rather than a silent one. Separately worth considering: do not arm auto-merge on a branch that any agent session is still pushing to. | session 2026-08-31, answer-page colour fix (PR #2481) | 2026-08-31 | | #HDCF2B | P2 | issue | Caring Contacts: patient names now travel in the URL because caseload search is a server-side GET | Task 5b made the Patients directory search match patient names, and the filter is a GET form (server-side filtering was mandated by the brief; Ruling 13 forbids client state; the approved design requires name search). So a search reaches the address bar, browser history, any Referer header and any server access log as ?q=. Before Task 5b only synthetic identifiers went there. Judged NOT a defect by both the implementer's reviewer and the controller -- there was no compliant alternative under the standing constraints -- and recorded deliberately rather than fixed quietly, because it is exactly the kind of residual that gets carried into a real build unnoticed. Decide before any deployment carrying real patient names: POST-and-redirect, a client boundary accepted as an exception to Ruling 13, or an accepted risk with logging scrubbed. | Task 5b review finding I-4, 2026-08-25 | 2026-08-24 | +| #707F09 | P3 | issue | The developer hub's pending-requests panel reads empty during local dev, because no lifecycle regenerates the snapshot for the dev server | Raised by Codex review on PR #2530 (scripts/generate-outstanding-issues-snapshot.mjs) and verified rather than accepted on trust. THE FINDING IS CORRECT: npm run dev and npm run ensure invoke no prebuild, so ledger-snapshot.ts imports the committed JSON and the ledger page's 'requests not yet applied' count and list render empty while requests exist in docs/outstanding-issues-inbox/. That is the #338 class of failure the developer hub exists to prevent - a panel silently under-reporting. IT IS NOT A REGRESSION FROM PR #2530, and this matters for how it gets fixed: measured on main at 45a3dcacb before that branch existed, the committed snapshot already carried counts.pending 0 while 8 inbox requests were tracked in the same commit. The panel was already blank in dev. PR #2530 changed it from accidentally blank to blank by design, and added a gate that keeps it that way, so the fix now has to be deliberate. THE OBVIOUS FIX IS WRONG: adding a predev that generates the pending-inclusive form would write a populated pending into the tracked working tree on every dev-server start, which is precisely the churn #Y090R5 records and PR #2530 removed - and check:outstanding-issues-snapshot now fails on a committed non-empty pending, so a developer would be left with a permanently dirty tracked file that the gate refuses. Two shapes are worth weighing instead. (a) A development-only reader: the ledger page merges docs/outstanding-issues-inbox/*.json from disk when NODE_ENV is not production, leaving the committed artefact untouched. Cheap, but it adds an environment-conditional code path to a module whose docstring records that reading docs/ live was deliberately rejected because the production image never copies docs/ - the divergence would be benign here (production gets the true list from prebuild) but the reasoning must be written down rather than quietly reversed. (b) Generate the pending-inclusive form to a SEPARATE gitignored sidecar that the page reads when present, so nothing tracked is ever dirtied; costs a second artefact and a tolerant reader. Neither is urgent: the panel is administrator-gated, 404s for non-admins in production, and is correct in the deployed image because prebuild regenerates it there. | Codex review on PR #2530, 2026-09-02 | 2026-09-02 | +| #5ECZQA | P3 | issue | Batch image signed-url route swallows per-item createSignedUrls errors and still returns 200 | src/app/api/images/signed-urls/route.ts:105 checks only the top-level signed.error returned by createSignedUrls. supabase-js returns a per-path result array of { error, path, signedUrl }, so a single path that fails to sign yields an entry with no usable signedUrl while the top-level error stays null. The loop at :112-122 then skips that image because of the if (signedUrl) guard, and the route returns HTTP 200 with the image silently absent from the urls map. The client (src/lib/batch-signed-urls.ts) treats a missing key as "not returned" rather than "failed", so a figure disappears from the document view with nothing logged and no error surfaced anywhere. This is a milder instance of exactly the silent-failure class that #Z61JRT was about, and it survived that fix because the fix replaced getPublicUrl with createSignedUrls without adding per-item error handling. It is not a privacy or tenancy defect: the owner-scope gate at :70-88 has already run, so only images the caller is entitled to reach ever get to the signing step. The impact is diagnosability and a confusing partial render, not exposure. FIX: inspect each per-path result, and either surface a per-image error field in the response so the client can distinguish "failed" from "not found", or log the failing paths through the existing observability layer so a systematic storage problem is visible rather than presenting as scattered missing figures. Prefer the first: the response shape is already a per-id record, so an error discriminant fits without a breaking change. Note the singular route src/app/api/images/[id]/signed-url/route.ts:71 has the mirror-image gap, dereferencing signed.data.signedUrl without a null guard where the batch route has an explicit !signed.data check. Worth aligning both in the same change. | Found while verifying #Z61JRT on main 45a3dca, 2026-09-02; supabase-schema-guardian review of src/app/api/images/signed-urls/route.ts | 2026-09-02 | +| #9ZGNW7 | P3 | issue | Developer-hub CODE still flips perf_changed, so an admin-only mockup route pulls a full Lighthouse run | Found while fixing #EFETZT on 2026-09-02 and deliberately NOT fixed in that PR, because it means editing a fail-closed CI classification surface. PR #2530 added data/repo-awareness-snapshot.json to perfExclusionPatterns in scripts/ci-change-scope.mjs, mirroring the carve-out data/outstanding-issues-snapshot.json already had for the same reason (PR #2302). That closes the common case - a handoff PR that only regenerates the snapshot no longer pays a ~7-minute Lighthouse budget run against a budget the change cannot move. It does NOT close the code case. src/components/developer-area/hub/** and src/lib/developer-area/** match the generic 'src' entry in perfPatterns (ci-change-scope.mjs:226) and are not excluded, because only the ROUTE WRAPPER lives under the excluded src/app/mockups prefix - the panel components live one directory hop away under src/components. So a PR touching the developer hub's own code still triggers lighthouse-budget for /mockups/development/**, which 404s for non-admins in production (src/app/mockups/layout.tsx and src/proxy.ts gate it behind DEVELOPER_AREA_HEADER) and cannot appear in either budgeted journey. WHY IT WAS LEFT: the exclusion list is a fail-closed safety surface, and widening it by directory prefix risks exempting a future component that IS reachable from a budgeted route. The safe shape is probably an explicit list of the developer-hub component and lib paths rather than a prefix, pinned by an assertScope self-test beside the two that already exist (ci-change-scope.mjs:1022), plus a test proving a non-hub file under src/components still flips perf_changed. Cost of leaving it is bounded and only paid by developer-hub PRs, which are rare. | session 2026-09-02, PR #2530; verification-router review | 2026-09-02 | +| #1M0J6D | P2 | issue | Web-container sessions start on a shallow clone, so two committed specs fail with messages that read as content errors | Measured 2026-09-02 in a Claude Code web container on origin/main 45a3dcacb. The session's checkout is a SHALLOW CLONE (depth 102). Two specs in the committed suite then fail, stably across two full runs, and neither message says anything about history depth: tests/clinical-hazard-controls.test.ts fails with 'CLINICAL-TRUTH-AUTHORITY: reviewedCommit does not exist 883f1007a85cd4e02198f39c12c4a4e467d4b89e' plus 'path is absent from reviewedCommit', and tests/rag-plan-package-parity.test.ts fails with 'manifest reconciledBase is unavailable locally: f3d1a3cce2c943ad3083425ed9c7c46dbef23087' from scripts/build-rag-plan-packages.mjs --check --require-origin-main. Both read as governance-manifest corruption, which is exactly the wrong conclusion: the commits are real and simply absent from a truncated history. PROOF: git fetch --deepen=2000 takes the clone from 102 to 5462 commits, both commits then resolve under git cat-file -e, and the two specs pass 27/27 with no code change. AGENTS.md already prescribes this remedy under 'Deleting code you believe is dead' for check:dead-code-candidate ('assessed on a shallow clone, where nothing can be dated - run git fetch --deepen=2000 first'), so the fix is known; it is simply not applied at session start and these two specs are not covered by that note. COST: a full npm run test is about six minutes, so a session can burn two of them before recognising the pattern, and the plausible-looking failure text invites someone to 'fix' a hazard manifest that is not broken. OPTIONS, cheapest first: have .claude/hooks/session-start.sh deepen the clone when git rev-parse --is-shallow-repository reports true (it already provisions Node and runs npm ci, so it is the natural place and the cost is one fetch); or make the two checks detect a shallow repository and fail with that as the stated reason rather than naming the commit; or document it in docs/testing.md. The first is preferred because it fixes the dead-code gate's known problem at the same time. | session 2026-09-02, PR #2530 verification | 2026-09-02 | +| #6APN03 | P2 | task | Corpus health panel and the hub document count have never been seen against the real library | Both merged (#2504, #2512) and both were built in a cloud container with no live database and no browser, so every test uses stand-in data. Confirmation needs a machine with live Supabase config and a signed-in administrator: run npm run ensure, open /mockups/development and check the environment strip shows a real document count rather than 'document count unavailable', then open /mockups/development/corpus-health and check the four status tiles show numbers rather than 'Not read'. Then record which of the five spread cases resolveQualitySpread reports for extraction quality. An unverified report says every document may carry an identical placeholder quality_score. A uniform reading is a prompt to investigate and NOT a confirmed fault: assessDocumentIndexQuality starts the score at 1 and only subtracts penalties before rounding to three decimals, so a cleanly extracted corpus legitimately scores 1.000 for every document. Treat a uniform 0.00 as the suspicious case, since 0 is the column default and extraction_quality defaults to unknown, and corroborate against the issues array and metrics JSON on the same rows before recording anything against the scoring pipeline. Full context in docs/corpus-health-panel-handover.md. | docs/corpus-health-panel-handover.md | 2026-09-02 | ## Resolved / archive @@ -623,3 +620,11 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | #XHADPV | issue | repo-awareness snapshot emits two per-section documentation counts that nothing reads | Resolved; verified against origin/main d1bb2c197 on 2026-09-02. The row asked for a deliberate choice between rendering the stored per-section counts and no longer emitting them, and the second option was taken. Reading the committed artifact rather than the generator: data/repo-awareness-snapshot.json now carries 21 entries under documentation.sections, and the union of every key across all of them is exactly {name}. The documents and uncatalogued fields the row counted as 30 dead numbers are gone from the snapshot entirely, so the generator no longer emits a count that could disagree with its own list, and the documentation page's habit of computing section.documents.length at render is now the only source of a count rather than an undocumented exception to the plan's rule. Section count has grown from the 15 the row recorded to 21, which is ordinary growth in the documentation set and not a change of contract. | 2026-09-02 | | #5YKAQ5 | issue | sentry.server.config falls back to the service-role key when the publishable key is absent | Resolved; verified against origin/main d1bb2c197 on 2026-09-02. The row asked for the fallback to be dropped so that a missing publishable key disables tracing rather than escalating to the service-role secret. That is exactly what the code now does. In src/sentry.server.config.ts, supabaseTracingIntegrations() reads NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY and returns an empty integration list when either is absent, so no Supabase client is constructed at all. Proved by absence as well as by reading: grep for SERVICE_ROLE across src/sentry.server.config.ts returns zero matches, so the secret is no longer instantiated in that file under any branch. The privacy posture the row noted as already correct is unchanged -- the integration still sets sendOperationData: false, so no PostgREST filter values or mutation bodies reach Sentry -- and the whole path stays inert unless a DSN is set and the tracing sample rate is above zero. | 2026-09-02 | | #DVRZA4 | rec | prettierignore lists one generated data snapshot but not its sibling | Resolved; verified against origin/main d1bb2c197 on 2026-09-02. The asymmetry the row recorded is gone. .prettierignore now lists both generated data snapshots, each with its own reason comment: data/outstanding-issues-snapshot.json at line 53 (generated by scripts/generate-outstanding-issues-snapshot.mjs, ignored so regeneration stays churn-free and check:outstanding-issues-snapshot compares like with like) and data/repo-awareness-snapshot.json at line 57 (generated by scripts/generate-repo-awareness-snapshot.ts, ignored so the generator's own formatting is preserved for check:repo-awareness-snapshot). The row explicitly left the direction open -- add the newer entry or remove the older one -- and it was settled by adding the newer one, which is the direction that keeps both generators authoritative over their own output. | 2026-09-02 | +| #Z61JRT | issue | The batch image endpoint calls getPublicUrl against a bucket pinned private, so public-corpus figures silently fail to load | Resolved; verified against origin/main 45a3dcacb54a6f5901986b9ea194448ff61e19cc on 2026-09-02, and confirmed against the live project. The getPublicUrl branch this row describes no longer exists: src/app/api/images/signed-urls/route.ts now signs every valid image uniformly through createSignedUrls at :100-110, with a comment at :94-96 recording that the singular route always signs regardless of corpus visibility and that matching it avoids a silent breakage if the bucket policy is ever tightened. The route no longer imports isPublicDocument at all, and a grep for getPublicUrl across src, worker, scripts and supabase returns zero matches repo-wide. Landed in b22639b (PR #2399, 2026-08-27). The latent privacy edge the row warned about is closed on both sides. The bucket was NOT flipped public: a read-only live query on 2026-09-02 returns storage.buckets.public = false for both clinical-images and clinical-documents, matching the on-conflict re-assertion in 20260717139000_create_storage_buckets.sql, which re-pins false on every replay. And isPublicDocument was tightened separately under #N8B176, so a non-null owner carrying public_corpus metadata can no longer take a public branch anywhere. Flipping the bucket would also not have produced a working read path: the storage RLS policy "image storage owner read" keys on the first path segment equalling auth.uid(), which a published ownerless document never satisfies, so signed URLs minted with the service-role key are the only mechanism that serves public-corpus figures. Residual defect logged separately as a new inbox request: createSignedUrls per-item errors are still swallowed, so a single failed path yields HTTP 200 with that image silently absent from the urls map. | 2026-09-02 | +| #875H6T | rec | Ward Flow: six agreed enhancements not yet assigned to a phase | Closed in PR #2520. All six assigned in docs/ward-flow-roadmap.md, each with its reason recorded under the file's own rule that an unexplained assignment gets moved by the next person who finds it inconvenient. Phase 6: the sixty-second guided tour and named demo-clock moments - every beat the tour scripts already exists after Phase 5 (priority queue, coordinator screen, discharge confirmation, egress board), so neither needs anything built first and both serve what Phase 6 was promoted for. Phase 8: 'why not here?' (the inverse of decision 14's can-take-them overlay, needing Phase 7's bed-type matching and Phase 8's distance) and the out-of-area ledger (where its own text already put it). Phase 9: the ward prediction track record (needs accumulated history, and a sixth figure would reopen decision 13) and 'waiting since' (Phase 9 already names waiting-time equity). No new phase and no rewritten phase description; the enhancements themselves remain to be built inside those phases. | 2026-09-02 | +| #CTA8CR | issue | A stale 'D4 is OFF' production-deploy claim survives uncorrected in two places in the live-drift forensics doc | Fixed in PR #2520. Verified against main at 45a3dca: the two sites the row named, and docs/database-remediation-coordination.md:159, were already corrected before this session, and the third instance the row cited as the convention had its pointer added by PR #2476. One bare claim survived at the Phase 4 production-push step, where the pre-flight's 'exactly the four new versions' reading was offered as confirmation that auto-deploy is off - the precise inference §D4 refutes, since it tested deploy-while-open rather than deploy-on-merge. It now carries the same one-line superseded pointer. The claim itself stands: the document's convention is to point forward, not rewrite a forensics record. | 2026-09-02 | +| #EFETZT | issue | The repo-awareness snapshot goes stale on any PR that waits, and it already reddened CI once | Fixed the merge-conflict half. The committed snapshot now carries only content that can merge: review_state.records is ordered by head (a uniformly distributed sha) instead of date-descending, so two branches appending a review record insert hundreds of lines apart and git resolves both hunks; review_state.counts is gone because an aggregate over an append-only set changes on both sides of every append and no ordering can disperse that (reviewStateCounts() derives it at render); and REVISION_INPUTS now excludes docs/branch-review-records/** and the rotated archives, so a ledger:append no longer moves captured_revision. Snapshot version bumped to repo-awareness-snapshot-v2 so a stale committed file fails loudly. The staleness half was already fixed by the COMPARED_CONTENT_KEYS exclusion. Also carved data/repo-awareness-snapshot.json into ci-change-scope perfExclusionPatterns, mirroring the sibling: regenerating it was flipping perf_changed and forcing a ~7-minute Lighthouse run against a budget the change cannot move, which was part of this row's measured per-occurrence cost. | 2026-09-02 | +| #N8B176 | issue | isPublicDocument ranks caller-adjacent metadata above owner_id | Resolved; verified against origin/main 45a3dcacb54a6f5901986b9ea194448ff61e19cc on 2026-09-02. src/lib/documents/is-public-document.ts no longer short-circuits on metadata markers. It now applies an explicit private veto first (metadata.private, metadata.is_private, metadata.visibility === 'private' and the three row-level spellings, all returning false) and then reaches a single conjunctive grant at :58 requiring recordedOwnerId(record, metadata) === null and metadata.public_corpus === true together. owner_id is therefore authoritative and mandatory, and the strict null comparison also rejects a merely absent owner key, so an orphan or a partially shaped record fails closed. The four legacy aliases (is_public, public, visibility === 'public', source_kind === 'registry_record') no longer grant anything. This is the same definition the database uses: publish_approved_documents sets owner_id = null and stamps metadata.public_corpus = true in one statement (schema.sql:8619-8629), the canonical read predicate withOwnerReadScope requires both signals (src/lib/public-api-access.ts:113-116), and the rollback migration 20260826090000 accounts for the population with the same conjunction. The second, weaker definition of public that this row objected to is gone. Pinned by tests/documents-is-public.test.ts, seven cases including "does not grant public access from legacy metadata aliases", "does not treat registry-shaped metadata as a publication", "does not treat an orphaned null owner as public" and "requires a null owner and the publication marker together". Both that file and tests/public-api-access.test.ts pass: 19 tests across 2 files, run locally on 2026-09-02. Two findings from the review worth carrying forward. First, source_kind === 'registry_record' was the most dangerous of the five aliases rather than the most innocuous: registry entries are always owner-scoped (src/lib/registry-corpus.ts types ownerId as non-nullable at :29, writes it at :179, and registryDocumentRowPreservingOwner refuses any owner change at :206-208), so honouring that marker published every tenant's registry rows by construction. Their owners still read them through the owner_id.eq arm of withOwnerReadScope, so the removal costs nothing. Second, isPublicDocument and isPublicDocumentRow now have zero production callers; the only importer is tests/documents-is-public.test.ts via the barrel at src/lib/documents/index.ts. Do not sweep them as dead code: they are a hardened contract pinned by a committed test, which is exactly the shape the AGENTS.md dead-code rule protects. | 2026-09-02 | +| #ZM8902 | issue | docs:check-links passes over a broken relative markdown link in a spec, so a binding document went missing unnoticed | Closed in PR #2520. The checker itself already resolved relative markdown links against the containing file, so the class was not skipped - a probe document linking to ../../caring-contacts/design-handoff.md was reported as MISSING. What was absent was the half the row turns on, 'a check that cannot fail': tests/check-docs-links.test.ts covered only two helper exports, leaving the link classification one refactor away from reopening. main()'s per-document collection is now exported as collectDocumentFailures({ target, markdown, targetAnchorsCache }), so the gate and its tests run one implementation, and three tests pin the exact escaped shape, including that the checked count rises when the link is present so 'resolved' cannot be confused with 'skipped'. Watched failing under a deliberate mutation before being trusted; gate output unchanged at 4736 references. | 2026-09-02 | +| #JFRCZ4 | issue | check:repo-awareness-snapshot needs git, and nobody has established what it does without it | Established by the git archive export the row asked for, and the probe found a real defect rather than confirming the skip. Outside any repository the gate logs a skip and exits 0, which was already correct. But an export extracted INSIDE another checkout answered git rev-parse --is-inside-work-tree with the OUTER repository's true; the generator then read that repository, found all 566 documents untracked, and threw an error the skip path could not recognise - exit 1 with a six-hundred-path dump and no explanation, exactly the unexplained failure this row predicted. isGitRepository now requires a repository ROOTED at the checkout (comparing git rev-parse --show-toplevel to cwd, normalised for Windows separators and case), pinned by a nested-repository fixture test. The isGitRepoImpl false branch also had no direct coverage - the existing test drove the message-sniffing path instead - and now has a test asserting generation is never reached. Behaviour is documented in docs/codebase-index.md, including that a skip and a pass share exit code 0 so the message must be read. Also silenced git's stderr in listDocumentPaths, which made a successful filesystem fallback print 'fatal: not a git repository'. | 2026-09-02 | +| #Y090R5 | issue | data/outstanding-issues-snapshot.json is a generated file every inbox PR must regenerate, so any two concurrent ledger PRs conflict on it and the loser must re-resolve after every main merge | Both symptoms closed. The core conflict was already fixed before this row was actioned - check-outstanding-issues-snapshot.mjs excludes pending and counts.pending, so an inbox PR no longer has to regenerate the snapshot - but two residues were live and are now fixed. First, regenerating still WROTE pending into the committed file (measured: generate() returned counts.pending 8 against a file committed with 0), so npm run build via prebuild, or npm run docs:update, dirtied the tree and re-armed the conflict for whoever committed the result; the generator now emits an empty pending by default and only fills it under --with-pending, which prebuild passes, so the built image still shows the true unapplied list while the committed artefact never carries another branch's queued requests. Second, the clinicalRisk:true misclassification named on this row is fixed: scripts/pr-policy.mjs now exempts data/repo-awareness-snapshot.json and data/outstanding-issues-snapshot.json by exact path from the blanket data/ clinical-risk rule, pinned by self-test assertions including one proving a neighbour under data/ keeps its risk. Ledger PRs no longer need a Clinical Governance Preflight for files holding no clinical data. | 2026-09-02 |