Audit database fixes: feedback supersede, index generation prune, MFA inside site-content RPCs
Requested by Josh · project thread
Summary
The 19 October 2026 repository audit's database fixes, combined into one draft at the owner's request as five separate migrations. Merge only inside the owner's approved database-change window, and only after the owner's explicit word-for-word yes for this version of the PR. The owner's 17:44 UTC "Yes merge 3677" covered the earlier three-migration version only. Merging applies all five migrations to the live database within seconds.
Before: a "supported" tap on an answer blocked a later "Report a problem" on the same answer with a 409 (F-02). Every reindex left all earlier generations' artifact rows and image files in place, and the only pruner was a manual all-tenant job with no row ceiling (F-09). The site-content publish, retire and reconciliation-plan database functions checked the administrator role but not recent MFA, so an older administrator token could call them directly and skip the 15-minute check the route applies (review finding on #3675).
After: a problem report replaces an earlier "supported" verdict and reaches the review queue as a new, untriaged report. A "supported" tap never replaces a problem report. When a document's committed index generation moves, older generations for that one document are pruned within a small time and row budget. Any image that a remaining chunk, memory card or summary still names is kept, and so is any chunk that the summary, a remaining section or a remaining memory card still names. Image files that nothing still points at are queued for the existing storage janitor, which keeps each job's provenance. The three site-content functions now require the same recent multi-factor sign-in as the route.
How:
| Migration | Finding | Change |
|---|---|---|
20261019155000_answer_feedback_problem_supersedes_verified | F-02 | Column-scoped UPDATE grant to service_role on the report columns, plus a before-update guard trigger. The route tries one owner- and answer-hash-scoped conditional update on a unique conflict before returning 409. |
20261019155200_prune_superseded_index_generations_on_commit | F-09 | prune_superseded_document_index_generations and an AFTER UPDATE trigger on documents. Keeps the new, the replaced, the live-pointer, open-job and pending-candidate generations. At most 2000 rows per commit in batches of 250, a deadline before every batch, lock_timeout = 100ms, failures logged as a warning. The commit function and retrieval functions are unchanged. |
20261019170000_site_content_rpc_recent_administrator_assurance | #3675 review | Private helper applying the route's rule (aal2, administrator claim, a password/otp/totp/webauthn sign-in within 900 seconds), called after each function's existing administrator check. Bodies otherwise verbatim, grants restated unchanged. |
20261019180000_prune_keeps_array_cited_images | F-09 review (Codex, Gitar) | Replaces the prune function body only (same signature, owner, grants and trigger). Keeps any image a remaining chunk (image_ids), memory card or summary (source_image_ids) names and never queues its file. Prunes images last, after chunks, so an image cited only by rows pruned in the same pass goes in that pass. |
20261019183000_prune_keeps_array_cited_chunks | F-09 review (Codex) | Replaces the prune function body only again. Keeps any chunk the summary (source_chunk_ids), a remaining section (chunk_ids) or memory card (source_chunk_ids) names, so a summary from an earlier generation is never left pointing at deleted chunks. Chunks still prune after cards and sections. |
Outside the database: scripts/cleanup-storage.ts skips any queued image path a document_images row still references, and merges its outcome into the job's existing metadata (cleanupJobMetadata), so a prune job keeps source_document_id and pruned_generation_ids.
Areas touched: Data platform and access, Source intake and indexing, Answer engine
RAG impact: no retrieval behaviour change — retrieval functions, ranking and selection are untouched; the prune only removes rows of generations older than the replaced one, which retrieval already excludes because it reads committed generations only.
Clinical Governance Preflight
- Source-backed claims still require linked source verification before clinical use
- No patient-identifiable document workflow was introduced or expanded without explicit governance approval
- Supabase target remains
PsychSift Production(sjrfecxgysukkwxsowpy) - Service-role keys and private document access remain server-only
- Demo/synthetic content remains clearly separated from real clinical sources
- Source metadata, review status, and outdated/unknown-source behavior remain conservative
- Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed
Verification
- Checks run:
- SQL behaviour tests against a locally replayed database, each run on its own:
tests/sql/answer-feedback-supersede.sqlpassed.tests/sql/index-generation-prune.sqlprinted "INDEX_GENERATION_PRUNE_SQL_PASS" on e73460cc4. That run covers a slow prune under a 3 s statement timeout, a no-headroom commit, image citations through chunk, card and summary arrays, chunk citations through summary, section and card arrays, and same-pass pruning of an image only a pruned chunk cited and of a chunk only a pruned section cited. With the new chunk guard removed, it fails on the chunk-citation assertion.tests/sql/site-content-rpc-recent-administrator-assurance.sqlpassed 30 cases. - On e73460cc4:
check:migration-immutabilityreported "331 shipped migrations unchanged" andcheck:migration-rolepassed. The 30 migration, schema, drift, grant and prune unit test files passed, "Tests 597 passed (597)". - Earlier on this branch: function grants (137 OK), owner-scope and clinical-hazard-controls all passed,
tests/storage-cleanup-safety.test.tspassed, andnpm run test(full offline unit suite) passed: "Test Files 2012 passed (2012)", "Tests 32706 passed | 2 skipped (32708)". - New migrations sealed with the generator (new entries only, no reseal). The drift manifest was regenerated with
npm run drift:manifest.
- SQL behaviour tests against a locally replayed database, each run on its own:
Not run locally: browser suites (no UI change), live drift (needs the live database, runs after merge).
Risk and rollout
- Data deletion: F-09 permanently deletes artifact rows for older index generations, for each document, the first time that document's committed generation moves after merge. The new and the immediately replaced generation are always kept, so one-step rollback of a reindex still works. Image files are only queued for the existing janitor, not deleted inline.
- Residual risk: a single batch of 250 rows or fewer that overruns the second half of the caller's statement timeout can still cancel and roll back that one commit. The commit can be retried, and the next commit finishes the prune.
- Feedback: a superseding problem report gets a fresh id and timestamp, so it appears as a new report in the review queue.
- Site content: an administrator whose last multi-factor sign-in is older than 15 minutes now gets an error from a direct database call, matching the route.
- Rollback: ship a new migration that drops the trigger and helper functions. Rows already pruned are not recoverable except by reindexing that document.
- Provider or production effects: merging applies five migrations to the live database. The post-merge
live-driftworkflow must then pass bothcheck:driftandcheck:migration-history.
Spending approval (Supabase preview branch)
Recorded before this PR was opened. Supabase opens one preview database for this PR, which is billable and not covered by the Spend Cap.
- Purpose: preview replay of the migrations for this draft.
- Service: Supabase branching, one preview branch, default compute.
- Estimate: about US$0.32 per day. Total over the 14-day limit about US$4.50.
- Hard cap: US$5. Enforced by closing this PR (which removes the preview branch) if it is still unmerged on 2 November 2026. No extra branches or compute upgrades.
- Approval: Josh, 19 October 2026 17:04 UTC, project thread message "Go ahead with all recommendations" (recommendation A: one combined PR, capped at about US$5, closed if unmerged after two weeks).
- Actual usage and cost will be reported when the branch closes, where Supabase shows it.
Notes
- Audit report:
/mnt/project-files/reviews/2026-10-19-repository-audit.md(project files). - The stuck-jobs draft (F-20) is dropped because the owner kept the 16 October decision.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XpAQzPLSw4ncZyrFjoWBA3
Summary by CodeRabbit
-
New Features
- Problem reports can now replace a previously verified answer verdict for the same interaction and answer, while duplicate reports remain rejected.
- Publishing, retiring, or reconciling site content now requires recent, strong administrator authentication.
-
Improvements
- Superseded document index data is pruned in bounded batches while preserving content that is still referenced or in use.
- Cleanup avoids removing referenced image files, and cleanup failures won’t block document updates.