From 203dfe837b35cfb117ee28b05ac624b7d6e40bc4 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 28 Aug 2026 18:54:50 +0900 Subject: [PATCH 1/3] feat(ask): persist public claim admission Require an exact cited public post and PROV-O evidence binding before Global Ask sends an opted-in claim to the existing verification client. Signed-off-by: Codex --- CHANGELOG.md | 4 + backend/app/global_ask_queue.py | 96 ++++++++++++++++--- ...15-global-ask-public-claim-verification.md | 5 + .../0269-persisted-public-claim-admission.md | 55 +++++++++++ docs/adr/README.md | 1 + docs/product-requirements.md | 7 +- lineageweave/public_claim_envelope.py | 64 +++++++++++++ migrations/0257_public_claim_envelope.sql | 88 +++++++++++++++++ .../rollback/0257_public_claim_envelope.sql | 5 + tests/test_global_ask_queue.py | 55 +++++++++++ tests/test_migration_replay.py | 17 ++++ tests/test_public_claim_envelope.py | 36 +++++++ 12 files changed, 420 insertions(+), 13 deletions(-) create mode 100644 docs/adr/0269-persisted-public-claim-admission.md create mode 100644 lineageweave/public_claim_envelope.py create mode 100644 migrations/0257_public_claim_envelope.sql create mode 100644 migrations/rollback/0257_public_claim_envelope.sql create mode 100644 tests/test_public_claim_envelope.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e8fce83..c328e7b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project are documented here. Format follows ## [Unreleased] +- Global Ask public verification now admits only bounded, provenance-bearing + persisted claims attached to exact cited public posts; missing admission + fails closed without token-overlap egress. + ### Added - Evidence Operations now presents cited claim, rebid, handover, external, diff --git a/backend/app/global_ask_queue.py b/backend/app/global_ask_queue.py index 9a32cd5d7..bd0d9ffcd 100644 --- a/backend/app/global_ask_queue.py +++ b/backend/app/global_ask_queue.py @@ -52,6 +52,10 @@ cited_post_summaries, historical_body_limitations, ) +from lineageweave.public_claim_envelope import ( + PersistedPublicClaimEnvelope, + envelope_from_authorized_row, +) from lineageweave.semantic_query import NullSemanticQueryClient, SemanticQueryClient from lineageweave.temporal_expressions import resolve_korean_relative_time @@ -103,6 +107,30 @@ _logger = logging.getLogger(__name__) +_AUTHORIZED_PUBLIC_CLAIM_ENVELOPES_SQL = """ + select envelope.public_claim_envelope_id, + envelope.source_post_id, + envelope.claim_kind_code, + envelope.claim_text + from public_claim_envelope envelope + join source_post post on post.post_id = envelope.source_post_id + join provenance_assertion assertion + on assertion.assertion_id = envelope.provenance_assertion_id + and assertion.relation_code = 'prov_was_derived_from' + join provenance_resource_binding evidence + on evidence.resource_id = assertion.object_resource_id + and evidence.node_type_code = 'node_post' + and evidence.node_id = envelope.source_post_id + where envelope.egress_eligible + and post.visibility_code = 'public' + and envelope.source_post_id = any($1::uuid[]) + and ($2::timestamptz is null or ( + envelope.created_at <= $2 and post.created_at <= $2 + )) + order by envelope.created_at, envelope.public_claim_envelope_id + limit 4 +""" + class _SafeJobError(Exception): """Failure whose bounded message is safe to persist for the requester.""" @@ -188,16 +216,29 @@ async def _verify_public_claims( *, verify_external: bool, client: ClaimVerificationClient, + persisted_envelopes: tuple[PersistedPublicClaimEnvelope, ...] | None = None, ) -> tuple[str, tuple[ClaimVerificationResult, ...]]: - """Verify only cited claims explicitly marked safe for public egress.""" + """Verify only cited claims explicitly marked safe for public egress. + + Production passes persisted envelopes. The legacy source projection is + retained only as an explicit compatibility seam for callers that have not + adopted the admission store; Global Ask never uses that heuristic path. + """ if not verify_external: return VERIFICATION_SKIPPED, () cited_ids = frozenset(cited_post_ids) + claims = ( + tuple(envelope.verification_candidate() for envelope in persisted_envelopes) + if persisted_envelopes is not None + else tuple( + claim + for claim in public_claim_candidates(sources, question) + if set(claim.source_post_ids).issubset(cited_ids) + ) + ) claims = tuple( - claim - for claim in public_claim_candidates(sources, question) - if set(claim.source_post_ids).issubset(cited_ids) + claim for claim in claims if set(claim.source_post_ids).issubset(cited_ids) ) if not claims: return VERIFICATION_NO_PUBLIC_CLAIMS, () @@ -219,6 +260,28 @@ async def _verify_public_claims( ) +async def load_authorized_public_claim_envelopes( + conn: asyncpg.Connection, + cited_post_ids: list[str], + *, + knowledge_cutoff: datetime | None, +) -> tuple[PersistedPublicClaimEnvelope, ...]: + """Load bounded persisted claims for exact cited public evidence posts.""" + + if not cited_post_ids: + return () + rows = await conn.fetch( + _AUTHORIZED_PUBLIC_CLAIM_ENVELOPES_SQL, + cited_post_ids, + knowledge_cutoff, + ) + return tuple( + envelope + for row in rows + if (envelope := envelope_from_authorized_row(row)) is not None + ) + + async def load_job_visibility( conn: asyncpg.Connection, job_id: str, account_id: str ) -> tuple[set[str], set[str], bool, bool]: @@ -372,6 +435,7 @@ def can_see(row: asyncpg.Record) -> bool: [], verify_external=verify_external, client=verification_client, + persisted_envelopes=(), ) delivery = build_ask_delivery("", (), ()) return { @@ -433,14 +497,16 @@ def can_see(row: asyncpg.Record) -> bool: _ASK_RETRY_MESSAGE, ) from exc cited_ids = list(answer.cited_post_ids) - verification_status, external_claims = await _verify_public_claims( - question_text, - usable_sources, - cited_ids, - verify_external=verify_external, - client=verification_client, - ) async with pool.acquire() as conn: + persisted_envelopes = ( + await load_authorized_public_claim_envelopes( + conn, + cited_ids, + knowledge_cutoff=knowledge_cutoff, + ) + if verify_external + else () + ) if knowledge_cutoff is None: lineage_graph = await lineage_graphs_for_posts(conn, can_see, cited_ids) images = await cited_post_images(conn, cited_ids) @@ -452,6 +518,14 @@ def can_see(row: asyncpg.Record) -> bool: cited_ids, checked_by=knowledge_cutoff, ) + verification_status, external_claims = await _verify_public_claims( + question_text, + usable_sources, + cited_ids, + verify_external=verify_external, + client=verification_client, + persisted_envelopes=persisted_envelopes, + ) cited_posts = cited_post_summaries(usable_sources, cited_ids) cited_events = cited_post_events(usable_sources, cited_ids) cited_evidence = cited_post_evidence(usable_sources, cited_ids) diff --git a/docs/adr/0215-global-ask-public-claim-verification.md b/docs/adr/0215-global-ask-public-claim-verification.md index ca70f566e..c3e0eda50 100644 --- a/docs/adr/0215-global-ask-public-claim-verification.md +++ b/docs/adr/0215-global-ask-public-claim-verification.md @@ -31,6 +31,11 @@ carried by a cited public source. Private sources, Keyman/person facts, raw source hints, source bodies, TEPP artifacts, fast-mlsirm artifacts, prompts, credentials, and uncited facts never form a public query. +ADR 0269 strengthens admission: the production queue now requires a persisted, +PROV-O-bound public-claim envelope for an exact cited post. Question-token +overlap is retained only as legacy library compatibility and is not a runtime +egress decision. + SearXNG retrieves at most five bounded snippets for at most four claims. Result URLs must be HTTP(S), must not be search pages, localhost, `.local`, or literal non-global addresses, and are never fetched by LineageWeave. The untrusted diff --git a/docs/adr/0269-persisted-public-claim-admission.md b/docs/adr/0269-persisted-public-claim-admission.md new file mode 100644 index 000000000..ded6de975 --- /dev/null +++ b/docs/adr/0269-persisted-public-claim-admission.md @@ -0,0 +1,55 @@ +# ADR 0269: Persist public-claim admission before external verification + +## Status + +Accepted + +## Context + +ADR 0215 defines opt-in public verification and keeps external evidence +separate from internal authority. Its first implementation nominated semantic +facts by token overlap with the question. Token overlap is neither provenance +nor a governed claim-admission decision, and it can change when wording changes. + +The abandoned draft PR #679 proposed replacing that implementation wholesale. +The current Global Ask queue, cutoff behavior, authorization scope, SearXNG +validation, and contextual-orchestrator verifier have since evolved and remain +authoritative. Only the persisted admission boundary is still missing. + +## Decision + +`public_claim_envelope` stores one bounded claim kind, exact claim text, source +post, PROV-O `prov:wasDerivedFrom` assertion, and egress decision. The evidence +resource must bind to that same source post. Only organization presence, public +event, and public relationship kinds are admitted; person, Keyman, measurement, +prompt, and source-body payloads have no storage code. + +Production Global Ask loads at most four envelopes whose source post is both +public and cited in the completed answer. The per-question opt-in remains the +durable consent boundary. A cutoff excludes envelopes or source posts created +after that cutoff. Changing a post from public revokes egress eligibility. + +The persisted envelope supplies candidates to the existing ADR 0215 verifier. +It does not replace SearXNG URL validation, contextual-orchestrator adjudication, +or the distinction between external URLs and internal post citations. No claim +is inferred from question-token overlap in the production path. When no current, +authorized envelope exists, verification reports no public claims and performs +no external request. + +## Consequences + +- Public egress admission is stable, reviewable, and provenance-bearing. +- Existing verification transport and outcome contracts remain unchanged. +- A producer must persist a governed envelope before a claim becomes eligible; + absence stays unavailable rather than being repaired heuristically. +- Draft PR #679 remains historical evidence for the missing boundary and is not + merged wholesale over the current semantic stack. + +## References + +Lebo, T., Sahoo, S., & McGuinness, D. (Eds.). (2013). *PROV-O: The PROV +ontology*. World Wide Web Consortium. https://www.w3.org/TR/prov-o/ + +Thorne, J., Vlachos, A., Christodoulopoulos, C., & Mittal, A. (2018). FEVER: A +large-scale dataset for fact extraction and verification. In *Proceedings of +NAACL-HLT 2018* (pp. 809–819). https://doi.org/10.18653/v1/N18-1074 diff --git a/docs/adr/README.md b/docs/adr/README.md index 44bd443fd..2aedc44e0 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -21,6 +21,7 @@ decision from them. | [`storybook-inventory.md`](../storybook-inventory.md) | [0118](0118-uiux-standard-guide-v3-design-overhaul.md), [0184](0184-ontology-provenance-explorer.md), [0222](0222-project-nodes-in-ontology-neighborhood.md), [0256](0256-evidence-bearing-voice-combinations.md) | | [`POSTGRESQL_CONCURRENCY_REFERENCES.md`](../doctoring/POSTGRESQL_CONCURRENCY_REFERENCES.md) | [0204](0204-analysis-run-short-transaction-delivery.md), [0213](0213-global-ask-embedding-pool-release.md) | | [`GLOBAL_ASK_PUBLIC_VERIFICATION_REFERENCES.md`](../doctoring/GLOBAL_ASK_PUBLIC_VERIFICATION_REFERENCES.md) | [0215](0215-global-ask-public-claim-verification.md) | +| Persisted public-claim admission | [0269](0269-persisted-public-claim-admission.md) | | [`GLOBAL_ASK_KNOWLEDGE_CUTOFF_REFERENCES.md`](../doctoring/GLOBAL_ASK_KNOWLEDGE_CUTOFF_REFERENCES.md) | [0216](0216-global-ask-knowledge-cutoff.md) | | [`GLOBAL_ASK_QUERY_REWRITE_REFERENCES.md`](../doctoring/GLOBAL_ASK_QUERY_REWRITE_REFERENCES.md) | [0217](0217-evidence-constrained-semantic-query-rewrite.md) | | [`MCP_GLOBAL_ASK_REFERENCES.md`](../doctoring/MCP_GLOBAL_ASK_REFERENCES.md) | [0218](0218-current-contract-mcp-global-ask.md) | diff --git a/docs/product-requirements.md b/docs/product-requirements.md index a911445b7..305da5704 100644 --- a/docs/product-requirements.md +++ b/docs/product-requirements.md @@ -386,8 +386,9 @@ stale evidence from a previously opened post. ### PRD-FR-5A — Opt-in public claim verification - Persist an explicit per-question opt-in before any external search begins. -- Nominate only cited, public semantic/KG facts; source bodies, private facts, - personal facts, and measurement outputs never become external queries. +- Admit only persisted, provenance-bearing claims for exact cited public posts; + source bodies, private facts, personal facts, measurement outputs, and claims + nominated from question-token overlap never become external queries (ADR 0269). - Retrieve bounded public evidence through SearXNG and adjudicate through contextual-orchestrator's verification mode. - Report supported, refuted, and not-enough-information outcomes without @@ -398,6 +399,8 @@ stale evidence from a previously opened post. Acceptance: leaving the control off causes no public request; hidden or uncited facts cause no public request; unavailable services fail closed; and each displayed public judgment retains its originating internal evidence IDs. +An absent or unauthorized persisted envelope performs no external request and +reports that no public claim is available rather than fabricating admission. ### PRD-FR-5B — Knowledge-cutoff Global Ask diff --git a/lineageweave/public_claim_envelope.py b/lineageweave/public_claim_envelope.py new file mode 100644 index 000000000..a33d960d4 --- /dev/null +++ b/lineageweave/public_claim_envelope.py @@ -0,0 +1,64 @@ +"""Persisted admission envelopes for public Global Ask verification. + +The envelope decides which already-cited public assertion may leave the +workspace boundary. Retrieval and adjudication remain owned by the existing +claim-verification clients; this module never derives a claim from question +tokens or source text. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from .claim_verification import PublicClaimCandidate + +ADMITTED_PUBLIC_CLAIM_KINDS = frozenset( + { + "claim_organization_presence", + "claim_public_event", + "claim_public_relationship", + } +) + + +@dataclass(frozen=True) +class PersistedPublicClaimEnvelope: + """One governed claim and its exact authorized source-post provenance.""" + + public_claim_envelope_id: str + source_post_id: str + claim_kind_code: str + claim_text: str + + def verification_candidate(self) -> PublicClaimCandidate: + """Project the persisted envelope into the existing verifier contract.""" + + return PublicClaimCandidate( + claim_text=self.claim_text, + claim_kind=self.claim_kind_code, + source_post_ids=(self.source_post_id,), + ) + + +def envelope_from_authorized_row(row: Any) -> PersistedPublicClaimEnvelope | None: + """Validate a database row already filtered by ABAC and PROV-O binding.""" + + kind = str(row["claim_kind_code"] or "").strip() + envelope_id = str(row["public_claim_envelope_id"] or "").strip() + source_post_id = str(row["source_post_id"] or "").strip() + claim_text = str(row["claim_text"] or "").strip() + if ( + kind not in ADMITTED_PUBLIC_CLAIM_KINDS + or not envelope_id + or not source_post_id + or not claim_text + or len(claim_text) > 800 + ): + return None + return PersistedPublicClaimEnvelope( + public_claim_envelope_id=envelope_id, + source_post_id=source_post_id, + claim_kind_code=kind, + claim_text=claim_text, + ) diff --git a/migrations/0257_public_claim_envelope.sql b/migrations/0257_public_claim_envelope.sql new file mode 100644 index 000000000..86cfed3f6 --- /dev/null +++ b/migrations/0257_public_claim_envelope.sql @@ -0,0 +1,88 @@ +-- Migration 0257: provenance-bearing public-claim admission envelope. +-- Replay-safe under ADR 0166. Verification opt-in remains on global_ask_job. + +insert into common_lookup_value + (lookup_category, lookup_code, lookup_label, display_order) +values + ('public_claim_kind', 'claim_organization_presence', 'Organization presence', 0), + ('public_claim_kind', 'claim_public_event', 'Public event', 1), + ('public_claim_kind', 'claim_public_relationship', 'Public relationship', 2) +on conflict (lookup_code) do nothing; + +create table if not exists public_claim_envelope ( + public_claim_envelope_id uuid primary key default uuid_generate_v4(), + source_post_id uuid not null references source_post (post_id), + provenance_assertion_id uuid not null references provenance_assertion (assertion_id), + claim_kind_code text not null references common_lookup_value (lookup_code), + claim_text text not null, + egress_eligible boolean not null default false, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + unique (source_post_id, claim_kind_code, claim_text), + check (char_length(btrim(claim_text)) between 1 and 800) +); + +create index if not exists public_claim_envelope_egress_idx + on public_claim_envelope (source_post_id, created_at) + where egress_eligible; + +create or replace function validate_public_claim_envelope() +returns trigger +language plpgsql +as $$ +declare + visibility text; + claim_category text; + evidence_post_id uuid; + provenance_relation text; +begin + select lookup_category into claim_category + from common_lookup_value where lookup_code = new.claim_kind_code; + if claim_category is distinct from 'public_claim_kind' then + raise exception 'public_claim_kind_required'; + end if; + + select post.visibility_code into visibility + from source_post post where post.post_id = new.source_post_id; + select assertion.relation_code, binding.node_id + into provenance_relation, evidence_post_id + from provenance_assertion assertion + join provenance_resource_binding binding + on binding.resource_id = assertion.object_resource_id + and binding.node_type_code = 'node_post' + where assertion.assertion_id = new.provenance_assertion_id; + + if new.egress_eligible and visibility is distinct from 'public' then + raise exception 'public_claim_requires_public_post'; + end if; + if provenance_relation is distinct from 'prov_was_derived_from' + or evidence_post_id is distinct from new.source_post_id then + raise exception 'public_claim_requires_source_post_provenance'; + end if; + return new; +end; +$$; + +drop trigger if exists validate_public_claim_envelope on public_claim_envelope; +create trigger validate_public_claim_envelope + before insert or update on public_claim_envelope + for each row execute function validate_public_claim_envelope(); + +create or replace function revoke_private_public_claim_envelopes() +returns trigger +language plpgsql +as $$ +begin + if old.visibility_code = 'public' and new.visibility_code <> 'public' then + update public_claim_envelope + set egress_eligible = false, updated_at = now() + where source_post_id = new.post_id and egress_eligible; + end if; + return new; +end; +$$; + +drop trigger if exists revoke_private_public_claim_envelopes on source_post; +create trigger revoke_private_public_claim_envelopes + after update of visibility_code on source_post + for each row execute function revoke_private_public_claim_envelopes(); diff --git a/migrations/rollback/0257_public_claim_envelope.sql b/migrations/rollback/0257_public_claim_envelope.sql new file mode 100644 index 000000000..5d71cfc6c --- /dev/null +++ b/migrations/rollback/0257_public_claim_envelope.sql @@ -0,0 +1,5 @@ +drop trigger if exists revoke_private_public_claim_envelopes on source_post; +drop function if exists revoke_private_public_claim_envelopes(); +drop trigger if exists validate_public_claim_envelope on public_claim_envelope; +drop function if exists validate_public_claim_envelope(); +drop table if exists public_claim_envelope; diff --git a/tests/test_global_ask_queue.py b/tests/test_global_ask_queue.py index 8bca80b88..30abe2e50 100644 --- a/tests/test_global_ask_queue.py +++ b/tests/test_global_ask_queue.py @@ -10,6 +10,7 @@ from backend.app.global_ask_queue import load_job_visibility from lineageweave import claim_verification as cv from lineageweave.post_chat import ChatAnswer, ChatSourceDocument +from lineageweave.public_claim_envelope import PersistedPublicClaimEnvelope class _AvailableClient: @@ -162,6 +163,60 @@ def test_public_verification_keeps_external_urls_out_of_internal_citations() -> assert results[0].evidence[0].url not in results[0].source_post_ids +def test_persisted_envelope_is_production_admission_not_question_overlap() -> None: + """A stored cited envelope reaches the verifier without token nomination.""" + + client = _VerificationClient() + envelope = PersistedPublicClaimEnvelope( + public_claim_envelope_id="envelope-1", + source_post_id="public-post", + claim_kind_code="claim_public_event", + claim_text="Synthetic launch happened.", + ) + + status_code, results = asyncio.run( + global_ask_queue._verify_public_claims( + "A question with no overlapping words", + [], + ["public-post"], + verify_external=True, + client=client, + persisted_envelopes=(envelope,), + ) + ) + + assert status_code == cv.VERIFICATION_COMPLETED + assert results[0].claim_text == "Synthetic launch happened." + assert results[0].source_post_ids == ("public-post",) + + +def test_persisted_envelope_must_name_a_cited_post() -> None: + """A stored but uncited envelope never crosses the public verifier.""" + + client = _VerificationClient() + envelope = PersistedPublicClaimEnvelope( + public_claim_envelope_id="envelope-1", + source_post_id="other-public-post", + claim_kind_code="claim_public_relationship", + claim_text="Synthetic organizations announced a relationship.", + ) + + status_code, results = asyncio.run( + global_ask_queue._verify_public_claims( + "relationship", + [], + ["cited-public-post"], + verify_external=True, + client=client, + persisted_envelopes=(envelope,), + ) + ) + + assert status_code == cv.VERIFICATION_NO_PUBLIC_CLAIMS + assert results == () + assert client.calls == 0 + + def test_malformed_public_verification_is_unavailable() -> None: """Malformed provider/search envelopes do not discard a completed answer.""" source = cv.GlobalAskSourceDocument( diff --git a/tests/test_migration_replay.py b/tests/test_migration_replay.py index 733189357..873f6dd78 100644 --- a/tests/test_migration_replay.py +++ b/tests/test_migration_replay.py @@ -184,6 +184,23 @@ def test_global_ask_migrations_are_safe_to_replay() -> None: assert "create table if not exists global_ask_job_process_unit_scope" in scope_sql +def test_public_claim_envelope_migration_is_replay_safe_and_provenance_bound() -> None: + """Persisted public egress admission requires the exact PROV-O source post.""" + + sql = ( + Path(__file__).resolve().parents[1] + / "migrations" + / "0257_public_claim_envelope.sql" + ).read_text(encoding="utf-8").casefold() + + assert "create table if not exists public_claim_envelope" in sql + assert "provenance_assertion_id uuid not null" in sql + assert "prov_was_derived_from" in sql + assert "evidence_post_id is distinct from new.source_post_id" in sql + assert "public_claim_requires_public_post" in sql + assert "on conflict (lookup_code) do nothing" in sql + + def test_channel_weight_migration_preserves_raw_source_grouping() -> None: migration = ( Path(__file__).resolve().parents[1] diff --git a/tests/test_public_claim_envelope.py b/tests/test_public_claim_envelope.py new file mode 100644 index 000000000..f0cbb9579 --- /dev/null +++ b/tests/test_public_claim_envelope.py @@ -0,0 +1,36 @@ +"""Persisted public-claim admission boundary regressions.""" + +from lineageweave.claim_verification import PublicClaimCandidate +from lineageweave.public_claim_envelope import envelope_from_authorized_row + + +def _row(**overrides: object) -> dict[str, object]: + row: dict[str, object] = { + "public_claim_envelope_id": "00000000-0000-0000-0000-000000000101", + "source_post_id": "00000000-0000-0000-0000-000000000201", + "claim_kind_code": "claim_public_event", + "claim_text": "Synthetic project reached its published milestone.", + } + row.update(overrides) + return row + + +def test_persisted_envelope_projects_exact_claim_and_provenance() -> None: + """Admission preserves the stored claim and its one evidence post.""" + + envelope = envelope_from_authorized_row(_row()) + + assert envelope is not None + assert envelope.verification_candidate() == PublicClaimCandidate( + claim_text="Synthetic project reached its published milestone.", + claim_kind="claim_public_event", + source_post_ids=("00000000-0000-0000-0000-000000000201",), + ) + + +def test_persisted_envelope_rejects_unregistered_or_malformed_claims() -> None: + """Person-like and malformed rows cannot be repaired into egress claims.""" + + assert envelope_from_authorized_row(_row(claim_kind_code="person")) is None + assert envelope_from_authorized_row(_row(claim_text="")) is None + assert envelope_from_authorized_row(_row(claim_text="x" * 801)) is None From 64d8576ec7ad750632dd022ac4f098a48636789f Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 28 Aug 2026 19:02:04 +0900 Subject: [PATCH 2/3] test(ask): seed persisted claim provenance Apply migration 0257 in the API fixture and create a synthetic PROV-O-bound envelope for the opt-in verification contract. Signed-off-by: Codex --- backend/tests/test_api.py | 41 ++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 45f5740f6..a7ed9a746 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -237,6 +237,7 @@ "0250_operations_case_analysis_input.sql", "0251_product_semantic_catalog.sql", "0253_voice_semantic_taxonomy.sql", + "0257_public_claim_envelope.sql", ) ) @@ -5669,14 +5670,40 @@ def verify(self, claim): with closing(psycopg2.connect(seeded_db["dsn"])) as conn, conn.cursor() as cur: cur.execute( """ - insert into post_project_mention - (post_id, project_key, project_name, evidence_text, confidence, - ontology_iri, extraction_method) - values (%s, 'synthetic-apollo', 'Apollo', 'Public project evidence', - 1.0, 'https://contextualwisdomlab.github.io/LineageWeave/ontology#Project', - 'synthetic_test') + with claim_resource as ( + insert into provenance_resource (resource_iri, resource_label) + values ('urn:lineageweave:test:public-claim', 'Synthetic public claim') + returning resource_id + ), claim_type as ( + insert into provenance_resource_type (resource_id, class_code) + select resource_id, 'prov_entity' from claim_resource + ), post_resource as ( + insert into provenance_resource (resource_iri, resource_label) + values ('urn:lineageweave:test:public-post-evidence', 'Synthetic source post') + returning resource_id + ), post_type as ( + insert into provenance_resource_type (resource_id, class_code) + select resource_id, 'prov_entity' from post_resource + ), post_binding as ( + insert into provenance_resource_binding + (resource_id, node_type_code, node_id) + select resource_id, 'node_post', %s from post_resource + ), assertion as ( + insert into provenance_assertion + (subject_resource_id, relation_code, object_resource_id) + select claim_resource.resource_id, 'prov_was_derived_from', + post_resource.resource_id + from claim_resource, post_resource + returning assertion_id + ) + insert into public_claim_envelope + (source_post_id, provenance_assertion_id, claim_kind_code, + claim_text, egress_eligible) + select %s, assertion_id, 'claim_public_event', + 'Synthetic Apollo event was published.', true + from assertion """, - (seeded_db["public_post_id"],), + (seeded_db["public_post_id"], seeded_db["public_post_id"]), ) conn.commit() From 0b42edb547ff1067b59cef9b7a203360f96ea1d9 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 28 Aug 2026 19:04:32 +0900 Subject: [PATCH 3/3] fix(test): persist typed claim fixture Create PROV-O resource types before the assertion and retain the synthetic project mention used solely for authorized Ask retrieval. Signed-off-by: Codex --- backend/tests/test_api.py | 81 +++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index a7ed9a746..b0a1cb8e5 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -5670,40 +5670,55 @@ def verify(self, claim): with closing(psycopg2.connect(seeded_db["dsn"])) as conn, conn.cursor() as cur: cur.execute( """ - with claim_resource as ( - insert into provenance_resource (resource_iri, resource_label) - values ('urn:lineageweave:test:public-claim', 'Synthetic public claim') - returning resource_id - ), claim_type as ( - insert into provenance_resource_type (resource_id, class_code) - select resource_id, 'prov_entity' from claim_resource - ), post_resource as ( - insert into provenance_resource (resource_iri, resource_label) - values ('urn:lineageweave:test:public-post-evidence', 'Synthetic source post') - returning resource_id - ), post_type as ( - insert into provenance_resource_type (resource_id, class_code) - select resource_id, 'prov_entity' from post_resource - ), post_binding as ( - insert into provenance_resource_binding - (resource_id, node_type_code, node_id) - select resource_id, 'node_post', %s from post_resource - ), assertion as ( - insert into provenance_assertion - (subject_resource_id, relation_code, object_resource_id) - select claim_resource.resource_id, 'prov_was_derived_from', - post_resource.resource_id - from claim_resource, post_resource - returning assertion_id - ) - insert into public_claim_envelope - (source_post_id, provenance_assertion_id, claim_kind_code, - claim_text, egress_eligible) - select %s, assertion_id, 'claim_public_event', - 'Synthetic Apollo event was published.', true - from assertion + insert into post_project_mention + (post_id, project_key, project_name, evidence_text, confidence, + ontology_iri, extraction_method) + values (%s, 'synthetic-apollo', 'Apollo', 'Public project evidence', + 1.0, 'https://contextualwisdomlab.github.io/LineageWeave/ontology#Project', + 'synthetic_test') """, - (seeded_db["public_post_id"], seeded_db["public_post_id"]), + (seeded_db["public_post_id"],), + ) + cur.execute( + "insert into provenance_resource (resource_iri, resource_label) " + "values ('urn:lineageweave:test:public-claim', 'Synthetic public claim') " + "returning resource_id" + ) + claim_resource_id = cur.fetchone()[0] + cur.execute( + "insert into provenance_resource_type (resource_id, class_code) " + "values (%s, 'prov_entity')", + (claim_resource_id,), + ) + cur.execute( + "insert into provenance_resource (resource_iri, resource_label) " + "values ('urn:lineageweave:test:public-post-evidence', 'Synthetic source post') " + "returning resource_id" + ) + post_resource_id = cur.fetchone()[0] + cur.execute( + "insert into provenance_resource_type (resource_id, class_code) " + "values (%s, 'prov_entity')", + (post_resource_id,), + ) + cur.execute( + "insert into provenance_resource_binding (resource_id, node_type_code, node_id) " + "values (%s, 'node_post', %s)", + (post_resource_id, seeded_db["public_post_id"]), + ) + cur.execute( + "insert into provenance_assertion " + "(subject_resource_id, relation_code, object_resource_id) " + "values (%s, 'prov_was_derived_from', %s) returning assertion_id", + (claim_resource_id, post_resource_id), + ) + assertion_id = cur.fetchone()[0] + cur.execute( + "insert into public_claim_envelope " + "(source_post_id, provenance_assertion_id, claim_kind_code, claim_text, egress_eligible) " + "values (%s, %s, 'claim_public_event', " + "'Synthetic Apollo event was published.', true)", + (seeded_db["public_post_id"], assertion_id), ) conn.commit()