From 836a54e4766724958aff05d45e926f2e58dc5ad4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:35:25 +0800 Subject: [PATCH 1/2] fix: align live migration history Record the generated production migration versions and keep the legacy unfenced commit overload unavailable to service_role. Regenerated drift state and verified live drift, readiness, concurrency, and retrieval contracts. --- docs/branch-review-ledger.md | 1 + docs/forward-codify-retrieval-rpcs-workorder.md | 8 ++++---- supabase/drift-manifest.json | 7 +++---- ...0260713062107_restore_text_fallback_lexical_score.sql} | 0 ...l => 20260713062125_fence_index_generation_commit.sql} | 0 ...> 20260713062132_purge_expired_rag_response_cache.sql} | 0 ... => 20260713062139_route_enrichment_through_agent.sql} | 0 supabase/schema.sql | 3 +-- tests/supabase-schema.test.ts | 3 +++ 9 files changed, 12 insertions(+), 10 deletions(-) rename supabase/migrations/{20260713110000_restore_text_fallback_lexical_score.sql => 20260713062107_restore_text_fallback_lexical_score.sql} (100%) rename supabase/migrations/{20260713120000_fence_index_generation_commit.sql => 20260713062125_fence_index_generation_commit.sql} (100%) rename supabase/migrations/{20260713121000_purge_expired_rag_response_cache.sql => 20260713062132_purge_expired_rag_response_cache.sql} (100%) rename supabase/migrations/{20260713122000_route_enrichment_through_agent.sql => 20260713062139_route_enrichment_through_agent.sql} (100%) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 15dd0df13..e8b137b87 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -50,3 +50,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-11 | codex/repository-review-remediation | 70ec6409a11a85e1678eb4b320519624673a94a0 | comprehensive repository report remediation | Revalidated all 17 findings from the 2026-07-09 comprehensive review. Remediated the current workflow injection, document scope, numeric faithfulness, ingestion lease/ownership, transactional enrichment replacement, request and upload budgets, browser identity isolation, PHI retention, public DTO, cache cancellation/versioning, evidence labelling, modal focus, misleading controls, telemetry, orphan-module issues, and two server/client loading-boundary failures exposed during browser QA. The runbook filename was already fixed on the reviewed head. | TypeScript, lint, focused Vitest (68/68), full offline Vitest (1,607/1,607; 1 skipped), production build, client-bundle secret scan, Docker schema replay and regenerated drift manifest, isolated full migration reset, local lease-reclaim concurrency proof, cache/enrichment SQL smoke, configured production-readiness (`READY`), Chromium document-scope/modal QA (4/4), manual disabled-control accessibility snapshots, and `git diff --check` passed. Read-only live drift found 26 unexpected differences, including the three unapplied remediation functions; no live mutation was performed. | | 2026-07-11 | codex/responsive-accessibility-audit | 66883b7c86f606e617db4bee2bab6f85fff59bdc | responsive and accessibility audit | P2 fixed: the mobile expandable clinical table no longer wraps semantic table content in a duplicate ARIA button, and its full-screen dialog now traps keyboard focus while preserving Escape dismissal and focus return. Added responsive ARIA and focus regression coverage. No additional high-confidence responsive or accessibility defect was reproduced across audited primary app modes and 320px-1440px widths. | Multi-width DOM/geometry/contrast audit; a11y media (2/2); overlap (12/12); table Vitest (6/6); TypeScript; lint/static checks; full Vitest (1,598 passed, 1 skipped); `npm run verify:ui` (132/132); Prettier; `git diff --check`. Provider checks skipped. | | 2026-07-13 | codex/repository-review-remediation | b72cefd2f5c0da79788cc0f8d0d40837c711ae92 | live-drift reconciliation and release review | Reconciled production migration history and live-ahead governance/retrieval definitions without mutating live; removed the migration-version collision; made captured OUT-signature changes fresh-replay-safe; preserved production ACLs; added a forward lexical-score correction; and reduced read-only live drift from 27 differences to five changes fully explained by the unapplied remediation migrations. No remaining high-confidence source defect was found in the reviewed scope. | Docker schema replay and regenerated manifest; isolated full Supabase migration reset; focused Vitest 74/74; full Vitest 1,712 passed/1 skipped; lint; typecheck; production build and client-bundle secret scan; configured production-readiness READY; targeted Chromium scope/modal/control QA 4/4; read-only live drift; Supabase security advisor clear. Live apply not run because authorization remained read-only. | +| 2026-07-13 | codex/repository-review-remediation | 452275824294564a1e08e6bec169bd4af744d09a | live migration apply and post-apply review | Applied the four reviewed forward migrations to `Clinical KB Database`, aligned repository filenames to the generated production versions, and corrected the schema snapshot so the legacy unfenced commit overload remains inaccessible to `service_role`. Live drift is clean and no active ingestion/enrichment overlap or duplicate open ingestion group was found. | `check:drift` clean; production readiness READY; Docker schema replay; focused concurrency/retrieval Vitest 166/166; offline RAG 36 fixtures and 60/60 contract tests; M13 live guard; retrieval-owner migration and schema health; live lexical retrieval 12 results with truthful scores; direct live concurrency/ACL assertions. Full provider retrieval-quality evaluation exceeded the local command window; deterministic live retrieval checks passed. | diff --git a/docs/forward-codify-retrieval-rpcs-workorder.md b/docs/forward-codify-retrieval-rpcs-workorder.md index b225a38db..1db0afe4a 100644 --- a/docs/forward-codify-retrieval-rpcs-workorder.md +++ b/docs/forward-codify-retrieval-rpcs-workorder.md @@ -1,9 +1,9 @@ # Work-order — forward-codify live-ahead retrieval RPC bodies (drift backlog #0) -**Status: reconciled in source on 2026-07-13; live apply remains pending explicit write approval.** -The production-current definitions were captured read-only, preserved under their actual migration -versions, replayed successfully in scratch PostgreSQL, and removed from the drift allowlist. The -four forward remediation migrations remain unapplied to live. +**Status: complete.** The production-current definitions were captured read-only, preserved under +their actual migration versions, and replayed successfully in scratch PostgreSQL. The four forward +remediation migrations were applied to live on 2026-07-13 as `20260713062107`, `20260713062125`, +`20260713062132`, and `20260713062139`. Post-apply drift and production-readiness checks passed. Historical blockers at authoring time (2026-07-12): diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index 4646e717f..14475412d 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,8 +1,8 @@ { - "generated_at": "2026-07-12T19:37:18.660Z", + "generated_at": "2026-07-13T06:24:25.373Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127", - "schema_sha256": "954cbd1034a1aaef729db486497ae3c2216680de1360cbf2957e6f6faec2e572", + "schema_sha256": "4b3d331b28acce1d7656db0b31afa8ac533a8c9e3f1f2c89f42f298f2b16cb25", "replay_seconds": 13, "snapshot": { "views": [ @@ -6191,8 +6191,7 @@ }, { "acl": [ - "postgres=X/postgres", - "service_role=X/postgres" + "postgres=X/postgres" ], "def_hash": "594b1f715fbbfa1df1b1f1183a7fef5a", "signature": "public.commit_document_index_generation(uuid,uuid,text,integer,integer,integer,jsonb,jsonb,jsonb)" diff --git a/supabase/migrations/20260713110000_restore_text_fallback_lexical_score.sql b/supabase/migrations/20260713062107_restore_text_fallback_lexical_score.sql similarity index 100% rename from supabase/migrations/20260713110000_restore_text_fallback_lexical_score.sql rename to supabase/migrations/20260713062107_restore_text_fallback_lexical_score.sql diff --git a/supabase/migrations/20260713120000_fence_index_generation_commit.sql b/supabase/migrations/20260713062125_fence_index_generation_commit.sql similarity index 100% rename from supabase/migrations/20260713120000_fence_index_generation_commit.sql rename to supabase/migrations/20260713062125_fence_index_generation_commit.sql diff --git a/supabase/migrations/20260713121000_purge_expired_rag_response_cache.sql b/supabase/migrations/20260713062132_purge_expired_rag_response_cache.sql similarity index 100% rename from supabase/migrations/20260713121000_purge_expired_rag_response_cache.sql rename to supabase/migrations/20260713062132_purge_expired_rag_response_cache.sql diff --git a/supabase/migrations/20260713122000_route_enrichment_through_agent.sql b/supabase/migrations/20260713062139_route_enrichment_through_agent.sql similarity index 100% rename from supabase/migrations/20260713122000_route_enrichment_through_agent.sql rename to supabase/migrations/20260713062139_route_enrichment_through_agent.sql diff --git a/supabase/schema.sql b/supabase/schema.sql index 4e5802409..1c8e9b3df 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -4826,8 +4826,7 @@ revoke execute on function public.jsonb_merge_deep(jsonb, jsonb) from public, an grant execute on function public.jsonb_merge_deep(jsonb, jsonb) to service_role; revoke execute on function public.apply_document_metadata_patch(uuid, jsonb) from public, anon, authenticated; grant execute on function public.apply_document_metadata_patch(uuid, jsonb) to service_role; -revoke execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) from public, anon, authenticated; -grant execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) to service_role; +revoke execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) from public, anon, authenticated, service_role; revoke execute on function public.cleanup_abandoned_document_index_generations(uuid, integer, boolean) from public, anon, authenticated; grant execute on function public.cleanup_abandoned_document_index_generations(uuid, integer, boolean) to service_role; revoke execute on function public.is_committed_document_generation(uuid, jsonb) from public, anon, authenticated; diff --git a/tests/supabase-schema.test.ts b/tests/supabase-schema.test.ts index 9f77b700b..23781334a 100644 --- a/tests/supabase-schema.test.ts +++ b/tests/supabase-schema.test.ts @@ -257,6 +257,9 @@ describe("Supabase schema Data API grants", () => { ); } expect(schema).toContain( + "revoke execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) from public, anon, authenticated, service_role", + ); + expect(schema).not.toContain( "grant execute on function public.commit_document_index_generation(uuid, uuid, text, integer, integer, integer, jsonb, jsonb, jsonb) to service_role", ); expect(atomicReindexMigration).toContain("atomic reindex patch did not match match_document_chunks_hybrid"); From 9ed693e968271ef9ba0d8939ffd2f5556e05ad35 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:06:57 +0800 Subject: [PATCH 2/2] fix(db): harden enrichment job locking --- docs/branch-review-ledger.md | 4 +- ...forward-codify-retrieval-rpcs-workorder.md | 8 +-- supabase/drift-manifest.json | 11 ++-- ...3062139_route_enrichment_through_agent.sql | 54 ++++++++++++------- supabase/schema.sql | 54 ++++++++++++------- tests/supabase-schema.test.ts | 23 ++++++++ 6 files changed, 104 insertions(+), 50 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b429d272a..77f056e66 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -50,5 +50,5 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-11 | codex/repository-review-remediation | 70ec6409a11a85e1678eb4b320519624673a94a0 | comprehensive repository report remediation | Revalidated all 17 findings from the 2026-07-09 comprehensive review. Remediated the current workflow injection, document scope, numeric faithfulness, ingestion lease/ownership, transactional enrichment replacement, request and upload budgets, browser identity isolation, PHI retention, public DTO, cache cancellation/versioning, evidence labelling, modal focus, misleading controls, telemetry, orphan-module issues, and two server/client loading-boundary failures exposed during browser QA. The runbook filename was already fixed on the reviewed head. | TypeScript, lint, focused Vitest (68/68), full offline Vitest (1,607/1,607; 1 skipped), production build, client-bundle secret scan, Docker schema replay and regenerated drift manifest, isolated full migration reset, local lease-reclaim concurrency proof, cache/enrichment SQL smoke, configured production-readiness (`READY`), Chromium document-scope/modal QA (4/4), manual disabled-control accessibility snapshots, and `git diff --check` passed. Read-only live drift found 26 unexpected differences, including the three unapplied remediation functions; no live mutation was performed. | | 2026-07-11 | codex/responsive-accessibility-audit | 66883b7c86f606e617db4bee2bab6f85fff59bdc | responsive and accessibility audit | P2 fixed: the mobile expandable clinical table no longer wraps semantic table content in a duplicate ARIA button, and its full-screen dialog now traps keyboard focus while preserving Escape dismissal and focus return. Added responsive ARIA and focus regression coverage. No additional high-confidence responsive or accessibility defect was reproduced across audited primary app modes and 320px-1440px widths. | Multi-width DOM/geometry/contrast audit; a11y media (2/2); overlap (12/12); table Vitest (6/6); TypeScript; lint/static checks; full Vitest (1,598 passed, 1 skipped); `npm run verify:ui` (132/132); Prettier; `git diff --check`. Provider checks skipped. | | 2026-07-13 | codex/repository-review-remediation | b72cefd2f5c0da79788cc0f8d0d40837c711ae92 | live-drift reconciliation and release review | Reconciled production migration history and live-ahead governance/retrieval definitions without mutating live; removed the migration-version collision; made captured OUT-signature changes fresh-replay-safe; preserved production ACLs; added a forward lexical-score correction; and reduced read-only live drift from 27 differences to five changes fully explained by the unapplied remediation migrations. No remaining high-confidence source defect was found in the reviewed scope. | Docker schema replay and regenerated manifest; isolated full Supabase migration reset; focused Vitest 74/74; full Vitest 1,712 passed/1 skipped; lint; typecheck; production build and client-bundle secret scan; configured production-readiness READY; targeted Chromium scope/modal/control QA 4/4; read-only live drift; Supabase security advisor clear. Live apply not run because authorization remained read-only. | -| 2026-07-13 | codex/repository-review-remediation | 452275824294564a1e08e6bec169bd4af744d09a | live migration apply and post-apply review | Applied the four reviewed forward migrations to `Clinical KB Database`, aligned repository filenames to the generated production versions, and corrected the schema snapshot so the legacy unfenced commit overload remains inaccessible to `service_role`. Live drift is clean and no active ingestion/enrichment overlap or duplicate open ingestion group was found. | `check:drift` clean; production readiness READY; Docker schema replay; focused concurrency/retrieval Vitest 166/166; offline RAG 36 fixtures and 60/60 contract tests; M13 live guard; retrieval-owner migration and schema health; live lexical retrieval 12 results with truthful scores; direct live concurrency/ACL assertions. Full provider retrieval-quality evaluation exceeded the local command window; deterministic live retrieval checks passed. | -| 2026-07-13 | codex/fix-48h-review-findings-current | 49735663370735a60870d065ed0de3b9d34e077f | last-48-hours PR remediation | Revalidated the last-48-hours findings on current main after PRs #538 and #540; retained only unique fixes across auth/cache isolation, stale-response protection, upload/routing/UI behavior, RAG coalescing, telemetry, worktree tooling, and SAST enforcement. No remaining high-confidence local defect was found in the changed scope. The approved live drift check reported only the five differences already explained by unapplied migrations from #540. | Focused Vitest 107/107; `npm run verify:pr-local` (1,762 passed, 1 skipped; production build and client-bundle scan; offline RAG 60/60); critical Chromium 8/8; live `check:drift`; `git diff --check`. Full Chromium remains advisory after the earlier runner hang; the required critical subset passed on current main. | +| 2026-07-13 | codex/repository-review-remediation | 452275824294564a1e08e6bec169bd4af744d09a | live migration apply and post-apply review | Applied the four reviewed forward migrations to `Clinical KB Database`, aligned repository filenames to the generated production versions, and corrected the schema snapshot so the legacy unfenced commit overload remains inaccessible to `service_role`. Live drift is clean and no active ingestion/enrichment overlap or duplicate open ingestion group was found. | Ran `npm run check:drift`: passed clean. Ran `npm run check:production-readiness`: READY. Ran Docker schema replay: passed. Ran focused concurrency/retrieval Vitest: 166/166 passed. Ran offline RAG: 36 fixtures and 60/60 contract tests passed. Ran M13, retrieval-owner, schema-health, lexical-retrieval, concurrency, and ACL live probes: passed; lexical retrieval returned 12 truthfully scored results. Not completed: full provider retrieval-quality evaluation exceeded the local command window; deterministic live retrieval checks passed. | +| 2026-07-13 | codex/fix-48h-review-findings-current | 49735663370735a60870d065ed0de3b9d34e077f | last-48-hours PR remediation | Revalidated the last-48-hours findings on current main after PRs #538 and #540; retained only unique fixes across auth/cache isolation, stale-response protection, upload/routing/UI behavior, RAG coalescing, telemetry, worktree tooling, and SAST enforcement. No remaining high-confidence local defect was found in the changed scope. The approved live drift check reported only the five differences already explained by unapplied migrations from #540. | Focused Vitest 107/107; `npm run verify:pr-local` (1,762 passed, 1 skipped; production build and client-bundle scan; offline RAG 60/60); critical Chromium 8/8; live `check:drift`; `git diff --check`. Full Chromium remains advisory after the earlier runner hang; the required critical subset passed on current main. | diff --git a/docs/forward-codify-retrieval-rpcs-workorder.md b/docs/forward-codify-retrieval-rpcs-workorder.md index 1db0afe4a..6c5373b3b 100644 --- a/docs/forward-codify-retrieval-rpcs-workorder.md +++ b/docs/forward-codify-retrieval-rpcs-workorder.md @@ -1,9 +1,9 @@ # Work-order — forward-codify live-ahead retrieval RPC bodies (drift backlog #0) -**Status: complete.** The production-current definitions were captured read-only, preserved under -their actual migration versions, and replayed successfully in scratch PostgreSQL. The four forward -remediation migrations were applied to live on 2026-07-13 as `20260713062107`, `20260713062125`, -`20260713062132`, and `20260713062139`. Post-apply drift and production-readiness checks passed. +**Status: complete.** The production-current definitions were captured read-only and preserved under +their actual migration versions. Ran the scratch PostgreSQL replay: passed. Ran the reviewed live +migration apply: applied `20260713062107`, `20260713062125`, `20260713062132`, and `20260713062139` +on 2026-07-13. Ran `npm run check:drift`: passed. Ran `npm run check:production-readiness`: READY. Historical blockers at authoring time (2026-07-12): diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index 5dc1c446c..acf341110 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-07-13T07:24:51.999Z", + "generated_at": "2026-07-13T08:06:40.593Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127", - "schema_sha256": "d88316aa2edeb597b439663b323ab169b12c0e75bc57eafcf489b1d340bacf47", - "replay_seconds": 17, + "schema_sha256": "0a30a1ea8c8dbb3fd520115d28a17d99c94d9bc5164efe6c287c3b48969be81b", + "replay_seconds": 16, "snapshot": { "views": [ { @@ -6271,8 +6271,7 @@ }, { "acl": [ - "postgres=X/postgres", - "service_role=X/postgres" + "postgres=X/postgres" ], "def_hash": "594b1f715fbbfa1df1b1f1183a7fef5a", "signature": "public.commit_document_index_generation(uuid,uuid,text,integer,integer,integer,jsonb,jsonb,jsonb)" @@ -6595,7 +6594,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "9020d937b00df3a36669e870c4aae17b", + "def_hash": "baa8c0bda37cc561026baecdf1cca1c2", "signature": "public.request_indexing_v3_enrichment(uuid,uuid)" }, { diff --git a/supabase/migrations/20260713062139_route_enrichment_through_agent.sql b/supabase/migrations/20260713062139_route_enrichment_through_agent.sql index 5625cd72c..1659ad542 100644 --- a/supabase/migrations/20260713062139_route_enrichment_through_agent.sql +++ b/supabase/migrations/20260713062139_route_enrichment_through_agent.sql @@ -9,7 +9,39 @@ set search_path = public, extensions, pg_temp as $$ declare v_job_id uuid; + v_job_status text; begin + -- Validate without taking the document lock. The job row is created/locked + -- first below so request and claim paths share one lock order. + perform 1 + from public.documents + where id = p_document_id and owner_id = p_owner_id and status = 'indexed'; + if not found then + raise exception using errcode = 'P0001', message = 'document_not_available_for_enrichment'; + end if; + + insert into public.indexing_v3_agent_jobs ( + document_id, status, enrichment_status, attempt_count, max_attempts, version, metadata + ) values ( + p_document_id, 'pending', 'pending', 0, 3, 'visual-core-v3', '{}'::jsonb + ) + on conflict (document_id) do nothing + returning id, status into v_job_id, v_job_status; + + if v_job_id is null then + select id, status into v_job_id, v_job_status + from public.indexing_v3_agent_jobs + where document_id = p_document_id + for update; + end if; + + if v_job_id is null then + raise exception using errcode = 'P0001', message = 'enrichment_job_unavailable'; + end if; + if v_job_status = 'processing' then + raise exception using errcode = 'P0001', message = 'enrichment_active'; + end if; + perform 1 from public.documents where id = p_document_id and owner_id = p_owner_id and status = 'indexed' @@ -34,29 +66,13 @@ begin next_run_at = null, last_error = null, updated_at = now() - where document_id = p_document_id - and status <> 'processing' - returning id into v_job_id; - - if v_job_id is null then - if exists ( - select 1 from public.indexing_v3_agent_jobs - where document_id = p_document_id and status = 'processing' - ) then - raise exception using errcode = 'P0001', message = 'enrichment_active'; - end if; - insert into public.indexing_v3_agent_jobs ( - document_id, status, enrichment_status, attempt_count, max_attempts, version, metadata - ) values ( - p_document_id, 'pending', 'pending', 0, 3, 'visual-core-v3', '{}'::jsonb - ) - returning id into v_job_id; - end if; + where id = v_job_id; update public.documents set metadata = (coalesce(metadata, '{}'::jsonb) - 'indexing_v3_agent_locked_by' - 'indexing_v3_agent_locked_at' - - 'indexing_v3_agent_last_error' - 'indexing_v3_agent_next_run_at') + - 'indexing_v3_agent_last_error' - 'indexing_v3_agent_next_run_at' + - 'indexing_v3_agent_attempt_count' - 'indexing_v3_agent_max_attempts') || jsonb_build_object( 'enrichment_status', 'pending', 'indexing_v3_agent_status', 'pending', diff --git a/supabase/schema.sql b/supabase/schema.sql index 7ceaec52d..5b86248e5 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -5384,7 +5384,39 @@ set search_path = public, extensions, pg_temp as $$ declare v_job_id uuid; + v_job_status text; begin + -- Validate without taking the document lock. The job row is created/locked + -- first below so request and claim paths share one lock order. + perform 1 + from public.documents + where id = p_document_id and owner_id = p_owner_id and status = 'indexed'; + if not found then + raise exception using errcode = 'P0001', message = 'document_not_available_for_enrichment'; + end if; + + insert into public.indexing_v3_agent_jobs ( + document_id, status, enrichment_status, attempt_count, max_attempts, version, metadata + ) values ( + p_document_id, 'pending', 'pending', 0, 3, 'visual-core-v3', '{}'::jsonb + ) + on conflict (document_id) do nothing + returning id, status into v_job_id, v_job_status; + + if v_job_id is null then + select id, status into v_job_id, v_job_status + from public.indexing_v3_agent_jobs + where document_id = p_document_id + for update; + end if; + + if v_job_id is null then + raise exception using errcode = 'P0001', message = 'enrichment_job_unavailable'; + end if; + if v_job_status = 'processing' then + raise exception using errcode = 'P0001', message = 'enrichment_active'; + end if; + perform 1 from public.documents where id = p_document_id and owner_id = p_owner_id and status = 'indexed' @@ -5409,29 +5441,13 @@ begin next_run_at = null, last_error = null, updated_at = now() - where document_id = p_document_id - and status <> 'processing' - returning id into v_job_id; - - if v_job_id is null then - if exists ( - select 1 from public.indexing_v3_agent_jobs - where document_id = p_document_id and status = 'processing' - ) then - raise exception using errcode = 'P0001', message = 'enrichment_active'; - end if; - insert into public.indexing_v3_agent_jobs ( - document_id, status, enrichment_status, attempt_count, max_attempts, version, metadata - ) values ( - p_document_id, 'pending', 'pending', 0, 3, 'visual-core-v3', '{}'::jsonb - ) - returning id into v_job_id; - end if; + where id = v_job_id; update public.documents set metadata = (coalesce(metadata, '{}'::jsonb) - 'indexing_v3_agent_locked_by' - 'indexing_v3_agent_locked_at' - - 'indexing_v3_agent_last_error' - 'indexing_v3_agent_next_run_at') + - 'indexing_v3_agent_last_error' - 'indexing_v3_agent_next_run_at' + - 'indexing_v3_agent_attempt_count' - 'indexing_v3_agent_max_attempts') || jsonb_build_object( 'enrichment_status', 'pending', 'indexing_v3_agent_status', 'pending', diff --git a/tests/supabase-schema.test.ts b/tests/supabase-schema.test.ts index 23781334a..478018379 100644 --- a/tests/supabase-schema.test.ts +++ b/tests/supabase-schema.test.ts @@ -84,6 +84,10 @@ const indexingV3AgentJobsMigration = readFileSync( new URL("../supabase/migrations/20260702190000_indexing_v3_agent_jobs_table.sql", import.meta.url), "utf8", ).replace(/\s+/g, " "); +const routeEnrichmentThroughAgentMigration = readFileSync( + new URL("../supabase/migrations/20260713062139_route_enrichment_through_agent.sql", import.meta.url), + "utf8", +).replace(/\s+/g, " "); const clinicalRegistryRecordsMigration = readFileSync( new URL("../supabase/migrations/20260703020000_clinical_registry_records.sql", import.meta.url), "utf8", @@ -387,6 +391,25 @@ describe("Supabase schema Data API grants", () => { expect(schema).toContain("grant execute on function public.invoke_indexing_v3_agent(integer) to service_role"); }); + it("keeps enrichment requests conflict-safe with job-first locking and complete reset metadata", () => { + for (const sql of [schema, routeEnrichmentThroughAgentMigration]) { + const start = sql.indexOf("create or replace function public.request_indexing_v3_enrichment"); + const end = sql.indexOf("$$;", start); + const body = sql.slice(start, end); + expect(start).toBeGreaterThanOrEqual(0); + expect(end).toBeGreaterThan(start); + expect(body).toContain("on conflict (document_id) do nothing"); + expect(body).toContain("select id, status into v_job_id, v_job_status"); + expect(body).toContain("from public.indexing_v3_agent_jobs"); + expect(body).toContain("for update"); + expect(body).toContain("v_job_status = 'processing'"); + expect(body.indexOf("from public.indexing_v3_agent_jobs")).toBeLessThan( + body.lastIndexOf("from public.documents"), + ); + expect(body).toContain("'indexing_v3_agent_attempt_count' - 'indexing_v3_agent_max_attempts'"); + } + }); + it("drops the stale duplicate ingestion_job_stages document index", () => { for (const sql of [schema, dropDuplicateStageIndexMigration]) { expect(sql).toContain("drop index if exists public.ingestion_job_stages_doc_idx");