Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -966,7 +966,7 @@ due time, the note's id and that phone's endpoint in `alert_reminder_times`, nev

Administrator source intake also records admission events in `document_admission_events`.

`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_evidence_revisions`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `document_title_words`, `document_publication_approvals`, `document_corpus_access_state`, `document_corpus_access_snapshots`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `image_caption_cache`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `rag_visual_eval_cases`, `rag_visual_eval_runs`, `rag_answer_feedback`, `clinical_registry_records`, `clinical_registry_record_sources`, `clinical_quality_feedback_triage`, `clinical_quality_feedback_triage_events`, `medication_records`, `differential_records`, `source_review_events`, `user_favourites`, `user_favourite_sets`, `user_preferences`, `work_admin_paperwork`, `work_backups`, `work_booking_courses`, `work_course_bookings`, `work_hospitals`, `work_hospital_teams`, `work_role_grants`, `alert_reminder_times`, `alert_brief_sent`, `api_rate_limits`, `api_rate_limit_subjects`, `audit_logs`, `storage_cleanup_jobs`, `on_call_entries`, `cme_years`, `cme_requirements`, `cme_routines`, `cme_entries`, `cme_allocations`, `cme_evidence`, `cme_year_snapshots`, `cme_year_amendments`, `cme_plan_goals`, `cme_plan_goal_carries`, `cme_entry_goals`, `cme_training_periods`, `cme_training_milestones`, `cme_missed_sessions`, `cme_entry_drafts`, `calendar_feed_tokens`, `on_call_shift_imports`, `on_call_shifts`, `on_call_services`, `on_call_service_sites`, `on_call_service_members`, `on_call_service_invitations`, `on_call_service_entries`, `on_call_service_reports`, `on_call_service_orientation`, `site_content_publications`, `site_content_reconciliation_plans`, `site_content_public_records`, `site_content_sync_state`, `site_content_sync_events`, `site_content_sync_event_plans`, `site_content_sync_worker_invocations`, `site_content_releases`, `site_content_release_records`, `site_content_release_receipts`
`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_evidence_revisions`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `document_index_generation_snapshots`, `document_title_words`, `document_publication_approvals`, `document_corpus_access_state`, `document_corpus_access_snapshots`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `image_caption_cache`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `rag_visual_eval_cases`, `rag_visual_eval_runs`, `rag_answer_feedback`, `clinical_registry_records`, `clinical_registry_record_sources`, `clinical_quality_feedback_triage`, `clinical_quality_feedback_triage_events`, `medication_records`, `differential_records`, `source_review_events`, `user_favourites`, `user_favourite_sets`, `user_preferences`, `work_admin_paperwork`, `work_backups`, `work_booking_courses`, `work_course_bookings`, `work_hospitals`, `work_hospital_teams`, `work_role_grants`, `alert_reminder_times`, `alert_brief_sent`, `api_rate_limits`, `api_rate_limit_subjects`, `audit_logs`, `storage_cleanup_jobs`, `on_call_entries`, `cme_years`, `cme_requirements`, `cme_routines`, `cme_entries`, `cme_allocations`, `cme_evidence`, `cme_year_snapshots`, `cme_year_amendments`, `cme_plan_goals`, `cme_plan_goal_carries`, `cme_entry_goals`, `cme_training_periods`, `cme_training_milestones`, `cme_missed_sessions`, `cme_entry_drafts`, `calendar_feed_tokens`, `on_call_shift_imports`, `on_call_shifts`, `on_call_services`, `on_call_service_sites`, `on_call_service_members`, `on_call_service_invitations`, `on_call_service_entries`, `on_call_service_reports`, `on_call_service_orientation`, `site_content_publications`, `site_content_reconciliation_plans`, `site_content_public_records`, `site_content_sync_state`, `site_content_sync_events`, `site_content_sync_event_plans`, `site_content_sync_worker_invocations`, `site_content_releases`, `site_content_release_records`, `site_content_release_receipts`

