From 2c1d54dc903da07a586b4185294587c17322bcfc Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 7 Nov 2026 08:33:30 +0800 Subject: [PATCH 1/2] fix(db): bind provider facts to pending readmission authority --- supabase/drift-manifest.json | 8 +-- ...pending_readmission_provider_authority.sql | 34 ++++++++++++ supabase/schema.sql | 36 +++++++++++++ tests/pending-reindex-authority.test.ts | 11 ++++ tests/sql/pending-reindex-authority.sql | 54 ++++++++++++++++++- 5 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 supabase/migrations/20261107001000_pending_readmission_provider_authority.sql diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index 5173611452..71a3bdcfb9 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-11-05T02:55:57.204Z", + "generated_at": "2026-11-07T00:21:10.881Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127@sha256:be60aee15997daca475b710b734bc6bfe52cd544dcd7e9fd2ff58210b6747d83", - "schema_sha256": "f36f7fac5757e214d4cbc2dd86388ff80e25ec1ccb30533070c0e98b61d75ac2", - "replay_seconds": 22, + "schema_sha256": "02a9c65cb1aac72fee3847320103940d073f0f9945e93ca39b0b5319fbe011da", + "replay_seconds": 32, "snapshot": { "views": [ { @@ -20476,7 +20476,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "21536e668b04513cb0dd89fcff792df9", + "def_hash": "b2706874c70aa052aee195b932f8b57a", "signature": "public.read_ingestion_generation_inventory(jsonb,uuid)" }, { diff --git a/supabase/migrations/20261107001000_pending_readmission_provider_authority.sql b/supabase/migrations/20261107001000_pending_readmission_provider_authority.sql new file mode 100644 index 0000000000..105e577958 --- /dev/null +++ b/supabase/migrations/20261107001000_pending_readmission_provider_authority.sql @@ -0,0 +1,34 @@ +-- DRAFT: provider facts observe a readmission's declared pending authority while +-- retrieval continues to use the old flat ranking metadata until completion. +-- Validate the pending admission binding and unchanged live snapshot first. +-- Existing event/source/lease/permission checks and ACLs remain unchanged. +begin; +set local lock_timeout='5s'; +set local statement_timeout='120s'; + +do $$ +declare signature regprocedure:='public.read_ingestion_generation_inventory(jsonb,uuid)'::regprocedure; + definition text;needle text;replacement text; +begin + definition:=pg_get_functiondef(signature); + needle:=$old$ for pa_pair in select * from (values + ('source_role','sourceRole'),('authority_basis','authorityBasis'),('licence_policy','licencePolicy'),$old$; + replacement:=$new$ -- The immutable event above binds the declaration to this document/source. + -- A changed live snapshot (including withdrawn rights) must never be hidden + -- by overlaying pending values. Missing/foreign/malformed blocks fail closed. + if pa_metadata ? 'pending_reindex_authority' then + if not ingestion_generation_evidence.pending_reindex_authority_valid(pa_metadata) + or exists(select 1 from jsonb_each(pa_metadata#>'{pending_reindex_authority,prior}') prior + where coalesce(pa_metadata->prior.key,'null'::jsonb) is distinct from prior.value) + then raise exception 'provider_authority_admission_mismatch';end if; + pa_metadata:=pa_metadata||(pa_metadata#>'{pending_reindex_authority,values}'); + end if; + for pa_pair in select * from (values + ('source_role','sourceRole'),('authority_basis','authorityBasis'),('licence_policy','licencePolicy'),$new$; + if length(definition)-length(replace(definition,needle,''))<>length(needle) + or strpos(definition,'pending_reindex_authority')<>0 + or not exists(select 1 from pg_proc where oid=signature and proowner='postgres'::regrole) + then raise exception 'pending_provider_authority_preimage';end if; + execute replace(definition,needle,replacement); +end $$; +commit; diff --git a/supabase/schema.sql b/supabase/schema.sql index 199d671f57..0c7d97505e 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -41875,3 +41875,39 @@ end $$; -- Patched validators retain their postgres ownership and existing ACLs. commit; + +-- Source migration: 20261107001000_pending_readmission_provider_authority.sql +-- DRAFT: provider facts observe a readmission's declared pending authority while +-- retrieval continues to use the old flat ranking metadata until completion. +-- Validate the pending admission binding and unchanged live snapshot first. +-- Existing event/source/lease/permission checks and ACLs remain unchanged. +begin; +set local lock_timeout='5s'; +set local statement_timeout='120s'; + +do $$ +declare signature regprocedure:='public.read_ingestion_generation_inventory(jsonb,uuid)'::regprocedure; + definition text;needle text;replacement text; +begin + definition:=pg_get_functiondef(signature); + needle:=$old$ for pa_pair in select * from (values + ('source_role','sourceRole'),('authority_basis','authorityBasis'),('licence_policy','licencePolicy'),$old$; + replacement:=$new$ -- The immutable event above binds the declaration to this document/source. + -- A changed live snapshot (including withdrawn rights) must never be hidden + -- by overlaying pending values. Missing/foreign/malformed blocks fail closed. + if pa_metadata ? 'pending_reindex_authority' then + if not ingestion_generation_evidence.pending_reindex_authority_valid(pa_metadata) + or exists(select 1 from jsonb_each(pa_metadata#>'{pending_reindex_authority,prior}') prior + where coalesce(pa_metadata->prior.key,'null'::jsonb) is distinct from prior.value) + then raise exception 'provider_authority_admission_mismatch';end if; + pa_metadata:=pa_metadata||(pa_metadata#>'{pending_reindex_authority,values}'); + end if; + for pa_pair in select * from (values + ('source_role','sourceRole'),('authority_basis','authorityBasis'),('licence_policy','licencePolicy'),$new$; + if length(definition)-length(replace(definition,needle,''))<>length(needle) + or strpos(definition,'pending_reindex_authority')<>0 + or not exists(select 1 from pg_proc where oid=signature and proowner='postgres'::regrole) + then raise exception 'pending_provider_authority_preimage';end if; + execute replace(definition,needle,replacement); +end $$; +commit; diff --git a/tests/pending-reindex-authority.test.ts b/tests/pending-reindex-authority.test.ts index 9e7de3d3fc..01329a8155 100644 --- a/tests/pending-reindex-authority.test.ts +++ b/tests/pending-reindex-authority.test.ts @@ -15,6 +15,17 @@ function functionBody(sql: string, header: string) { } describe("deferred readmission ranking authority migration", () => { + it("keeps provider facts pending-aware without changing leases, permissions, completions or readmission", () => { + const name = "20261107001000_pending_readmission_provider_authority.sql"; + const forward = read(`supabase/migrations/${name}`); + expect(forward).toContain("public.read_ingestion_generation_inventory(jsonb,uuid)"); + expect(forward).toContain("pending_reindex_authority_valid(pa_metadata)"); + expect(forward).toContain("coalesce(pa_metadata->prior.key,'null'::jsonb) is distinct from prior.value"); + expect(forward).toContain("pa_metadata:=pa_metadata||(pa_metadata#>'{pending_reindex_authority,values}')"); + expect(forward).toContain("pending_provider_authority_preimage"); + expect(forward).not.toMatch(/\b(?:grant|revoke|create function|update public|insert into)\b/i); + expect(read("supabase/schema.sql")).toContain(`-- Source migration: ${name}\n${forward}`); + }); it("changes the authority digest only by reading pending values for the three flat keys it already bound", () => { const header = "create or replace function public.lock_ingestion_document_authority("; const previous = functionBody( diff --git a/tests/sql/pending-reindex-authority.sql b/tests/sql/pending-reindex-authority.sql index d33572c330..fb85331f98 100644 --- a/tests/sql/pending-reindex-authority.sql +++ b/tests/sql/pending-reindex-authority.sql @@ -12,7 +12,7 @@ insert into public.documents(id,owner_id,title,file_name,file_type,file_size,sto '{"corpus_scope":"uploaded_local","admission_event_id":"7a000000-0000-4000-8000-000000000020","admission_policy_version":"trusted-document-admission-v1","provider_processing":"permitted","authority":{"providerProcessing":"permitted","licencePolicy":"review_required","sourceRole":"clinical_guideline","rightsEvidenceRef":"synthetic"},"licence_policy":"review_required","source_role":"clinical_guideline","rights_evidence_ref":"synthetic"}'::jsonb), ('7a000000-0000-4000-8000-000000000003','7a000000-0000-4000-8000-000000000009','Synthetic readmission','readmit.pdf','application/pdf',10, '7a000000-0000-4000-8000-000000000009/readmit.pdf',repeat('c',64),'indexed', - '{"corpus_scope":"uploaded_local","content_mode":"indexed_content","licence_policy":"review_required","source_role":"clinical_reference","publisher":"Superseded synthetic publisher","document_status":"outdated"}'::jsonb); + '{"corpus_scope":"uploaded_local","content_mode":"indexed_content","licence_policy":"review_required","source_role":"clinical_reference","publisher":"Superseded synthetic publisher","document_status":"outdated","content_hash":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","confidentiality_scope":"guidelines-only"}'::jsonb); set local session_replication_role=origin; -- 1. Without a pending block the digest input is byte-identical to the previous formula. @@ -69,9 +69,53 @@ begin or not ingestion_generation_evidence.pending_reindex_authority_valid(after) then raise exception 'TEST_PENDING_BLOCK_WRONG %',pending;end if; end $$; +-- Composed boundary: real readmission -> current leased attempt -> provider facts. +-- No provider, embedding or real source is used. Before the forward fix this +-- raises provider_authority_admission_mismatch on the deferred publisher/role. +do $$ +declare doc uuid:='7a000000-0000-4000-8000-000000000003';j public.ingestion_jobs; + generation uuid:='7a000000-0000-4000-8000-000000000041';fence jsonb;facts jsonb;m jsonb;bad jsonb;key text; +begin + select * into strict j from public.ingestion_jobs where document_id=doc; + update public.ingestion_jobs set status='processing',locked_by='synthetic-provider-facts',locked_at=clock_timestamp(),lease_duration_seconds=600, + lease_token='7a000000-0000-4000-8000-000000000040',lease_expires_at=clock_timestamp()+interval '10 minutes', + lease_expected_generation_id=null,lease_write_generation_id=generation, + lease_authority_digest=public.lock_ingestion_document_authority(doc,false)->>'authority_digest' + where id=j.id returning * into j; + fence:=jsonb_build_object('version','ingestion-provider-authority-read-v1','fence',jsonb_build_object( + 'job_kind','node','job_id',j.id,'document_id',doc,'worker_id',j.locked_by,'lease_token',j.lease_token, + 'expected_generation_id',null,'authority_digest',j.lease_authority_digest)); + facts:=public.read_ingestion_generation_inventory(fence,generation); + if facts#>>'{providerAuthority,declaredAuthority,publisher}' is distinct from 'Synthetic publisher' + or facts#>>'{providerAuthority,declaredAuthority,sourceRole}' is distinct from 'clinical_guideline' + or facts#>>'{providerAuthority,declaredAuthority,currentness}' is distinct from 'current' + then raise exception 'TEST_PENDING_PROVIDER_FACTS_MISSING';end if; + select metadata into m from public.documents where id=doc; + if m->>'publisher' is distinct from 'Superseded synthetic publisher' or m->>'document_status' is distinct from 'outdated' + then raise exception 'TEST_PROVIDER_FACTS_CHANGED_LIVE_RANKING';end if; + -- These mutations are transaction-local synthetic data only. Recompute the + -- fence where needed so rejection is proved at the pending/provider boundary. + foreach key in array array['foreign','malformed','values','withdrawal','stale'] loop + bad:=case key + when 'foreign' then jsonb_set(m,'{pending_reindex_authority,admissionEventId}','"7a000000-0000-4000-8000-000000000099"') + when 'malformed' then jsonb_set(m,'{pending_reindex_authority,version}','"unknown"') + when 'values' then jsonb_set(m,'{pending_reindex_authority,values,publisher}','"Unbound synthetic publisher"') + when 'withdrawal' then m||'{"licence_policy":"index_forbidden"}'::jsonb + else m||'{"publisher":"Changed live publisher"}'::jsonb end; + update public.documents set metadata=bad where id=doc; + begin + perform public.read_ingestion_generation_inventory(fence,generation); + raise exception 'TEST_BAD_PROVIDER_PENDING_ACCEPTED %',key; + exception when raise_exception then + if sqlerrm not in ('provider_authority_admission_mismatch','provider_authority_current_attempt_required') then raise;end if; + end; + end loop; + update public.documents set metadata=m where id=doc; +end $$; + -- 3. Applying is digest-neutral, writes the declared values, clears the block and is idempotent. do $$ -declare doc uuid:='7a000000-0000-4000-8000-000000000003';before text;after text;m jsonb; +declare doc uuid:='7a000000-0000-4000-8000-000000000003';before text;after text;m jsonb;j public.ingestion_jobs;facts jsonb; begin before:=(public.lock_ingestion_document_authority(doc,false))->>'authority_digest'; if before is null then raise exception 'TEST_PENDING_DOCUMENT_NOT_LOCKABLE';end if; @@ -82,6 +126,12 @@ begin or m->'review_date' is distinct from 'null'::jsonb then raise exception 'TEST_PENDING_NOT_APPLIED %',m;end if; after:=(public.lock_ingestion_document_authority(doc,false))->>'authority_digest'; if after is distinct from before then raise exception 'TEST_APPLY_MOVED_DIGEST';end if; + select * into strict j from public.ingestion_jobs where document_id=doc; + facts:=public.read_ingestion_generation_inventory(jsonb_build_object('version','ingestion-provider-authority-read-v1','fence', + jsonb_build_object('job_kind','node','job_id',j.id,'document_id',doc,'worker_id',j.locked_by,'lease_token',j.lease_token, + 'expected_generation_id',null,'authority_digest',j.lease_authority_digest)),j.lease_write_generation_id); + if facts#>>'{providerAuthority,declaredAuthority,publisher}' is distinct from 'Synthetic publisher' + then raise exception 'TEST_APPLIED_NONPENDING_PROVIDER_FACTS_CHANGED';end if; perform ingestion_generation_evidence.apply_pending_reindex_authority(doc); if (select metadata from public.documents where id=doc) is distinct from m then raise exception 'TEST_APPLY_NOT_IDEMPOTENT';end if; end $$; From 9cde4de092afa38549419edc46824917c721eaf9 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:00:50 +0800 Subject: [PATCH 2/2] Seal approved pending-authority forward migration --- supabase/applied-migration-hashes.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/supabase/applied-migration-hashes.json b/supabase/applied-migration-hashes.json index fb4f8f39ba..606bbb8646 100644 --- a/supabase/applied-migration-hashes.json +++ b/supabase/applied-migration-hashes.json @@ -1,7 +1,7 @@ { "note": "sha256 of every migration this repository has shipped, with newlines normalised. An entry whose hash changes means an applied migration was edited, which cannot reach the live database. See scripts/check-migration-immutability.mjs.", "generator": "scripts/check-migration-immutability.mjs", - "sealed_at": "2026-11-06T22:37:39.020Z", + "sealed_at": "2026-11-07T00:50:50.537Z", "versions": { "20260527000000_bulk_ingestion.sql": "813e8e314064a6dcbaaa55e2765a8ce901d5783b304b4a7af7bb7417a07daf7c", "20260527001000_documents_policy_cleanup.sql": "559c26f3172bf75e545a41adef75538b2bf648ea7d53c8eb3d3ea7c244c65a48", @@ -359,6 +359,7 @@ "20261104000000_managed_full_pdf_summary_projection.sql": "568613b9b33116bfe2ce6eff84e2eddb30d05a29b240c46305e51394c8d1e9c7", "20261105000000_managed_reviewed_rendered_pdf.sql": "db78a109c166c2bce3f60b7cfacd945800a9556898b17035d46481ade347b0f5", "20261106184724_defer_readmission_ranking_authority.sql": "8d5f352c2f9a9212090e03fd69beff0133f32a4ce75d2dc0274fbe41a5efa2ee", - "20261106223559_full_pdf_packet_parent_lookups.sql": "b1adaefb172f6e857f5dfaae7c432e7f004555f6395b545f7c85837b5bba4eb7" + "20261106223559_full_pdf_packet_parent_lookups.sql": "b1adaefb172f6e857f5dfaae7c432e7f004555f6395b545f7c85837b5bba4eb7", + "20261107001000_pending_readmission_provider_authority.sql": "03bc83824e73b6100274202073dbf90d08eee076e8a2edf86f786ac343b8b786" } }