From ff94ed49230a5f1db01d3a17fa4d0d3f16c9277a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:13:28 +0800 Subject: [PATCH 1/3] chore(ledger): reconcile 42 queued inbox requests into canonical ledger --- data/outstanding-issues-snapshot.json | 277 ++++-------------- .../0352fe12-97aa-47f5-8c51-882b1cfdba3a.json | 0 .../0491531b-a3d2-43a5-a128-23cdafa4291f.json | 0 .../07b99ccd-1ea8-481d-8fb6-b008dec8bffa.json | 0 .../07fb8bac-3a22-4c68-868e-608db15ffa85.json | 0 .../108effa9-9644-4e33-a05c-2521a3611545.json | 0 .../1f5a2893-bd6e-4811-af85-1d01032568d0.json | 0 .../204114bb-0149-4156-8313-f44677f2b2d8.json | 0 .../22e504b1-b9c8-4f51-8089-ad5effc65f38.json | 0 .../2888651b-75d8-4de4-9d8f-83edcbcf51e8.json | 0 .../29f57740-548d-4855-90cd-91e255609398.json | 0 .../300aebe4-a12d-4f20-9abe-126f5bf6acc1.json | 0 .../372fd777-a0ec-43f1-b501-c5dc6fe65a20.json | 0 .../3c0b6354-a44c-4003-aa08-a1277f3f7d22.json | 0 .../4036f064-f46e-4542-9bd5-f0af6a05de45.json | 0 .../4057f884-2ca6-4b0b-8825-6e13fbaa1a43.json | 0 .../48b91a49-e08a-4366-831f-fb730937b6b5.json | 0 .../49f30608-6d21-402d-82fb-c12fcca2f3d4.json | 0 .../53815705-077d-47d6-a6e7-c70f282868df.json | 0 .../5819e43f-b0ed-4716-8726-629c846a7661.json | 0 .../586aac8d-81b0-4bae-aeac-ad74bf5d1484.json | 0 .../5c835fa5-ce89-4846-a4e2-ffdc3e0dd608.json | 0 .../650a5947-a481-4795-9fbf-6d83bd2fd2b6.json | 0 .../68165d49-89c4-4e74-8f43-715d0e2e95e8.json | 0 .../6b90cb8e-eec5-4bb1-8406-d00e123c8527.json | 0 .../6c22b076-2b5d-4257-b494-9fe3bbe2f6d6.json | 0 .../6ec0c564-eabf-48a4-a8ef-504a05248244.json | 0 .../703d5505-d0ee-4420-8dfd-ac552f1b2d3c.json | 0 .../76686841-1611-45c5-a7e6-ba344cb97dbd.json | 0 .../77c1617b-9ad2-43af-9eeb-200edb5eb56d.json | 0 .../7d07b0a1-4d8e-4919-a79e-9d8da0684489.json | 0 .../7e0bbcd4-339e-4d4f-b89d-2d0d19bdd16f.json | 0 .../8ddc7abc-d54a-4ee1-896b-cf9a537ec878.json | 0 .../94c31907-2905-4cde-92df-df4a7255e3d4.json | 0 .../971829c7-e93c-4387-8eaa-da66511214e1.json | 0 .../9c3f3aaa-112a-466d-911e-5d7250e04a64.json | 0 .../9fcfa105-cf74-462b-9a10-3601894600e0.json | 0 .../ab70f2e3-ad80-443c-b8ec-a663ea13ea85.json | 0 .../d1a2de2a-15ef-4449-9dba-b2ce3724ab1e.json | 0 .../d372d7d9-a9c1-4c19-8f73-9e505a09b7d8.json | 0 .../eadf6de9-e3bc-4dab-b6d0-da55cab98e43.json | 0 .../f1e6326b-13b6-4dd3-bec3-73be77fd8aee.json | 0 .../ff46ad04-9610-484c-9ead-283d41da3455.json | 0 docs/outstanding-issues.md | 55 ++-- 44 files changed, 83 insertions(+), 249 deletions(-) rename docs/outstanding-issues-inbox/{ => applied}/0352fe12-97aa-47f5-8c51-882b1cfdba3a.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/0491531b-a3d2-43a5-a128-23cdafa4291f.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/07b99ccd-1ea8-481d-8fb6-b008dec8bffa.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/07fb8bac-3a22-4c68-868e-608db15ffa85.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/108effa9-9644-4e33-a05c-2521a3611545.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/1f5a2893-bd6e-4811-af85-1d01032568d0.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/204114bb-0149-4156-8313-f44677f2b2d8.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/22e504b1-b9c8-4f51-8089-ad5effc65f38.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/2888651b-75d8-4de4-9d8f-83edcbcf51e8.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/29f57740-548d-4855-90cd-91e255609398.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/300aebe4-a12d-4f20-9abe-126f5bf6acc1.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/372fd777-a0ec-43f1-b501-c5dc6fe65a20.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/3c0b6354-a44c-4003-aa08-a1277f3f7d22.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/4036f064-f46e-4542-9bd5-f0af6a05de45.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/4057f884-2ca6-4b0b-8825-6e13fbaa1a43.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/48b91a49-e08a-4366-831f-fb730937b6b5.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/49f30608-6d21-402d-82fb-c12fcca2f3d4.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/53815705-077d-47d6-a6e7-c70f282868df.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/5819e43f-b0ed-4716-8726-629c846a7661.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/586aac8d-81b0-4bae-aeac-ad74bf5d1484.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/5c835fa5-ce89-4846-a4e2-ffdc3e0dd608.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/650a5947-a481-4795-9fbf-6d83bd2fd2b6.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/68165d49-89c4-4e74-8f43-715d0e2e95e8.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/6b90cb8e-eec5-4bb1-8406-d00e123c8527.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/6c22b076-2b5d-4257-b494-9fe3bbe2f6d6.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/6ec0c564-eabf-48a4-a8ef-504a05248244.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/703d5505-d0ee-4420-8dfd-ac552f1b2d3c.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/76686841-1611-45c5-a7e6-ba344cb97dbd.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/77c1617b-9ad2-43af-9eeb-200edb5eb56d.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/7d07b0a1-4d8e-4919-a79e-9d8da0684489.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/7e0bbcd4-339e-4d4f-b89d-2d0d19bdd16f.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/8ddc7abc-d54a-4ee1-896b-cf9a537ec878.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/94c31907-2905-4cde-92df-df4a7255e3d4.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/971829c7-e93c-4387-8eaa-da66511214e1.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/9c3f3aaa-112a-466d-911e-5d7250e04a64.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/9fcfa105-cf74-462b-9a10-3601894600e0.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/ab70f2e3-ad80-443c-b8ec-a663ea13ea85.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/d1a2de2a-15ef-4449-9dba-b2ce3724ab1e.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/d372d7d9-a9c1-4c19-8f73-9e505a09b7d8.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/eadf6de9-e3bc-4dab-b6d0-da55cab98e43.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/f1e6326b-13b6-4dd3-bec3-73be77fd8aee.json (100%) rename docs/outstanding-issues-inbox/{ => applied}/ff46ad04-9610-484c-9ead-283d41da3455.json (100%) diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index 7cd433474c..1531361777 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -1,17 +1,17 @@ { "version": "outstanding-issues-snapshot-v1", "ledger_revision": { - "sha": "4fe1131ebb326209fd5d7d379253de9ffb061ae2", - "committed_at": "2026-09-07T04:17:48+00:00" + "sha": "5fafd89facabfcb30752ba7b0163b14ada2acbd2", + "committed_at": "2026-09-07T04:19:50Z" }, "counts": { - "open": 121, + "open": 102, "p1": 7, - "p2": 78, - "p3": 36, + "p2": 70, + "p3": 25, "queued": 7, "pending": 0, - "resolved": 501 + "resolved": 525 }, "queue": [ { @@ -255,24 +255,6 @@ "source": "Task 11a fix-round-2 review (Ruling 34) and owner decision 2026-08-21; docs/caring-contacts/phase-2a-build-record.md", "added": "2026-08-20" }, - { - "id": "#000GN4", - "priority": "P2", - "type": "issue", - "summary": "A hardcoded topic denylist refuses in-corpus psychiatric queries with zero retrieval and caches the empty result", - "detail": "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.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", - "added": "2026-08-23" - }, - { - "id": "#ZBAC9D", - "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": "CONSOLIDATED 2026-09-02, replacing two records that were pending simultaneously (one from PR #2526 on main, one from this branch) and so failed the ledger planner. DELETION HALF: CLOSED. public.documents, document_labels, document_summaries and document_table_facts use ON DELETE RESTRICT, with the live migration and a schema proof pinning the four visibility tables and their exact restrict action. CODE DEFECT (open by predicate, zero by data): public.retrieval_owner_matches resolves the public sentinel to row_owner_id IS NULL, and retrieval_owner_matches_v2 does the same for include_public; neither requires metadata.public_corpus = true, so an ownerless row without the marker could enter a public retrieval result. EXPOSURE, measured twice on owner-approved read-only reads of ref sjrfecxgysukkwxsowpy (2026-09-01 and 2026-09-02, unchanged between them): total documents 2851; owner_id NOT NULL = 0; owner_id IS NULL = 2851; of those metadata.public_corpus = true = 2851; EXPOSED COUNT = 0. Those reads also established document_corpus_access_state.mode = 'public', meaning set_document_corpus_access_mode('public') has been run against live 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 the 20260825025032 header describes, and it is why the defect currently has no blast radius. PRIORITY P2, not P1 - but the exposure is zero by DATA STATE, not by predicate, so any future ownerless insert that skips the marker re-opens it silently. CORRECTION to the previously recorded NEXT step: 'put metadata.public_corpus = true inside the retrieval contract's public branch' CANNOT BE WRITTEN AS DESCRIBED. retrieval_owner_matches receives two uuids and never sees document metadata; doing it at the call sites means twelve distinct retrieval functions, putting every retrieval path in the blast radius of a predicate change against a corpus that is 2851/2851 ownerless - where a marker missing from one row is a total search outage rather than a degradation. ROUTE TAKEN (PR #2547, open, not merged): constrain the WRITE side so the property retrieval already assumes is true by construction - a CHECK on public.documents that an ownerless row is either published (metadata->'public_corpus' is not distinct from 'true'::jsonb; null-safe, because a CHECK passes on NULL) or quarantined (status = 'failed'), added NOT VALID then validated separately, with a fail-fast guard migration; plus the one genuinely unfiltered read path, get_related_document_metadata, gaining 'and d.status = indexed'. The measured 2851/2851 marked population is what VALIDATE CONSTRAINT runs against, so it is expected to pass rather than abort. STILL OPEN: the PR needs an approved live-database window (merging applies it within seconds), and the retrieval predicates themselves are deliberately left alone.", - "source": "consolidation of the #2526 record (live reads 2026-09-01/02, ref sjrfecxgysukkwxsowpy) and this branch's schema analysis; PR #2547 supabase/migrations/20260902110500 and 20260902111500", - "added": "2026-08-23" - }, { "id": "#ZK460W", "priority": "P2", @@ -282,15 +264,6 @@ "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", "added": "2026-08-23" }, - { - "id": "#VK8ZYY", - "priority": "P2", - "type": "issue", - "summary": "Medication source_status current is derived from a substring and never expires", - "detail": "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.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", - "added": "2026-08-23" - }, { "id": "#NCAWAF", "priority": "P2", @@ -314,8 +287,8 @@ "priority": "P2", "type": "issue", "summary": "Enrichment artifact families can be permanently lost, the designed repair function is called by nothing, and no monitoring exists", - "detail": "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.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", + "detail": "UPDATE 2026-09-07 (offline repo read on main at 62638c17; no provider access). TWO of the four recorded claims hold, ONE holds with a corrected mechanism, and the central technical premise is REFUTED by the code on main. REFUTED: 'deletes an artifact family BEFORE calling OpenAI and re-inserting, one family at a time, never staged-then-swapped'. In all four writers in supabase/functions/indexing-v3-agent/index.ts - upsertMemoryCardsFromSections, upsertSectionIndexUnits, upsertVisualArtifacts, upsertCoreEmbeddingFields - the embeddingBatch await completes BEFORE sql.begin is entered, and the delete and re-insert share one Postgres transaction. A provider outage therefore aborts before any delete happens, and a failed insert rolls the delete back; the 'permanently empty family' failure mode does not follow from this code. tests/indexing-v3-agent.test.ts pins that ordering statically for all four. CONFIRMED: repair_strict_enrichment_gate_batch is invoked by nothing - every repo-wide hit is the migration, schema mirror, generated types, docs, or a schema-text assertion. CONFIRMED: no monitoring - needs_enrichment_artifacts appears in no script and no workflow. CONFIRMED WITH CORRECTED MECHANISM: both terminal states are excluded from claim eligibility forever, but by two different routes - claim_indexing_v3_agent_jobs excludes needs_enrichment_artifacts by NAME in 'status not in (...)', while failed is excluded through 'attempt_count < max_attempts', because agentFailureDecision only writes failed once attempts are spent. NEW FINDING, the reason the recorded fix would not have worked: repair_strict_enrichment_gate_batch touches documents.metadata, document_index_quality and ingestion_jobs and NEVER touches indexing_v3_agent_jobs, so even wired to a caller it could not have unstuck a stuck document. ADDRESSED in this change: migration 20260907041700 adds the indexing_v3_agent_jobs reset with fresh-processing and fresh-pending guards, scripts/repair-strict-enrichment-gate.ts is the operator caller (dry-run first, --apply --yes, health-probed, deliberately not automated), and check:enrichment-health counts the stuck states. Re-verified separately: current deep-memory writes already stage producer-scoped generations and commit them through commit_document_deep_memory_generation, so the stale follow-up request from PR #2548 was not carried into this replacement.", + "source": "supabase/functions/indexing-v3-agent/index.ts; tests/indexing-v3-agent.test.ts; supabase/migrations/20260724060000_atomic_reindex_agent_guard.sql; supabase/migrations/20260625033425_strict_enrichment_gate_repair.sql; src/lib/deep-memory.ts and tests/deep-memory-transaction-sql.test.ts; offline audit on main 62638c17, 2026-09-07", "added": "2026-08-23" }, { @@ -327,15 +300,6 @@ "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", "added": "2026-08-23" }, - { - "id": "#CJCH2E", - "priority": "P3", - "type": "issue", - "summary": "Ingestion panel reports 'could not reach' when it reached the endpoint but could not parse the body", - "detail": "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.", - "source": "Ingestion panel review, 2026-08-25", - "added": "2026-08-25" - }, { "id": "#EG4Q7W", "priority": "P3", @@ -345,24 +309,6 @@ "source": "docs/caring-contacts/phase-2a-build-record.md deferred list item 2", "added": "2026-08-24" }, - { - "id": "#KMM6R6", - "priority": "P2", - "type": "task", - "summary": "The Caring Contacts service safety stop halts sending across every patient and team, but the rule that it is stored as ONE record rather than one row per team is currently carried only by a field name (reportedByTeamId) and a doc comment in src/lib/caring-contacts/service-state.ts. Migration 0003 (Phase 2A Task 11) must enforce it in the schema with a fixed-key singleton row plus a test, and every dispatch path must read that one record regardless of the dispatching team. Without it, a stop raised by one team would leave every other team still sending during an incident.", - "detail": "", - "source": "session 2026-08-19", - "added": "2026-08-19" - }, - { - "id": "#EWWJVX", - "priority": "P2", - "type": "task", - "summary": "Caring Contacts Phase 2B — the screens", - "detail": "Phase 2A closed at Task 19 with one production screen built (/caring-contacts, Today) plus the frozen 24-overlay renderer. Plan 2B builds the remaining screens: patients, patient overview, patient and agreement, pathway selection, personalisation, review and activation, plan detail, schedule, contact and delivery exception, governed templates, team, guidance, reports — plus the Today dashboard body itself (referral queue, needs-action list, sending windows, recent activity, summary counts). Their rules and data already exist from Phase 1 and Tasks 3-11; only the surfaces are missing. The visual specification for each is the committed mockup atlas: 26 of its 44 images have no production counterpart today, listed in docs/caring-contacts/phase-2a-visual-differences.md. Building a screen also means giving its rail/dock destination an href in shell.tsx (Ruling 52) and raising its overlays through openWorkspaceOverlay, which nothing yet does.", - "source": "docs/caring-contacts/phase-2a-visual-differences.md; docs/caring-contacts/phase-2a-sdd-archive/task-19-report.md", - "added": "2026-08-22" - }, { "id": "#4VKAA1", "priority": "P2", @@ -381,15 +327,6 @@ "source": "Caring Contacts design session 2026-08-19; hazard log H-00/H-04/H-05", "added": "2026-08-18" }, - { - "id": "#J43Z6B", - "priority": "P2", - "type": "rec", - "summary": "The tenancy boundary is app-code only, so a single missing owner predicate has nothing behind it; make the property mechanical", - "detail": "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.", - "source": "repo-wide audit at 3ed1932 (six domain reviewers), re-verified against main 1bb362f", - "added": "2026-08-23" - }, { "id": "#QCNE6N", "priority": "P2", @@ -444,15 +381,6 @@ "source": "session 2026-08-25", "added": "2026-08-25" }, - { - "id": "#HDAP8N", - "priority": "P3", - "type": "rec", - "summary": "The document cover-thumbnail hook and its /api/documents/[id]/cover route have no product consumer since the source drawer stopped rendering a front-page thumbnail", - "detail": "The answer source drawer was the only caller of useDocumentCoverImageId (src/components/clinical-dashboard/use-document-cover.ts). Removing the front-page thumbnail from the drawer left that hook, and the /api/documents/[id]/cover route it fetches, reachable only from tests. check:dead-code-candidate REFUSES deletion on three counts — pinned by tests/use-document-cover.dom.test.tsx, present there as a string literal, and undateable on a shallow clone — so nothing was deleted. Decide deliberately: either a future surface adopts the cover (document detail, source rail card), or the hook, its route, its tests and the coverImageId plumbing are retired together after a deepened clone re-runs the gate. Do not delete on reachability alone.", - "source": "session 2026-09-01, sidebar-alignment-cleanup branch", - "added": "2026-09-01" - }, { "id": "#1BHXEF", "priority": "P3", @@ -471,15 +399,6 @@ "source": "PR #2419 verification sweep, 2026-08-27; re-measured in Chromium 2026-09-02 on claude/ui-fixes-q04yq7 (base origin/main 45a3dca), 32-cell viewport x state x deviceScaleFactor matrix, all 1200-tall cells 0px; docs/search-chrome-behaviour.md invariant 24; pinned by tests/ui-chrome-scroll.spec.ts", "added": "2026-08-27" }, - { - "id": "#MPZTBR", - "priority": "P3", - "type": "issue", - "summary": "The scope-statement footer contradicts itself: an audit says mount it on every mode home, the code says it was deliberately removed from all of them", - "detail": "#PM9SP1's FIX text said to 'relabel to Clinical reference - not validated decision support and mount the footer on the other mode homes'. The relabel half landed (PRs #2497, #2499). The mount half conflicts with a recorded decision in the code: src/components/mode-home-template.tsx:216-219 states 'No mode home renders this any more: the line under the composer was removed from every home page. The sole remaining call site is the therapy-compass page footer, which sits at the bottom of the sub-routes and is explicitly not rendered on the therapy home (showFooter={!isHome} in workspace.tsx).' So one source says mount it everywhere and the other says it was deliberately taken off everywhere. NEXT ACTION: owner ruling on which is current, then make the other match. If the footer stays off mode homes, amend the #PM9SP1 fix text so a future session does not re-add it; if it should return, that is a deliberate reversal of the recorded decision and the comment at mode-home-template.tsx:216-219 must be updated in the same change. Not urgent: every surface that renders retrieved clinical content already carries its own scope line (verified by a repo-wide sweep 2026-09-01) - the open question is the shared mode-home composer footer only.", - "source": "Design-system + app review session 2026-09-01; conflict found while fixing #PM9SP1, verified against mode-home-template.tsx and therapy-compass/workspace.tsx", - "added": "2026-09-01" - }, { "id": "#YTR84P", "priority": "P2", @@ -579,15 +498,6 @@ "source": "PRs #2549 (claude/migration-history-guards) and #2552 (claude/reindex-reaper)", "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": "#XGKJ8D", "priority": "P2", @@ -597,15 +507,6 @@ "source": "PR #2550 (claude/drift-semantics); Codex review finding on the chain/mirror parity gate", "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": "#E6BB64", "priority": "P2", @@ -633,15 +534,6 @@ "source": "Claude Code cloud session 2026-09-02; read on origin/claude/ward-flow-phases-6-7-design at 1888ad1", "added": "2026-09-02" }, - { - "id": "#WKFSV6", - "priority": "P3", - "type": "issue", - "summary": "Retiring answer-evidence-popups removed the repo's only source-text heading-hierarchy contract, and the DOM-level coverage does not reach mockup gallery pages", - "detail": "RECONSTRUCTED 2026-09-06, partially verified. answer-evidence-popups is confirmed gone: it survives only as references in docs/outstanding-issues.md, the archived q3 branch-review ledger and two applied inbox records, with no occurrence anywhere in src/ or tests/. Heading-hierarchy assertions now exist only in tests/ward-community-index.test.ts and tests/ward-referral-screens.dom.test.tsx, both Ward Flow surfaces, so the claim that no coverage reaches mockup gallery pages or source-text rendering matches what is in the suite today. NOT VERIFIED: the exact contract the retired component asserted, which was not recorded before it was deleted. NEXT ACTION: decide whether a source-text heading-hierarchy contract is still wanted. If it is, write it fresh against the surface that renders source text today rather than trying to recover the retired component's version, and point it at the mockup gallery pages the row says were never covered.", - "source": "session 2026-09-02", - "added": "2026-09-02" - }, { "id": "#3F76JZ", "priority": "P2", @@ -651,15 +543,6 @@ "source": "Claude Code cloud session 2026-09-02; PRs #2522 and #2542, star count via GitHub API", "added": "2026-09-02" }, - { - "id": "#S76Z3Y", - "priority": "P3", - "type": "issue", - "summary": "Two pieces of dead wiring found beside the mockup surface: an orphaned production component and a proxy redirect for a route that no longer exists", - "detail": "RECONSTRUCTED 2026-09-06, and this one could NOT be re-derived from the row. The summary names two artefacts, an orphaned production component and a proxy redirect for a route that no longer exists, without naming either, and the detail field was empty, so the specifics are not recoverable from the ledger. next.config.ts does carry header/rewrite rules including a /mockups/:path* entry and a therapy-compass-data rewrite block, but nothing there could be matched to the row's claim with confidence, and guessing at it would be worse than recording the gap. NEXT ACTION: re-run the discovery rather than trying to recall it. npm run check:knip reports unimported and unused exports and is the tool most likely to surface the orphaned component; the redirect half needs the rewrite and redirect entries in next.config.ts checked one by one against the routes in docs/site-map.md, which is generated and therefore current. Record whatever the sweep finds here, with names this time. If the sweep finds nothing, close this row as not reproducible rather than leaving it open indefinitely. PROCESS NOTE: this row is the clearest example of the cost of an empty detail field. Everything the finder knew was lost at the moment of recording, and the work now has to be done again from scratch.", - "source": "session 2026-09-02", - "added": "2026-09-02" - }, { "id": "#6APN03", "priority": "P2", @@ -669,42 +552,6 @@ "source": "docs/corpus-health-panel-handover.md", "added": "2026-09-02" }, - { - "id": "#1NMMZS", - "priority": "P3", - "type": "issue", - "summary": "Caring Contacts: the plural job-title exemption also permits preceding-verb commercial phrasing, because the companion list only guards words that follow the word", - "detail": "Measured by the clinical-governance-reviewer on commit 33e1ffd, 2026-09-02, and left open deliberately rather than fixed there. CARING_CONTACTS_PROHIBITED_LANGUAGE in tests/helpers/caring-contacts-prohibited-language.ts exempts 'leads' when one of five job-title qualifiers (incident, programme, clinical, service, team) sits immediately before it, per the owner decision that closed #AGRAKQ. The commercial-companion branch that stops an exemption licensing what follows it guards words AFTER the word -- generation, capture, nurturing, magnet, pipeline, numbers -- which is where singular commercial English puts them ('lead generation', 'lead capture'). Plural commercial English puts them BEFORE ('capture leads', 'convert leads', 'unconverted leads'), and nothing guards that position. Measured strings refused before the change and permitted after: 'Capture clinical leads', 'Convert team leads', 'Nurture clinical leads', 'Generate service leads', 'Qualify incident leads', 'Score service leads', 'Export clinical leads', 'Track clinical leads', 'Reactivate service leads', 'Unconverted service leads', 'Warm clinical leads', 'Cold team leads', 'Total clinical leads', 'clinical leads dashboard', 'team leads funnel', 'service leads report', 'clinical leads outreach', 'service leads enrichment', 'Our team leads are up 20% this quarter.', 'How many clinical leads did we get this month?'. Bare commercial plurals are all still refused ('sales leads', 'new leads', 'warm leads', 'qualified leads', 'leads capture', a lone 'Leads'), as is 'clinical leads capture'. FAILURE SCENARIO: someone builds a referral-intake view in the workspace and labels it 'Unconverted service leads' or 'clinical leads dashboard'; the vocabulary scan passes and CRM funnel framing lands on the clinician-facing surface of a suicide-prevention programme, which is the drift this guard exists to catch. No such string exists in the tree today. WHY NOT FIXED: the singular pair has the same shape of hole -- 'Capture the clinical lead' was permitted on both surfaces before any of this -- so it is a pre-existing structural gap made easier to reach, not a new class. Both obvious fixes cost more than they buy. Requiring a determiner, (? path is a source record page by testing it against a literal list ([\"search\",\"topics\",\"publishers\",\"method\"]), and an unlisted route is classified a detail page, which suppresses the shared composer with nothing failing. Context: adding /sources/search in the Sources mode-home PR required hand-adding it to that list; missing it would have shipped a catalogue with no way to search it. Same failure shape as the phoneModeGroups omission the same PR fixed - a second hand-maintained list of routes that no gate compares against the registry. Confidence: high, read directly at src/lib/search-shell-props.ts:97-105. Owner: frontend.", - "source": "session 2026-09-02, Sources mode-home PR (claude/sources-mode-dropdown-home-mzw4f5)", - "added": "2026-09-02" - }, { "id": "#Y183KM", "priority": "P2", @@ -777,15 +624,6 @@ "source": "session 2026-09-02, observed during verify:ui on claude/sources-mode-dropdown-home-mzw4f5", "added": "2026-09-02" }, - { - "id": "#JAEKM4", - "priority": "P2", - "type": "issue", - "summary": "Shared-shell testid duplicates under Playwright strict mode: service-actions-trigger (PR #2536) and sources-topics-main (PR #2591) both resolve to 2 elements, one nested inside GlobalSearchShell's mobile-composer-reserve-pad wrapper and one outside it", - "detail": "Recurring, reproducible 'Production UI critical' failure, not a random flake: strict-mode violation on getByTestId resolving to 2 elements — one aka mobile-composer-reserve-pad.getByTestId(x), one aka getByTestId(x).nth(1). Seen on tests/ui-tools.spec.ts (service-actions-trigger, PR #2536 precedent) and tests/ui-sources.spec.ts:88 (sources-topics-main, PR #2591, run 33868983554). Confirmed unrelated to either PR's own diff (files touched by those PRs don't include global-search-shell.tsx, sources-browse-client.tsx, or mode-home-template.tsx). Root cause not yet isolated — likely a hydration-timing or route-transition double-render inside the GlobalSearchShell/PageSecondaryNavigation tree. GlobalSearchShell is a 'one owner' contract component (AGENTS.md Search chrome behaviour) — a fix needs docs/search-chrome-behaviour.md read first and its own focused PR + npm run verify:phone-chrome, not a rushed patch riding an unrelated PR.", - "source": "session 2026-09-04", - "added": "2026-09-04" - }, { "id": "#H3PXP0", "priority": "P3", @@ -822,15 +660,6 @@ "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", "added": "2026-09-02" }, - { - "id": "#ZKR5YK", - "priority": "P3", - "type": "rec", - "summary": "The mode pill sends every mode to the shared home, including the four that own a real home of their own", - "detail": "Owner ruling on whether selecting a mode that owns a functional home should navigate to that home rather than /?mode=. Why: changeMode in global-search-shell.tsx always builds appModeSelectionHref, so picking Sources, Medication, Favourites or Documents lands on the generic shared home, not the surface that mode actually owns; only Tools is special-cased to push its canonical href. Consistent across modes, which is why it was deliberately left unchanged when /sources gained a mode home on 2026-09-02, but it means that home is reachable only by direct link, the mode nav or the Tools launcher. Context: overlaps open PR #2556, a full decision brief on whether the mode concept should survive at all - if that brief is actioned this row is subsumed and should be closed rather than worked. Confidence: high on the mechanism (read at global-search-shell.tsx:717-764), low that this is the right change to make before the brief is decided. Owner: product.", - "source": "session 2026-09-02, Sources mode-home PR (claude/sources-mode-dropdown-home-mzw4f5); overlaps PR #2556", - "added": "2026-09-02" - }, { "id": "#Q6WD1M", "priority": "P1", @@ -840,15 +669,6 @@ "source": "session 2026-09-02", "added": "2026-09-02" }, - { - "id": "#DFMVMN", - "priority": "P3", - "type": "task", - "summary": "Medication considerations panel formats its two not-assessed sentences differently — the contraindication one uses a bare comma join, the advisory one a serial-and helper", - "detail": "In src/components/clinical-dashboard/medication-considerations.tsx as landed in PR #2538 (head ef7c55ab), the advisory not-assessed sentence renders its input list through formatInputList(), a helper added in that PR which produces \"eGFR\", \"eGFR and QTc\", \"eGFR, QTc and hepatic status\". The contraindication sentence one InlineNotice above still renders result.unassessed.join(\", \"). With a single missing input the two read identically; with two or more they read inconsistently, and the bare join is the case formatInputList's own doc comment argues against, because the sentence continues into a relative clause (\"which this profile does not include\") that a comma-only list runs straight into. NEXT STEP: apply formatInputList() to the contraindication sentence too. It is identity for a single input (it returns items.join(\"\") when length <= 1), so no existing assertion in tests/medication-interaction-surfaces.dom.test.tsx breaks; add a two-input case for the contraindication tier so the shared formatting is pinned rather than incidental.", - "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", - "added": "2026-09-02" - }, { "id": "#YJGWDZ", "priority": "P3", @@ -921,24 +741,6 @@ "source": "Ward Builder Three closing report, measured at adcd8bcb5", "added": "2026-09-02" }, - { - "id": "#NADB8P", - "priority": "P3", - "type": "issue", - "summary": "on_call_entries.linked_document_ids is uuid[], so Postgres cannot enforce a foreign key to documents", - "detail": "supabase/migrations/20260904120000_on_call_entries.sql:25 declares linked_document_ids uuid[] not null default '{}'. Postgres cannot place a foreign key on an array element, so nothing at the database layer stops an entry referencing a deleted or non-existent document. Behaviour is conservative: the viewer resolves each id and simply omits links it cannot resolve, so a stale id degrades to a missing link rather than a wrong or broken one. No symptom has been observed. Recommendation: LEAVE AS-IS. Enforcing it properly means restructuring the link into a join table (on_call_entry_documents) with a real foreign key and a migration to backfill, which is disproportionate to a defect that fails safely. Revisit only if entries ever start displaying stale links in practice, or if the join table is wanted for another reason (per-link ordering, labels, or reverse lookup from a document to the entries citing it).", - "source": "Session 2026-09-05, owner-reported loose end after the On Call mode build", - "added": "2026-09-05" - }, - { - "id": "#42M061", - "priority": "P2", - "type": "issue", - "summary": "Tailwind never scans the mockups tree, so any arbitrary utility written only inside a mockup component is silently never emitted", - "detail": "`src/app/globals.css` opens with `@source not \"./mockups\"` and `@source not \"../components/**/*mockup*\"`, and `src/app/mockups/mockups.css` re-emits utilities from `../` and `../../components` without covering the excluded component files. The consequence is silent: an arbitrary utility written only inside `src/components/caring-contacts/mockups/**` lands on the element as a class and no CSS rule is ever generated for it, so it has no effect and nothing fails. Confirmed against the built stylesheet during the Caring Contacts design audit (PR #2574): `min-h-[var(--space-10)]` on the prototype's phone dock has no rule at all, which is why that dock measures 41px rather than the 64px its code reads as; the pre-existing `min-w-[46rem]` continuity strip and `min-w-[42rem]` team table widths have likewise never applied. It also caused a real CI failure in that PR: a derived `pb-[calc(...)]` phone reserve computed to 0px, the fixed dock covered the end of every long phone page, and two 390px journeys in `ui-caring-contact-mockup.spec.ts` timed out; bisected in a scratch worktree rather than guessed. That instance is fixed by using utilities the tree can emit, but the class of defect remains open for every mockup component. Fixing it properly means widening Tailwind's scan against a deliberate exclusion, which has bundle-budget consequences and needs its own measured decision; a narrower alternative is a static gate that fails when an arbitrary-value utility appears in an unscanned mockup path.", - "source": "Caring Contacts design audit, PR #2574", - "added": "2026-09-04" - }, { "id": "#WM4DNW", "priority": "P2", @@ -975,24 +777,6 @@ "source": "Token-layer collapse, branch claude/token-layer-collapse-itskb0, browser-measured 2026-09-04", "added": "2026-09-04" }, - { - "id": "#0ZTPX9", - "priority": "P2", - "type": "issue", - "summary": "Ten CSS custom properties referenced with no fallback and never declared render as nothing on a clinical board", - "detail": "tests/design-token-contract.test.ts fails. board.module.css and ward-management.module.css reference --clinical-border-subtle, --clinical-text-muted, --clinical-border, --clinical-surface, --clinical-text and --ward-surface with no fallback and no declaration. An undeclared custom property with no fallback does not error - it renders as nothing, so a clinician sees a missing border or invisible text while every test stays green. Reproduced at adcd8bcb5: 90 tests RAN, 3 failed, real exit 1. Ownership went unclaimed repeatedly across five chats.", - "source": "Ward Builder Three, reproduced not relayed, at adcd8bcb5", - "added": "2026-09-02" - }, - { - "id": "#GCTFD7", - "priority": "P2", - "type": "issue", - "summary": "Undeclared CSS custom properties make two ward-board controls stop looking like controls", - "detail": "board/board.module.css sets background and border on .awayButton (a button) and .leavingSelect (a select), both min-height 3rem tap targets, from --clinical-surface/--clinical-border/--clinical-text, none of which is declared anywhere; ward-management.module.css does the same with --ward-surface on .blockerInput and .blockerButton:disabled. background and border are non-inherited so they fall away; color is inherited so text survives. Every missing token is an isolated absentee from a family that otherwise exists (--clinical-accent and --ward-border are declared), so the fix is the correct existing token name, not a fallback. Nine of the ten bad references have a real declared token they should have used; --clinical-border-subtle has no analog and needs an owner decision. Caught by tests/design-token-contract.test.ts. Measured at fb17db7b1.", - "source": "Ward Builder One closing sweep 2026-09-02", - "added": "2026-09-02" - }, { "id": "#S2741S", "priority": "P2", @@ -1181,6 +965,51 @@ "detail": "Failed Unit coverage on PR #2664 head e8bbcdba1 (run 34038808851, job 101501794483): tests/ward-flow-chat-control.test.ts > 'serializes cross-role lease acquisition across processes' expected stderr matching /already held|worktree already held/ but got 'lease acquisition lock is unreadable at /tmp/ward-chat-control-BzcaPW/.git/ward-flow-chat-control/acquire.lock.json; inspect it rather than bypassing custody'. ROOT CAUSE is in main's code, not the test. withLeaseAcquisitionLock in scripts/ward-flow/chat-control.mjs writes the lock with writeFileSync(lockPath, canonicalJson(owner), { encoding: 'utf8', flag: 'wx' }). The wx flag makes the CREATE exclusive, but creation and content are two steps, so a competitor can hit EEXIST in the window after the inode exists and before the holder's bytes are flushed, read the file back empty, and land in the catch at line 381. That branch calls fail() immediately with no retry, so a transient partial read is fatal, while the surrounding loop already retries the EEXIST path 200 times. Under CI load the window is wide enough to hit; the same test passed 3/3 locally on the same head, so it will not reproduce on a quiet machine. FIX: publish the lock atomically. Write a fully formed temp file at `${lockPath}.${owner.token}` then linkSync(staging, lockPath), which is atomic and throws EEXIST when already held, and rmSync the staging file in a finally. linkSync needs adding to the node:fs import; renameSync is already imported but is wrong here because it overwrites. The narrower alternative is to count unreadable reads and only fail() after a bounded number, but that leaves the race in place and only makes it rarer. Not fixed in PR #2664 because that PR is source-governance only and touches no Ward Flow file; the session branch constraint prevented opening a separate fix PR. Do not weaken or quarantine the test to clear it, the test is asserting the correct contract. Full diagnosis posted at https://github.com/BigSimmo/Database/pull/2664#issuecomment-5559938335", "source": "PR #2664 CI run 34038808851 job 101501794483, 2026-09-06; scripts/ward-flow/chat-control.mjs:357-401", "added": "2026-09-06" + }, + { + "id": "#YSKD8J", + "priority": "P2", + "type": "issue", + "summary": "Every main CI run concludes cancelled because release-browser-matrix hits its own 70 minute timeout, so main has had no browser coverage at all", + "detail": "MECHANISM IDENTIFIED 2026-09-07 by job-level timing, and it is NOT the spending or concurrency cap this row originally hypothesised. That hypothesis is withdrawn, and the per-run concurrency group is not implicated. release-browser-matrix carries timeout-minutes: 70 (.github/workflows/ci.yml). Measured on two consecutive main runs: run 34100540973 (f3ea7cb), job 101675121681, started 08:30:56Z and completed 09:41:19Z = 70m23s, with its 'Full browser UI matrix' step ending in conclusion cancelled. Run 34104496596 (c8cc72f), job 101689976803, started 09:22:47Z and completed 10:33:07Z = 70m20s, identical shape. Both land exactly on the configured timeout. GitHub reports a timed-out job as cancelled, and one cancelled job makes the whole run conclusion cancelled. That is the entire explanation for main's run-level redness, and it also explains the earlier temporal pattern (runs stopped concluding cancelled when the merge queue quietened and the matrix presumably completed inside 70 minutes). In both runs every other job succeeded, PR required included: Change scope, Static PR checks, Unit coverage, Build, Safety and config, Caring Contacts database, Production UI (1)(2)(3), Visual baselines, Lighthouse budget. CONSEQUENCE, and it is worse than the original row implied: the matrix now produces NO result rather than a red one. Main's Firefox and WebKit coverage is currently zero, not failing. The sibling row recording 15 Firefox/WebKit failures describes the last state in which the matrix still finished, at 48.3m on edbd29f. It has since crossed 70 minutes, so those 15 failures are no longer even being reported. NEXT ACTION: find why the matrix went from roughly 48 minutes to over 70. The first candidate is the 15 failing tests themselves, because a failing Playwright test spends its full timeout and then pays for trace and video capture, so failures are disproportionately expensive and a growing failure set is self-accelerating. Fix the failures first and re-measure. Do NOT simply raise timeout-minutes: that buys a longer run without restoring a verdict, and it hides the regression that made the suite slower. If the suite is genuinely too long after the failures are fixed, shard it the way Production UI is already sharded into three jobs rather than extending one 70 minute job. ORIGINAL EVIDENCE, retained: of the 8 completed CI runs on main sampled 2026-09-06, 6 concluded cancelled and 2 failed, none succeeded. Volume context: 132 merge commits to main in 24 hours, 19 in one 3-hour window. The 2026-08-20 queue-eviction mechanism was ruled out and stays ruled out: commit 19ee948 carries group: CI-${{ github.run_id }} for push events plus cancel-in-progress: ${{ github.event_name != 'push' }}, so a main push run has a unique group and cannot be superseded.", + "source": "Session 2026-09-07. Original hypothesis from run-level reads on 2026-09-06; corrected the same day by job-level timing on runs 34100540973 and 34104496596, which both show release-browser-matrix ending at its 70 minute timeout. Offline repo read of ci.yml plus GitHub Actions job reads. No provider mutation.", + "added": "2026-09-07" + }, + { + "id": "#1BKK79", + "priority": "P2", + "type": "issue", + "summary": "#686WHW closed early: an archived ledger row still cannot be corrected, and a competing close still loses its outcome text silently", + "detail": "Correction to #686WHW (issue-ulid 01M1GFJ8FM686WHWQXSYTSBJGT), archived 2026-09-06 with outcome 'Handled concurrent issue row closure idempotently in reconcile.' The original request (2026-09-02, PR #2521 vs reconciliation #2542) named three candidate fixes; only the first is implemented. Verified in scripts/ledger-inbox.mjs at 2026-09-07: (1) FIXED — applyRequest's idempotent path (around line 147-156) now returns the markdown unchanged instead of throwing when a done/update request targets a row already in the archive table, so a losing session's CI no longer reds on an unrelated PR. (2) NOT FIXED — the CLI's own fingerprint check for the update action (around line 727-744) still throws 'ledger request rejected: is not in Open items' whenever the target row is archived; only the done action gets the archived-row no-op branch, so there is still no way to queue a correction against an archived row's outcome text at all. (3) NOT FIXED — the idempotent no-op in (1) silently keeps whichever outcome text reconciliation applied first and discards the second session's request outright; it does not compare, merge, or prefer the later/more-accurate outcome, so a more accurate second close is still lost exactly as the original row described. Net effect: an archived row's outcome remains permanently uncorrectable through the tooling, and a race's loser still loses its findings, just without the CI failure. Flagged by Codex review (discussion_r3946596431) on PR #2701. Candidate fix carried over from the original row: add an explicit amend-outcome action (or allow update to target archive rows) so a later, more accurate outcome can be applied to an already-archived row.", + "source": "Codex review PR #2701 discussion_r3946596431; corrects #686WHW", + "added": "2026-09-07" + }, + { + "id": "#0H0S89", + "priority": "P2", + "type": "rec", + "summary": "Do not refresh the Lighthouse performance baseline until main has one genuinely green run", + "detail": "The Lighthouse budget cells on the release browser matrix have drifted from their recorded baseline, and the obvious remedy (re-record the baseline from a recent main run) is unsafe right now. All four cells drifted together, which points at an environment/runner-level shift rather than any one PR, and main is not currently in a known-good state because release-browser-matrix fails on every run with 15 Firefox/WebKit failures. Refreshing the baseline from a red main would stamp unattributable drift as 'the new normal' and permanently lose the ability to attribute it. NEXT ACTION, in this order: (1) fix or triage the release-browser-matrix Firefox/WebKit failures, (2) obtain one main run where the whole matrix is genuinely green, (3) only then re-record the Lighthouse baseline from that SHA and note the SHA in the commit message. STOP RULE: if step 2 cannot be reached, do not proceed to step 3 - raise it with the owner instead. Related but distinct: #QSHHGK covers the bundle-budget baseline refresh, which is a different artefact.", + "source": "Session 2026-09-07 (DSM diagnosis page work). Reversal of my own earlier advice in the same session: I first suggested refreshing the baseline, then withdrew that after establishing main is red. Offline analysis plus GitHub Actions run reads; no baseline was changed.", + "added": "2026-09-07" + }, + { + "id": "#XSZ4XV", + "priority": "P2", + "type": "task", + "summary": "Audit remediation leftovers still open after #4BE39H's archival: Docling model pin, preview font/noindex, offline-page branding, calculator governance link", + "detail": "Correction to #4BE39H (issue-ulid 01M1PW9PQG4BE39HC59GZ0B8NR), archived 2026-09-06 with outcome 'Completed unblocked audit leftovers L45, L131, and sentry dependency.' That outcome text only covers 3 of the 7 items the row's own prior detail named, and the row was fully archived anyway. The 4 not covered by the outcome are still unresolved at 2026-09-07: (1) L49 Docling model revision pin — Dockerfile.worker:86 (docling-tools models download --output-dir /opt/docling-models) and eval/docling/Dockerfile:44 run the same command with no --revision/pin argument, so the models fetched at build time are unpinned; the fix is in PR #2629's body per the original request. (2) L5 fonts/noindex owner decision — public/brand/preview.html and docs/brand/preview.html are still byte-identical (no change applied), so the decision remains unmade. (3) L126 offline-page branding + CACHE_VERSION bump — public/offline.html brand mark is unchanged and public/sw.js CACHE_VERSION is still 2026-08-28-v1, so no bump has occurred. (4) Calculator governance link — data/calculators/evidence.json:9 still points source:governance.url at the github.com blob URL (https://github.com/BigSimmo/Database/blob/main/docs/superpowers/specs/2026-09-01-calculators-clinical-safety.md), which an end user cannot open, per the original request's exact concern. Flagged by Codex review (discussion_r3946596430) on PR #2701; the archived #4BE39H row cannot itself be amended (see #686WHW), which is why this is a new row rather than an edit.", + "source": "Codex review PR #2701 discussion_r3946596430; corrects #4BE39H", + "added": "2026-09-07" + }, + { + "id": "#9Z197J", + "priority": "P2", + "type": "issue", + "summary": "release-browser-matrix fails on every main run with 15 Firefox/WebKit failures, and it is not in PR required so it never blocks a merge", + "detail": "Evidence, main run 34065301954 on edbd29f (#2690): every other job passed - Static PR checks, Unit coverage, Safety and config, Build, Visual baselines, Caring Contacts, Production UI (1)(2)(3), Lighthouse budget, and PR required itself. The sole failure was release-browser-matrix: '15 failed, 80 skipped, 1397 passed (48.3m)'. The identical single-job failure occurred on the preceding main commit cae0655 (#2689), so it is persistent and independent of the change under test. ALL 15 failures are Firefox (5) or WebKit (10); Chromium had zero. They are viewport and scroll-geometry assertions, e.g. tests/ui-tools.spec.ts:3172 'long mobile service details paint to the viewport edge with no bottom search dock' expected footerBottom <= 820 but received 838.546875 on webkit. Others: ui-formulation-result-cards.spec.ts:35 (both engines), ui-smoke.spec.ts:1202/2939/1648/5171/5961, ui-stress.spec.ts:436, ui-tools.spec.ts:2927, ui-phone-scroll-page-owned.spec.ts:98 (x2), ui-phone-scroll.spec.ts:236, ui-route-coverage.spec.ts:554, ui-specifiers.spec.ts:181 (axe violation). WHY IT IS INVISIBLE: release-browser-matrix is not part of the PR required aggregate - PR required concluded success on edbd29f while the run overall went red - so this paints every main run red without blocking any merge, and trains readers to ignore main's status. Distinct from #055, which is a process task to run the release gate before a handoff; this row is that the matrix is currently and persistently red. NEXT ACTION: triage the 15 as one focused piece of work (they cluster into phone-chrome scroll geometry, document viewer composer, service detail footer clearance, and one specifiers axe violation). VISIBILITY REMEDY, corrected 2026-09-07 after review: do NOT simply add release-browser-matrix to the pr-required needs list. ci.yml gates the job on workflow_dispatch, schedule, refs/heads/release/*, or refs/heads/main only, so it never runs on a pull_request event at all, and the comment above it records that it must deliberately not wait on pr-required (#023 - a blocking weekly dependency audit once skipped the matrix entirely). A needs edit alone would aggregate a skipped job and change nothing. Fix the post-merge signal instead so a red matrix on main is visible and owned rather than ambient. Making it genuinely merge-blocking is a separate and much larger change - a pull_request trigger plus an aggregate redesign that runs a roughly 50 minute matrix before every merge - and must be costed and decided on its own rather than assumed here. SUPERSEDED IN PART 2026-09-07: the matrix no longer reports these 15 failures at all. It now exceeds its 70 minute timeout on every main run and is recorded as cancelled, so main's Firefox and WebKit coverage is currently zero rather than red. The 48.3m/15-failure evidence above is the last state in which the job still finished. Fix the timeout cause first (see the sibling row on release-browser-matrix timing out), because the 15 failures cannot be re-measured until the job completes again.", + "source": "session 2026-09-06 diagnosis of main CI; runs 34065301954 and 34064564709", + "added": "2026-09-07" } ], "pending": [] diff --git a/docs/outstanding-issues-inbox/0352fe12-97aa-47f5-8c51-882b1cfdba3a.json b/docs/outstanding-issues-inbox/applied/0352fe12-97aa-47f5-8c51-882b1cfdba3a.json similarity index 100% rename from docs/outstanding-issues-inbox/0352fe12-97aa-47f5-8c51-882b1cfdba3a.json rename to docs/outstanding-issues-inbox/applied/0352fe12-97aa-47f5-8c51-882b1cfdba3a.json diff --git a/docs/outstanding-issues-inbox/0491531b-a3d2-43a5-a128-23cdafa4291f.json b/docs/outstanding-issues-inbox/applied/0491531b-a3d2-43a5-a128-23cdafa4291f.json similarity index 100% rename from docs/outstanding-issues-inbox/0491531b-a3d2-43a5-a128-23cdafa4291f.json rename to docs/outstanding-issues-inbox/applied/0491531b-a3d2-43a5-a128-23cdafa4291f.json diff --git a/docs/outstanding-issues-inbox/07b99ccd-1ea8-481d-8fb6-b008dec8bffa.json b/docs/outstanding-issues-inbox/applied/07b99ccd-1ea8-481d-8fb6-b008dec8bffa.json similarity index 100% rename from docs/outstanding-issues-inbox/07b99ccd-1ea8-481d-8fb6-b008dec8bffa.json rename to docs/outstanding-issues-inbox/applied/07b99ccd-1ea8-481d-8fb6-b008dec8bffa.json diff --git a/docs/outstanding-issues-inbox/07fb8bac-3a22-4c68-868e-608db15ffa85.json b/docs/outstanding-issues-inbox/applied/07fb8bac-3a22-4c68-868e-608db15ffa85.json similarity index 100% rename from docs/outstanding-issues-inbox/07fb8bac-3a22-4c68-868e-608db15ffa85.json rename to docs/outstanding-issues-inbox/applied/07fb8bac-3a22-4c68-868e-608db15ffa85.json diff --git a/docs/outstanding-issues-inbox/108effa9-9644-4e33-a05c-2521a3611545.json b/docs/outstanding-issues-inbox/applied/108effa9-9644-4e33-a05c-2521a3611545.json similarity index 100% rename from docs/outstanding-issues-inbox/108effa9-9644-4e33-a05c-2521a3611545.json rename to docs/outstanding-issues-inbox/applied/108effa9-9644-4e33-a05c-2521a3611545.json diff --git a/docs/outstanding-issues-inbox/1f5a2893-bd6e-4811-af85-1d01032568d0.json b/docs/outstanding-issues-inbox/applied/1f5a2893-bd6e-4811-af85-1d01032568d0.json similarity index 100% rename from docs/outstanding-issues-inbox/1f5a2893-bd6e-4811-af85-1d01032568d0.json rename to docs/outstanding-issues-inbox/applied/1f5a2893-bd6e-4811-af85-1d01032568d0.json diff --git a/docs/outstanding-issues-inbox/204114bb-0149-4156-8313-f44677f2b2d8.json b/docs/outstanding-issues-inbox/applied/204114bb-0149-4156-8313-f44677f2b2d8.json similarity index 100% rename from docs/outstanding-issues-inbox/204114bb-0149-4156-8313-f44677f2b2d8.json rename to docs/outstanding-issues-inbox/applied/204114bb-0149-4156-8313-f44677f2b2d8.json diff --git a/docs/outstanding-issues-inbox/22e504b1-b9c8-4f51-8089-ad5effc65f38.json b/docs/outstanding-issues-inbox/applied/22e504b1-b9c8-4f51-8089-ad5effc65f38.json similarity index 100% rename from docs/outstanding-issues-inbox/22e504b1-b9c8-4f51-8089-ad5effc65f38.json rename to docs/outstanding-issues-inbox/applied/22e504b1-b9c8-4f51-8089-ad5effc65f38.json diff --git a/docs/outstanding-issues-inbox/2888651b-75d8-4de4-9d8f-83edcbcf51e8.json b/docs/outstanding-issues-inbox/applied/2888651b-75d8-4de4-9d8f-83edcbcf51e8.json similarity index 100% rename from docs/outstanding-issues-inbox/2888651b-75d8-4de4-9d8f-83edcbcf51e8.json rename to docs/outstanding-issues-inbox/applied/2888651b-75d8-4de4-9d8f-83edcbcf51e8.json diff --git a/docs/outstanding-issues-inbox/29f57740-548d-4855-90cd-91e255609398.json b/docs/outstanding-issues-inbox/applied/29f57740-548d-4855-90cd-91e255609398.json similarity index 100% rename from docs/outstanding-issues-inbox/29f57740-548d-4855-90cd-91e255609398.json rename to docs/outstanding-issues-inbox/applied/29f57740-548d-4855-90cd-91e255609398.json diff --git a/docs/outstanding-issues-inbox/300aebe4-a12d-4f20-9abe-126f5bf6acc1.json b/docs/outstanding-issues-inbox/applied/300aebe4-a12d-4f20-9abe-126f5bf6acc1.json similarity index 100% rename from docs/outstanding-issues-inbox/300aebe4-a12d-4f20-9abe-126f5bf6acc1.json rename to docs/outstanding-issues-inbox/applied/300aebe4-a12d-4f20-9abe-126f5bf6acc1.json diff --git a/docs/outstanding-issues-inbox/372fd777-a0ec-43f1-b501-c5dc6fe65a20.json b/docs/outstanding-issues-inbox/applied/372fd777-a0ec-43f1-b501-c5dc6fe65a20.json similarity index 100% rename from docs/outstanding-issues-inbox/372fd777-a0ec-43f1-b501-c5dc6fe65a20.json rename to docs/outstanding-issues-inbox/applied/372fd777-a0ec-43f1-b501-c5dc6fe65a20.json diff --git a/docs/outstanding-issues-inbox/3c0b6354-a44c-4003-aa08-a1277f3f7d22.json b/docs/outstanding-issues-inbox/applied/3c0b6354-a44c-4003-aa08-a1277f3f7d22.json similarity index 100% rename from docs/outstanding-issues-inbox/3c0b6354-a44c-4003-aa08-a1277f3f7d22.json rename to docs/outstanding-issues-inbox/applied/3c0b6354-a44c-4003-aa08-a1277f3f7d22.json diff --git a/docs/outstanding-issues-inbox/4036f064-f46e-4542-9bd5-f0af6a05de45.json b/docs/outstanding-issues-inbox/applied/4036f064-f46e-4542-9bd5-f0af6a05de45.json similarity index 100% rename from docs/outstanding-issues-inbox/4036f064-f46e-4542-9bd5-f0af6a05de45.json rename to docs/outstanding-issues-inbox/applied/4036f064-f46e-4542-9bd5-f0af6a05de45.json diff --git a/docs/outstanding-issues-inbox/4057f884-2ca6-4b0b-8825-6e13fbaa1a43.json b/docs/outstanding-issues-inbox/applied/4057f884-2ca6-4b0b-8825-6e13fbaa1a43.json similarity index 100% rename from docs/outstanding-issues-inbox/4057f884-2ca6-4b0b-8825-6e13fbaa1a43.json rename to docs/outstanding-issues-inbox/applied/4057f884-2ca6-4b0b-8825-6e13fbaa1a43.json diff --git a/docs/outstanding-issues-inbox/48b91a49-e08a-4366-831f-fb730937b6b5.json b/docs/outstanding-issues-inbox/applied/48b91a49-e08a-4366-831f-fb730937b6b5.json similarity index 100% rename from docs/outstanding-issues-inbox/48b91a49-e08a-4366-831f-fb730937b6b5.json rename to docs/outstanding-issues-inbox/applied/48b91a49-e08a-4366-831f-fb730937b6b5.json diff --git a/docs/outstanding-issues-inbox/49f30608-6d21-402d-82fb-c12fcca2f3d4.json b/docs/outstanding-issues-inbox/applied/49f30608-6d21-402d-82fb-c12fcca2f3d4.json similarity index 100% rename from docs/outstanding-issues-inbox/49f30608-6d21-402d-82fb-c12fcca2f3d4.json rename to docs/outstanding-issues-inbox/applied/49f30608-6d21-402d-82fb-c12fcca2f3d4.json diff --git a/docs/outstanding-issues-inbox/53815705-077d-47d6-a6e7-c70f282868df.json b/docs/outstanding-issues-inbox/applied/53815705-077d-47d6-a6e7-c70f282868df.json similarity index 100% rename from docs/outstanding-issues-inbox/53815705-077d-47d6-a6e7-c70f282868df.json rename to docs/outstanding-issues-inbox/applied/53815705-077d-47d6-a6e7-c70f282868df.json diff --git a/docs/outstanding-issues-inbox/5819e43f-b0ed-4716-8726-629c846a7661.json b/docs/outstanding-issues-inbox/applied/5819e43f-b0ed-4716-8726-629c846a7661.json similarity index 100% rename from docs/outstanding-issues-inbox/5819e43f-b0ed-4716-8726-629c846a7661.json rename to docs/outstanding-issues-inbox/applied/5819e43f-b0ed-4716-8726-629c846a7661.json diff --git a/docs/outstanding-issues-inbox/586aac8d-81b0-4bae-aeac-ad74bf5d1484.json b/docs/outstanding-issues-inbox/applied/586aac8d-81b0-4bae-aeac-ad74bf5d1484.json similarity index 100% rename from docs/outstanding-issues-inbox/586aac8d-81b0-4bae-aeac-ad74bf5d1484.json rename to docs/outstanding-issues-inbox/applied/586aac8d-81b0-4bae-aeac-ad74bf5d1484.json diff --git a/docs/outstanding-issues-inbox/5c835fa5-ce89-4846-a4e2-ffdc3e0dd608.json b/docs/outstanding-issues-inbox/applied/5c835fa5-ce89-4846-a4e2-ffdc3e0dd608.json similarity index 100% rename from docs/outstanding-issues-inbox/5c835fa5-ce89-4846-a4e2-ffdc3e0dd608.json rename to docs/outstanding-issues-inbox/applied/5c835fa5-ce89-4846-a4e2-ffdc3e0dd608.json diff --git a/docs/outstanding-issues-inbox/650a5947-a481-4795-9fbf-6d83bd2fd2b6.json b/docs/outstanding-issues-inbox/applied/650a5947-a481-4795-9fbf-6d83bd2fd2b6.json similarity index 100% rename from docs/outstanding-issues-inbox/650a5947-a481-4795-9fbf-6d83bd2fd2b6.json rename to docs/outstanding-issues-inbox/applied/650a5947-a481-4795-9fbf-6d83bd2fd2b6.json diff --git a/docs/outstanding-issues-inbox/68165d49-89c4-4e74-8f43-715d0e2e95e8.json b/docs/outstanding-issues-inbox/applied/68165d49-89c4-4e74-8f43-715d0e2e95e8.json similarity index 100% rename from docs/outstanding-issues-inbox/68165d49-89c4-4e74-8f43-715d0e2e95e8.json rename to docs/outstanding-issues-inbox/applied/68165d49-89c4-4e74-8f43-715d0e2e95e8.json diff --git a/docs/outstanding-issues-inbox/6b90cb8e-eec5-4bb1-8406-d00e123c8527.json b/docs/outstanding-issues-inbox/applied/6b90cb8e-eec5-4bb1-8406-d00e123c8527.json similarity index 100% rename from docs/outstanding-issues-inbox/6b90cb8e-eec5-4bb1-8406-d00e123c8527.json rename to docs/outstanding-issues-inbox/applied/6b90cb8e-eec5-4bb1-8406-d00e123c8527.json diff --git a/docs/outstanding-issues-inbox/6c22b076-2b5d-4257-b494-9fe3bbe2f6d6.json b/docs/outstanding-issues-inbox/applied/6c22b076-2b5d-4257-b494-9fe3bbe2f6d6.json similarity index 100% rename from docs/outstanding-issues-inbox/6c22b076-2b5d-4257-b494-9fe3bbe2f6d6.json rename to docs/outstanding-issues-inbox/applied/6c22b076-2b5d-4257-b494-9fe3bbe2f6d6.json diff --git a/docs/outstanding-issues-inbox/6ec0c564-eabf-48a4-a8ef-504a05248244.json b/docs/outstanding-issues-inbox/applied/6ec0c564-eabf-48a4-a8ef-504a05248244.json similarity index 100% rename from docs/outstanding-issues-inbox/6ec0c564-eabf-48a4-a8ef-504a05248244.json rename to docs/outstanding-issues-inbox/applied/6ec0c564-eabf-48a4-a8ef-504a05248244.json diff --git a/docs/outstanding-issues-inbox/703d5505-d0ee-4420-8dfd-ac552f1b2d3c.json b/docs/outstanding-issues-inbox/applied/703d5505-d0ee-4420-8dfd-ac552f1b2d3c.json similarity index 100% rename from docs/outstanding-issues-inbox/703d5505-d0ee-4420-8dfd-ac552f1b2d3c.json rename to docs/outstanding-issues-inbox/applied/703d5505-d0ee-4420-8dfd-ac552f1b2d3c.json diff --git a/docs/outstanding-issues-inbox/76686841-1611-45c5-a7e6-ba344cb97dbd.json b/docs/outstanding-issues-inbox/applied/76686841-1611-45c5-a7e6-ba344cb97dbd.json similarity index 100% rename from docs/outstanding-issues-inbox/76686841-1611-45c5-a7e6-ba344cb97dbd.json rename to docs/outstanding-issues-inbox/applied/76686841-1611-45c5-a7e6-ba344cb97dbd.json diff --git a/docs/outstanding-issues-inbox/77c1617b-9ad2-43af-9eeb-200edb5eb56d.json b/docs/outstanding-issues-inbox/applied/77c1617b-9ad2-43af-9eeb-200edb5eb56d.json similarity index 100% rename from docs/outstanding-issues-inbox/77c1617b-9ad2-43af-9eeb-200edb5eb56d.json rename to docs/outstanding-issues-inbox/applied/77c1617b-9ad2-43af-9eeb-200edb5eb56d.json diff --git a/docs/outstanding-issues-inbox/7d07b0a1-4d8e-4919-a79e-9d8da0684489.json b/docs/outstanding-issues-inbox/applied/7d07b0a1-4d8e-4919-a79e-9d8da0684489.json similarity index 100% rename from docs/outstanding-issues-inbox/7d07b0a1-4d8e-4919-a79e-9d8da0684489.json rename to docs/outstanding-issues-inbox/applied/7d07b0a1-4d8e-4919-a79e-9d8da0684489.json diff --git a/docs/outstanding-issues-inbox/7e0bbcd4-339e-4d4f-b89d-2d0d19bdd16f.json b/docs/outstanding-issues-inbox/applied/7e0bbcd4-339e-4d4f-b89d-2d0d19bdd16f.json similarity index 100% rename from docs/outstanding-issues-inbox/7e0bbcd4-339e-4d4f-b89d-2d0d19bdd16f.json rename to docs/outstanding-issues-inbox/applied/7e0bbcd4-339e-4d4f-b89d-2d0d19bdd16f.json diff --git a/docs/outstanding-issues-inbox/8ddc7abc-d54a-4ee1-896b-cf9a537ec878.json b/docs/outstanding-issues-inbox/applied/8ddc7abc-d54a-4ee1-896b-cf9a537ec878.json similarity index 100% rename from docs/outstanding-issues-inbox/8ddc7abc-d54a-4ee1-896b-cf9a537ec878.json rename to docs/outstanding-issues-inbox/applied/8ddc7abc-d54a-4ee1-896b-cf9a537ec878.json diff --git a/docs/outstanding-issues-inbox/94c31907-2905-4cde-92df-df4a7255e3d4.json b/docs/outstanding-issues-inbox/applied/94c31907-2905-4cde-92df-df4a7255e3d4.json similarity index 100% rename from docs/outstanding-issues-inbox/94c31907-2905-4cde-92df-df4a7255e3d4.json rename to docs/outstanding-issues-inbox/applied/94c31907-2905-4cde-92df-df4a7255e3d4.json diff --git a/docs/outstanding-issues-inbox/971829c7-e93c-4387-8eaa-da66511214e1.json b/docs/outstanding-issues-inbox/applied/971829c7-e93c-4387-8eaa-da66511214e1.json similarity index 100% rename from docs/outstanding-issues-inbox/971829c7-e93c-4387-8eaa-da66511214e1.json rename to docs/outstanding-issues-inbox/applied/971829c7-e93c-4387-8eaa-da66511214e1.json diff --git a/docs/outstanding-issues-inbox/9c3f3aaa-112a-466d-911e-5d7250e04a64.json b/docs/outstanding-issues-inbox/applied/9c3f3aaa-112a-466d-911e-5d7250e04a64.json similarity index 100% rename from docs/outstanding-issues-inbox/9c3f3aaa-112a-466d-911e-5d7250e04a64.json rename to docs/outstanding-issues-inbox/applied/9c3f3aaa-112a-466d-911e-5d7250e04a64.json diff --git a/docs/outstanding-issues-inbox/9fcfa105-cf74-462b-9a10-3601894600e0.json b/docs/outstanding-issues-inbox/applied/9fcfa105-cf74-462b-9a10-3601894600e0.json similarity index 100% rename from docs/outstanding-issues-inbox/9fcfa105-cf74-462b-9a10-3601894600e0.json rename to docs/outstanding-issues-inbox/applied/9fcfa105-cf74-462b-9a10-3601894600e0.json diff --git a/docs/outstanding-issues-inbox/ab70f2e3-ad80-443c-b8ec-a663ea13ea85.json b/docs/outstanding-issues-inbox/applied/ab70f2e3-ad80-443c-b8ec-a663ea13ea85.json similarity index 100% rename from docs/outstanding-issues-inbox/ab70f2e3-ad80-443c-b8ec-a663ea13ea85.json rename to docs/outstanding-issues-inbox/applied/ab70f2e3-ad80-443c-b8ec-a663ea13ea85.json diff --git a/docs/outstanding-issues-inbox/d1a2de2a-15ef-4449-9dba-b2ce3724ab1e.json b/docs/outstanding-issues-inbox/applied/d1a2de2a-15ef-4449-9dba-b2ce3724ab1e.json similarity index 100% rename from docs/outstanding-issues-inbox/d1a2de2a-15ef-4449-9dba-b2ce3724ab1e.json rename to docs/outstanding-issues-inbox/applied/d1a2de2a-15ef-4449-9dba-b2ce3724ab1e.json diff --git a/docs/outstanding-issues-inbox/d372d7d9-a9c1-4c19-8f73-9e505a09b7d8.json b/docs/outstanding-issues-inbox/applied/d372d7d9-a9c1-4c19-8f73-9e505a09b7d8.json similarity index 100% rename from docs/outstanding-issues-inbox/d372d7d9-a9c1-4c19-8f73-9e505a09b7d8.json rename to docs/outstanding-issues-inbox/applied/d372d7d9-a9c1-4c19-8f73-9e505a09b7d8.json diff --git a/docs/outstanding-issues-inbox/eadf6de9-e3bc-4dab-b6d0-da55cab98e43.json b/docs/outstanding-issues-inbox/applied/eadf6de9-e3bc-4dab-b6d0-da55cab98e43.json similarity index 100% rename from docs/outstanding-issues-inbox/eadf6de9-e3bc-4dab-b6d0-da55cab98e43.json rename to docs/outstanding-issues-inbox/applied/eadf6de9-e3bc-4dab-b6d0-da55cab98e43.json diff --git a/docs/outstanding-issues-inbox/f1e6326b-13b6-4dd3-bec3-73be77fd8aee.json b/docs/outstanding-issues-inbox/applied/f1e6326b-13b6-4dd3-bec3-73be77fd8aee.json similarity index 100% rename from docs/outstanding-issues-inbox/f1e6326b-13b6-4dd3-bec3-73be77fd8aee.json rename to docs/outstanding-issues-inbox/applied/f1e6326b-13b6-4dd3-bec3-73be77fd8aee.json diff --git a/docs/outstanding-issues-inbox/ff46ad04-9610-484c-9ead-283d41da3455.json b/docs/outstanding-issues-inbox/applied/ff46ad04-9610-484c-9ead-283d41da3455.json similarity index 100% rename from docs/outstanding-issues-inbox/ff46ad04-9610-484c-9ead-283d41da3455.json rename to docs/outstanding-issues-inbox/applied/ff46ad04-9610-484c-9ead-283d41da3455.json diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 6511f71ab7..dbec7d35f3 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -98,31 +98,22 @@ removed after current-main verification; it is not missing recommended work. | #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 | | #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 | | #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 | CONSOLIDATED 2026-09-02, replacing two records that were pending simultaneously (one from PR #2526 on main, one from this branch) and so failed the ledger planner. DELETION HALF: CLOSED. public.documents, document_labels, document_summaries and document_table_facts use ON DELETE RESTRICT, with the live migration and a schema proof pinning the four visibility tables and their exact restrict action. CODE DEFECT (open by predicate, zero by data): public.retrieval_owner_matches resolves the public sentinel to row_owner_id IS NULL, and retrieval_owner_matches_v2 does the same for include_public; neither requires metadata.public_corpus = true, so an ownerless row without the marker could enter a public retrieval result. EXPOSURE, measured twice on owner-approved read-only reads of ref sjrfecxgysukkwxsowpy (2026-09-01 and 2026-09-02, unchanged between them): total documents 2851; owner_id NOT NULL = 0; owner_id IS NULL = 2851; of those metadata.public_corpus = true = 2851; EXPOSED COUNT = 0. Those reads also established document_corpus_access_state.mode = 'public', meaning set_document_corpus_access_mode('public') has been run against live 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 the 20260825025032 header describes, and it is why the defect currently has no blast radius. PRIORITY P2, not P1 - but the exposure is zero by DATA STATE, not by predicate, so any future ownerless insert that skips the marker re-opens it silently. CORRECTION to the previously recorded NEXT step: 'put metadata.public_corpus = true inside the retrieval contract's public branch' CANNOT BE WRITTEN AS DESCRIBED. retrieval_owner_matches receives two uuids and never sees document metadata; doing it at the call sites means twelve distinct retrieval functions, putting every retrieval path in the blast radius of a predicate change against a corpus that is 2851/2851 ownerless - where a marker missing from one row is a total search outage rather than a degradation. ROUTE TAKEN (PR #2547, open, not merged): constrain the WRITE side so the property retrieval already assumes is true by construction - a CHECK on public.documents that an ownerless row is either published (metadata->'public_corpus' is not distinct from 'true'::jsonb; null-safe, because a CHECK passes on NULL) or quarantined (status = 'failed'), added NOT VALID then validated separately, with a fail-fast guard migration; plus the one genuinely unfiltered read path, get_related_document_metadata, gaining 'and d.status = indexed'. The measured 2851/2851 marked population is what VALIDATE CONSTRAINT runs against, so it is expected to pass rather than abort. STILL OPEN: the PR needs an approved live-database window (merging applies it within seconds), and the retrieval predicates themselves are deliberately left alone. | consolidation of the #2526 record (live reads 2026-09-01/02, ref sjrfecxgysukkwxsowpy) and this branch's schema analysis; PR #2547 supabase/migrations/20260902110500 and 20260902111500 | 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 | -| #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 | | #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 | +| #W98GR7 | P2 | issue | Enrichment artifact families can be permanently lost, the designed repair function is called by nothing, and no monitoring exists | UPDATE 2026-09-07 (offline repo read on main at 62638c17; no provider access). TWO of the four recorded claims hold, ONE holds with a corrected mechanism, and the central technical premise is REFUTED by the code on main. REFUTED: 'deletes an artifact family BEFORE calling OpenAI and re-inserting, one family at a time, never staged-then-swapped'. In all four writers in supabase/functions/indexing-v3-agent/index.ts - upsertMemoryCardsFromSections, upsertSectionIndexUnits, upsertVisualArtifacts, upsertCoreEmbeddingFields - the embeddingBatch await completes BEFORE sql.begin is entered, and the delete and re-insert share one Postgres transaction. A provider outage therefore aborts before any delete happens, and a failed insert rolls the delete back; the 'permanently empty family' failure mode does not follow from this code. tests/indexing-v3-agent.test.ts pins that ordering statically for all four. CONFIRMED: repair_strict_enrichment_gate_batch is invoked by nothing - every repo-wide hit is the migration, schema mirror, generated types, docs, or a schema-text assertion. CONFIRMED: no monitoring - needs_enrichment_artifacts appears in no script and no workflow. CONFIRMED WITH CORRECTED MECHANISM: both terminal states are excluded from claim eligibility forever, but by two different routes - claim_indexing_v3_agent_jobs excludes needs_enrichment_artifacts by NAME in 'status not in (...)', while failed is excluded through 'attempt_count < max_attempts', because agentFailureDecision only writes failed once attempts are spent. NEW FINDING, the reason the recorded fix would not have worked: repair_strict_enrichment_gate_batch touches documents.metadata, document_index_quality and ingestion_jobs and NEVER touches indexing_v3_agent_jobs, so even wired to a caller it could not have unstuck a stuck document. ADDRESSED in this change: migration 20260907041700 adds the indexing_v3_agent_jobs reset with fresh-processing and fresh-pending guards, scripts/repair-strict-enrichment-gate.ts is the operator caller (dry-run first, --apply --yes, health-probed, deliberately not automated), and check:enrichment-health counts the stuck states. Re-verified separately: current deep-memory writes already stage producer-scoped generations and commit them through commit_document_deep_memory_generation, so the stale follow-up request from PR #2548 was not carried into this replacement. | supabase/functions/indexing-v3-agent/index.ts; tests/indexing-v3-agent.test.ts; supabase/migrations/20260724060000_atomic_reindex_agent_guard.sql; supabase/migrations/20260625033425_strict_enrichment_gate_repair.sql; src/lib/deep-memory.ts and tests/deep-memory-transaction-sql.test.ts; offline audit on main 62638c17, 2026-09-07 | 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 | -| #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 | -| #KMM6R6 | P2 | task | The Caring Contacts service safety stop halts sending across every patient and team, but the rule that it is stored as ONE record rather than one row per team is currently carried only by a field name (reportedByTeamId) and a doc comment in src/lib/caring-contacts/service-state.ts. Migration 0003 (Phase 2A Task 11) must enforce it in the schema with a fixed-key singleton row plus a test, and every dispatch path must read that one record regardless of the dispatching team. Without it, a stop raised by one team would leave every other team still sending during an incident. | | session 2026-08-19 | 2026-08-19 | -| #EWWJVX | P2 | task | Caring Contacts Phase 2B — the screens | Phase 2A closed at Task 19 with one production screen built (/caring-contacts, Today) plus the frozen 24-overlay renderer. Plan 2B builds the remaining screens: patients, patient overview, patient and agreement, pathway selection, personalisation, review and activation, plan detail, schedule, contact and delivery exception, governed templates, team, guidance, reports — plus the Today dashboard body itself (referral queue, needs-action list, sending windows, recent activity, summary counts). Their rules and data already exist from Phase 1 and Tasks 3-11; only the surfaces are missing. The visual specification for each is the committed mockup atlas: 26 of its 44 images have no production counterpart today, listed in docs/caring-contacts/phase-2a-visual-differences.md. Building a screen also means giving its rail/dock destination an href in shell.tsx (Ruling 52) and raising its overlays through openWorkspaceOverlay, which nothing yet does. | docs/caring-contacts/phase-2a-visual-differences.md; docs/caring-contacts/phase-2a-sdd-archive/task-19-report.md | 2026-08-22 | | #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 | -| #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 | | #W53DY5 | 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 | | #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 | | #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 | | #JZA0XK | P2 | task | Caring Contacts: no browser evidence exists for any activation-wizard stage, including a two-write middle state where a clinician presses a writing control a second time | Root cause, unchanged since Task 7: the isolated Playwright server seeds no referral, and the wizard route requires one, so every browser case lands on the no-referral state and one explicitly asserts the wizard has count 0. Three implementers independently declined to fabricate a referral id, correctly — it would render the identical screen while claiming to prove a stage. What that leaves unproven has grown with each task and is now the largest coverage gap in the workspace. (1) HYDRATION: the wizard is the first deliberate Client Component here — every other screen is a Server Component that works with JavaScript disabled — and its draft uses useSyncExternalStore with a getServerSnapshot. jsdom has no RSC payload and performs no hydration, so that argument is untested. (2) THE SENSITIVE INPUTS: the patient's name and mobile number, and the fictional-number caution beside them, have no evidence at 320px, in dark, under forced colours, or in print. Tap-target assertions read the class rather than the rendered box. (3) THE TWO-WRITE MIDDLE STATE, and this is the one that raises the priority: stage 4 creates the plan and then starts it, and created-but-not-started is a reachable, recoverable state whose screen tells a clinician the plan exists and that pressing again finishes THE SAME plan. It is the only screen in this workspace that asks someone to press a writing control a second time, and it has never been seen in a browser. (4) Two type=date inputs and a live schedule preview whose content changes with the chosen date. Close it when something in the workspace lists referrals and a browser case can reach the wizard with a real one; at that point assert the wizard mounts, a draft value survives a reload, the caution renders, and the created-but-not-started screen appears and its second press is safe. | session 2026-08-25 | 2026-08-25 | -| #HDAP8N | P3 | rec | The document cover-thumbnail hook and its /api/documents/[id]/cover route have no product consumer since the source drawer stopped rendering a front-page thumbnail | The answer source drawer was the only caller of useDocumentCoverImageId (src/components/clinical-dashboard/use-document-cover.ts). Removing the front-page thumbnail from the drawer left that hook, and the /api/documents/[id]/cover route it fetches, reachable only from tests. check:dead-code-candidate REFUSES deletion on three counts — pinned by tests/use-document-cover.dom.test.tsx, present there as a string literal, and undateable on a shallow clone — so nothing was deleted. Decide deliberately: either a future surface adopts the cover (document detail, source rail card), or the hook, its route, its tests and the coverImageId plumbing are retired together after a deepened clone re-runs the gate. Do not delete on reachability alone. | session 2026-09-01, sidebar-alignment-cleanup branch | 2026-09-01 | | #1BHXEF | P3 | issue | Ward Flow demo clock mockup test flakes in Advisory UI: passed and failed on consecutive heads of the same unrelated PR | tests/ui-ward-roles.spec.ts :: '@mockup Ward screen > the demo clock control advances a held bed toward expiry, and reads as demo scaffolding' failed the Advisory UI job on PR #2437 head 9855e706 (run 33096941258, classifier said 'needs investigation'), having PASSED on the immediately preceding head 8bab0c9d of the same branch. The intervening commits touched only tests/ui-smoke.spec.ts and an unrelated medication merge; PR #2437 changed no ward file. It also passed locally on that branch head (chromium-mockups, 1 passed, 5.5s). It is a timing-sensitive demo countdown (see PR #2392 'pin demo countdown clock'), so the pinned clock likely still has a real-time-dependent path. Not in tests/flake-ledger.json. Advisory UI is not among the twelve jobs the pr-required aggregate depends on (.github/workflows/ci.yml, pr-required needs: changes, static-pr, safety, coverage, ingestion-sast, build, container-images, ui-critical-fast, ui-critical, lighthouse-budget, db-reset-verify, caring-contacts-db) and never runs on main pushes -- it is gated on advisory_ui_changed -- so this only ever surfaces on PRs that touch a mockup surface, which is why it can rot unnoticed. Next action: reproduce by running the spec repeatedly on one SHA; if it reproduces three times per docs/testing.md flake policy, quarantine via tests/flake-ledger.json, otherwise harden the clock pin. Do not weaken the assertion to silence it. | Session unblocking PR #2437, 2026-08-27: observed in CI run 33096941258 and reproduced-clean locally. | 2026-08-27 | | #6KR6BR | P3 | issue | /calculators/search keeps a 2px residual scroll range at 1280x1200 after the dead-scroll sweep | Found 2026-08-27 while verifying PR #2419 (dead scroll on pages that fit the window) across 39 routes x 5 viewports in Chromium: every page that fits reported a scroll range of exactly 0 except /calculators/search, which retained 2px at 1280x1200. RE-MEASURED 2026-09-02 IN CHROMIUM AGAINST A LOCAL DEV SERVER ON BRANCH claude/ui-fixes-q04yq7 (base origin/main 45a3dca) AND IT DOES NOT REPRODUCE. Matrix: viewports 1280x1200, 1280x800, 1440x1200 and 1024x1200, each in the browse (catalogue) state and three submitted states (?q=phq, ?q=phq&run=1, ?q=zzzznomatch with no match), each at deviceScaleFactor 1 and 2, measured the way tests/ui-chrome-scroll.spec.ts measures - both document.scrollingElement.scrollHeight - innerHeight and #main-content.scrollHeight - clientHeight, with the spec's 800ms then 400ms settle reads. Every 1200-tall cell read 0/0 on both reads and both scale factors; the submitted state reached by typing into the composer and pressing Enter (landing on ?q=depression&run=1) also read 0. THE ONLY NON-ZERO CELLS ARE GENUINE CONTENT: at 1280x800 the browse catalogue reports 218px, because the catalogue's own content is a fixed 1018px tall. A height sweep at 1280 wide puts the fit boundary at 1018-1020px: 1000 -> 18px, 1020 and above -> 0. So at the reported 1280x1200 the page clears the window by 182px and the residual is not marginal or sub-pixel - it is absent by a wide margin. The scroll owner is the document at every desktop cell (#main-content computes overflow-y visible), so the 2px could not have been hiding in the other container. Demo mode does not weaken this: the server was in demo mode (the answer route returns the synthetic-corpus refusal), but src/components/calculators/search-page.tsx reads static module data only, with no Supabase or demo branch, so the catalogue's length is identical in live mode. LIKELIEST EXPLANATION: page content changed between 2026-08-27 and now, since the residual was attributed to real content rather than to the calc(100dvh - chrome estimate) class PR #2419 retired. WHAT LANDED WITH THIS UPDATE: /calculators/search is now pinned at zero in both states by the existing gate - two cases added to the 'pages that fit the window have no scroll range' block in tests/ui-chrome-scroll.spec.ts, at the exact reported 1280x1200 viewport (the block's other cases keep their 1440x1200 default via a new optional per-case viewport field). Both pass; a mutation to a 1000-tall viewport makes the browse case fail with 'reserves 18px of scroll past its content', so the assertion is live rather than vacuous. NO PRODUCTION CODE WAS CHANGED and no exception was recorded under invariant 24, because there is nothing to except. NEXT: leave the row open at P3 only until a future sweep re-measures; if it stays 0 there too, close it as not reproducible. Stop: do not reintroduce a viewport-estimate floor, do not clip the route's overflow, and do not shrink a min-h-12 tap target to chase a residual. | PR #2419 verification sweep, 2026-08-27; re-measured in Chromium 2026-09-02 on claude/ui-fixes-q04yq7 (base origin/main 45a3dca), 32-cell viewport x state x deviceScaleFactor matrix, all 1200-tall cells 0px; docs/search-chrome-behaviour.md invariant 24; pinned by tests/ui-chrome-scroll.spec.ts | 2026-08-27 | -| #MPZTBR | P3 | issue | The scope-statement footer contradicts itself: an audit says mount it on every mode home, the code says it was deliberately removed from all of them | #PM9SP1's FIX text said to 'relabel to Clinical reference - not validated decision support and mount the footer on the other mode homes'. The relabel half landed (PRs #2497, #2499). The mount half conflicts with a recorded decision in the code: src/components/mode-home-template.tsx:216-219 states 'No mode home renders this any more: the line under the composer was removed from every home page. The sole remaining call site is the therapy-compass page footer, which sits at the bottom of the sub-routes and is explicitly not rendered on the therapy home (showFooter={!isHome} in workspace.tsx).' So one source says mount it everywhere and the other says it was deliberately taken off everywhere. NEXT ACTION: owner ruling on which is current, then make the other match. If the footer stays off mode homes, amend the #PM9SP1 fix text so a future session does not re-add it; if it should return, that is a deliberate reversal of the recorded decision and the comment at mode-home-template.tsx:216-219 must be updated in the same change. Not urgent: every surface that renders retrieved clinical content already carries its own scope line (verified by a repo-wide sweep 2026-09-01) - the open question is the shared mode-home composer footer only. | Design-system + app review session 2026-09-01; conflict found while fixing #PM9SP1, verified against mode-home-template.tsx and therapy-compass/workspace.tsx | 2026-09-01 | | #YTR84P | P2 | task | Ward Flow pinned clock: the provider fix is on main; D5's rendered branch is untested AND unrendered, so it needs an owner decision before any test | CORRECTED 2026-09-02 from a cloud session, verified against origin/main 45a3dca and origin/claude/ward-flow-phases-6-7-design 1888ad1. Four of this row's claims were stale and its central NEXT ACTION was not executable as written. (1) THE FIX IS ON MAIN. ward-flow-provider.tsx uses the pinned instant verbatim, and tests/ward-flow-provider.dom.test.tsx already pins 450 (07:30) and 1265 (21:05) asserting now is not NOW_ANCHOR. Action (1) is done. (2) claude/serene-heyrovsky-7a5e53 is REDUNDANT - close it; that answers action (4). Do not cherry-pick 62f798c2a; it is already superseded. (3) The morning page is not on main at all, and the Phase 6 branch fixed the same defect independently on 2026-08-30, so 'still carries the defect' is false everywhere. That branch IS reachable from a cloud container: git fetch --depth=50 origin claude/ward-flow-phases-6-7-design. (4) The blast radius is 51 call sites across 20 test files (47 NOW_ANCHOR, 4 deliberately not), not 40. (5) NullHandoverHarness and DirectFrozenHarness NO LONGER EXIST on that branch; action (3) has no target and should not be searched for. THE ONE REMAINING ACTION IS NOT A TEST, IT IS A DECISION. Action (2) said to render MorningPage inside WardFlowProvider initialNow={7*60+30}. THAT TEST CANNOT PASS, for a reason unrelated to the clock: owner decision WB-DB-11 ('ONE VIEW, ALWAYS LIVE') removed the fixed/live split, so MorningPage renders MorningBody and nothing else. NoHandoverYet (which holds 'The 08:00 handover has not been taken for this day'), ViewControl, buildFrozenMorning and the FrozenMorning/MorningView types are all exported and NONE is rendered or called. Pinning before 08:00 renders the ordinary live page. So D5 must first be either retired with the fixed view, or the fixed view restored - only then is initialNow={7*60+30} the right mechanism, and the pinned-clock fix has already made it available. Coverage as measured: D5's clock rule is well covered (ward-morning-rollup.test.ts pins morningHandoverInstant null at 07:59 and 01:00) but its RENDERED failure branch has NO coverage of any kind. SEPARATE UNRECORDED BREAKAGE FOUND: tests/ui-ward-morning.spec.ts still clicks ward-morning-view-fixed/-live, which ViewControl holds and nothing renders, so that spec cannot pass against branch head. CONTEXT FOR WEIGHING ANY OF THIS: the branch's PR #2466 was CLOSED UNMERGED on 2026-08-31 - 299 commits ahead, 276 files, mergeable_state dirty, titled 'DRAFT, not for merge' - and all of it is design scratch under src/app/mockups/ward-flow/**, which 404s in production. DONE THIS PASS on main: correction blocks above the retained buggy snippet in docs/superpowers/plans/2026-08-19-ward-flow-phase-3-role-screens.md and docs/ward-flow-phase-3-workspace/task-4-brief.md (the only places that still TAUGHT the defect); docs/ward-flow-pinned-clock-handover.md rewritten to be true; and two mutation-proven screen-level tests in tests/ward-discharge-board.dom.test.tsx driving releaseBand's two now-dependent branches through a rendered board (both red under the reintroduced defect, all six pre-existing tests green). Still open because the D5 decision is real and untaken. Offline only. | Claude Code cloud session 2026-09-02; verified against origin/main 45a3dca, origin/claude/ward-flow-phases-6-7-design 1888ad1, and closed PR #2466 | 2026-08-27 | | #RBK2J7 | P2 | issue | Lighthouse desktop-root LCP reads 100-175ms above main on a feature branch, decays run over run, and reddened PR #2422 once | NEW INSTANCE 2026-09-06 on PR #2664, desktop-root, same 785.647ms baseline and same +20%/+100ms tolerance (effective ceiling 943ms). Seven runs of code that cannot reach the measured route: 997 FAIL, PASS, PASS, PASS, 989 FAIL, PASS, PASS. The two failures sit 8ms apart, which is runner-class spread rather than a decaying regression, so this reading is closer in shape to #4TXR6Z (bimodal between runners) than to the monotone 961->925->900->852 decay this row originally recorded. Import-reachability was traced for PR #2664: the diff is 16 files of source-governance data, a gate script, a catalogue provider and docs, and nothing on it is imported by the desktop root render. main passed the same check on the same day. The one permitted CI re-run was spent and passed. RESOLUTION TAKEN: the owner authorised the skip-lighthouse-budget label on PR #2664 rather than touching the baseline or the tolerance, which the pr-required aggregate accepts because it calls require_skipped_or_success 'lighthouse-budget'. Label semantics worth recording: on.pull_request.types has no 'labeled' entry, so the label is only read at synchronize and takes effect on the next push, not when applied. Stop rules from this row are unchanged - do not raise the baseline or tolerance to clear a red PR. | PR #2422 CI runs 33067093750 (attempts 1-2), 33070896021, 33072264164; main runs 33063088413, 33065297542 | 2026-08-27 | | #BS3SN9 | P1 | task | Close remaining privacy provider, legal, and clinical approvals | The PsychSift owner approved the production HMAC control and verified retention schedules on 2026-09-01. Release remains blocked on six items: OpenAI ZDR and countersigned DPA evidence, Railway sensitive-health terms, APP 8 and APP 1/5 privacy-adviser approval, and clinical PHI-minimisation acceptance. Evidence authority: docs/governance/privacy-readiness.v1.json and docs/governance/privacy-closeout-2026-09-01.md. | session 2026-09-01 owner attestation closeout | 2026-09-01 | @@ -134,20 +125,12 @@ removed after current-main verification; it is not missing recommended work. | #ACWXN6 | P2 | issue | Two clinical-risk Caring Contacts fixes merged with no automated review, because the Cursor spend limit silently downgraded the approval agent and Bugbot to neutral | RECONSTRUCTED 2026-09-06, and corroborated the same day on PR #2668. The pattern is that a review agent which cannot run reports a NEUTRAL or informational result rather than a failure, so a PR presents as having been reviewed when no review happened. Observed on #2668: the Cursor Approval Agent and the Cursor Security Agent both completed with conclusion 'neutral'; Bugbot posted 'Bugbot couldn't run - usage limit reached'; the Codex connector posted 'You have reached your Codex usage limits for code reviews'; and CodeRabbit posted that the repository receives no automatic review because it has fewer than 10 stars. Four of the repository's review surfaces were therefore silently absent on a single PR, none of them as a failing check. WHY IT MATTERS BEYOND COST: a neutral conclusion satisfies branch protection, so on a clinical-risk change the absence of review is invisible at merge time. NEXT ACTION: decide whether an unavailable review agent should block a clinical-risk PR or merely be reported, and if it should block, make the absence a failing required check rather than a neutral one. The CodeRabbit star-count gate is a separate and already-tracked premise failure (#3F76JZ). | session 2026-09-02 | 2026-09-02 | | #0JGJTK | P2 | issue | The calculators mockup still serves prescribing, ECT and admission directives that PR #2491 removed from production on clinical-safety grounds | RECONSTRUCTED AND CONFIRMED 2026-09-06. src/components/calculator-mockups/calculator-pathways.ts still carries directive clinical instructions of the three kinds PR #2491 removed from the production calculators on clinical-safety grounds. Present today: line 65 'Screen for bipolarity before prescribing' with detail 'Run the MDQ below'; line 70 'Assess psychotic features and ECT indications'; line 240 'Consider admission or intensive community follow-up'; line 244 'Admission usually indicated - ensure immediate safety'. Production and mockup calculators are separate trees (src/components/calculators and src/components/calculator-mockups), which is how the removal reached one and not the other. WHY IT MATTERS: the mockup routes are developer-gated rather than unreachable, and the text is indistinguishable in tone from the production copy that was judged unsafe. A reader who lands on the mockup gets guidance the repository has already decided not to give. NEXT ACTION: apply the same removal to calculator-pathways.ts that #2491 applied to production, or, if these pathways are wanted as design fixtures, replace the directive wording with plainly non-clinical placeholder text. A gate that keeps the two trees aligned on this specific class of copy would stop it recurring. | session 2026-09-02 | 2026-09-02 | | #CC8D30 | P2 | task | Two read-only live-database reads are owed before the migration-history guard and reindex-reaper work can be closed | Both are SELECTs that change nothing, and neither can be run from an agent session (no credentials, and the standing instruction is never to touch the live database). (1) Migration-history classification for the four newly declared no-op migrations, needed to decide whether each requires a 20260804110240-pattern validation guard plus a reviewed drift-allowlist entry: select jsonb_agg(row) from jsonb_array_elements(public.schema_drift_snapshot() -> 'migration_history') as row where row ->> 'version' in ('20260629100000','20260702170000','20260708160000','20260709150000'); Note the no_ddl predicate currently cannot express 'select 1 where false;', so that predicate needs review before any allowlist entry is written, and it must not be widened to make an entry pass. (2) Reindex-reaper alarm baseline, needed before REINDEX_REAPER_ENABLED is ever set, because a non-zero would_alert_forever means the weekly probe opens a permanently red issue the moment it is armed: select (select count(*) from public.documents) as documents_total, (select count(*) from public.documents where index_generation_id is null) as documents_missing_pointer, (select count(distinct c.document_id) from public.document_chunks c join public.documents d on d.id = c.document_id where c.index_generation_id is not null and d.index_generation_id is null) as would_alert_forever; | PRs #2549 (claude/migration-history-guards) and #2552 (claude/reindex-reaper) | 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 | | #XGKJ8D | P2 | issue | pg_cron is created by migration 20260901033250 but never declared in supabase/schema.sql, so the mirror understates the live extension set | Found by the first real CI run of the new chain/mirror parity gate (PR #2550, Migration replay run 33599464239). The gate now promotes chain-only extensions to unexpected_live findings and prints '### Divergence (1) - [extensions] unexpected_live pg_cron'. pg_cron is deliberately NOT allowlisted in supabase/chain-mirror-allowlist.json because it is a genuine mirror gap, not the emulator-image asymmetry that pg_net is. Fixing it means adding the extension declaration to supabase/schema.sql and regenerating supabase/drift-manifest.json, whose embedded sha256 the edit invalidates; regeneration needs Docker, so this is owner/migration-batch work rather than something an offline session can do. Until it lands, the parity gate reports one known divergence on every database-touching PR. | PR #2550 (claude/drift-semantics); Codex review finding on the chain/mirror parity gate | 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 | | #E6BB64 | P2 | issue | Archived #Z61JRT carries an incorrect and dangerous public-bucket RLS claim | CORRECTION to the archived #Z61JRT close outcome. Archived rows are immutable history and are not edited, so the erroneous sentence stays in the archive and this row is the correction of record. Anyone reading that archived outcome must read this row with it. THE ERROR: the #Z61JRT outcome states "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." That is wrong, and wrong in the unsafe direction. WHY IT IS WRONG: the owner-folder RLS policy on storage.objects governs authenticated reads of a PRIVATE bucket. It has no bearing on a public one. The installed client documents this explicitly for getPublicUrl at node_modules/@supabase/storage-js/src/packages/StorageFileApi.ts, which lists the required permissions as "buckets table permissions: none" and "objects table permissions: none". A public bucket therefore serves its objects to anyone with the URL, with no RLS evaluated at all and no token required. THE CORRECT STATEMENT: if clinical-images were ever flipped to public, every object in it would become anonymously readable by URL, without authentication. The owner-read policy would not stop it. That is precisely the hazard the original #Z61JRT row warned about, so the archived text inverts the risk it was recording and would invite a reader to believe flipping the bucket is inert. CURRENT LIVE STATE: the bucket was not flipped. A user-authorised read-only query on 2026-09-02 returned storage.buckets.public = false for both clinical-images and clinical-documents. The delivered fix in the batch image route is also unaffected: it signs uniformly via createSignedUrls and never calls getPublicUrl, and grep for getPublicUrl across src, worker, scripts and supabase returns zero matches. So there is no live exposure and no code change is owed by this correction. What was wrong was the stated reason a bucket flip would be harmless, not the conclusion that the bucket must stay private. WHAT THE MIGRATION ACTUALLY GUARANTEES, AND WHAT IT DOES NOT: 20260717139000_create_storage_buckets.sql carries "on conflict (id) do update set public = false", so it pins the bucket private whenever that migration itself executes — that is, on a fresh replay such as a local db reset or the CI migration-replay job. It does NOT re-run against the live project. Migrations apply once and are then recorded in the remote history table; check-migration-history-alignment.ts classifies only local versions ABSENT from remote history as pending apply, so an already-recorded version is never pending again and its ON CONFLICT clause never fires again. An earlier draft of this row claimed a manual dashboard flip would be "reverted by the next migration run". That was false, and false in the same containment-overstating direction as the error this row exists to correct. SO THE REAL CONTROL IS DETECTION, NOT PREVENTION: if an operator flipped clinical-images to public in the dashboard, it would STAY public until a person explicitly restored it. schema_drift_snapshot captures storage_buckets, so npm run check:drift would surface the changed public value as drift, and the post-merge live-drift workflow is where that surfaces. But check:drift only reports; it repairs nothing. Remediation is manual — flip it back in the dashboard or apply a new migration that re-asserts public = false, since only a NEW version would be pending and therefore actually execute. PROVENANCE: the primary error was raised by the Codex reviewer as a P2 finding on PR #2559 against docs/outstanding-issues.md, verified against the installed storage-js source rather than accepted on assertion. The original claim came from a schema review earlier in the same session and was carried into the close outcome without being checked. The secondary error — the false automatic-reversion claim in this very correction — was then raised by the same reviewer on PR #2563 and verified against check-migration-history-alignment.ts before this text was rewritten. Both corrections were made before this request was reconciled, so the ledger never carried the second error. NEXT: no code fix. Treat this row as the authoritative reading of that archived outcome. If a future task ever proposes making clinical-images public, this row is the reason not to. Do not rely on the migration to undo such a change on the live project; rely on check:drift to notice it and on a person to put it back. | Codex review finding (P2) on PR #2559, docs/outstanding-issues.md:625, 2026-09-02; verified against node_modules/@supabase/storage-js StorageFileApi.ts getPublicUrl remarks | 2026-09-02 | | #97W4FD | P2 | issue | A change that WEAKENS a clinical safety guard living under tests/ escapes the governance preflight gate, and PR #2521 demonstrated it rather than hypothesised it | Raised by the clinical-governance-reviewer on PR #2521, 2026-09-02, and then demonstrated by that same PR one commit later. scripts/pr-policy.mjs decides whether a PR must carry a completed '## Clinical Governance Preflight' by matching changed paths against clinicalRiskPatterns, which covers src/lib/*clinical*, src/app/api/, supabase/, src/data/ and similar. It has no pattern for tests/**. But several of this repository's clinical safety guards ARE tests: tests/caring-contacts-interface-vocabulary.test.ts is the only software check on prohibited wording across five source trees; tests/helpers/caring-contacts-prohibited-language.ts is the single shared definition of that vocabulary; tests/caring-contacts-overlay-definitions.test.ts holds the 24 frozen overlay rows against docs/caring-contacts/interaction-matrix.md; and the Ruling [143] parity block is what stops the message-side 'lead' rule drifting looser than the screen's. THE GATE IS DIRECTION-BLIND: it reads paths, not intent, so it cannot tell strengthening a guard from removing one. THIS IS DEMONSTRATED, NOT HYPOTHESISED. An earlier version of this row listed 'relaxing the lead lookbehind' among the changes that would slip through. The next commit on that same branch, 33e1ffd, did exactly that: it loosened the interface prohibited-language rule so plural job titles are permitted on screen, which is a deliberate owner-approved change to clinical-copy wording, and classifyPullRequestFiles returned clinicalRisk:false for it, so no preflight was required and no reviewer was prompted to look for clinical consequences. The preflight was completed voluntarily and the change was reviewed by a subagent, both by choice; nothing in the required checks asked for either. A second near miss on the same PR: its first draft exempted src/lib/caring-contacts/message-rules.ts as a whole file, which would have put the five sentences a discharged patient reads by SMS permanently outside the scan. A subagent review caught it. Nothing in the required checks would have. CANDIDATE FIXES, increasing cost: name the specific guard test files in clinicalRiskPatterns, cheap and precise but needs maintaining as guards are added; or match tests/** paths that import CARING_CONTACTS_PROHIBITED_LANGUAGE or the message rules, self-maintaining but needs an import scan in pr-policy; or a narrower gate firing only when such a file's diff is net-deleting. Prefer whichever keeps a false positive cheap: the cost of a wrong classification is one preflight section a human fills in, and the cost of the current gap is an unreviewed weakening of a patient-facing safety guard. | clinical-governance-reviewer on PR #2521, 2026-09-02; demonstrated by commit 33e1ffd on the same PR | 2026-09-02 | | #ZWJ71W | P3 | issue | tests/ui-ward-morning.spec.ts drives view controls that WB-DB-11 stopped rendering, so it cannot pass against the Phase 6 branch head | Found 2026-09-02 while correcting #YTR84P, and recorded separately because it is a different defect from the pinned-clock one and would be missed inside that row's prose. On branch claude/ward-flow-phases-6-7-design at 1888ad1: tests/ui-ward-morning.spec.ts clicks ward-morning-view-fixed and ward-morning-view-live (lines 51-52 and 195-206). Those test ids live in ViewControl (morning-page.tsx around lines 481-487), but owner decision WB-DB-11 ('ONE VIEW, ALWAYS LIVE') left MorningPage rendering MorningBody and nothing else, so ViewControl is exported and rendered by nothing. The spec therefore cannot pass as written. Same root cause as the D5 problem recorded in #YTR84P - the fixed/live split was removed while the code and tests that depend on it were left behind - so both should be settled by the same decision: either restore the fixed view, or retire it and remove the orphaned code and assertions together. SCOPE AND URGENCY: low. That branch's PR #2466 was closed unmerged on 2026-08-31 (299 commits ahead of main, mergeable_state dirty, titled 'DRAFT, not for merge'), and all of it is synthetic design scratch under src/app/mockups/ward-flow/**, which 404s in production. Nothing on main is affected. Recorded so it is not rediscovered from scratch by whoever takes the D5 decision. Offline only - the branch fetches normally from a cloud container with git fetch --depth=50 origin claude/ward-flow-phases-6-7-design. | Claude Code cloud session 2026-09-02; read on origin/claude/ward-flow-phases-6-7-design at 1888ad1 | 2026-09-02 | -| #WKFSV6 | P3 | issue | Retiring answer-evidence-popups removed the repo's only source-text heading-hierarchy contract, and the DOM-level coverage does not reach mockup gallery pages | RECONSTRUCTED 2026-09-06, partially verified. answer-evidence-popups is confirmed gone: it survives only as references in docs/outstanding-issues.md, the archived q3 branch-review ledger and two applied inbox records, with no occurrence anywhere in src/ or tests/. Heading-hierarchy assertions now exist only in tests/ward-community-index.test.ts and tests/ward-referral-screens.dom.test.tsx, both Ward Flow surfaces, so the claim that no coverage reaches mockup gallery pages or source-text rendering matches what is in the suite today. NOT VERIFIED: the exact contract the retired component asserted, which was not recorded before it was deleted. NEXT ACTION: decide whether a source-text heading-hierarchy contract is still wanted. If it is, write it fresh against the surface that renders source text today rather than trying to recover the retired component's version, and point it at the mockup gallery pages the row says were never covered. | session 2026-09-02 | 2026-09-02 | | #3F76JZ | P2 | issue | The 2026-08-22 decision to accept intermittent CodeRabbit review rests on a premise that is false: it is blocked by a star-count eligibility gate, not the spending cap | Supersedes the premise of closed row #CCZ4HB, whose owner decision (2026-08-22, option c) was to leave the CodeRabbit spending cap alone and accept intermittent review from that bot. MEASURED 2026-09-02 across PRs #2522 and #2542, three times, verbatim: 'This repository does not receive automatic reviews because it has fewer than 10 stars.' The same run config reported Plan: Team and profile CHILL, reading .coderabbit.yaml. GitHub API confirms stargazers_count: 0. That is a categorical ELIGIBILITY gate, not a consumption limit, so the spending cap is irrelevant to it and 'intermittent' understates the position - CodeRabbit reviews nothing here at all. Every lever in docs/decisions/ccz4hb-review-coverage.md aims at conserving or buying credits and therefore cannot restore coverage. A SECOND separate gate: drafts are skipped ('Draft PRs are not automatically reviewed by default'), so a draft is skipped for one reason and an undrafted PR for the other; undrafting buys no review and does escalate CI to the full heavy set. THE TWO-REVIEWER FRAMING IS STALE TOO: the Codex connector WORKS and is currently the only automated reviewer - it raised a correct P2 on #2522 (a handover doc contradicting its own status banner, fixed in 87ee0c7) - while Cursor Bugbot is the tool actually reporting 'usage limit reached' and is not analysed in that document at all. NOT VERIFIED - do this first: CodeRabbit's published policy, changelog and support channels were NOT checked. Whether the star gate is new, whether it applies to the Team plan as configured, whether private or paid repositories are exempt, and whether it can be waived are all unknown. ASK THE VENDOR before acting; if the gate proves waivable or misapplied the preserved budget analysis may become operative again. WHY THIS MATTERS: that document's own argument is that this is a clinical reference tool with one maintainer where robot review is the only review that happens at all. The owner accepted degraded coverage on the understanding it was intermittent and budget-driven; both halves are wrong, so the decision deserves revisiting on correct facts. DONE THIS PASS: correction addendum at the top of docs/decisions/ccz4hb-review-coverage.md (analysis preserved, not rewritten), its stale 'awaiting the user's decision' status header corrected, the owner-decision section in docs/agents/pull-request-workflow.md annotated with the correction while keeping the decision visible, and the .coderabbit.yaml path_filters comment stopped from asserting the credit rationale (filters unchanged - excluding prose from a code reviewer is defensible on its own merits). Offline only. | Claude Code cloud session 2026-09-02; PRs #2522 and #2542, star count via GitHub API | 2026-09-02 | -| #S76Z3Y | P3 | issue | Two pieces of dead wiring found beside the mockup surface: an orphaned production component and a proxy redirect for a route that no longer exists | RECONSTRUCTED 2026-09-06, and this one could NOT be re-derived from the row. The summary names two artefacts, an orphaned production component and a proxy redirect for a route that no longer exists, without naming either, and the detail field was empty, so the specifics are not recoverable from the ledger. next.config.ts does carry header/rewrite rules including a /mockups/:path* entry and a therapy-compass-data rewrite block, but nothing there could be matched to the row's claim with confidence, and guessing at it would be worse than recording the gap. NEXT ACTION: re-run the discovery rather than trying to recall it. npm run check:knip reports unimported and unused exports and is the tool most likely to surface the orphaned component; the redirect half needs the rewrite and redirect entries in next.config.ts checked one by one against the routes in docs/site-map.md, which is generated and therefore current. Record whatever the sweep finds here, with names this time. If the sweep finds nothing, close this row as not reproducible rather than leaving it open indefinitely. PROCESS NOTE: this row is the clearest example of the cost of an empty detail field. Everything the finder knew was lost at the moment of recording, and the work now has to be done again from scratch. | session 2026-09-02 | 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 | -| #1NMMZS | P3 | issue | Caring Contacts: the plural job-title exemption also permits preceding-verb commercial phrasing, because the companion list only guards words that follow the word | Measured by the clinical-governance-reviewer on commit 33e1ffd, 2026-09-02, and left open deliberately rather than fixed there. CARING_CONTACTS_PROHIBITED_LANGUAGE in tests/helpers/caring-contacts-prohibited-language.ts exempts 'leads' when one of five job-title qualifiers (incident, programme, clinical, service, team) sits immediately before it, per the owner decision that closed #AGRAKQ. The commercial-companion branch that stops an exemption licensing what follows it guards words AFTER the word -- generation, capture, nurturing, magnet, pipeline, numbers -- which is where singular commercial English puts them ('lead generation', 'lead capture'). Plural commercial English puts them BEFORE ('capture leads', 'convert leads', 'unconverted leads'), and nothing guards that position. Measured strings refused before the change and permitted after: 'Capture clinical leads', 'Convert team leads', 'Nurture clinical leads', 'Generate service leads', 'Qualify incident leads', 'Score service leads', 'Export clinical leads', 'Track clinical leads', 'Reactivate service leads', 'Unconverted service leads', 'Warm clinical leads', 'Cold team leads', 'Total clinical leads', 'clinical leads dashboard', 'team leads funnel', 'service leads report', 'clinical leads outreach', 'service leads enrichment', 'Our team leads are up 20% this quarter.', 'How many clinical leads did we get this month?'. Bare commercial plurals are all still refused ('sales leads', 'new leads', 'warm leads', 'qualified leads', 'leads capture', a lone 'Leads'), as is 'clinical leads capture'. FAILURE SCENARIO: someone builds a referral-intake view in the workspace and labels it 'Unconverted service leads' or 'clinical leads dashboard'; the vocabulary scan passes and CRM funnel framing lands on the clinician-facing surface of a suicide-prevention programme, which is the drift this guard exists to catch. No such string exists in the tree today. WHY NOT FIXED: the singular pair has the same shape of hole -- 'Capture the clinical lead' was permitted on both surfaces before any of this -- so it is a pre-existing structural gap made easier to reach, not a new class. Both obvious fixes cost more than they buy. Requiring a determiner, (? | P2 | issue | match_document_embedding_fields_text bypasses retrieval_owner_matches and is fail-open on a null owner_filter | Found while fixing #ZBAC9D (2026-09-02, offline schema read at 45a3dcacb54a). public.match_document_embedding_fields_text (supabase/schema.sql:6845-6860) does not call public.retrieval_owner_matches at all. Its owner predicate is 'and (owner_filter is null or d.owner_id = owner_filter)', which is fail-OPEN on a null owner_filter - it returns every owner's rows - and which returns nothing for the public sentinel, because no real row has owner_id equal to the all-zeroes uuid. That is the exact shape migration 20260708160001_retrieval_owner_matches_fail_closed.sql removed from retrieval_owner_matches ('fail CLOSED (was: true) - no DB-level global escape hatch'), reintroduced in a sibling function. It currently has NO application caller: a grep over src/, worker/ and scripts/ finds none, so it is live-codified dead code rather than an active exposure. It was left out of the #ZBAC9D PR deliberately to keep that diff to the write-side invariant plus the one reachable read-side hole. FIX: either drop the function, or re-scope it to public.retrieval_owner_matches(owner_filter, d.owner_id) like its siblings. Either way it is a retrieval-surface migration and needs the same approved live-database window and a schema-contract pin. | supabase/schema.sql:6845-6860; supabase/migrations/20260708160001_retrieval_owner_matches_fail_closed.sql:20-32; offline audit during #ZBAC9D, 2026-09-02 | 2026-09-02 | -| #72282V | P2 | issue | Package 15's CSRF Origin check has unit coverage only and its browser suite is red | PR #2627 adds src/lib/api-csrf.ts, which rejects a state-changing API request whose Origin does not match the addressed host. No browser or Playwright journey exercised it; Production UI (3) is red on that PR and CI triage classifies it not baselined, so the main comparison says nothing either way. A same-origin flow arriving with an Origin that neither Host nor X-Forwarded-Host reflects - a rewriting proxy in front of Railway - would now be refused where it previously passed. Must not merge until that browser suite is green or the failure is shown not to belong to the change. Recorded 2026-09-04. | session 2026-09-04 | 2026-09-04 | -| #KN2KP2 | P2 | issue | The Sources sub-route exclusion list is hand-maintained, so a new /sources route silently loses its search composer | Derive the reserved suffixes in src/lib/search-shell-props.ts from modeSecondaryNavigationRegistry.sources, or add a contract test asserting every registered sources nav route keeps searchComposerVisible. Why: that function decides a /sources/ path is a source record page by testing it against a literal list (["search","topics","publishers","method"]), and an unlisted route is classified a detail page, which suppresses the shared composer with nothing failing. Context: adding /sources/search in the Sources mode-home PR required hand-adding it to that list; missing it would have shipped a catalogue with no way to search it. Same failure shape as the phoneModeGroups omission the same PR fixed - a second hand-maintained list of routes that no gate compares against the registry. Confidence: high, read directly at src/lib/search-shell-props.ts:97-105. Owner: frontend. | session 2026-09-02, Sources mode-home PR (claude/sources-mode-dropdown-home-mzw4f5) | 2026-09-02 | | #Y183KM | P2 | issue | Generated Supabase types were rebuilt from schema.sql, not the live database, and the escapes that hid drift are gone | PR #2629 (audit L118) adds five tables and nine callable RPCs to src/lib/supabase/database.types.ts, written by hand from supabase/schema.sql because the Supabase CLI is absent and the container Postgres has no pgvector. It also removes the as-never and as-unknown-as escapes at src/lib/ingestion-mutation-safety.ts:186,257 and scripts/check-drift.ts:537. If the live schema has drifted from schema.sql, a drifted column now surfaces as a runtime error rather than being swallowed - the safer direction, but a real behaviour change under drift. Only the post-merge live-drift workflow, with check:drift and check:migration-history both green, confirms the file matches production. Separately, document_corpus_access_snapshots carries owner_id so the tenancy scan now tiers it as direct: the first API route that queries it needs owner scoping. Recorded 2026-09-04. | session 2026-09-04 | 2026-09-04 | | #8KFQ3X | P2 | issue | 50 of the 51 committed WA MHA form PDFs cannot be opened without a password, so no text-extraction or OCR path can reach their content — measured, not inferred | Measured 2026-09-02 with PyMuPDF 1.28.0 (MuPDF 1.29.0), the same library worker/python/extract_pdf_assets.py uses, against the committed bytes in public/forms-pdf/. RESULT: fitz.open() reports needs_pass=1 and is_encrypted=True for 50 of 51 files; page_count is 0, doc.authenticate('') returns 0 (the empty user password is REJECTED), and loading page 0 raises ValueError('document closed or encrypted'). MuPDF additionally logs 'corrupt object stream' because the streams stay encrypted. form-12a.pdf is the sole exception: it opens, has no /Encrypt, and yields 3274 characters of first-page text. The encryption dictionary on a representative locked file (form-10a.pdf) reads /Filter/Standard /V 4 /R 4 /Length 128 /P -1084 with a non-empty /U string. WHY THIS IS RECORDED: a concurrent review comment on PR #2544 asserted the opposite — that 'PyMuPDF does not enforce /P permission bits' and '-1084 restricts permissions without requiring a user password to open', so 'these files open normally and yield their text layer'. That is false for these specific files, and the measurement above is the disproof. The distinction matters because /P alone would indeed not block opening; these files also carry a non-empty user password, which does. CONSEQUENCE: any ingestion of these assets fails at the open call, before should_ocr_page() is ever consulted, so the OCR fallback cannot rescue them; whether the JavaScript fallback in src/lib/extractors/document.ts behaves differently was NOT measured. It also confirms the passwordProtected badge is correct for those 50 and was wrong only for form-12a.pdf, which is exactly what PR #2531 fixed. NEXT STEP: decide whether these forms are ever intended to be indexed. If yes, they need an unlocked source from the WA Chief Psychiatrist rather than a code change, because no extractor setting can defeat a user password. If no, record that decision so a future ingestion attempt is not planned against them. | Direct measurement of public/forms-pdf/*.pdf with PyMuPDF, 2026-09-02; disputes a review comment on PR #2544 | 2026-09-02 | | #Q33JV6 | P2 | issue | Crisis-line re-verification cadence is stated as six months in the new record and twelve months in the audit | docs/care-plan/crisis-lines-verification.md proposes a six-monthly cadence with no repository precedent; the 2026-09-02 audit finding L4 proposed twelve. The fixtures.ts header comment no longer states an interval at all, so the document is the single source - but the interval itself is unsettled and is a clinical judgement for the owner. Also unresolved: the Lifeline 13 11 14 and 13YARN 13 92 76 numbers in src/lib/caring-contacts/message-rules.ts:117 carry no source and no verification date anywhere in the repository, and nothing ages them. Raised during audit remediation P10/P21 (2026-09-04). | session 2026-09-04 | 2026-09-04 | @@ -156,14 +139,11 @@ removed after current-main verification; it is not missing recommended work. | #7ZA9S9 | P2 | issue | Formulation strength badges take the last number in a listed range, so a multi-strength product badges one arbitrary strength | PREMISE VERIFIED ON MAIN, not assumed. PR #2580 merged as 60eca45 on 2026-09-03, and origin/main now carries H1's fix: src/lib/medication-badges.ts:120 reads matchAll(/(\d+(?:\.\d+)?)\s*mg\b/gi), so the decimal case (varenicline "Tablets (0.5 mg, 1 mg)" formerly badging as "5 mg", a tenfold misread) and the concentration case are closed. This row is the remainder H1 deliberately did not touch, which #2580's own reviewer flagged as pre-existing rather than introduced. H1 is an audit finding in docs/audit/full-repository-audit-2026-09-02.md, not a row in this ledger, so nothing here closes it; it is recorded as landed only so this row's premise is checkable. THE DEFECT: morphine's "Tablets IR (10, 20 mg)" badges as "20 mg tablet". The number is a real strength of a real product, so this is not H1's tenfold misread, but the choice of which strength to show is arbitrary and a clinician glancing at the badge is given no signal that 10 mg also exists. SCOPE NOT YET MEASURED: only the morphine case is confirmed. First step is to count how many of the 328 catalogue records state a comma-separated or hyphenated strength list in their formulation text. FIX candidates, in preference order: (a) render the full range when a formulation states more than one strength, so the badge reads the same as the source, (b) suppress the badge for multi-strength products, matching the conservative choice H1 already made for concentration-only and combination products. Do NOT simply switch the regex from the last match to the first without deciding the question, since that is equally arbitrary in the other direction. GUARD: the corpus-wide invariant that a badged number must be a whole strength token and not a concentration is now on main in tests/medication-badges.test.ts, arriving with #2580. It constrains any change here but does NOT by itself pin which of several listed strengths is chosen, so this work needs its own focused assertion on the multi-strength shape. | Reviewer note on PR #2580 (audit fixes P1, finding H1), recorded 2026-09-03; the same PR body carries it as a suggested follow-up finding. Codex's P2 review of PR #2585 correctly caught that this row originally asserted H1's fix as present while it was still unmerged, and that it cited a guard main did not yet have. Both were re-verified against origin/main after #2580 merged as 60eca45, and the row now states the evidence rather than the assumption. | 2026-09-03 | | #NHGFXR | P2 | issue | Fourth occurrence of the shared-shell testid duplication race, now with a mechanism: CaringContactsShell's streamed/placed duplicate is only partly guarded | PR #2600 (claude/clinical-guide-redesign-ir5zcj), CI run 33868074584 job 101007656433 (Production UI shard 2): tests/ui-caring-contacts-workspace.spec.ts:1992 failed with 'strict mode violation: getByTestId(caring-contacts-phone-dock) resolved to 2 elements', identical markup, one via .first() one via the role/name locator. NOT PR #2600's fault: its diff (public/llms.txt, src/lib/brand.ts, ClinicalSidebar.tsx, master-search-header.tsx, ClinicalDashboard.tsx, calculators/mockups, globals.css, and matching tests) touches nothing under src/app/caring-contacts/** or src/components/caring-contacts/**. Source check: data-testid=caring-contacts-phone-dock has exactly one render site, shell.tsx:492 (nav aria-label Phone workspace) -- no second render path exists. STRONGER EVIDENCE THAN THE PRIOR TWO OCCURRENCES (service-actions-trigger on PR #2536/ui-tools.spec.ts, sources-topics-main on PR #2591/ui-sources.spec.ts:88, neither root-caused): this test file already documents and partially guards the exact mechanism. openWorkspace() (ui-caring-contacts-workspace.spec.ts:315-334) navigates with waitUntil:'load' then has a settle-wait comment: 'React streams the segment under loading.tsx's Suspense boundary into a hidden holder before moving it into place, so a production page sampled too early carries a second, inert copy of the whole shell. Settle on exactly one before measuring anything' -- followed by awaiting getByTestId('caring-contacts-rail') to reach count 1. Every /caring-contacts/* route dynamic-imports CaringContactsShell via next/dynamic (confirmed in page.tsx, patients/page.tsx, team/page.tsx, templates/page.tsx, schedule/page.tsx, reports/page.tsx, guidance/page.tsx, patients/[patientId]/page.tsx, templates/[pathwayId]/page.tsx), which is exactly the next/dynamic-under-Suspense shape session 2026-09-02's inbox note (id 5ad9c07e, re caring-contacts-guidance on PR #2536) hypothesized but did not verify. HYPOTHESIS, STILL NOT LIVE-VERIFIED: the settle-wait only polls caring-contacts-rail to count 1; it does not also wait for caring-contacts-phone-dock (or other shell-internal testids) to settle. Rail and dock are siblings inside the same streamed shell subtree, so Playwright's retrying assertion can resolve rail to 1 at a moment when dock, reconciled on a different tick, is still doubled -- which would explain a failure at line 1992 despite the openWorkspace() guard having already passed for that same navigation. Could not reproduce live in this session: this sandbox's egress policy returns 403 for cdn.playwright.dev (confirmed via the proxy README and status endpoint), so the Playwright chromium install command could not fetch the pinned v1234 build and no Playwright browser run was possible here -- this is a policy denial, not something to route around, so it was reported rather than retried. NEXT STEP for whoever picks this up: with a working browser, extend openWorkspace()'s settle-wait to also poll getByTestId('caring-contacts-phone-dock') for count 1 (mirroring the existing rail wait) before any per-width assertion, or generalize the wait to every shell-internal landmark the test suite queries; then re-run tests/ui-caring-contacts-workspace.spec.ts several times under load to confirm the race clears. If confirmed, the same generalized settle-wait likely explains the caring-contacts-guidance occurrence (PR #2536) and is worth checking against sources-topics-main and service-actions-trigger too, since both of those pages may have their own dynamic-import/Suspense-streamed shells with no equivalent settle-wait at all. | session investigating PR #2600 CI failure, 2026-09-04 | 2026-09-04 | | #57QDCS | P3 | issue | Caring Contacts activation wizard intermittently renders two page roots after a reload, tripping strict mode | Reproduce and fix the duplicate page root, or add a narrower guard; do not quarantine on one reproduction (flake policy needs three on the same SHA). Why: tests/ui-caring-contacts-activation.spec.ts:186 ('keeps a typed draft across a page reload') failed once in a full verify:ui run with 'strict mode violation: getByTestId(caring-contacts-plan-wizard) resolved to 2 elements', the second carrying different consent copy - the persistent hidden streamed S: clone signature that docs/search-chrome-behaviour.md invariant 17 describes for duplicate page-root data-testids. Context: seen 2026-09-02 in the chromium-caring-contacts-seeded project during verify:ui on an unrelated Sources branch (649 passed, this 1 failed); the same spec then passed 4/4 on an isolated re-run, and the branch under test touches nothing reachable from /caring-contacts, which lives outside the (search-app) shell. So it is intermittent and pre-existing, not diff-induced. Confidence: high that it is unrelated to that branch, low on root cause - one reproduction, trace captured at test-results/ui-caring-contacts-activat-cc88a--draft-across-a-page-reload-chromium-caring-contacts-seeded/trace.zip but not analysed. Owner: Caring Contacts. Depends on: nothing. | session 2026-09-02, observed during verify:ui on claude/sources-mode-dropdown-home-mzw4f5 | 2026-09-02 | -| #JAEKM4 | P2 | issue | Shared-shell testid duplicates under Playwright strict mode: service-actions-trigger (PR #2536) and sources-topics-main (PR #2591) both resolve to 2 elements, one nested inside GlobalSearchShell's mobile-composer-reserve-pad wrapper and one outside it | Recurring, reproducible 'Production UI critical' failure, not a random flake: strict-mode violation on getByTestId resolving to 2 elements — one aka mobile-composer-reserve-pad.getByTestId(x), one aka getByTestId(x).nth(1). Seen on tests/ui-tools.spec.ts (service-actions-trigger, PR #2536 precedent) and tests/ui-sources.spec.ts:88 (sources-topics-main, PR #2591, run 33868983554). Confirmed unrelated to either PR's own diff (files touched by those PRs don't include global-search-shell.tsx, sources-browse-client.tsx, or mode-home-template.tsx). Root cause not yet isolated — likely a hydration-timing or route-transition double-render inside the GlobalSearchShell/PageSecondaryNavigation tree. GlobalSearchShell is a 'one owner' contract component (AGENTS.md Search chrome behaviour) — a fix needs docs/search-chrome-behaviour.md read first and its own focused PR + npm run verify:phone-chrome, not a rushed patch riding an unrelated PR. | session 2026-09-04 | 2026-09-04 | | #H3PXP0 | P3 | issue | A stored source_status of "outdated" on a medication, service or differential record can never be cleared — nothing writes that column back | PR #2536 deliberately preserves a stored "outdated" through read-time re-derivation, because supersession is a recorded clinical judgement that age can neither establish nor refute. That is correct, but it makes the value permanent: verified 2026-09-02 that no code path writes source_status back on medication_records, clinical_registry_records or differential_records — the only writers are the insert-time recordToRow/presentationToRow/diagnosisToRow paths, which derive from source text and can never emit "outdated", and grep for '"outdated"' across src, scripts, worker and supabase finds no update or upsert against any of those three tables. So a record that is legitimately re-sourced stays red forever. Stuck-red is the safe direction, but it is stuck, and it means the badge stops tracking reality. A precedent already exists for documents and is the shape to copy: supabase/migrations/20260711164441_domain_1_source_review_lifecycle.sql defines record_source_review(), which writes an immutable source_review_events row and then sets documents.metadata.document_status to "outdated" for rejected/decommissioned/superseded and back to "current"/"review_due" otherwise (lines 41-122). NEXT STEP: an equivalent recorded-supersession flow for the three record tables that can both set and clear the column, evidence-bearing in the same way, so clearing is a recorded decision rather than an age calculation. | session 2026-09-02 — follow-up from PRs #2538/#2536/#2531 | 2026-09-02 | | #FE8BFA | P2 | issue | Ward Flow test fixture asserts a shape nobody checks: as unknown as BedRelease hides 9 discrepancies | tests/ward-release-band-day-boundary.test.ts:34 builds a BedRelease with 'as unknown as', which suppresses every check. BedRelease requires 11 fields; the fixture supplies 8. Six required are absent (waitingOn, blocker, blockedBy, preparing, preparationNote, confirmedBy) and three are phantom (blocked, blockReason, basis). blocker is absent while blockReason is phantom, so a plausible-looking name stands where the real field should be, and blocker is what the blocked-discharges breakdown counts. Verified independently by three chats at master 268fcd6a8. | Ward Builder Three closing sweep, measured at adcd8bcb5 | 2026-09-02 | | #2NRB8V | P3 | issue | Three unscoped getByTestId('caring-contacts-guidance') assertions can hit a strict-mode violation while the lazily-imported shell is still being placed, reddening unrelated PRs | OBSERVED ONCE, then cleared by a single re-run. CI run 33690849576 attempt 1, job 'Production UI (2)', head 22fd4da9e of PR #2536: tests/ui-caring-contacts-workspace.spec.ts:2670 failed with 'strict mode violation: getByTestId("caring-contacts-guidance") resolved to 2 elements', the two being an unscoped match and getByRole('main').getByTestId(...), with identical class attributes. 212 other tests in the shard passed. Attempt 2 of the same job on the same commit passed, so the condition is intermittent rather than a standing break. classify-playwright-failures.mjs reported 'needs investigation' and the flake ledger is empty, so nothing had recorded it. NOT PR #2536's: that diff is twelve medication and documentation files and touches no Caring Contacts code; the same job passed on its previous head 957a0387. WHAT THE SOURCE SHOWS: data-testid='caring-contacts-guidance' is rendered in exactly one place, ProgrammeGuidance at src/components/caring-contacts/workspace/programme-guidance.tsx:63; src/app/caring-contacts/guidance/page.tsx renders it once, and CaringContactsShell interpolates {children} once (shell.tsx:392). So the second node is not a second render site in the source. HYPOTHESIS, NOT VERIFIED — I did not reproduce it: that page loads its shell through next/dynamic, and the test navigates with waitUntil:'load', which resolves before React finishes relocating out-of-order streamed content. During that window the streamed copy and the placed copy can both be in the DOM, which matches the two locators exactly. Anything that makes the runner slower widens the window, which is consistent with a first-attempt failure clearing on re-run. NEXT STEP: scope the locator to the landmark rather than the document — page.getByRole('main').getByTestId('caring-contacts-guidance') — at all three unscoped sites (lines 2294, 2669 and 2803 as of origin/main 705c2f64a), or await the heading before resolving the locator. Confirm the hypothesis first by reading the retained trace (artifact production-ui-diagnostics-33690849576-shard2) rather than changing the test on this reasoning alone. Do NOT quarantine it: one reproduction is below the repository's three-on-the-same-SHA bar, and the fix is a locator change rather than a suppression. | CI run 33690849576 attempts 1 and 2 on PR #2536, 2026-09-02 | 2026-09-02 | | #ZV7H8Q | P2 | issue | Services and differentials governance still returns the frozen source_status column verbatim, and derives it with a substring test that matches its own negation | Two defects, one fix. (a) FROZEN COLUMN: src/lib/registry-records.ts:112 rowGovernance() returns registrySourceStatus(row.source_status) (line 119) and src/lib/differential-records.ts:105 rowGovernance() returns differentialSourceStatus(row.source_status) (line 112). That column is written once at insert time by recordToRow/presentationToRow/diagnosisToRow and never ages, so a row written while its sources were fresh keeps claiming "current" indefinitely — the exact defect PR #2536 fixed for medications by re-deriving on the read path (see the rowGovernance doc comment on the #2536 head, which spells out why the stored column must stop being the answer). Both feed clinician-facing governance: /api/registry/records, /api/differentials, and the services and differentials detail pages. (b) SUBSTRING: src/lib/registry-records.ts:42 uses status.includes("checked") and src/lib/differential-records.ts:37 uses reviewStatus.includes("checked") \|\| includes("current") — both substrings also match "not checked" and "unchecked", which would grade an explicitly unchecked source as "current". Latent today, not live: verified 2026-09-02 that data/services-snapshot.json (219 records) carries only "Added/checked via internet review" or "Extracted from uploaded deep research only", and neither snapshot contains any negative form. src/lib/medication-records.ts:30 already carries the veto regex /\b(?:not\s+checked\|unchecked\|unverified)\b/i. NEXT STEP: mirror #2536's read-time derivation and that negative-form veto into both modules; keep #2536's rule that a stored "outdated" survives re-derivation. | session 2026-09-02 — follow-up from PRs #2538/#2536/#2531 | 2026-09-02 | -| #ZKR5YK | P3 | rec | The mode pill sends every mode to the shared home, including the four that own a real home of their own | Owner ruling on whether selecting a mode that owns a functional home should navigate to that home rather than /?mode=. Why: changeMode in global-search-shell.tsx always builds appModeSelectionHref, so picking Sources, Medication, Favourites or Documents lands on the generic shared home, not the surface that mode actually owns; only Tools is special-cased to push its canonical href. Consistent across modes, which is why it was deliberately left unchanged when /sources gained a mode home on 2026-09-02, but it means that home is reachable only by direct link, the mode nav or the Tools launcher. Context: overlaps open PR #2556, a full decision brief on whether the mode concept should survive at all - if that brief is actioned this row is subsumed and should be closed rather than worked. Confidence: high on the mechanism (read at global-search-shell.tsx:717-764), low that this is the right change to make before the brief is decided. Owner: product. | session 2026-09-02, Sources mode-home PR (claude/sources-mode-dropdown-home-mzw4f5); overlaps PR #2556 | 2026-09-02 | | #Q6WD1M | P1 | issue | Ward Flow places patients with no eligibility check in the reducer at all | RECONSTRUCTED 2026-09-06. This row was recorded on 2026-09-02 with an EMPTY detail field, so as written nobody could act on it. What follows was rebuilt from the code and its history. ORIGINAL FINDING: the coordinator's placement path in src/components/ward-management/ward-flow-reducer.ts applied no eligibility judgement gate at all, while ACCEPT_REFERRAL at the other end rejected on the first failing gate of any kind. One end refused everything, the other refused nothing. THAT ASYMMETRY IS CLOSED. PR #2571 (commit b401b65cc, 2026-09-03, the day after this row was raised) introduced SUITABILITY_GATES and eligibilityRefusal(), now called on three paths: REFER_TO_UNITS (~line 1075), ACCEPT_REFERRAL (~1158) and the coordinator pull (~1365). A failing gate inside SUITABILITY_GATES refuses unless a reason from OVERRIDE_REASONS is recorded; the physical gates (allocatable_bed, capacity_freshness, specialling) refuse outright and are deliberately not overridable. WHY THIS ROW MUST STAY OPEN ANYWAY. The reducer's own comment above the pull-path call (~1355-1363) warns that a red there proves nothing on its own: the specialling refusal immediately above fires for a patient who fails both checks, and reading that as 'the engine now enforces eligibility' NEARLY CLOSED THIS FINDING FALSELY once already. It asks for proof on a pair whose ONLY failing gate is a judgement gate. That proof does not exist today. The only reducer-level suitability assertion is tests/ward-referral-reducer.test.ts:451, which exercises ACCEPT_REFERRAL rather than the pull path, on fixture RF-001 where BOTH age and security fail. tests/ward-eligibility.test.ts does cover cohort, but tests the pure eligibility() function, not the state transition. NEXT ACTION: add one reducer-level test on the coordinator pull path using a movement/unit pair whose only failing gate is cohort, asserting the transition is refused and the rejection contains 'failed gate cohort:'. Then close. Do not close on the wiring alone. GRADING NOTE FOR THE OWNER: the live hazard this P1 named is fixed; what remains is a missing proof. That arguably makes it P2 rather than P1, but a clinical-safety regrade is the owner's call, so the grade is left as recorded and flagged here. | session 2026-09-02 | 2026-09-02 | -| #DFMVMN | P3 | task | Medication considerations panel formats its two not-assessed sentences differently — the contraindication one uses a bare comma join, the advisory one a serial-and helper | In src/components/clinical-dashboard/medication-considerations.tsx as landed in PR #2538 (head ef7c55ab), the advisory not-assessed sentence renders its input list through formatInputList(), a helper added in that PR which produces "eGFR", "eGFR and QTc", "eGFR, QTc and hepatic status". The contraindication sentence one InlineNotice above still renders result.unassessed.join(", "). With a single missing input the two read identically; with two or more they read inconsistently, and the bare join is the case formatInputList's own doc comment argues against, because the sentence continues into a relative clause ("which this profile does not include") that a comma-only list runs straight into. NEXT STEP: apply formatInputList() to the contraindication sentence too. It is identity for a single input (it returns items.join("") when length <= 1), so no existing assertion in tests/medication-interaction-surfaces.dom.test.tsx breaks; add a two-input case for the contraindication tier so the shared formatting is pinned rather than incidental. | session 2026-09-02 — follow-up from PRs #2538/#2536/#2531 | 2026-09-02 | | #YJGWDZ | P3 | issue | Forms PDF manifest records only passwordProtected, folding two separate facts together — the committed PDFs permit printing and form-filling but block modification, text extraction and assembly | Replaces cancelled request e3133c1b, which asserted a false OCR-routing mechanism (see that cancellation's reason). The permission-bits finding itself stands and is restated here without the incorrect claim. data/forms-pdf-manifest.json records one boolean per asset, passwordProtected, generated by scripts/build-forms-pdf-manifest.mjs (PR #2531, now on main). Verified 2026-09-02 against the committed bytes: 50 of the 51 files in public/forms-pdf/ carry /P -1084 in their encryption dictionary; form-12a.pdf is the one exception, with no /P entry found by a strings scan and worth confirming separately. -1084 is 0xFFFFFBC4, which per the PDF permissions bit table permits print (bit 3) and fill-in form fields (bit 9) while blocking modify contents (bit 4), copy/extract text (bit 5), modify annotations (bit 6) and assemble document (bit 11). That is a materially different clinician-facing fact from 'requires a password to open': these forms can be printed and filled but not edited or copied from, and the single badge does not say so. NO INGESTION CLAIM IS MADE HERE. The OCR path is governed by should_ocr_page() in worker/python/extract_pdf_assets.py, which reads only extracted text length and image coverage and never consults permission bits; the extractor has no password handling, so what actually happens to these files on ingestion is untested and would need measuring, not inferring. NEXT STEP: derive an editingRestricted fact (or the decoded permission bits) alongside passwordProtected in scripts/build-forms-pdf-manifest.mjs, regenerate with that script rather than by hand, surface it as its own line in src/components/forms/form-detail-page.tsx instead of folding two facts into one badge, and extend tests/forms.test.ts to pin both fields. | PR #2531 bytes read + Codex review finding on PR #2544, 2026-09-02 | 2026-09-02 | | #78MRNJ | P3 | issue | Unbounded recursive delete in tests/ward-flow-chat-control.test.ts has no retry guard | An rmSync on a scratch path with recursive:true and no bounded retry, flagged by tests/test-runner-safety.test.ts. The same file already uses the retry-bounded shape elsewhere, so the fix is to copy the existing local template. Matters more than usual on a machine that has been dropping shell commands. Reproduced at fb17db7b1. | Ward Builder One closing sweep 2026-09-02 | 2026-09-02 | | #EP65NR | P2 | issue | Caring Contacts: the workspace's front door is still a placeholder while every screen behind it is built | /caring-contacts ("Today") is the route the live tools catalogue links to and the first screen anyone lands on, and src/app/caring-contacts/page.tsx renders one

reading "What this screen will show" plus a paragraph in future tense. No data at all. Meanwhile Patients, Patient overview, Schedule, Templates, Template detail, Team, Guidance, Reports and the activation wizard are all fully built, and the domain layer a Today screen would need already exists (schedule-view.ts for contacts due, team-workload.ts for unclaimed work and coverage, patients-directory-filter.ts for plans needing a decision). So the workspace reads as unfinished on the one screen a first-time reader judges it by, and the impression is the opposite of the truth. NOT built during the 2026-09-02 design audit, deliberately: what a clinician sees FIRST about live suicide-prevention plans is a clinical design decision about triage order and emphasis, not a layout question, and it wants the owner's call before any agent picks the content. Two adjacent things to know before scheduling it: the draft plan docs/superpowers/plans/2026-09-02-caring-contact-phase-3-demonstrable.md covers populated-data work and lists the missing contact-detail and system-states routes as an explicit Phase 2 question rather than absorbing them; and patient-visible copy is never authored by an agent, though a Today screen needs none - every string on it is clinician-facing. | Multi-agent Caring Contacts design audit, 2026-09-02; verified against src/app/caring-contacts/page.tsx on branch claude/caring-contacts-design-audit-fcay0l | 2026-09-02 | @@ -172,14 +152,10 @@ removed after current-main verification; it is not missing recommended work. | #4TXR6Z | P2 | issue | The Lighthouse mobile /documents/search cell is bimodal across runners and its two modes straddle the tolerance, so the gate fails intermittently on whichever PR draws a slow runner | Observed across four graded runs of PR #2536 on the pinned Chromium (chromium-1234, HeadlessChrome/151), baseline LCP 2282ms with a tolerance of +20% AND +100ms. BREACHED: head 30afc398 measured 2748ms (+466ms, +20.4%) in run 33628667758 attempt 1; head cda377fb measured 2753ms (+471ms, +20.6%) in run 33681754629. PASSED: the attempt-2 re-run of 30afc398 (number not read); head 957a0387 measured 2323ms (+41ms) in run 33684986161, which reported 'Every graded route is within tolerance of the committed baseline.' The gate samples each cell three times and calls a regression at 2 of 3, so it is already noise-tolerant within a run; the split here is BETWEEN runs, which that design does not cover. A 2323 vs 2750 spread on identical browser, identical route and near-identical code is about 18%, and the +20%/+100ms tolerance sits inside that gap, so the same commit can grade either way depending on the runner it lands on. NOT ESTABLISHED: which mode is the true one, whether the slow mode correlates with runner class, concurrent jobs on the host, or the isolated production build's cold start, and whether main alone reproduces it. main was never measured directly; the job is path-scoped and rarely runs on main pushes. WHY IT MATTERS: an intermittent red on a required check trains reviewers to re-run rather than read, and it lands on PRs whose diff cannot reach the failing page — #2536 changes only medication files, while src/app/(search-app)/documents/search/page.tsx imports one symbol (Metadata from next) and nothing outside medication-named files calls /api/medications. NEXT STEP: characterise the cell before changing anything. Run the dispatch-only 'Refresh Lighthouse baseline' job (workflow_dispatch on ci.yml with refresh_lighthouse_baseline=true) two or three times against main, which measures the same routes on the pinned Chromium and commits nothing, and compare the spread. If the slow mode reproduces on main, attribute it before touching lighthouse-budget.json; if it does not, the fix is in how the cell is measured, not in the baseline. Do not refresh the baseline reactively to clear a red PR. | PR #2536 Lighthouse runs 33628667758, 33681754629 and 33684986161, 2026-09-02 | 2026-09-02 | | #Y2E4DX | P3 | issue | isProfileEmpty treats a recorded hepatic "none" as no information, so an affirmative "no hepatic impairment" profile still renders the empty state instead of a verdict | src/lib/medication-patient-alerts.ts:164-180 isProfileEmpty() short-circuits on hepatic with 'if (profile.hepatic && profile.hepatic !== "none") return false;' (line 176), so a profile whose only entry is hepatic: "none" — an affirmative clinical answer, not a blank — is still reported empty. Verified 2026-09-02 on origin/main and unchanged by PR #2538. The engine disagrees with it: evaluateRow() at line 269-276 treats "none" as an answer, not a gap, using 'if (!hepatic) missingGates.push("hepatic status")' and then testing membership only when hepatic !== "none" — so an entered "none" clears the hepatic gate. The consequence is that both consumers of isEmpty (MedicationConsiderations and the interactions block in src/components/clinical-dashboard/medication-considerations.tsx) render "Enter patient details above" and produce no verdict at all for that profile. Both surfaces therefore degrade conservatively — nothing is claimed that should not be — so this is a cosmetic inconsistency with the engine rather than a safety defect. NEXT STEP: its own change with its own tests, not a rider on other work: making a one-field profile produce evaluated output is a behaviour change, and the DOM tests in tests/medication-interaction-surfaces.dom.test.tsx and tests/patient-profile-panel.dom.test.tsx pin the current empty-state text. | session 2026-09-02 — follow-up from PRs #2538/#2536/#2531 | 2026-09-02 | | #5RWYHR | P2 | task | Two committed ED browser journeys have never once passed and are unverified on master | tests/ui-ward-roles.spec.ts carries two Emergency Department journeys added at 9af65681f and repaired at ed701752d, both now folded to master. Their first and only run failed for their own reasons: one used a test id missing its inbox- segment, the other clicked an aria-disabled control which Playwright refuses to click, so it timed out at 45s rather than failing. Both fixes are committed and NEITHER HAS BEEN RUN SINCE. Needs one Playwright window to confirm green or red. | Ward Builder Three closing report, measured at adcd8bcb5 | 2026-09-02 | -| #NADB8P | P3 | issue | on_call_entries.linked_document_ids is uuid[], so Postgres cannot enforce a foreign key to documents | supabase/migrations/20260904120000_on_call_entries.sql:25 declares linked_document_ids uuid[] not null default '{}'. Postgres cannot place a foreign key on an array element, so nothing at the database layer stops an entry referencing a deleted or non-existent document. Behaviour is conservative: the viewer resolves each id and simply omits links it cannot resolve, so a stale id degrades to a missing link rather than a wrong or broken one. No symptom has been observed. Recommendation: LEAVE AS-IS. Enforcing it properly means restructuring the link into a join table (on_call_entry_documents) with a real foreign key and a migration to backfill, which is disproportionate to a defect that fails safely. Revisit only if entries ever start displaying stale links in practice, or if the join table is wanted for another reason (per-link ordering, labels, or reverse lookup from a document to the entries citing it). | Session 2026-09-05, owner-reported loose end after the On Call mode build | 2026-09-05 | -| #42M061 | P2 | issue | Tailwind never scans the mockups tree, so any arbitrary utility written only inside a mockup component is silently never emitted | `src/app/globals.css` opens with `@source not "./mockups"` and `@source not "../components/**/*mockup*"`, and `src/app/mockups/mockups.css` re-emits utilities from `../` and `../../components` without covering the excluded component files. The consequence is silent: an arbitrary utility written only inside `src/components/caring-contacts/mockups/**` lands on the element as a class and no CSS rule is ever generated for it, so it has no effect and nothing fails. Confirmed against the built stylesheet during the Caring Contacts design audit (PR #2574): `min-h-[var(--space-10)]` on the prototype's phone dock has no rule at all, which is why that dock measures 41px rather than the 64px its code reads as; the pre-existing `min-w-[46rem]` continuity strip and `min-w-[42rem]` team table widths have likewise never applied. It also caused a real CI failure in that PR: a derived `pb-[calc(...)]` phone reserve computed to 0px, the fixed dock covered the end of every long phone page, and two 390px journeys in `ui-caring-contact-mockup.spec.ts` timed out; bisected in a scratch worktree rather than guessed. That instance is fixed by using utilities the tree can emit, but the class of defect remains open for every mockup component. Fixing it properly means widening Tailwind's scan against a deliberate exclusion, which has bundle-budget consequences and needs its own measured decision; a narrower alternative is a static gate that fails when an arbitrary-value utility appears in an unscanned mockup path. | Caring Contacts design audit, PR #2574 | 2026-09-04 | | #WM4DNW | P2 | issue | differential-records.ts asserts validation_status "locally_reviewed" from a literal, over a snapshot whose own governance says "Pending review" | src/lib/differential-records.ts:31 deriveGovernanceFromSnapshot() returns validation_status: "locally_reviewed" unconditionally (line 44), while data/differentials-snapshot.json governance.reviewStatus is "Pending review" (verified 2026-09-02) — the same call derives source_status: "review_due" from that string, so one field says pending and the other claims review happened. Both presentationToRow (line 55) and diagnosisToRow (line 78) write it into differential_records, and src/components/differentials/differential-detail-page.tsx:673-692 renders it to the clinician. src/lib/medication-records.ts:65-86 fixed exactly this and its comment states the reason: deriveTrust in src/lib/answer-render-policy.ts:146 accepts "locally_reviewed" as satisfying the authority gate for high-risk clinical claims (line 177), so the vocabulary must never be asserted from a literal. Verified caveat: deriveTrust reads documents.metadata.clinical_validation_status (src/lib/source-authority-registry.ts:410-424), not differential_records.validation_status, so the two share the vocabulary rather than one code path — the direct harm is an unearned governance badge on the differentials surface, and the shared vocabulary is why the medication sibling was fixed. NEXT STEP: derive it from recorded verification evidence as src/lib/registry-records.ts:47-48 does from verification.locallyVerified, or default to "unverified"; add a test pinning that a "Pending review" snapshot never produces "locally_reviewed". | session 2026-09-02 — follow-up from PRs #2538/#2536/#2531 | 2026-09-02 | | #XXH42K | P2 | issue | Third confirmed occurrence of the shared-shell duplicate-testid Playwright strict-mode bug, now in Caring Contacts workspace (caring-contacts-phone-dock, PR #2600) | Same error shape as the two occurrences already logged (see the outstanding-issue queued alongside PR #2613): getByTestId resolves to 2 elements for the same testid. This time: tests/ui-caring-contacts-workspace.spec.ts:1992, 'Template detail at 390px: the phone dock does not own navigation', getByTestId('caring-contacts-phone-dock') resolved to 2 identical