Team modes (Roster, Admin, Teaching): `admin_leave_balances`, `admin_settings`, `extra_time_records`, `on_call_service_member_events`, `registrar_assessments`, `registrar_training_milestones`, `roster_assignments`, `roster_calendar_links`, `roster_change_agreements`, `roster_changes`, `roster_draft_assignments`, `roster_drafts`, `roster_leave`, `roster_member_roles`, `roster_offer_alert_outbox`, `roster_offer_alert_devices`, `roster_open_post_alerts`, `roster_open_post_keys`, `roster_open_shifts`, `roster_publication_protections`, `roster_publication_seen`, `roster_publications`, `roster_rotation_preferences`, `roster_rotation_rounds`, `roster_shift_codes`, `roster_staffing_needs`, `roster_swaps`, `roster_team_settings`, `roster_unavailability`, `teaching_attendance`, `teaching_audit_events`, `teaching_calendar_optins`, `teaching_checkin_claims`, `teaching_collection_sections`, `teaching_collections`, `teaching_display_links`, `teaching_feedback_answers`, `teaching_feedback_replied`, `teaching_group_members`, `teaching_groups`, `teaching_member_roles`, `teaching_notice_reads`, `teaching_notices`, `teaching_occurrences`, `teaching_readiness`, `teaching_resource_saves`, `teaching_resources`, `teaching_series`, `teaching_supervision_entries`, `teaching_supervision_notes`, `teaching_supervision_pairings`, `teaching_team_settings`, `teaching_week_adds`, `web_push_subscriptions`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "3a784e6b-ff5e-42be-897f-2492a3754aea",
"createdOn": "2026-10-21",
"action": "update",
"payload": {
"id": "#7D8WZ2",
"detail": "UPDATE 2026-10-21: tests/hostile-document-real-extractor.test.ts now runs generated hostile PDFs (password-protected, truncated, not-a-PDF, blank page, prompt-injected with forged evidence fences and hidden white text) through upload admission, the REAL Python reader, the real chunker, the evidence ranker and the fenced answer source block, offline (runs in the CI coverage job, which installs PyMuPDF). Still open: the live leg (Storage upload, worker ingestion with embeddings, vector retrieval, generated answer) is not run anywhere. Next action: run it on staging once the staging rebuild (#3113) is done and the owner lifts the re-index freeze, with the owner's spend approval for embeddings and answers. PRIOR DETAIL: Seven-class helper coverage exists in tests/hostile-document-ingestion-matrix.test.ts, but it uses an extraction mock and fabricated search results, so no uploaded hostile document is checked against real ingestion, retrieval and answer paths. Close only when each class runs end to end.",
"baseRowFingerprint": "349c4edbdc550bbad485d7460ac918adc67543c76f5977feaf68cc2cd38c49a7",
"targetUlid": "01M51JV9GC7D8WZ29KQKRSF5HP"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "b8553cec-2ce1-42f8-a5dc-694e031e996e",
"createdOn": "2026-10-21",
"action": "done",
"payload": {
"id": "#EG02B7",
"outcome": "Already fixed 2026-10-19 by PR #3677: migration 20261019155200_prune_superseded_index_generations_on_commit.sql adds a trigger on the generation pointer that prunes superseded generations at every commit and queues their orphaned image objects in storage_cleanup_jobs for the janitor. Verified on main 81ba5aefa by the Library pipeline hardening thread; no further migration needed.",
"baseRowFingerprint": "89a7d8c1ffc00de7c23fa1a527e63e4b943088799914ec02ce10d43dfeb2a88a",
"targetUlid": "01M51JV9J6EG02B7D44998GPSQ"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 2,
"id": "f7ff9913-697d-47b8-b3df-ef6fac9ae528",
"createdOn": "2026-10-21",
"action": "update",
"payload": {
"id": "#2F8K1V",
"detail": "UPDATE 2026-10-21: documents.metadata.extraction_provenance now also records the chunker versions of the committed chunks and the OCR engine (Tesseract) versions and count of OCR'd pages (worker/extraction-provenance.ts), on top of the reader version, PyMuPDF version and table strategy recorded since PR #3632. npm run extraction:provenance (read-only; needs owner confirmation before a live run) lists documents by reader, chunker or OCR version, and npm run reindex:rollback -- --chunker-version already scopes a rollback. No migration needed. Remaining: documents indexed before these keys existed report 'not recorded' until their next index, and re-extraction by version is still an operator step. Close when the owner accepts that. PRIOR DETAIL: Verified offline: only a coarse indexing_version ('rag-deep-memory-v1') is stored; no extractor/OCR/table-strategy/chunker provenance exists in schema or worker.",
"baseRowFingerprint": "e95fa094042f40eebb3ec86a2355667d537b124f4e88aee1d638f6ad06e2b3ea",
"targetUlid": "01M3CHC29Y2F8K1VF3T2SYJQCG"
}
}
13 changes: 10 additions & 3 deletions docs/reindex-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,22 @@ The preflight response also includes active job counts and job metadata for oper
A commit keeps the new generation and the one it replaced (the prune trigger from
`20261019155200`), so there is exactly one generation to go back to, and only until the next
commit of that document. The script needs the `rollback_document_index_generation` function
(`20261021150000_rollback_document_index_generation.sql`).
(`20261021150000_rollback_document_index_generation.sql`, restoring generation details since
`20261021170000_restore_generation_metadata_on_rollback.sql`).

- Dry run is the default. For each document it prints the current and previous generation,
their chunk counts and commit times, and whether it would roll back.
- Choose one scope: `--document-id <uuid>[,<uuid>]`, `--committed-after <ISO time>` (the
current generation was committed after that time) or `--chunker-version <version>` (the
current generation's chunks carry that `chunker_version`). `--limit` caps the documents
(default 50, max 1000).
- `--apply` is refused (non-zero exit) unless `--i-understand-metadata-not-restored` is also given: a rollback currently leaves `documents.metadata` and `document_index_quality` describing the wrong generation until #3732 lands, which removes this guard.
- A rollback restores the target generation's details with the pointer (#3732): the
worker-owned `documents.metadata` keys (extraction quality, cover image, embedding model,
enrichment state, extraction provenance and metrics) and the `document_index_quality` row,
from snapshots taken as each generation is replaced. Owner-edited metadata such as the title
is left alone. A generation committed before those snapshots existed is refused with
`target_snapshot_missing` rather than rolled back with mismatched details. The dry run
reports that refusal too, so a document it lists as ready will not be refused for it at apply.
- `--apply` writes (it asks first unless `--yes` is given). It refuses a document with no
previous committed generation, or whose previous generation has no chunks. The database
function also refuses if the pointer moved since the dry run or an ingestion job for the
Expand All @@ -105,7 +112,7 @@ commit of that document. The script needs the `rollback_document_index_generatio

```bash
npm run reindex:rollback -- --chunker-version 1.3.1-page # dry run
npm run reindex:rollback -- --document-id <uuid> --apply --i-understand-metadata-not-restored # write
npm run reindex:rollback -- --document-id <uuid> --apply # write
```

## Recovery transaction (merged 18 October 2026)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
"check:rag:adversarial-fixtures": "node scripts/check-rag-adversarial-fixtures.mjs",
"reindex:cleanup-staged": "node scripts/run-tsx.mjs scripts/cleanup-abandoned-reindex-generations.ts",
"reindex:rollback": "node scripts/run-tsx.mjs scripts/reindex-rollback.ts",
"extraction:provenance": "node scripts/run-tsx.mjs scripts/extraction-provenance-report.ts",
"repair:enrichment-gate": "node scripts/run-tsx.mjs scripts/repair-strict-enrichment-gate.ts",
"check:enrichment-health": "node scripts/run-tsx.mjs scripts/check-enrichment-artifact-health.ts",
"images:re-stamp-generation": "node scripts/run-tsx.mjs scripts/reindex-image-generation-metadata.ts",
Expand Down
75 changes: 75 additions & 0 deletions scripts/extraction-provenance-report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Read-only report: which reader, OCR engine and chunker produced each document's committed
* index generation (audit F04, #2F8K1V). Use it to list the documents a parser defect touched,
* then pass those ids to a reviewed re-extraction or `npm run reindex:rollback`.
*
* npm run extraction:provenance
* npm run extraction:provenance -- --extractor-version 2026-10-21.merged-cell-provenance-v11 --list
* npm run extraction:provenance -- --chunker-version 1.3.1-page --list
* npm run extraction:provenance -- --ocr-engine-version 5.3.4 --list
*
* Selects only; it never writes, queues a job, chunks or re-indexes. It still reads the live
* database, so it needs the owner's confirmation before it is run against production.
* Documents indexed before the record existed report "not recorded"; list them with
* `--extractor-version "not recorded" --list`.
*/
import { loadEnvConfig } from "@next/env";
import {
buildProvenanceReport,
parseProvenanceArgs,
type ProvenanceDocumentRow,
} from "@/lib/extraction-provenance-report";
import { assertSupabaseHealthy, probeSupabaseHealth } from "@/lib/supabase/health";

loadEnvConfig(process.cwd());

const PAGE = 1000;

async function main() {
const args = parseProvenanceArgs(process.argv.slice(2));
const [{ env, requireServerEnv }, { createAdminClient }] = await Promise.all([
import("@/lib/env"),
import("@/lib/supabase/admin"),
]);
requireServerEnv();
const supabase = createAdminClient();

console.log("=== Extraction provenance report (read-only) ===");
console.log(`Supabase project: ${env.SUPABASE_PROJECT_NAME ?? "unknown"} (${env.SUPABASE_PROJECT_REF ?? "unknown"})`);
assertSupabaseHealthy(await probeSupabaseHealth(supabase), "Extraction provenance report");

const rows: ProvenanceDocumentRow[] = [];
// Keyset paging, so documents added or removed during the scan cannot shift a page.
for (let lastId: string | null = null; ;) {
let query = supabase.from("documents").select("id,title,metadata").order("id").limit(PAGE);
if (lastId) query = query.gt("id", lastId);
const { data, error } = await query;
if (error) throw new Error(error.message);
const page = (data ?? []) as ProvenanceDocumentRow[];
rows.push(...page);
if (page.length < PAGE) break;
lastId = page[page.length - 1].id;
}

const report = buildProvenanceReport(rows, args);
console.log(`Documents scanned: ${report.scanned}; matching filter: ${report.matched.length}`);
for (const [label, counts] of [
["Extractor version", report.byExtractorVersion],
["Chunker version", report.byChunkerVersion],
["OCR engine version", report.byOcrEngineVersion],
] as const) {
console.log(`\n${label}:`);
const entries = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b));
if (entries.length === 0) console.log(" (none)");
for (const [version, count] of entries) console.log(` ${version}: ${count}`);
}
if (args.listDocuments) {
console.log("\nDocuments:");
for (const document of report.matched) console.log(` ${document.id} ${document.title ?? "(untitled)"}`);
}
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
23 changes: 16 additions & 7 deletions scripts/reindex-rollback.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/**
* Roll documents back to their previous committed index generation.
*
Expand All @@ -7,11 +7,11 @@
* npm run reindex:rollback -- --document-id <uuid>[,<uuid>]
* npm run reindex:rollback -- --committed-after 2026-10-21T00:00:00Z
* npm run reindex:rollback -- --chunker-version 1.3.1-page
* npm run reindex:rollback -- --document-id <uuid> --apply --i-understand-metadata-not-restored [--yes] [--limit 50]
* npm run reindex:rollback -- --document-id <uuid> --apply [--yes] [--limit 50]
*
* --apply is refused (non-zero exit) without --i-understand-metadata-not-restored, because a
* rollback currently leaves documents.metadata and document_index_quality describing the wrong
* generation. PR 3732 removes this guard.
* The database function restores the target generation's worker-owned metadata and
* document_index_quality row with the pointer (#3732), and refuses with target_snapshot_missing
* when the target was committed before those snapshots were recorded.
*
* See docs/reindex-runbook.md, "Rolling back a reindex".
*/
Expand Down Expand Up @@ -132,6 +132,17 @@
});
return [...new Set(versions.filter((value): value is string => Boolean(value)))];
},
async hasGenerationSnapshot(documentId, generationId) {
const { count, error } = await supabase
.from("document_index_generation_snapshots")
.select("document_id", { count: "exact", head: true })
.eq("document_id", documentId)
.eq("index_generation_id", generationId)
.not("metadata", "is", null)
.not("quality", "is", null);
if (error) throw new Error(error.message);
return (count ?? 0) > 0;
},
async repoint(documentId, expectedGeneration, targetGeneration) {
const { data, error } = await supabase.rpc("rollback_document_index_generation", {
p_document_id: documentId,
Expand Down Expand Up @@ -170,9 +181,7 @@
console.log(`\nDocuments in scope: ${preview.length}; would roll back: ${ready}; refused: ${preview.length - ready}`);

if (!args.apply) {
console.log(
"\nDry run only. Re-run with --apply --i-understand-metadata-not-restored to repoint the documents above.",
);
console.log("\nDry run only. Re-run with --apply to roll back the documents above.");
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
return;
}
if (ready === 0) return;
Expand Down
Loading
Loading