From 8dd014d045b0a901d1682b88adcc2ab7cb574bba Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:08:54 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(db):=20Phase=206.2=20=E2=80=94=20valid?= =?UTF-8?q?ation=20guards=20+=20allowlist=20for=20the=20fifteen=20no-state?= =?UTF-8?q?ments=20history=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six fail-fast validation guard migrations (20260804110240 pattern) covering every object the fifteen hand-applied/mark-applied July versions left on live, and the fifteen migration_history allowlist entries that point at them (class validation, never bare). Guards validate only: absences, catalog comments + retention cron, document FKs, 46 operational index shapes, the index_generation_id promotion, and 15 function def_hashes read from schema_drift_snapshot() itself. Proven: full chain replay into the scratch image Applied 210/210 and CHAIN == MANIFEST (zero unexpected drift); seven mutation proofs raise and name their object; all six dry-ran green on production and a mutant fails there; applied to production by real db push (rows carry statements) and to staging by the Phase 2 method (md5-matched); staging drift comparison green. tests/migration-history-guards.test.ts: the create-index check now ignores string literals/comments (the reference pattern pins canonical `create index` text as data) and requires set local statement_timeout; 20260804110240 is pinned as satisfying the predicate. Ledger: #Q5JHBJ (#316 umbrella). RAG impact: no retrieval behaviour change. Co-Authored-By: Claude Fable 5 --- docs/audit/live-drift-forensics-2026-08.md | 66 ++++ docs/database-drift-detection.md | 35 +- supabase/drift-allowlist.json | 249 +++++++++++++ ...10000_validate_history_dropped_objects.sql | 80 +++++ ...alidate_history_comments_and_retention.sql | 85 +++++ ...validate_history_document_foreign_keys.sql | 83 +++++ ...idate_history_operational_index_shapes.sql | 333 ++++++++++++++++++ ...ate_history_index_generation_promotion.sql | 190 ++++++++++ ...10500_validate_history_function_bodies.sql | 101 ++++++ tests/migration-history-guards.test.ts | 32 +- 10 files changed, 1236 insertions(+), 18 deletions(-) create mode 100644 supabase/migrations/20260819110000_validate_history_dropped_objects.sql create mode 100644 supabase/migrations/20260819110100_validate_history_comments_and_retention.sql create mode 100644 supabase/migrations/20260819110200_validate_history_document_foreign_keys.sql create mode 100644 supabase/migrations/20260819110300_validate_history_operational_index_shapes.sql create mode 100644 supabase/migrations/20260819110400_validate_history_index_generation_promotion.sql create mode 100644 supabase/migrations/20260819110500_validate_history_function_bodies.sql diff --git a/docs/audit/live-drift-forensics-2026-08.md b/docs/audit/live-drift-forensics-2026-08.md index 713c58ca40..60202b49cf 100644 --- a/docs/audit/live-drift-forensics-2026-08.md +++ b/docs/audit/live-drift-forensics-2026-08.md @@ -1712,3 +1712,69 @@ objects}`; classes `validation` (mandatory from 2026-08-18), `superseded`, `no_d names before the list existed; passes with 44 entries (8 `monitor-candidate`, including the three §1.3-absent indexes on those tables: `document_chunks_anchor_idx`, `document_index_units_heading_path_idx`, `documents_registry_projection_lookup_idx`). + +### 6.2 completion — 2026-08-19 (`#Q5JHBJ`; owner-authorised production window) + +_Worker session for `#Q5JHBJ` only (`#316`, `#231`, `#1K6T35` untouched). Pre-flight per `#292`: none of +the seven open PRs (#2181, #2180, #2176, #2173, #2012, #2011, #2010) touches `supabase/**`, +`scripts/check-drift.ts`, `tests/migration-history-guards.test.ts` or this file. D4 is OFF, so the only +production writes are the explicit `db push` recorded below._ + +#### Step 1 — classification of the fifteen (every one `validation`; none earned `superseded` or `no_ddl`) + +The fifteen are the live §3.7 list minus the five seeded `superseded` entries. Note that +`20260702170000 fix_match_chunks_text_n1` (the `select 1;` placeholder) is **not** among them — it was +recorded with statements — and `20260712171500 codify_live_ahead_functions` **is**. + +| Version | File stem | Persistent effect on live | Guard (all `validation`) | +| ---------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| `20260701040000` | `drop_dead_drifted_hybrid_variants` | drops 7 dead functions by 6-arg signature (the 7-arg `_v2`/`_v3` overloads in `schema.sql` are different objects) | `20260819110000_validate_history_dropped_objects` (absence) | +| `20260702100000` | `add_claim_ingestion_jobs_comment` | `comment on function claim_ingestion_jobs` — a pg_description write, so not `no_ddl` (body is not empty / `select 1;`) | `20260819110100_validate_history_comments_and_retention` | +| `20260702110000` | `drop_redundant_indexes` | drops `documents_owner_hash_idx`, `ingestion_jobs_claim_idx` | `…110000_validate_history_dropped_objects` (absence) | +| `20260702120000` | `rag_retrieval_logs_retention` | 2 table comments + `cron.schedule('purge-rag-retrieval-logs')` (only where the `cron` schema exists) | `…110100_validate_history_comments_and_retention` | +| `20260702130000` | `storage_cleanup_jobs_document_fk` | orphan delete (not re-validatable) + FK `storage_cleanup_jobs_document_id_fkey` → `documents(id)` on delete set null | `20260819110200_validate_history_document_foreign_keys` | +| `20260702140000` | `fix_reset_document_index_duplicate` | `reset_document_index(uuid)` — no later creator | `20260819110500_validate_history_function_bodies` (def_hash) | +| `20260702150000` | `documents_owner_covering_index` | `documents_owner_id_covering_idx` — no later creator with executed statements | `20260819110300_validate_history_operational_index_shapes` | +| `20260702160000` | `fix_invoke_agent_url_to_guc` | `invoke_indexing_v3_agent(integer)`; the `ALTER DATABASE SET` is privilege-guarded with an in-function fallback (not pinned) | `…110500_validate_history_function_bodies` (def_hash) | +| `20260702180000` | `promote_index_generation_id_columns` | 6 `index_generation_id uuid` columns + 6 `*_document_generation_idx` + 3 functions | `20260819110400_validate_history_index_generation_promotion` | +| `20260712165915` | `reconcile_ingestion_index_shapes` | drop-and-recreate of 3 ingestion indexes | `…110300_validate_history_operational_index_shapes` | +| `20260712170500` | `codify_live_operational_indexes` | 44 `create index if not exists`; 42 persist (2 re-dropped by `172000`) | `…110300_validate_history_operational_index_shapes` | +| `20260712171000` | `reconcile_visual_eval_document_fks` | 2 FKs `rag_visual_eval_{cases,runs}_document_id_fkey` | `…110200_validate_history_document_foreign_keys` | +| `20260712171500` | `codify_live_ahead_functions` | 12 functions (4 later re-created by `20260714110000` / `20260724120000`) | `…110500_validate_history_function_bodies` (def_hash ×12) | +| `20260712172000` | `drop_redundant_table_fact_indexes` | drops `document_table_facts_document_id_idx`, `document_table_facts_owner_idx` | `…110000_validate_history_dropped_objects` (absence) | +| `20260712173000` | `add_legacy_index_health_batch_repair` | `backfill_legacy_index_health_batch(integer)` — sole creator | `…110500_validate_history_function_bodies` (def_hash) | + +**Why nothing is `superseded`:** the class needs one later migration with executed statements that +re-creates _every_ object the version creates, provable by `tests/migration-history-guards.test.ts`'s +`createsObject` (functions/indexes/tables/views/policies/triggers only). `commit_document_index_generation` +alone is re-created by `20260713062125`, but the other fourteen objects of `180000` are not, and no +version's drops, columns, constraints or comments can be expressed that way. **Why nothing is +`no_ddl`:** the class is defined (doc + test) as a file whose stripped body is empty or `select 1;`; +`COMMENT ON` is a catalog write, so the two comment versions get an `obj_description` validation guard +rather than a widened class. Phase 4's "43 names" for `170500` was a miscount: the file carries **44** +`create index if not exists` statements, of which **42** persist. + +**Guard design (all six follow `20260804110240`):** `set local` search_path / lock_timeout / +statement_timeout, one `do` block, validates only, exactly one `raise exception … Missing: %; Invalid: +%; Mismatched: %`. Indexes: `to_regclass` + `indisvalid AND indisready` + the normalised +`pg_get_indexdef` against the canonical definition pinned verbatim from +`supabase/drift-manifest.json` `snapshot.indexes[].def` (the rendered form production and staging were +measured against when live-drift reported zero index findings). Functions: the signature's `def_hash` +read from `public.schema_drift_snapshot()` itself, so the hashing formula and the `search_path=''` +rendering are identical by construction to the weekly check; ACLs are deliberately not pinned +(`acldefault()` renders the function owner, which differs on a preview branch; `check:drift` compares +ACLs every run). Columns: `pg_attribute` + `format_type = 'uuid'`. FKs: `pg_constraint` `contype f`, +`confrelid = public.documents`, `confdeltype n`, `conkey = {document_id}`. Absences: +`to_regprocedure` / `to_regclass` IS NULL with exact signatures (`extensions.vector`). Comments: +`obj_description` present and carrying the distinctive phrase. Cron: `execute` against `cron.job` only +when `to_regnamespace('cron')` is not null — staging and the scratch image have no pg_cron, and the +original migration returns early there too. `schema.sql` and `drift-manifest.json` are **unchanged** +(validation-only guards create nothing; manifest sha `328677d1c6f3` still matches). + +One repository test was sharpened rather than widened: the "validation guard must not create the +objects it validates" check in `tests/migration-history-guards.test.ts` ran its `create index` regex on +raw SQL, so the canonical `'create index … on …'` string literals that the `20260804110240` pattern +itself pins would have failed it (the check had never been exercised — no `validation` entry existed). +It now strips comments and string literals first, additionally requires `set local statement_timeout`, +and pins that `20260804110240` satisfies the predicate while a real `create index` statement still +fails it. diff --git a/docs/database-drift-detection.md b/docs/database-drift-detection.md index 94f7a605a0..2aabe0baef 100644 --- a/docs/database-drift-detection.md +++ b/docs/database-drift-detection.md @@ -123,22 +123,25 @@ How `check:drift` treats it (`scripts/check-drift.ts`): repo-ahead mechanism, not a special case. Deployment is a separately approved production migration window (plan approval map, Phase 6.1, after Phase 4). -**Expected first live run after deployment.** Only five versions are seeded in -the allowlist (below); the remaining §1.1 rows — `20260701040000 -drop_dead_drifted_hybrid_variants`, `20260702100000 -add_claim_ingestion_jobs_comment`, `20260702110000 drop_redundant_indexes`, -`20260702120000 rag_retrieval_logs_retention`, `20260702130000 -storage_cleanup_jobs_document_fk`, `20260702140000 -fix_reset_document_index_duplicate`, `20260702150000 -documents_owner_covering_index`, `20260702160000 fix_invoke_agent_url_to_guc`, -`20260702180000 promote_index_generation_id_columns`, and the 2026-07-12 batch -`20260712165915`…`20260712173000` — have **no repo-provable guard** and will be -reported as findings. That is the intended behaviour ("a history-repair row -without a validating guard migration becomes permanent, visible drift"); the -follow-up is to author fail-fast guard migrations for them (Phase 4.4 batches -cover the index ones) and allowlist each with a `validation` guard, not to -allowlist them bare. Versions the live probe does not report surface as stale -entries, which is how a wrong seed is caught. +**Live state after Phase 6.2 (2026-08-19).** The probe went live on production on +2026-08-18 (forensics §3.7) and reported exactly twenty no-statements rows: the +2026-07-01…07-02 cluster and the 2026-07-12 batch of §1.1. All twenty are now +covered — five seeded `superseded` entries (`20260701010000`, `020000`, `030000`, +`060000`, `20260702000000`) and fifteen `validation` entries pointing at the six +Phase 6.2 guard migrations `20260819110000`…`20260819110500` (dropped objects, +comments + retention cron, document foreign keys, operational index shapes, the +`index_generation_id` promotion, function bodies; see forensics §"6.2 +completion" for the per-version classification). Those guards were applied to +production by a real `supabase db push` and to staging by the Phase 2 method, so +their own history rows carry statements and can never themselves surface in the +probe. **Any `[migration_history] no_statements` line from now on is therefore +new history repair**, not known backlog: it means someone marked a version +applied without executing it and without shipping a guard — the exact event the +contract forbids. Treat it as a P1 finding: author the guard first, never +allowlist bare. Versions the live probe does not report surface as stale +entries, which is how a wrong seed is caught; against staging every +`migration_history` entry reads stale by design (staging's chain was replayed +with statements), so never run `--prune-stale` there. ## Guard-migration contract diff --git a/supabase/drift-allowlist.json b/supabase/drift-allowlist.json index c453ac2fb5..c492fc5607 100644 --- a/supabase/drift-allowlist.json +++ b/supabase/drift-allowlist.json @@ -39,6 +39,25 @@ "objects": ["search_schema_health"] } }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260701040000", + "reason": "2026-07-01 drop of seven dead live-only hybrid variant functions, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves all seven 6-argument signatures are ABSENT (the 7-argument _v2/_v3 overloads schema.sql declares are different objects)", + "guard": { + "class": "validation", + "migration": "20260819110000_validate_history_dropped_objects.sql", + "objects": [ + "eval_memory_retrieval_v2_v3", + "match_document_memory_cards_hybrid_v3", + "match_document_embedding_fields_rrf", + "match_document_embedding_fields_vector", + "match_document_chunks_hybrid_review_v1", + "match_document_embedding_fields_hybrid_v2", + "match_document_index_units_hybrid_v3" + ] + } + }, { "category": "migration_history", "kind": "no_statements", @@ -60,6 +79,236 @@ "migration": "20260713062125_fence_index_generation_commit.sql", "objects": ["commit_document_index_generation"] } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702100000", + "reason": "2026-07-02 COMMENT ON FUNCTION claim_ingestion_jobs(text,integer,integer), hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); a pg_description write rather than an empty file, so not no_ddl; the guard proves the comment is present and names the FOR UPDATE OF j, d SKIP LOCKED pattern", + "guard": { + "class": "validation", + "migration": "20260819110100_validate_history_comments_and_retention.sql", + "objects": ["claim_ingestion_jobs"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702110000", + "reason": "2026-07-02 drop of two redundant indexes, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves documents_owner_hash_idx and ingestion_jobs_claim_idx are ABSENT (check:drift would also report them as unexpected_live)", + "guard": { + "class": "validation", + "migration": "20260819110000_validate_history_dropped_objects.sql", + "objects": ["documents_owner_hash_idx", "ingestion_jobs_claim_idx"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702120000", + "reason": "2026-07-02 retention policy: comments on audit_logs / rag_retrieval_logs plus the pg_cron job purge-rag-retrieval-logs, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves both comments and, where the cron schema exists (production), the job row", + "guard": { + "class": "validation", + "migration": "20260819110100_validate_history_comments_and_retention.sql", + "objects": ["audit_logs", "rag_retrieval_logs", "purge-rag-retrieval-logs"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702130000", + "reason": "2026-07-02 orphan cleanup plus FK storage_cleanup_jobs.document_id -> documents(id) on delete set null, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves the constraint shape from pg_constraint (the data delete is not re-validatable)", + "guard": { + "class": "validation", + "migration": "20260819110200_validate_history_document_foreign_keys.sql", + "objects": ["storage_cleanup_jobs_document_id_fkey"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702140000", + "reason": "2026-07-02 authoritative reset_document_index(uuid) body (no later migration re-creates it), hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves the live def_hash equals supabase/drift-manifest.json", + "guard": { + "class": "validation", + "migration": "20260819110500_validate_history_function_bodies.sql", + "objects": ["reset_document_index"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702150000", + "reason": "2026-07-02 RLS covering index documents_owner_id_covering_idx (no later migration with executed statements re-creates it), hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves presence, indisvalid/indisready and the canonical pg_get_indexdef", + "guard": { + "class": "validation", + "migration": "20260819110300_validate_history_operational_index_shapes.sql", + "objects": ["documents_owner_id_covering_idx"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702160000", + "reason": "2026-07-02 GUC-based invoke_indexing_v3_agent(integer) body, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves the live def_hash equals the manifest; the ALTER DATABASE SET is privilege-guarded with an in-function fallback and is deliberately not validated", + "guard": { + "class": "validation", + "migration": "20260819110500_validate_history_function_bodies.sql", + "objects": ["invoke_indexing_v3_agent"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260702180000", + "reason": "2026-07-02 promotion of index_generation_id to typed uuid columns on six artifact tables with six partial indexes and three functions (commit_document_index_generation / cleanup_abandoned_document_index_generations were later re-created with statements, is_committed_artifact_generation(uuid,jsonb) was not), hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves every column type, every index shape, and the three current def_hashes", + "guard": { + "class": "validation", + "migration": "20260819110400_validate_history_index_generation_promotion.sql", + "objects": [ + "document_images.index_generation_id", + "document_table_facts.index_generation_id", + "document_embedding_fields.index_generation_id", + "document_index_units.index_generation_id", + "document_memory_cards.index_generation_id", + "document_sections.index_generation_id", + "document_images_document_generation_idx", + "document_table_facts_document_generation_idx", + "document_embedding_fields_document_generation_idx", + "document_index_units_document_generation_idx", + "document_memory_cards_document_generation_idx", + "document_sections_document_generation_idx", + "is_committed_artifact_generation", + "commit_document_index_generation", + "cleanup_abandoned_document_index_generations" + ] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260712165915", + "reason": "2026-07-12 reconciliation batch (stmt_count 0 on live): drop-and-recreate of three ingestion index shapes, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves presence, indisvalid/indisready and the canonical pg_get_indexdef of all three", + "guard": { + "class": "validation", + "migration": "20260819110300_validate_history_operational_index_shapes.sql", + "objects": [ + "import_batches_status_created_idx", + "ingestion_jobs_document_status_idx", + "ingestion_jobs_status_next_run_idx" + ] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260712170500", + "reason": "2026-07-12 reconciliation batch (stmt_count 0 on live): 44 create-if-not-exists operational indexes codified from production, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves the 42 that persist (presence, indisvalid/indisready, canonical pg_get_indexdef); document_table_facts_document_id_idx and document_table_facts_owner_idx were dropped again by 20260712172000 and are proven absent by that version's guard", + "guard": { + "class": "validation", + "migration": "20260819110300_validate_history_operational_index_shapes.sql", + "objects": [ + "audit_logs_owner_id_idx", + "clinical_registry_record_sources_owner_id_idx", + "differential_records_owner_slug_kind_uidx", + "document_chunks_document_id_idx", + "document_chunks_meta_rag_indexing_version_idx", + "document_embedding_fields_document_generation_idx", + "document_embedding_fields_document_id_idx", + "document_images_created_at_idx", + "document_images_document_generation_idx", + "document_images_document_id_idx", + "document_images_duplicate_group_idx", + "document_images_priority_idx", + "document_images_searchable_idx1", + "document_index_quality_document_id_idx", + "document_index_quality_owner_id_idx", + "document_index_units_document_generation_idx", + "document_index_units_document_id_idx", + "document_index_units_owner_document_created_idx", + "document_index_units_owner_id_idx", + "document_index_units_source_chunk_id_idx", + "document_index_units_source_image_id_idx", + "document_labels_owner_id_idx", + "document_memory_cards_document_generation_idx", + "document_memory_cards_document_id_idx", + "document_memory_cards_owner_document_created_idx", + "document_sections_document_generation_idx", + "document_sections_document_id_idx", + "document_summaries_owner_id_idx", + "document_table_facts_document_generation_idx", + "document_table_facts_title_row_param_trgm_idx", + "documents_indexed_updated_at_idx", + "image_caption_cache_owner_id_idx", + "import_batches_owner_id_idx", + "ingestion_job_stages_job_idx", + "ingestion_jobs_status_idx", + "ingestion_jobs_updated_at_idx", + "rag_queries_owner_id_idx", + "rag_query_misses_expected_chunk_id_idx", + "rag_query_misses_expected_document_id_idx", + "rag_query_misses_owner_id_idx", + "rag_response_cache_owner_id_idx", + "rag_retrieval_logs_owner_id_idx" + ] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260712171000", + "reason": "2026-07-12 reconciliation batch (stmt_count 0 on live): re-created document FKs on rag_visual_eval_cases / rag_visual_eval_runs with on delete set null, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves both constraint shapes from pg_constraint", + "guard": { + "class": "validation", + "migration": "20260819110200_validate_history_document_foreign_keys.sql", + "objects": ["rag_visual_eval_cases_document_id_fkey", "rag_visual_eval_runs_document_id_fkey"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260712171500", + "reason": "2026-07-12 reconciliation batch (stmt_count 0 on live): twelve live-ahead function bodies codified from production (four later re-created by 20260714110000 / 20260724120000), hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves every current def_hash equals supabase/drift-manifest.json, which live-drift proved equal to production", + "guard": { + "class": "validation", + "migration": "20260819110500_validate_history_function_bodies.sql", + "objects": [ + "get_related_document_metadata", + "get_visual_evidence_cards", + "match_document_chunks", + "match_document_chunks_hybrid", + "match_document_chunks_text", + "match_document_embedding_fields_text", + "match_document_table_facts_text", + "match_documents_for_query", + "repair_enrichment_quality_batch", + "repair_strict_enrichment_gate_batch", + "run_all_visual_eval_cases", + "run_visual_eval_case" + ] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260712172000", + "reason": "2026-07-12 reconciliation batch (stmt_count 0 on live): drop of two left-prefix table-fact indexes superseded by the canonical composites, hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves document_table_facts_document_id_idx and document_table_facts_owner_idx are ABSENT (the former was the Phase 4 unexpected_live orphan, dropped concurrently 2026-08-19)", + "guard": { + "class": "validation", + "migration": "20260819110000_validate_history_dropped_objects.sql", + "objects": ["document_table_facts_document_id_idx", "document_table_facts_owner_idx"] + } + }, + { + "category": "migration_history", + "kind": "no_statements", + "key": "20260712173000", + "reason": "2026-07-12 reconciliation batch (stmt_count 0 on live): backfill_legacy_index_health_batch(integer) (sole creator), hand-applied and mark-applied in the 2026-07 history-repair cluster (forensics section 1.1; live probe section 3.7 / Phase 4 step 7); the guard proves the live def_hash equals supabase/drift-manifest.json", + "guard": { + "class": "validation", + "migration": "20260819110500_validate_history_function_bodies.sql", + "objects": ["backfill_legacy_index_health_batch"] + } } ] } diff --git a/supabase/migrations/20260819110000_validate_history_dropped_objects.sql b/supabase/migrations/20260819110000_validate_history_dropped_objects.sql new file mode 100644 index 0000000000..0b74b65418 --- /dev/null +++ b/supabase/migrations/20260819110000_validate_history_dropped_objects.sql @@ -0,0 +1,80 @@ +-- Phase 6.2 history guard (plan section 6.2; forensics section 1.1 / 3.7 / '6.2 completion'): +-- validate that three hand-applied, mark-applied versions whose only persistent effect is a DROP +-- really left the objects absent. Ledger anchor #Q5JHBJ (#316 umbrella). +-- +-- 20260701040000 drop_dead_drifted_hybrid_variants -> seven dead live-only hybrid variant functions, +-- dropped by their exact 2026-07-01 signatures. The 7-argument (..., boolean) overloads of +-- match_document_embedding_fields_hybrid_v2 / match_document_memory_cards_hybrid_v3 that schema.sql +-- declares are DIFFERENT objects and are deliberately not touched here. +-- 20260702110000 drop_redundant_indexes -> documents_owner_hash_idx, ingestion_jobs_claim_idx +-- 20260712172000 drop_redundant_table_fact_indexes -> document_table_facts_document_id_idx, +-- document_table_facts_owner_idx (both re-created by the no-statements 20260712170500 and dropped +-- again here; production's orphan document_table_facts_document_id_idx was dropped concurrently in +-- the Phase 4 window, so absence is the recorded end state on both tiers). +-- +-- Like 20260804110240 this migration VALIDATES and never builds or drops: it only reads the catalog +-- and raises once, naming every object that is unexpectedly present. Timeouts use SET LOCAL so they +-- do not leak into later migrations applied on the same CLI session connection. Safe to re-run. + +set local search_path = public, extensions, pg_catalog; +set local lock_timeout = '5s'; +set local statement_timeout = '30s'; + +do $migration$ +declare + present_functions text[] := array[]::text[]; + present_indexes text[] := array[]::text[]; + required record; +begin + for required in + select * + from ( + values + -- 20260701040000 + ('eval_memory_retrieval_v2_v3', 'public.eval_memory_retrieval_v2_v3(extensions.vector,text,integer,double precision,uuid[],uuid)'), + -- 20260701040000 + ('match_document_memory_cards_hybrid_v3', 'public.match_document_memory_cards_hybrid_v3(extensions.vector,text,integer,double precision,uuid[],uuid)'), + -- 20260701040000 + ('match_document_embedding_fields_rrf', 'public.match_document_embedding_fields_rrf(extensions.vector,text,integer,integer,double precision,double precision,integer,double precision,double precision,uuid[],uuid)'), + -- 20260701040000 + ('match_document_embedding_fields_vector', 'public.match_document_embedding_fields_vector(extensions.vector,integer,double precision,uuid[],uuid)'), + -- 20260701040000 + ('match_document_chunks_hybrid_review_v1', 'public.match_document_chunks_hybrid_review_v1(extensions.vector,text,integer,double precision,uuid[],uuid,boolean)'), + -- 20260701040000 + ('match_document_embedding_fields_hybrid_v2', 'public.match_document_embedding_fields_hybrid_v2(extensions.vector,text,integer,double precision,uuid[],uuid)'), + -- 20260701040000 + ('match_document_index_units_hybrid_v3', 'public.match_document_index_units_hybrid_v3(extensions.vector,text,integer,double precision,uuid[],uuid)') + ) as t(object_name, signature) + loop + if to_regprocedure(required.signature) is not null then + present_functions := array_append(present_functions, required.signature); + end if; + end loop; + + for required in + select * + from ( + values + -- 20260702110000 + ('documents_owner_hash_idx'), + -- 20260702110000 + ('ingestion_jobs_claim_idx'), + -- 20260712172000 + ('document_table_facts_document_id_idx'), + -- 20260712172000 + ('document_table_facts_owner_idx') + ) as t(index_name) + loop + if to_regclass(format('public.%I', required.index_name)) is not null then + present_indexes := array_append(present_indexes, required.index_name); + end if; + end loop; + + if cardinality(present_functions) > 0 or cardinality(present_indexes) > 0 then + raise exception + 'History guard 20260819110000: objects that 20260701040000 / 20260702110000 / 20260712172000 dropped are still present; the recorded history claims a state this database does not have. Drop them out of band (DROP INDEX CONCURRENTLY for an index on a populated table), then apply this version. Missing: (none); Invalid: (none); Mismatched (unexpectedly present) functions: %; indexes: %', + coalesce(nullif(array_to_string(present_functions, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(present_indexes, ', '), ''), '(none)'); + end if; +end +$migration$; diff --git a/supabase/migrations/20260819110100_validate_history_comments_and_retention.sql b/supabase/migrations/20260819110100_validate_history_comments_and_retention.sql new file mode 100644 index 0000000000..f39c2fd95a --- /dev/null +++ b/supabase/migrations/20260819110100_validate_history_comments_and_retention.sql @@ -0,0 +1,85 @@ +-- Phase 6.2 history guard (plan section 6.2; forensics section 1.1 / 3.7 / '6.2 completion'): +-- validate the catalog comments and the retention cron job that two hand-applied, mark-applied +-- versions left behind. Ledger anchor #Q5JHBJ (#316 umbrella). +-- +-- 20260702100000 add_claim_ingestion_jobs_comment -> COMMENT ON FUNCTION public.claim_ingestion_jobs(text, +-- integer, integer) documenting the dual-lock FOR UPDATE OF j, d SKIP LOCKED pattern. A comment +-- survives CREATE OR REPLACE, so later bodies do not clear it. This is a pg_description write, not +-- an empty file, which is why the version is not a `no_ddl` allowlist class. +-- 20260702120000 rag_retrieval_logs_retention -> COMMENT ON TABLE public.audit_logs (kept +-- indefinitely), COMMENT ON TABLE public.rag_retrieval_logs (90-day purge), and the pg_cron job +-- purge-rag-retrieval-logs. The job is validated only where the cron schema exists: production has +-- pg_cron; staging and the bare scratch image do not, and the original migration itself returns +-- early there, so absence of the schema is the recorded no-op path rather than a failure. +-- +-- Like 20260804110240 this migration VALIDATES and never writes: it reads obj_description() and cron.job +-- (dynamically, so it parses on databases without the schema) and raises once, naming every failure. +-- Timeouts use SET LOCAL so they do not leak into later migrations on the same CLI session connection. + +set local search_path = public, extensions, pg_catalog; +set local lock_timeout = '5s'; +set local statement_timeout = '30s'; + +do $migration$ +declare + missing_objects text[] := array[]::text[]; + mismatched_objects text[] := array[]::text[]; + function_oid regprocedure; + comment_text text; + cron_job_present boolean; +begin + -- 20260702100000: claim_ingestion_jobs function comment + function_oid := to_regprocedure('public.claim_ingestion_jobs(text,integer,integer)'); + if function_oid is null then + missing_objects := array_append(missing_objects, 'claim_ingestion_jobs(text,integer,integer)'); + else + comment_text := obj_description(function_oid::oid, 'pg_proc'); + if comment_text is null then + missing_objects := array_append(missing_objects, 'comment on function claim_ingestion_jobs'); + elsif position('FOR UPDATE OF j, d SKIP LOCKED' in comment_text) = 0 then + mismatched_objects := array_append(mismatched_objects, 'comment on function claim_ingestion_jobs'); + end if; + end if; + + -- 20260702120000: audit_logs table comment + if to_regclass('public.audit_logs') is null then + missing_objects := array_append(missing_objects, 'audit_logs'); + else + comment_text := obj_description(to_regclass('public.audit_logs')::oid, 'pg_class'); + if comment_text is null then + missing_objects := array_append(missing_objects, 'comment on table audit_logs'); + elsif position('retained indefinitely' in comment_text) = 0 then + mismatched_objects := array_append(mismatched_objects, 'comment on table audit_logs'); + end if; + end if; + + -- 20260702120000: rag_retrieval_logs table comment + if to_regclass('public.rag_retrieval_logs') is null then + missing_objects := array_append(missing_objects, 'rag_retrieval_logs'); + else + comment_text := obj_description(to_regclass('public.rag_retrieval_logs')::oid, 'pg_class'); + if comment_text is null then + missing_objects := array_append(missing_objects, 'comment on table rag_retrieval_logs'); + elsif position('purge-rag-retrieval-logs' in comment_text) = 0 then + mismatched_objects := array_append(mismatched_objects, 'comment on table rag_retrieval_logs'); + end if; + end if; + + -- 20260702120000: the nightly purge job, only where pg_cron is installed + if to_regnamespace('cron') is not null then + execute 'select exists (select 1 from cron.job where jobname = $1)' + into cron_job_present + using 'purge-rag-retrieval-logs'; + if not coalesce(cron_job_present, false) then + missing_objects := array_append(missing_objects, 'cron job purge-rag-retrieval-logs'); + end if; + end if; + + if cardinality(missing_objects) > 0 or cardinality(mismatched_objects) > 0 then + raise exception + 'History guard 20260819110100: the catalog comments / retention cron job that 20260702100000 and 20260702120000 recorded are not in place; re-apply those two files out of band (COMMENT ON and cron.schedule are idempotent), then apply this version. Missing: %; Invalid: (none); Mismatched: %', + coalesce(nullif(array_to_string(missing_objects, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(mismatched_objects, ', '), ''), '(none)'); + end if; +end +$migration$; diff --git a/supabase/migrations/20260819110200_validate_history_document_foreign_keys.sql b/supabase/migrations/20260819110200_validate_history_document_foreign_keys.sql new file mode 100644 index 0000000000..04aa04f702 --- /dev/null +++ b/supabase/migrations/20260819110200_validate_history_document_foreign_keys.sql @@ -0,0 +1,83 @@ +-- Phase 6.2 history guard (plan section 6.2; forensics section 1.1 / 3.7 / '6.2 completion'): +-- validate the three document foreign keys that two hand-applied, mark-applied versions added. +-- Ledger anchor #Q5JHBJ (#316 umbrella). +-- +-- 20260702130000 storage_cleanup_jobs_document_fk -> storage_cleanup_jobs_document_id_fkey +-- 20260712171000 reconcile_visual_eval_document_fks -> rag_visual_eval_cases_document_id_fkey, +-- rag_visual_eval_runs_document_id_fkey +-- +-- Each must be a FOREIGN KEY on (document_id) referencing public.documents(id) with ON DELETE SET +-- NULL -- the shape supabase/schema.sql declares and supabase/drift-manifest.json pins +-- ('FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL'). The check reads +-- pg_constraint columns (contype / confrelid / confdeltype / conkey) rather than comparing +-- pg_get_constraintdef text, so the result does not depend on the session search_path rendering +-- 'documents' versus 'public.documents'. +-- +-- Like 20260804110240 this migration VALIDATES and never builds: it reads the catalog and raises once, +-- naming every failure. Timeouts use SET LOCAL so they do not leak into later migrations. + +set local search_path = public, extensions, pg_catalog; +set local lock_timeout = '5s'; +set local statement_timeout = '30s'; + +do $migration$ +declare + missing_constraints text[] := array[]::text[]; + mismatched_constraints text[] := array[]::text[]; + required record; + table_oid regclass; + con record; + document_column smallint; +begin + for required in + select * + from ( + values + -- 20260702130000 + ('storage_cleanup_jobs', 'storage_cleanup_jobs_document_id_fkey'), + -- 20260712171000 + ('rag_visual_eval_cases', 'rag_visual_eval_cases_document_id_fkey'), + -- 20260712171000 + ('rag_visual_eval_runs', 'rag_visual_eval_runs_document_id_fkey') + ) as t(table_name, constraint_name) + loop + table_oid := to_regclass(format('public.%I', required.table_name)); + if table_oid is null then + missing_constraints := array_append(missing_constraints, required.constraint_name); + continue; + end if; + + select c.contype, c.confrelid, c.confdeltype, c.conkey + into con + from pg_constraint as c + where c.conrelid = table_oid + and c.conname = required.constraint_name; + + if not found then + missing_constraints := array_append(missing_constraints, required.constraint_name); + continue; + end if; + + select a.attnum + into document_column + from pg_attribute as a + where a.attrelid = table_oid + and a.attname = 'document_id' + and not a.attisdropped; + + if con.contype <> 'f' + or con.confrelid is distinct from to_regclass('public.documents')::oid + or con.confdeltype <> 'n' + or con.conkey is distinct from array[document_column]::smallint[] then + mismatched_constraints := array_append(mismatched_constraints, required.constraint_name); + end if; + end loop; + + if cardinality(missing_constraints) > 0 or cardinality(mismatched_constraints) > 0 then + raise exception + 'History guard 20260819110200: the document foreign keys recorded by 20260702130000 / 20260712171000 are not in canonical form (FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL). Reconcile them out of band, then apply this version. Missing: %; Invalid: (none); Mismatched: %', + coalesce(nullif(array_to_string(missing_constraints, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(mismatched_constraints, ', '), ''), '(none)'); + end if; +end +$migration$; diff --git a/supabase/migrations/20260819110300_validate_history_operational_index_shapes.sql b/supabase/migrations/20260819110300_validate_history_operational_index_shapes.sql new file mode 100644 index 0000000000..40e573e4ae --- /dev/null +++ b/supabase/migrations/20260819110300_validate_history_operational_index_shapes.sql @@ -0,0 +1,333 @@ +-- Phase 6.2 history guard (plan section 6.2; forensics section 1.1 / 3.7 / '6.2 completion'): +-- validate the forty-six operational indexes created by three hand-applied, mark-applied versions. +-- Ledger anchor #Q5JHBJ (#316 umbrella). +-- +-- 20260702150000 documents_owner_covering_index -> documents_owner_id_covering_idx (1) +-- 20260712165915 reconcile_ingestion_index_shapes -> import_batches_status_created_idx, +-- ingestion_jobs_document_status_idx, +-- ingestion_jobs_status_next_run_idx (3) +-- 20260712170500 codify_live_operational_indexes -> the 42 of its 44 `create index if not exists` +-- statements that persist; document_table_facts_document_id_idx and document_table_facts_owner_idx +-- were dropped again by 20260712172000 and are proven ABSENT by 20260819110000 instead. +-- +-- Every canonical definition below is the rendered pg_get_indexdef that supabase/drift-manifest.json +-- pins for the index (snapshot.indexes[].def) -- the same text production and staging were measured +-- against when live-drift reported zero index findings (run 32171070287 and the staging comparison). +-- +-- Like 20260804110240 this migration VALIDATES and never builds: presence via to_regclass, +-- pg_index.indisvalid AND indisready, and the normalised pg_get_indexdef against the pinned definition +-- (normaliser kept in lockstep with normalizeIndexDefinition in tests/supabase-schema.test.ts). A drifted +-- hosted target must prebuild any missing index with CREATE INDEX CONCURRENTLY outside the migration +-- transaction, validate it, and only then apply this version. Timeouts use SET LOCAL. +-- +-- Name resolution is canonical-only: none of these has an entry in search_schema_health()'s +-- index_aliases map, so an alias is never accepted as evidence. + +set local search_path = public, extensions, pg_catalog; +set local lock_timeout = '5s'; +set local statement_timeout = '30s'; + +do $migration$ +declare + missing_indexes text[] := array[]::text[]; + invalid_indexes text[] := array[]::text[]; + mismatched_indexes text[] := array[]::text[]; + required record; + index_oid regclass; + is_valid boolean; + is_ready boolean; + actual_def text; + actual_normalized text; + expected_normalized text; +begin + for required in + select * + from ( + values + -- 20260702150000 + ( + 'documents_owner_id_covering_idx', + 'CREATE INDEX documents_owner_id_covering_idx ON public.documents USING btree (owner_id, id)' + ), + -- 20260712165915 + ( + 'import_batches_status_created_idx', + 'CREATE INDEX import_batches_status_created_idx ON public.import_batches USING btree (status, created_at DESC) WHERE (status = ANY (ARRAY[''queued''::text, ''processing''::text]))' + ), + -- 20260712165915 + ( + 'ingestion_jobs_document_status_idx', + 'CREATE INDEX ingestion_jobs_document_status_idx ON public.ingestion_jobs USING btree (document_id, status, created_at)' + ), + -- 20260712165915 + ( + 'ingestion_jobs_status_next_run_idx', + 'CREATE INDEX ingestion_jobs_status_next_run_idx ON public.ingestion_jobs USING btree (status, next_run_at, created_at) WHERE (status = ANY (ARRAY[''pending''::text, ''processing''::text, ''failed''::text]))' + ), + -- 20260712170500 + ( + 'audit_logs_owner_id_idx', + 'CREATE INDEX audit_logs_owner_id_idx ON public.audit_logs USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'clinical_registry_record_sources_owner_id_idx', + 'CREATE INDEX clinical_registry_record_sources_owner_id_idx ON public.clinical_registry_record_sources USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'differential_records_owner_slug_kind_uidx', + 'CREATE UNIQUE INDEX differential_records_owner_slug_kind_uidx ON public.differential_records USING btree (owner_id, slug, kind)' + ), + -- 20260712170500 + ( + 'document_chunks_document_id_idx', + 'CREATE INDEX document_chunks_document_id_idx ON public.document_chunks USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_chunks_meta_rag_indexing_version_idx', + 'CREATE INDEX document_chunks_meta_rag_indexing_version_idx ON public.document_chunks USING btree (((metadata ->> ''rag_indexing_version''::text)))' + ), + -- 20260712170500 + ( + 'document_embedding_fields_document_generation_idx', + 'CREATE INDEX document_embedding_fields_document_generation_idx ON public.document_embedding_fields USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260712170500 + ( + 'document_embedding_fields_document_id_idx', + 'CREATE INDEX document_embedding_fields_document_id_idx ON public.document_embedding_fields USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_images_created_at_idx', + 'CREATE INDEX document_images_created_at_idx ON public.document_images USING btree (created_at)' + ), + -- 20260712170500 + ( + 'document_images_document_generation_idx', + 'CREATE INDEX document_images_document_generation_idx ON public.document_images USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260712170500 + ( + 'document_images_document_id_idx', + 'CREATE INDEX document_images_document_id_idx ON public.document_images USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_images_duplicate_group_idx', + 'CREATE INDEX document_images_duplicate_group_idx ON public.document_images USING btree (document_id, visual_duplicate_group)' + ), + -- 20260712170500 + ( + 'document_images_priority_idx', + 'CREATE INDEX document_images_priority_idx ON public.document_images USING btree (document_id, clinical_priority_score DESC, page_number)' + ), + -- 20260712170500 + ( + 'document_images_searchable_idx1', + 'CREATE INDEX document_images_searchable_idx1 ON public.document_images USING btree (searchable)' + ), + -- 20260712170500 + ( + 'document_index_quality_document_id_idx', + 'CREATE INDEX document_index_quality_document_id_idx ON public.document_index_quality USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_index_quality_owner_id_idx', + 'CREATE INDEX document_index_quality_owner_id_idx ON public.document_index_quality USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'document_index_units_document_generation_idx', + 'CREATE INDEX document_index_units_document_generation_idx ON public.document_index_units USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260712170500 + ( + 'document_index_units_document_id_idx', + 'CREATE INDEX document_index_units_document_id_idx ON public.document_index_units USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_index_units_owner_document_created_idx', + 'CREATE INDEX document_index_units_owner_document_created_idx ON public.document_index_units USING btree (owner_id, document_id, created_at DESC)' + ), + -- 20260712170500 + ( + 'document_index_units_owner_id_idx', + 'CREATE INDEX document_index_units_owner_id_idx ON public.document_index_units USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'document_index_units_source_chunk_id_idx', + 'CREATE INDEX document_index_units_source_chunk_id_idx ON public.document_index_units USING btree (source_chunk_id)' + ), + -- 20260712170500 + ( + 'document_index_units_source_image_id_idx', + 'CREATE INDEX document_index_units_source_image_id_idx ON public.document_index_units USING btree (source_image_id)' + ), + -- 20260712170500 + ( + 'document_labels_owner_id_idx', + 'CREATE INDEX document_labels_owner_id_idx ON public.document_labels USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'document_memory_cards_document_generation_idx', + 'CREATE INDEX document_memory_cards_document_generation_idx ON public.document_memory_cards USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260712170500 + ( + 'document_memory_cards_document_id_idx', + 'CREATE INDEX document_memory_cards_document_id_idx ON public.document_memory_cards USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_memory_cards_owner_document_created_idx', + 'CREATE INDEX document_memory_cards_owner_document_created_idx ON public.document_memory_cards USING btree (owner_id, document_id, created_at DESC)' + ), + -- 20260712170500 + ( + 'document_sections_document_generation_idx', + 'CREATE INDEX document_sections_document_generation_idx ON public.document_sections USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260712170500 + ( + 'document_sections_document_id_idx', + 'CREATE INDEX document_sections_document_id_idx ON public.document_sections USING btree (document_id)' + ), + -- 20260712170500 + ( + 'document_summaries_owner_id_idx', + 'CREATE INDEX document_summaries_owner_id_idx ON public.document_summaries USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'document_table_facts_document_generation_idx', + 'CREATE INDEX document_table_facts_document_generation_idx ON public.document_table_facts USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260712170500 + ( + 'document_table_facts_title_row_param_trgm_idx', + 'CREATE INDEX document_table_facts_title_row_param_trgm_idx ON public.document_table_facts USING gin (lower(((((COALESCE(table_title, ''''::text) || '' ''::text) || COALESCE(row_label, ''''::text)) || '' ''::text) || COALESCE(clinical_parameter, ''''::text))) extensions.gin_trgm_ops)' + ), + -- 20260712170500 + ( + 'documents_indexed_updated_at_idx', + 'CREATE INDEX documents_indexed_updated_at_idx ON public.documents USING btree (updated_at, id) WHERE (status = ''indexed''::text)' + ), + -- 20260712170500 + ( + 'image_caption_cache_owner_id_idx', + 'CREATE INDEX image_caption_cache_owner_id_idx ON public.image_caption_cache USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'import_batches_owner_id_idx', + 'CREATE INDEX import_batches_owner_id_idx ON public.import_batches USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'ingestion_job_stages_job_idx', + 'CREATE INDEX ingestion_job_stages_job_idx ON public.ingestion_job_stages USING btree (job_id, started_at DESC)' + ), + -- 20260712170500 + ( + 'ingestion_jobs_status_idx', + 'CREATE INDEX ingestion_jobs_status_idx ON public.ingestion_jobs USING btree (status)' + ), + -- 20260712170500 + ( + 'ingestion_jobs_updated_at_idx', + 'CREATE INDEX ingestion_jobs_updated_at_idx ON public.ingestion_jobs USING btree (updated_at)' + ), + -- 20260712170500 + ( + 'rag_queries_owner_id_idx', + 'CREATE INDEX rag_queries_owner_id_idx ON public.rag_queries USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'rag_query_misses_expected_chunk_id_idx', + 'CREATE INDEX rag_query_misses_expected_chunk_id_idx ON public.rag_query_misses USING btree (expected_chunk_id)' + ), + -- 20260712170500 + ( + 'rag_query_misses_expected_document_id_idx', + 'CREATE INDEX rag_query_misses_expected_document_id_idx ON public.rag_query_misses USING btree (expected_document_id)' + ), + -- 20260712170500 + ( + 'rag_query_misses_owner_id_idx', + 'CREATE INDEX rag_query_misses_owner_id_idx ON public.rag_query_misses USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'rag_response_cache_owner_id_idx', + 'CREATE INDEX rag_response_cache_owner_id_idx ON public.rag_response_cache USING btree (owner_id)' + ), + -- 20260712170500 + ( + 'rag_retrieval_logs_owner_id_idx', + 'CREATE INDEX rag_retrieval_logs_owner_id_idx ON public.rag_retrieval_logs USING btree (owner_id)' + ) + ) as t(index_name, canonical_def) + loop + index_oid := to_regclass(format('public.%I', required.index_name)); + if index_oid is null then + missing_indexes := array_append(missing_indexes, required.index_name); + continue; + end if; + + select i.indisvalid, i.indisready, pg_get_indexdef(i.indexrelid) + into is_valid, is_ready, actual_def + from pg_index as i + where i.indexrelid = index_oid; + + if not coalesce(is_valid, false) or not coalesce(is_ready, false) then + invalid_indexes := array_append(invalid_indexes, required.index_name); + continue; + end if; + + -- Keep in lockstep with normalizeIndexDefinition in tests/supabase-schema.test.ts. + actual_normalized := actual_def; + actual_normalized := lower(actual_normalized); + actual_normalized := replace(actual_normalized, 'create index if not exists', 'create index'); + actual_normalized := regexp_replace(actual_normalized, ' extensions\.', ' ', 'g'); + actual_normalized := regexp_replace(actual_normalized, ' using btree', '', 'g'); + actual_normalized := regexp_replace(actual_normalized, 'where \(([^()]*)\)$', 'where \1'); + actual_normalized := replace(actual_normalized, ';', ''); + actual_normalized := regexp_replace(actual_normalized, '[[:space:]]+', ' ', 'g'); + actual_normalized := regexp_replace(actual_normalized, ' on ([^ ()]+) \(', ' on \1(', 'g'); + actual_normalized := btrim(actual_normalized); + + expected_normalized := required.canonical_def; + expected_normalized := lower(expected_normalized); + expected_normalized := replace(expected_normalized, 'create index if not exists', 'create index'); + expected_normalized := regexp_replace(expected_normalized, ' extensions\.', ' ', 'g'); + expected_normalized := regexp_replace(expected_normalized, ' using btree', '', 'g'); + expected_normalized := regexp_replace(expected_normalized, 'where \(([^()]*)\)$', 'where \1'); + expected_normalized := replace(expected_normalized, ';', ''); + expected_normalized := regexp_replace(expected_normalized, '[[:space:]]+', ' ', 'g'); + expected_normalized := regexp_replace(expected_normalized, ' on ([^ ()]+) \(', ' on \1(', 'g'); + expected_normalized := btrim(expected_normalized); + + if actual_normalized is distinct from expected_normalized then + mismatched_indexes := array_append(mismatched_indexes, required.index_name); + end if; + end loop; + + if cardinality(missing_indexes) > 0 + or cardinality(invalid_indexes) > 0 + or cardinality(mismatched_indexes) > 0 then + raise exception + 'History guard 20260819110300: operational indexes recorded by 20260702150000 / 20260712165915 / 20260712170500 are not present in canonical form; create missing indexes concurrently outside the migration transaction, validate them, then apply this version. Missing: %; Invalid: %; Mismatched: %', + coalesce(nullif(array_to_string(missing_indexes, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(invalid_indexes, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(mismatched_indexes, ', '), ''), '(none)'); + end if; +end +$migration$; diff --git a/supabase/migrations/20260819110400_validate_history_index_generation_promotion.sql b/supabase/migrations/20260819110400_validate_history_index_generation_promotion.sql new file mode 100644 index 0000000000..19cd822f5a --- /dev/null +++ b/supabase/migrations/20260819110400_validate_history_index_generation_promotion.sql @@ -0,0 +1,190 @@ +-- Phase 6.2 history guard (plan section 6.2; forensics section 1.1 / 3.7 / '6.2 completion'): +-- validate everything 20260702180000 promote_index_generation_id_columns left behind -- a hand-applied, +-- mark-applied version with three kinds of object. Ledger anchor #Q5JHBJ (#316 umbrella). +-- +-- columns index_generation_id uuid on document_images, document_table_facts, +-- document_embedding_fields, document_index_units, document_memory_cards, document_sections +-- indexes the six _document_generation_idx partial indexes (document_id, index_generation_id) +-- where index_generation_id is not null +-- functions is_committed_artifact_generation(uuid, jsonb) (new overload; the (jsonb, jsonb) one is +-- untouched), commit_document_index_generation(uuid, uuid, text, integer, integer, integer, +-- jsonb, jsonb, jsonb) -- the 9-argument overload; later migrations added the 11-argument one -- +-- and cleanup_abandoned_document_index_generations(uuid, integer, boolean). The last two were +-- re-created by later executed migrations (20260713062125 / 20260708130000), so the pinned +-- def_hash is the CURRENT canonical body, exactly as supabase/drift-manifest.json records it. +-- +-- Index definitions are the rendered pg_get_indexdef the manifest pins; function bodies are compared by +-- the manifest's def_hash, read from public.schema_drift_snapshot() so the hashing formula and +-- search_path are identical by construction to what the weekly live-drift check compares. ACLs are not +-- pinned here (check:drift compares them every run). +-- +-- Like 20260804110240 this migration VALIDATES and never builds: catalog reads only, one raise naming +-- every failure. Timeouts use SET LOCAL so they do not leak into later migrations. + +set local search_path = public, extensions, pg_catalog; +set local lock_timeout = '5s'; +set local statement_timeout = '60s'; + +do $migration$ +declare + missing_objects text[] := array[]::text[]; + invalid_objects text[] := array[]::text[]; + mismatched_objects text[] := array[]::text[]; + required record; + table_oid regclass; + column_type text; + index_oid regclass; + is_valid boolean; + is_ready boolean; + actual_def text; + actual_normalized text; + expected_normalized text; + snapshot jsonb; + live_hash text; +begin + -- 1. typed columns + for required in + select * + from ( + values + ('document_images.index_generation_id'), + ('document_table_facts.index_generation_id'), + ('document_embedding_fields.index_generation_id'), + ('document_index_units.index_generation_id'), + ('document_memory_cards.index_generation_id'), + ('document_sections.index_generation_id') + ) as t(qualified_column) + loop + table_oid := to_regclass(format('public.%I', split_part(required.qualified_column, '.', 1))); + if table_oid is null then + missing_objects := array_append(missing_objects, required.qualified_column); + continue; + end if; + select format_type(a.atttypid, a.atttypmod) + into column_type + from pg_attribute as a + where a.attrelid = table_oid + and a.attname = split_part(required.qualified_column, '.', 2) + and not a.attisdropped; + if column_type is null then + missing_objects := array_append(missing_objects, required.qualified_column); + elsif column_type <> 'uuid' then + mismatched_objects := array_append(mismatched_objects, required.qualified_column || ' (' || column_type || ')'); + end if; + end loop; + + -- 2. partial indexes + for required in + select * + from ( + values + -- 20260702180000 + ( + 'document_images_document_generation_idx', + 'CREATE INDEX document_images_document_generation_idx ON public.document_images USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260702180000 + ( + 'document_table_facts_document_generation_idx', + 'CREATE INDEX document_table_facts_document_generation_idx ON public.document_table_facts USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260702180000 + ( + 'document_embedding_fields_document_generation_idx', + 'CREATE INDEX document_embedding_fields_document_generation_idx ON public.document_embedding_fields USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260702180000 + ( + 'document_index_units_document_generation_idx', + 'CREATE INDEX document_index_units_document_generation_idx ON public.document_index_units USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260702180000 + ( + 'document_memory_cards_document_generation_idx', + 'CREATE INDEX document_memory_cards_document_generation_idx ON public.document_memory_cards USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ), + -- 20260702180000 + ( + 'document_sections_document_generation_idx', + 'CREATE INDEX document_sections_document_generation_idx ON public.document_sections USING btree (document_id, index_generation_id) WHERE (index_generation_id IS NOT NULL)' + ) + ) as t(index_name, canonical_def) + loop + index_oid := to_regclass(format('public.%I', required.index_name)); + if index_oid is null then + missing_objects := array_append(missing_objects, required.index_name); + continue; + end if; + + select i.indisvalid, i.indisready, pg_get_indexdef(i.indexrelid) + into is_valid, is_ready, actual_def + from pg_index as i + where i.indexrelid = index_oid; + + if not coalesce(is_valid, false) or not coalesce(is_ready, false) then + invalid_objects := array_append(invalid_objects, required.index_name); + continue; + end if; + + -- Keep in lockstep with normalizeIndexDefinition in tests/supabase-schema.test.ts. + actual_normalized := actual_def; + actual_normalized := lower(actual_normalized); + actual_normalized := replace(actual_normalized, 'create index if not exists', 'create index'); + actual_normalized := regexp_replace(actual_normalized, ' extensions\.', ' ', 'g'); + actual_normalized := regexp_replace(actual_normalized, ' using btree', '', 'g'); + actual_normalized := regexp_replace(actual_normalized, 'where \(([^()]*)\)$', 'where \1'); + actual_normalized := replace(actual_normalized, ';', ''); + actual_normalized := regexp_replace(actual_normalized, '[[:space:]]+', ' ', 'g'); + actual_normalized := regexp_replace(actual_normalized, ' on ([^ ()]+) \(', ' on \1(', 'g'); + actual_normalized := btrim(actual_normalized); + + expected_normalized := required.canonical_def; + expected_normalized := lower(expected_normalized); + expected_normalized := replace(expected_normalized, 'create index if not exists', 'create index'); + expected_normalized := regexp_replace(expected_normalized, ' extensions\.', ' ', 'g'); + expected_normalized := regexp_replace(expected_normalized, ' using btree', '', 'g'); + expected_normalized := regexp_replace(expected_normalized, 'where \(([^()]*)\)$', 'where \1'); + expected_normalized := replace(expected_normalized, ';', ''); + expected_normalized := regexp_replace(expected_normalized, '[[:space:]]+', ' ', 'g'); + expected_normalized := regexp_replace(expected_normalized, ' on ([^ ()]+) \(', ' on \1(', 'g'); + expected_normalized := btrim(expected_normalized); + + if actual_normalized is distinct from expected_normalized then + mismatched_objects := array_append(mismatched_objects, required.index_name); + end if; + end loop; + + -- 3. function bodies, hashed exactly as the drift probe hashes them + snapshot := public.schema_drift_snapshot(); + for required in + select * + from ( + values + ('public.is_committed_artifact_generation(uuid,jsonb)', '755e8ba3a7f52efcef604074d0832dde'), + ('public.commit_document_index_generation(uuid,uuid,text,integer,integer,integer,jsonb,jsonb,jsonb)', '594b1f715fbbfa1df1b1f1183a7fef5a'), + ('public.cleanup_abandoned_document_index_generations(uuid,integer,boolean)', '4ee148dcab28d6bbe10cfa542213f0f6') + ) as t(signature, expected_hash) + loop + select f.element ->> 'def_hash' + into live_hash + from jsonb_array_elements(coalesce(snapshot -> 'functions', '[]'::jsonb)) as f(element) + where f.element ->> 'signature' = required.signature; + + if live_hash is null then + missing_objects := array_append(missing_objects, required.signature); + elsif live_hash <> required.expected_hash then + mismatched_objects := array_append(mismatched_objects, required.signature || ' def_hash ' || live_hash); + end if; + end loop; + + if cardinality(missing_objects) > 0 + or cardinality(invalid_objects) > 0 + or cardinality(mismatched_objects) > 0 then + raise exception + 'History guard 20260819110400: the columns / partial indexes / functions recorded by 20260702180000 are not in canonical form; reconcile out of band (CREATE INDEX CONCURRENTLY for a missing index on a populated table), then apply this version. Missing: %; Invalid: %; Mismatched: %', + coalesce(nullif(array_to_string(missing_objects, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(invalid_objects, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(mismatched_objects, ', '), ''), '(none)'); + end if; +end +$migration$; diff --git a/supabase/migrations/20260819110500_validate_history_function_bodies.sql b/supabase/migrations/20260819110500_validate_history_function_bodies.sql new file mode 100644 index 0000000000..abd2c315f4 --- /dev/null +++ b/supabase/migrations/20260819110500_validate_history_function_bodies.sql @@ -0,0 +1,101 @@ +-- Phase 6.2 history guard (plan section 6.2; forensics section 1.1 / 3.7 / '6.2 completion'): +-- validate the fifteen function bodies that four hand-applied, mark-applied versions defined. +-- Ledger anchor #Q5JHBJ (#316 umbrella). +-- +-- 20260702140000 fix_reset_document_index_duplicate -> reset_document_index(uuid) +-- 20260702160000 fix_invoke_agent_url_to_guc -> invoke_indexing_v3_agent(integer); its +-- ALTER DATABASE SET app.indexing_v3_agent_base_url is privilege-guarded and the function carries +-- a fallback, so the GUC is deliberately not validated +-- 20260712171500 codify_live_ahead_functions -> get_related_document_metadata, +-- get_visual_evidence_cards, match_document_chunks, match_document_chunks_hybrid, +-- match_document_chunks_text, match_document_embedding_fields_text, match_document_table_facts_text, +-- match_documents_for_query, repair_enrichment_quality_batch, repair_strict_enrichment_gate_batch, +-- run_all_visual_eval_cases, run_visual_eval_case (12) +-- 20260712173000 add_legacy_index_health_batch_repair -> backfill_legacy_index_health_batch(integer) +-- +-- Four of the twelve 20260712171500 bodies were re-created by later executed migrations +-- (match_document_chunks / match_document_chunks_hybrid / match_document_chunks_text by 20260714110000, +-- match_document_table_facts_text by 20260724120000) and 20260818110000 then set work_mem on three, so each +-- pinned def_hash is the CURRENT canonical body: the value supabase/drift-manifest.json records, which +-- live-drift proved equal to production for every public function (zero function mismatches, run +-- 32171070287 after PR #2151 merged) and the staging comparison proved equal to the chain. +-- +-- Bodies are compared by def_hash read from public.schema_drift_snapshot(), so the hashing formula +-- (pg_get_functiondef with comments and whitespace stripped) and the search_path-'' rendering are +-- identical by construction to what the weekly check compares. ACLs are not pinned here because +-- acldefault() renders the function owner, which differs on a preview branch; check:drift compares +-- ACLs on every run. +-- +-- Like 20260804110240 this migration VALIDATES and never creates or replaces a function: one +-- snapshot read, one raise naming every failure. Timeouts use SET LOCAL. + +set local search_path = public, extensions, pg_catalog; +set local lock_timeout = '5s'; +set local statement_timeout = '60s'; + +do $migration$ +declare + missing_functions text[] := array[]::text[]; + mismatched_functions text[] := array[]::text[]; + required record; + snapshot jsonb; + live_hash text; +begin + snapshot := public.schema_drift_snapshot(); + + for required in + select * + from ( + values + -- 20260702140000 + ('public.reset_document_index(uuid)', '243f3960a32db0192d1cce2ebd050004'), + -- 20260702160000 + ('public.invoke_indexing_v3_agent(integer)', '3ffdb174b4cef3905a3ed45e9b0dab6b'), + -- 20260712171500 + ('public.get_related_document_metadata(uuid[],uuid)', 'ebb4dbc57321d9c0c4c011c52fc0dde1'), + -- 20260712171500 + ('public.get_visual_evidence_cards(uuid,integer)', '107d6e5eb6d846971b48102c258ab2eb'), + -- 20260712171500 + ('public.match_document_chunks(extensions.vector,integer,double precision,uuid,uuid)', 'cdf9d685c98bc8ff731a0422c29a47a4'), + -- 20260712171500 + ('public.match_document_chunks_hybrid(extensions.vector,text,integer,double precision,uuid[],uuid)', '5902c39286335c07714e498ea31513a0'), + -- 20260712171500 + ('public.match_document_chunks_text(text,integer,uuid[],uuid)', 'd135c628720cb8a4d86c2ade4cd3b26a'), + -- 20260712171500 + ('public.match_document_embedding_fields_text(text,integer,double precision,uuid[],uuid)', '6fa9e80ed59c16922806590e67083188'), + -- 20260712171500 + ('public.match_document_table_facts_text(text,integer,uuid[],uuid)', '0ef9a5dfbde03fe6d48d9223e245aa69'), + -- 20260712171500 + ('public.match_documents_for_query(text,integer,uuid)', '8248e8d1aec23bbad35aca0ddc4dc94d'), + -- 20260712171500 + ('public.repair_enrichment_quality_batch(integer)', '2258b6a4a9fd85bdf10a768778fb5d55'), + -- 20260712171500 + ('public.repair_strict_enrichment_gate_batch(integer)', '62892dfd90b840b55e07e3bdfc988339'), + -- 20260712171500 + ('public.run_all_visual_eval_cases(integer)', '22124c67028e9957d39788548f0f358a'), + -- 20260712171500 + ('public.run_visual_eval_case(uuid,integer)', 'c0b5662d2c6f63ff05a7ed05573845fe'), + -- 20260712173000 + ('public.backfill_legacy_index_health_batch(integer)', '2edbe5e02d8585898df0f3fd8f2ade8a') + ) as t(signature, expected_hash) + loop + select f.element ->> 'def_hash' + into live_hash + from jsonb_array_elements(coalesce(snapshot -> 'functions', '[]'::jsonb)) as f(element) + where f.element ->> 'signature' = required.signature; + + if live_hash is null then + missing_functions := array_append(missing_functions, required.signature); + elsif live_hash <> required.expected_hash then + mismatched_functions := array_append(mismatched_functions, required.signature || ' def_hash ' || live_hash); + end if; + end loop; + + if cardinality(missing_functions) > 0 or cardinality(mismatched_functions) > 0 then + raise exception + 'History guard 20260819110500: function bodies recorded by 20260702140000 / 20260702160000 / 20260712171500 / 20260712173000 do not match the canonical def_hash (supabase/drift-manifest.json). Re-apply the canonical body from supabase/schema.sql out of band, then apply this version. Missing: %; Invalid: (none); Mismatched: %', + coalesce(nullif(array_to_string(missing_functions, ', '), ''), '(none)'), + coalesce(nullif(array_to_string(mismatched_functions, ', '), ''), '(none)'); + end if; +end +$migration$; diff --git a/tests/migration-history-guards.test.ts b/tests/migration-history-guards.test.ts index f26c6779a6..dcd3f5eb5b 100644 --- a/tests/migration-history-guards.test.ts +++ b/tests/migration-history-guards.test.ts @@ -63,6 +63,18 @@ function stripSql(sql: string): string { .trim(); } +/** + * Executable SQL only: comments and single-quoted string literals removed. A + * validation guard pins the canonical `create index … on …` text of the objects + * it proves as a string literal (the 20260804110240 pattern), which is data, not + * a statement — the create-index check must not mistake it for a build. + */ +function executableSql(sql: string): string { + return stripSql(sql).replace(/'(?:[^']|'')*'/g, "''"); +} + +const CREATE_INDEX_STATEMENT = /create\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?[a-z_]/i; + describe("migration-history probe and guard-migration contract", () => { it("the v2 snapshot migration exists and check:drift knows its name", () => { expect(existsSync(join(migrationsDir, HISTORY_PROBE_MIGRATION))).toBe(true); @@ -103,8 +115,12 @@ describe("migration-history probe and guard-migration contract", () => { // A validation guard never builds: it raises when the objects are // missing/invalid/mismatched (the 20260804110240 pattern). expect(guardSql, `${label}: validation guard must raise`).toMatch(/raise\s+exception/i); - expect(guardSql, `${label}: validation guard must not create the objects it validates`).not.toMatch( - /create\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?[a-z_]/i, + expect( + executableSql(guardSql), + `${label}: validation guard must not create the objects it validates`, + ).not.toMatch(CREATE_INDEX_STATEMENT); + expect(guardSql, `${label}: validation guard must scope its timeouts with set local`).toMatch( + /set\s+local\s+statement_timeout/i, ); for (const object of guard.objects ?? []) { expect(guardSql, `${label}: guard does not mention object ${object}`).toContain(object); @@ -133,6 +149,18 @@ describe("migration-history probe and guard-migration contract", () => { } }); + it("the reference validation guard 20260804110240 satisfies the validation predicate", () => { + // The pattern every validation guard copies pins canonical `create index …` + // text inside string literals; the executable-SQL check must keep accepting + // it while still rejecting a real build statement. + const reference = readMigration("20260804110240_restore_rag_search_health_indexes.sql"); + expect(reference).toMatch(/raise\s+exception/i); + expect(executableSql(reference)).not.toMatch(CREATE_INDEX_STATEMENT); + expect(executableSql(`${reference}\ncreate index if not exists oops_idx on public.documents(id);`)).toMatch( + CREATE_INDEX_STATEMENT, + ); + }); + it("no pre-contract class is used for a version at or after the contract date", () => { for (const entry of historyEntries) { if (entry.key >= GUARD_CONTRACT_VERSION) { From aceb66fc936821397175aead919a47b54ee455ad Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:27:12 +0800 Subject: [PATCH 2/2] docs(db): Phase 6.2 completion evidence, board, drift-detection live state, ledger requests Forensics section '6.2 completion' (classification, chain-replay and mutation proofs, production window paste, staging parity, live-drift run 32251326536: drift zero, job red only on the Phase 0 Align-migration-history step PGRST106), coordination board row 6, database-drift-detection live-state paragraph, #Q5JHBJ update request and a new P2 for the align step. Co-Authored-By: Claude Fable 5 --- docs/audit/live-drift-forensics-2026-08.md | 172 ++++++++++++++++++ docs/database-remediation-coordination.md | 6 +- .../bbac85a9-de92-42c5-b9e2-3b1fa2a89a56.json | 11 ++ .../cc60253d-6efe-44ab-a615-8584ab0983b6.json | 14 ++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 docs/outstanding-issues-inbox/bbac85a9-de92-42c5-b9e2-3b1fa2a89a56.json create mode 100644 docs/outstanding-issues-inbox/cc60253d-6efe-44ab-a615-8584ab0983b6.json diff --git a/docs/audit/live-drift-forensics-2026-08.md b/docs/audit/live-drift-forensics-2026-08.md index 60202b49cf..87c6c37d15 100644 --- a/docs/audit/live-drift-forensics-2026-08.md +++ b/docs/audit/live-drift-forensics-2026-08.md @@ -1778,3 +1778,175 @@ itself pins would have failed it (the check had never been exercised — no `val It now strips comments and string literals first, additionally requires `set local statement_timeout`, and pins that `20260804110240` satisfies the predicate while a real `create index` statement still fails it. + +#### Step 2 — hand-repair sweep: the chain reproduces every recorded repair (local chain replay) + +Every hand repair recorded in §2.3, §3.3, §Phase 4 and §Phase 4 completion was checked the same way: +replay the **whole** `supabase/migrations` chain in version order into the scratch +`supabase/postgres:17.6.1.127` image (`roles.sql` + storage scaffold first, each file in its own +transaction like `supabase migration up` — the local stand-in for CI's `Migration replay` and the +preview branch), then compare the replayed `schema_drift_snapshot()` to `supabase/drift-manifest.json` +with the repo's own `compareDriftSnapshots()`: + +``` +Applied 210/210 (all six 20260819110* history guards included). +Chain replay vs manifest (generated 2026-08-18T18:15:50.121Z from schema.sql 328677d1c6f3…): compared 6 extensions, 38 tables, 1 views, 93 functions, 210 indexes, 48 policies, 170 constraints, 26 triggers, 2 storage_buckets; live probe no_history_table, history rows 0. +CHAIN == MANIFEST: no unexpected drift (migration_history excluded: bare image has no history table). +``` + +So the chain now reproduces, unaided, every state that was once repaired by hand: `token_estimate` and +the `gen_random_uuid()` defaults (§3.3, via `20260818112000`), the three hybrid RPC bodies (§3.5, via +`20260818113000`), `document_chunks_content_trgm_idx` (§3.3(d) / Phase 4 step 8, via +`20260819100150`), the 20 restored indexes and 2 drops (§Phase 4 completion), the four duplicate-version +bodies (§2.4 finding 2, by ordering), and every object the fifteen no-statements versions created (the +six new guards pass at the end of the chain). **No further reconcile migration was needed and nothing +was escalated.** + +The guards were also proven able to fail, per the "checks that cannot fail" rule — seven mutants run +against the replayed database, each in its own rolled-back transaction: + +``` +MUTATION 110500 wrong def_hash: raised and named public.reset_document_index(uuid) def_hash +MUTATION 110300 wrong index def: raised and named Mismatched: documents_owner_id_covering_idx +MUTATION 110300 dropped index: raised and named Missing: audit_logs_owner_id_idx +MUTATION 110000 present index: raised and named indexes: documents_owner_hash_idx +MUTATION 110400 wrong column type: raised and named document_sections.index_generation_id (text) +MUTATION 110200 fk cascade: raised and named Mismatched: storage_cleanup_jobs_document_id_fkey +MUTATION 110100 comment removed: raised and named Missing: comment on table audit_logs +scratch image cron schema present: f (cron branch skipped here; production has pg_cron) +``` + +#### Step 3 — gates + +- `npx vitest run tests/migration-history-guards.test.ts tests/drift-detection.test.ts tests/supabase-schema.test.ts tests/search-health-index-coverage.test.ts tests/migration-history-placeholders.test.ts tests/hosted-migration-role-guard.test.ts` — `Test Files 6 passed (6) · Tests 113 passed (113)`. +- `npm run check:migration-role` — `Hosted migration-role guard passed: active hosted SQL/tooling uses postgres and immutable applied history is unchanged.` +- `npm run check:drift -- --self-test` — `check-drift: all offline self-tests passed.` +- `npm run format` — whole tree, exit 0 (committed). +- `npm run verify:pr-local` — exit 0: `Test Files 682 passed | 2 skipped (684) · Tests 7398 passed | 57 skipped (7455)`, `Offline RAG fixture and manifest validation passed (36 golden cases, 26 suites)`, `failed: (none)` — none of this host's known environmental reds fired on this run. + +#### Step 4 — production window (`sjrfecxgysukkwxsowpy`; dedicated worktree, CLI 2.114.0, never linked from the main checkout) + +`supabase db query --linked --project-ref sjrfecxgysukkwxsowpy` reached production from the unlinked +worktree (the CLI wrote only a `.temp/linked-project.json` marker, removed at the end; the main checkout +stayed on `ikoiolksxqxfxgiyqpnu` throughout). Read-only pre-flight: + +``` +db postgres · usr postgres · total_rows 204 · latest_version 20260819100300 · documents 2851 · no_statements 20 · new_versions_present 0 +no_statements_versions: 20260701010000 20260701020000 20260701030000 20260701040000 20260701060000 20260702000000 20260702100000 20260702110000 20260702120000 20260702130000 20260702140000 20260702150000 20260702160000 20260702180000 20260712165915 20260712170500 20260712171000 20260712171500 20260712172000 20260712173000 +``` + +**All six guards were dry-run read-only on production before anything was pushed and all six passed** +(`rows: []`, no error). That pass was then shown to be meaningful rather than an ignored DO block: the +`110500` guard with one deliberately wrong hash **failed on production** with `Mismatched: +public.reset_document_index(uuid) def_hash 243f3960a32db0192d1cce2ebd050004` — i.e. the live hash is the +manifest value the real guard pins — and the branches that could only be exercised on production were: +`cron_schema true · purge_job_rows 1 · rrl_comment true · claim_comment true`. + +`supabase migration list --linked --project-ref sjrfecxgysukkwxsowpy`: **204** matched rows, **0** +remote-only, pending = exactly the six new versions (`20260819110000`…`110500`), all after the remote +tip, so no `--include-all`. Then the real push (dry-run first, identical plan): + +``` +$ supabase db push --linked --project-ref sjrfecxgysukkwxsowpy --skip-vault --yes +Applying migration 20260819110000_validate_history_dropped_objects.sql... +Applying migration 20260819110100_validate_history_comments_and_retention.sql... +Applying migration 20260819110200_validate_history_document_foreign_keys.sql... +Applying migration 20260819110300_validate_history_operational_index_shapes.sql... +Applying migration 20260819110400_validate_history_index_generation_promotion.sql... +Applying migration 20260819110500_validate_history_function_bodies.sql... +{"upToDate":false,"dryRun":false,"migrations":[...6 files...],"seeds":[],"roles":[],"message":"Finished supabase db push."} +``` + +After: `migration list` **210** rows, pending **0**, remote-only **0**; `total_rows 210 · +latest_version 20260819110500 · documents 2851`; the probe still reports `ok` with **20** history rows +(the probe lists rows, the allowlist clears them); and the six new rows carry executed statements — the +CLI's per-statement shape, **not** the mark-applied shape — so none of them can ever surface in the probe: + +``` +20260819110000 validate_history_dropped_objects stmt_count 4 no_statements false +20260819110100 validate_history_comments_and_retention stmt_count 4 no_statements false +20260819110200 validate_history_document_foreign_keys stmt_count 4 no_statements false +20260819110300 validate_history_operational_index_shapes stmt_count 4 no_statements false +20260819110400 validate_history_index_generation_promotion stmt_count 4 no_statements false +20260819110500 validate_history_function_bodies stmt_count 4 no_statements false +``` + +`migration repair` was never used; no vault secret was read or written; no data row was touched. + +#### Step 5 — staging (`ikoiolksxqxfxgiyqpnu`, Phase 2 method through `db query`) + +The Supabase MCP connector was not authenticated in this session, so the §2.2 method ran over the CLI's +management-API `db query`: each file's content verbatim, then the explicit history row with the +repository's version and name in the same call; `apply_migration` and `db push` not used. Identity read +before every step (`documents = 0` abort condition, in code): + +``` +[before] staging ikoiolksxqxfxgiyqpnu · usr postgres · total_rows 204 · latest 20260819100300 · no_statements 0 · documents 0 · document_chunks 0 · new_versions_present 0 +20260819110000 · validate_history_dropped_objects · stmt_count 1 · bytes 4562 · md5 c213ec244b8a0331b10e08c1ce96242d · matches repo file true +20260819110100 · validate_history_comments_and_retention · stmt_count 1 · bytes 4697 · md5 c733b24e5f00f0ebb11168ee21d97e27 · matches repo file true +20260819110200 · validate_history_document_foreign_keys · stmt_count 1 · bytes 3662 · md5 d90bf2d0d7d9e296b7a072d1bfc94d67 · matches repo file true +20260819110300 · validate_history_operational_index_shapes · stmt_count 1 · bytes 16230 · md5 a4834c1ef3135b9b0d337473b0946552 · matches repo file true +20260819110400 · validate_history_index_generation_promotion · stmt_count 1 · bytes 9587 · md5 29bffb90c5ac9af8620bd510cff7f8ac · matches repo file true +20260819110500 · validate_history_function_bodies · stmt_count 1 · bytes 5907 · md5 5dc3494b1f79df910df6f99b13af656a · matches repo file true +[after] staging ikoiolksxqxfxgiyqpnu · usr postgres · total_rows 210 · latest 20260819110500 · no_statements 0 · documents 0 · document_chunks 0 · new_versions_present 6 +``` + +Drift comparison exactly as Phase 4 step 6 (staging `schema_drift_snapshot()` fetched over the CLI; +same manifest, allowlist, `compareDriftSnapshots()`, `historyEntryProblems` and staleness pre-check; +`--prune-stale` NOT used): + +``` +Target: staging ikoiolksxqxfxgiyqpnu · documents 0 · migrations 210 +Drift manifest: generated 2026-08-18T18:15:50.121Z from schema.sql 328677d1c6f3… +Compared 6 extensions, 38 tables, 1 views, 93 functions, 210 indexes, 48 policies, 170 constraints, 26 triggers, 2 storage_buckets against live (snapshot_version 2, probe ok, migration_history rows 0). +Stale allowlist entries (20) — no longer matching (NOT pruned): + ? [migration_history] no_statements 20260701010000 … 20260712173000 (all twenty — production's rows, as §2.4 predicts) +No unexpected drift. +EXIT=0 +``` + +#### Step 6 — live-drift dispatched on the branch: drift is ZERO; the job is red for a different, latent reason + +Dispatched on `claude/migration-history-drift-allowlist-37444c` (head `8dd014d04` — on `main` the +allowlist would still be the seeded five): **Actions run +[`32251326536`](https://github.com/BigSimmo/Database/actions/runs/32251326536)**, 2026-08-19T12:11:04Z. +Step `Compare live schema drift`: **success**: + +``` +Drift manifest: generated 2026-08-18T18:15:50.121Z from schema.sql 328677d1c6f3… +Compared 6 extensions, 38 tables, 1 views, 93 functions, 210 indexes, 48 policies, 170 constraints, 26 triggers, 2 storage_buckets against live. + ~ [migration_history] no_statements … ×20 (five superseded + fifteen validation — every row allowed, each printed with its guard reason) +No unexpected schema drift between live and supabase/schema.sql. +``` + +| Category | §Phase 4 step 7 (`32171070287`) | This run (`32251326536`) | +| -------------------- | ------------------------------: | -----------------------: | +| `missing_live` | 0 | **0** | +| `unexpected_live` | 0 | **0** | +| function `def_hash` | 1 (merge-pending) | **0** | +| `migration_history` | 15 unexpected | **0** | +| **Total unexpected** | **16** | **0** | + +**`#316`'s live-drift finding set is empty for the first time since 2026-07-26.** + +**But the job concluded `failure`, and that is honest, not drift.** The next step, `Align migration +history for Supabase Preview` (`npm run check:migration-history`, added by Phase 0 in PR #1939), ran for +the **first time ever** — on every run since it was added the compare step failed first and it was +`skipped`, and the last green run (`29700973962`, 2026-07-19) predates it. It fails with: + +``` +Local migration versions: 210 +Unable to read remote schema_migrations via Accept-Profile (status 406: {"code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: public, graphql_public","message":"Invalid schema: supabase_migrations"}) +``` + +`scripts/check-migration-history-alignment.ts` reads `supabase_migrations.schema_migrations` through +PostgREST with `Accept-Profile: supabase_migrations`, which this project has never exposed to the Data +API — so the step can only ever fail here. The routing job therefore kept pinned issue #1963 open ("Job +result: failure") even though the findings block it captured is empty. This is a latent Phase 0 tooling +defect that zero drift has now exposed, **not** a change this task should absorb into a migration PR: +the fix is either an owner dashboard decision (expose `supabase_migrations` read-only to the service +role) or rewriting the alignment read onto the management API / `supabase migration list` (which needs +the `SUPABASE_ACCESS_TOKEN` secret of `#183`), or a service-role RPC listing versions (a new migration +with its own window). Queued as its own ledger item from this session; until it is fixed the weekly job +will stay red on that step alone and the pinned issue will not self-close — **the drift block, which is +what the issue was opened for, is clear.** diff --git a/docs/database-remediation-coordination.md b/docs/database-remediation-coordination.md index 52697981a0..01d97bf10c 100644 --- a/docs/database-remediation-coordination.md +++ b/docs/database-remediation-coordination.md @@ -91,7 +91,7 @@ trusted as-is._ | 3 — Reconciliation (reframed) | ☑ COMPLETE 2026-08-18 — repo-side, staging-proven, **and live on production** (auto-applied; see §3.7) | **#2106** merged `72aa18865` (migrations `20260818110000` work_mem ×10, `111000` eight schema-only objects, `112000` chain-stale columns; forensics §3.1–3.6); **#2111** merged `9b52eb075` (staging proof + `20260818113000` forward-codify of three hybrid RPC bodies the chain never received; `#316` final + `#183` rider + 2 captures + 3 cancels queued) | D1 codify-as-live, D2 exemption granted. Manifest `def_hash` == live production hash 10/10 — the ten RPC "divergences" are closed as a mirror gap. §3.3: `token_estimate` was chain-stale (added by `112000`, no-op on prod); `rag_visual_eval_*` id defaults rebound to `pg_catalog.gen_random_uuid()` (no-op on prod); (d) production's restored `document_chunks_content_trgm_idx` (`coalesce(content,'')` form, `8499c3d3…`) IS canonical — staging carries the 2026-06-06 form; repair = Phase 4.4 guard migration + staging drop-and-recreate. Staging at 199 migrations, single drift residual = that index. **New finding:** `20260712000000`'s owner step never landed for three hybrid RPCs — chain-stale, forward-codified by `113000` (not a production hole). Tooling: `scripts/check-drift.ts:192` clips column diffs to 240 chars (never names the column) — queued P3; PR template lacks `RAG impact:` guidance — queued P3. | | 4 — Index restoration | ☑ COMPLETE 2026-08-19 — production **and** staging | **#2151** (branch `claude/db-remediation-phase4-indexes-a1661a`, head `5d3dca4dd`); forensics §Phase 4 completion; live-drift run `32171070287` | Owner-authorised off-peak window, D4 **OFF** so nothing auto-deployed. All **20** `missing_live` indexes rebuilt with `CREATE INDEX CONCURRENTLY` from canonical definitions — Batch A 14/14, Batch B 6/6, every one `indisvalid AND indisready` with normalised `pg_get_indexdef` matching canonical; zero invalid builds, zero retries, zero skips, zero lock waits; no transactional build attempted; `#102` held out. Both `unexpected_live` orphans **DROPPED** concurrently (not codified) — each is a strict leading-column subset of a present canonical index and the chain already commands both drops (`20260620000000`, `20260703030000`/`20260708000000`); neither is in `schema.sql`, so the drop moves live _into_ mirror agreement. Live now 210 indexes vs manifest 210, zero invalid anywhere. Codified by real `db push` (never `migration repair`; all four history rows carry executed statements): `20260819100000`/`100100` guard Batch A/B, **`20260819100200` discharges the 4.4 trigram-guard debt** that `20260804110240` never covered, `20260819100300` takes `required_indexes` 22→30 adopting **all 8** Phase 6.3 monitor-candidates (`search-health-unmonitored-indexes.json` 44→36, no `monitor-candidate` left; production `ok: true`). **Staging also closed:** `document_chunks_content_trgm_idx` rebuilt into the canonical `coalesce(content,'')` form (§3.3(d)), the four migrations applied by the Phase 2 method with all md5s matching, `check:drift` vs staging **green, 0 unexpected (was 19)**, corpus untouched. Live-drift **37 → 16**: `missing_live` **20 → 0**, `unexpected_live` **2 → 0**; the lone function mismatch is merge-pending and proven (live `def_hash 85df52de…` == this branch's regenerated manifest). **Two escalations, not absorbed:** PITR is OFF on production (new P2 queued) and the `migration_history` block did **not** drop — no allowlist entry was earned (empty intersection), so the 15 stay `#Q5JHBJ`'s work. | | 5 — Measure | ◐ PARTIAL (incident) | #1960 §Phase 5 | Text fast path 31,610 → 1,535 ms; hybrid 8,519 ms residual. Full EXPLAIN set, `work_mem` before/after (only if D1 changes values), `check:production-readiness` pending. | -| 6 — Future-proofing | ◐ REPO-SIDE COMPLETE 2026-08-18 | **#2058** merged `9c660af1f` (14 paths verified on `main`); follow-up #2090 merged `79e5d2a68` | 6.1 migration `20260818090000_schema_drift_snapshot_history_probe.sql` built + mirrored + manifest regenerated, **LIVE on production since 2026-08-18** (auto-applied on merge; §3.7 verified probe `ok`, 20 `migration_history` rows = 15 unallowlisted §1.1 + 5 seeded). Next for 6.2: fail-fast guard migrations for the 15 — never bare allowlisting. 6.2 contract + `supabase/drift-allowlist.json` (5 seeded superseded guards; ~18 §1.1 versions deliberately NOT allowlisted → first post-deploy run reports them; fix = fail-fast guard migrations, never bare allowlisting). 6.3 `tests/search-health-index-coverage.test.ts` + unmonitored list (44 entries, 8 monitor-candidates). New P2 row queued (inbox `d6ce8a1d`). Stale branch `claude/database-drift-allowlist-48839e` (tip `3fe1deafe`) was recreated after merge — redundant, owner may delete. | +| 6 — Future-proofing | ☑ COMPLETE 2026-08-19 (6.1 live; 6.2 guards live on both tiers; 6.3 repo-side) | **#2058** merged `9c660af1f` (6.1 / 6.2 contract / 6.3); follow-up #2090 merged `79e5d2a68`; **Phase 6.2 guards: branch `claude/migration-history-drift-allowlist-37444c`** (forensics §"6.2 completion"); live-drift run `32251326536` | 6.1 probe LIVE on production since 2026-08-18. **6.2 done 2026-08-19 (`#Q5JHBJ`)**: all fifteen no-statements versions classified — every one `validation`, none `superseded`/`no_ddl` (no single later executed migration re-creates every object; `COMMENT ON` is a catalog write) — and guarded by six fail-fast migrations `20260819110000`…`110500` (dropped objects / comments + cron / document FKs / 46 operational index shapes / `index_generation_id` promotion / 15 function `def_hash`es read from `schema_drift_snapshot()`). Allowlist 5 superseded + 15 validation = 20. Proof: whole chain replayed into the scratch image `Applied 210/210`, `CHAIN == MANIFEST` (every hand repair in §2.3/§3.3/Phase 4 reproduced by the chain; no reconcile needed), seven mutants raise; all six dry-ran green on production and a mutant failed there; real `db push` (pending = exactly six; rows `no_statements false`; 210 rows; corpus untouched); staging by the Phase 2 method, six md5-matched, drift green. **Live-drift on the branch: `No unexpected schema drift`, all 20 history rows allowed — `#316`'s finding set is EMPTY for the first time since 2026-07-26.** Job still red on the Phase 0 `Align migration history` step (PGRST106: `supabase_migrations` not exposed to PostgREST; it ran for the first time ever) — queued as its own P2, owner decision (expose schema / use access token / RPC). Stale branch `claude/database-drift-allowlist-48839e` (tip `3fe1deafe`) redundant, owner may delete. | | 7 — Deferred | ☐ not started | — | Each its own session, per playbook. | **Owner decisions (2026-08-18):** D1 = codify-as-live (128 MB on the four hybrids, 64 MB on the @@ -128,7 +128,9 @@ open PR #2130 queues 7 more requests but carries **no** reconcile transaction. O link a dedicated worktree for production reads and `supabase unlink` after; `supabase db query --linked --project-ref ` works read-only via the management API without a DB password. -**Where the programme stands after Phase 4 (2026-08-19).** The index track of `#316` is closed on both +**Where the programme stands after Phase 6.2 (2026-08-19, later the same day).** Drift is **green on both tiers**: the fifteen `migration_history` rows now carry `validation` guards, live-drift run `32251326536` (dispatched on the 6.2 branch) reports `No unexpected schema drift` with all 20 history rows allowed, and the staging comparison is green with the 20 production-scoped entries reading stale as designed. `#316`'s finding set is empty for the first time since 2026-07-26. **Two things still need the owner, not a worker:** (1) the live-drift job is red on its `Align migration history` step alone — `check:migration-history` cannot read `supabase_migrations` over PostgREST (PGRST106), a Phase 0 step that had never run before because drift always failed first; until it is fixed (expose the schema read-only / wire `SUPABASE_ACCESS_TOKEN` `#183` and use `supabase migration list` / add a versions RPC) the weekly job stays red and pinned issue #1963 will not self-close even though its drift block is empty; (2) PITR is still off on production (Phase 4 escalation). **Next dispatches:** merge the 6.2 PR (its CI `Migration replay` and Supabase Preview are the last chain proofs), then Phase 5 close-out (after-EXPLAIN set, `#231` re-test on healthy latency, `check:production-readiness`), then one serialized `issues:reconcile`. Every future migration still needs its own approved window and its own `db push` (D4 OFF). + +**Where the programme stands after Phase 4 (2026-08-19).****Where the programme stands after Phase 4 (2026-08-19).** The index track of `#316` is closed on both tiers and staging is at full parity, so the remaining live-drift findings are exactly one category: the fifteen `migration_history` no-statements rows, which are `#Q5JHBJ` and need fail-fast guard migrations, never bare allowlisting. Phase 4 earned no allowlist entry for any of them — measured, not diff --git a/docs/outstanding-issues-inbox/bbac85a9-de92-42c5-b9e2-3b1fa2a89a56.json b/docs/outstanding-issues-inbox/bbac85a9-de92-42c5-b9e2-3b1fa2a89a56.json new file mode 100644 index 0000000000..b69a22f5b6 --- /dev/null +++ b/docs/outstanding-issues-inbox/bbac85a9-de92-42c5-b9e2-3b1fa2a89a56.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "bbac85a9-de92-42c5-b9e2-3b1fa2a89a56", + "createdOn": "2026-08-19", + "action": "update", + "payload": { + "id": "#Q5JHBJ", + "detail": "PHASE 6.2 COMPLETE 2026-08-19 (owner-authorised production window). All fifteen no-statements versions classified and guarded with class validation (none earned superseded or no_ddl: no single later executed migration re-creates every object, and COMMENT ON is a catalog write, not an empty file). Six fail-fast guard migrations per 20260804110240: 20260819110000 dropped objects (absence of 7 functions + 4 indexes), 110100 catalog comments + purge-rag-retrieval-logs cron, 110200 three document FKs, 110300 forty-six operational index shapes, 110400 the index_generation_id promotion (6 columns + 6 indexes + 3 def_hashes), 110500 fifteen function def_hashes read from schema_drift_snapshot() itself. Allowlist now 20 entries (5 superseded + 15 validation). PROOF: full chain replay into the scratch image Applied 210/210 and CHAIN == MANIFEST (zero unexpected drift) -- every hand repair in sections 2.3/3.3/Phase 4 is reproduced by the chain, no reconcile migration needed; seven mutants raise and name their object; all six dry-ran green on production and a mutant fails there naming reset_document_index def_hash; production push real (migration list pending = exactly six, db push applied, rows stmt_count 4 no_statements false, 210 rows, documents 2851 untouched); staging by the Phase 2 method, six md5-matched rows, 210 rows, no_statements 0, drift comparison green. LIVE-DRIFT run 32251326536 on the branch: Compare step SUCCESS, all 20 history rows allowed, No unexpected schema drift -- #316's finding set is EMPTY for the first time since 2026-07-26. RESIDUAL, not this row's: the job still concludes failure because the Phase 0 step Align migration history (check:migration-history) ran for the first time ever and cannot read supabase_migrations over PostgREST (PGRST106, schema not exposed) -- queued as its own item. Evidence: forensics section '6.2 completion'.", + "baseRowFingerprint": "c504f6aab26c77516a2abc1ded4682f21181ce26ad9f201109cf92731e881d9c" + } +} diff --git a/docs/outstanding-issues-inbox/cc60253d-6efe-44ab-a615-8584ab0983b6.json b/docs/outstanding-issues-inbox/cc60253d-6efe-44ab-a615-8584ab0983b6.json new file mode 100644 index 0000000000..00d71a3f93 --- /dev/null +++ b/docs/outstanding-issues-inbox/cc60253d-6efe-44ab-a615-8584ab0983b6.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "cc60253d-6efe-44ab-a615-8584ab0983b6", + "createdOn": "2026-08-19", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "live-drift's Align migration history step fails on PGRST106 (supabase_migrations not exposed to PostgREST), so the job stays red and pinned issue #1963 cannot self-close even with zero drift", + "detail": "Exposed 2026-08-19 by Phase 6.2: live-drift run 32251326536 reported No unexpected schema drift (all 20 history rows allowed) but concluded failure because the next step, Align migration history for Supabase Preview (npm run check:migration-history, scripts/check-migration-history-alignment.ts, added in Phase 0 PR #1939), ran for the first time ever -- it was skipped on every earlier run because the compare step failed first, and the last green run (29700973962, 2026-07-19) predates it. It reads supabase_migrations.schema_migrations through PostgREST with Accept-Profile, which this project has never exposed (406 PGRST106: only public, graphql_public). The routing job therefore keeps #1963 open on job result even with an empty findings block. OPTIONS (owner decision): (a) expose supabase_migrations read-only to the service role in the dashboard; (b) rewrite the read onto the management API / supabase migration list using the SUPABASE_ACCESS_TOKEN secret (#183); (c) add a service-role RPC listing versions (new migration, own window). Until fixed, weekly live-drift stays red on that step alone; drift itself is green. Evidence: forensics section 6.2 completion step 6.", + "source": "Phase 6.2 session 2026-08-19; live-drift run 32251326536", + "issueUlid": "01M0CZ8XPMM54C4NDHX8VP2H8Z" + } +}