-
Notifications
You must be signed in to change notification settings - Fork 1
feat(ask): persist public claim admission #784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
203dfe8
4180e88
64d8576
b0d39cf
0b42edb
8cc7048
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| ) | ||
|
Comment on lines
230
to
242
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Production verification now requires a persisted envelope In the production path, Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| 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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Duplicate evidence bindings can crowd the four-envelope cap
The admission query joins
provenance_resource_bindingon the assertion's object resource. If that resource has multiplenode_postbindings to the same source post, the join emits the envelope more than once, and thelimit 4is applied to the joined rows, so duplicates can consume the budget and crowd out distinct envelopes. The insert trigger checks only that one binding matches, not uniqueness. Considerexists/distinctif duplicate bindings are possible.Was this helpful? React with 👍 or 👎 to provide feedback.