Kansho/backend/provenance.py
2026-08-26 10:42:20 +02:00

243 lines
8.5 KiB
Python

"""Intent-neutral provenance verification. Model output is untrusted input.
Canonical source texts come only from a locally built SourceRegistry.
Downstream stages consume a locally materialized VerifiedArtifact.
This module has no journal, dialogue, or product-intent vocabulary.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Mapping
COVERAGE_ALL_SELECTED_SOURCES = "all_selected_sources"
COVERAGE_SELECTED_EVIDENCE = "selected_evidence"
COVERAGE_MODES = frozenset({COVERAGE_ALL_SELECTED_SOURCES, COVERAGE_SELECTED_EVIDENCE})
ARTIFACT_KIND = "verified_artifact"
class ProvenanceError(Exception):
"""Fail-closed verification error. Domain mapping happens at the caller boundary."""
def __init__(self, reason: str, **details: Any):
super().__init__(reason)
self.reason = reason
self.details = details
@dataclass(frozen=True)
class SourceRecord:
source_id: str
role: str
text: str
metadata: Mapping[str, Any] = field(default_factory=dict)
class SourceRegistry:
"""Local authority for original content. Model text never replaces these records."""
def __init__(self, sources: list[SourceRecord] | None = None):
self._sources: dict[str, SourceRecord] = {}
self._order: list[str] = []
for source in sources or []:
self.add(source)
def add(self, source: SourceRecord) -> None:
source_id = (source.source_id or "").strip()
if not source_id:
raise ProvenanceError("missing_source_id")
if source_id in self._sources:
raise ProvenanceError("duplicate_source_id", source_id=source_id)
self._sources[source_id] = SourceRecord(
source_id=source_id,
role=source.role,
text=source.text,
metadata=dict(source.metadata or {}),
)
self._order.append(source_id)
def get(self, source_id: str) -> SourceRecord | None:
return self._sources.get(source_id)
def order(self) -> list[str]:
return list(self._order)
def items(self) -> list[SourceRecord]:
return [self._sources[source_id] for source_id in self._order]
@dataclass(frozen=True)
class EvidenceRef:
source_id: str
excerpt: str
@dataclass(frozen=True)
class VerifiedEvidence:
source_id: str
excerpt: str
start: int
end: int
@dataclass(frozen=True)
class VerificationPolicy:
allowed_roles: frozenset[str]
coverage: str = COVERAGE_ALL_SELECTED_SOURCES
selected_ids: tuple[str, ...] = ()
source_order: tuple[str, ...] = ()
include_unverified_annotations: bool = False
@dataclass(frozen=True)
class VerifiedArtifact:
coverage: str
source_order: tuple[str, ...]
sources: tuple[dict[str, str], ...]
evidence: tuple[VerifiedEvidence, ...]
annotations_unverified: tuple[dict[str, Any], ...] = ()
def texts(self) -> list[str]:
return [item.get("text") or "" for item in self.sources if item.get("text")]
def to_payload(self) -> dict[str, Any]:
payload: dict[str, Any] = {
"kind": ARTIFACT_KIND,
"coverage": self.coverage,
"source_order": list(self.source_order),
"order": [item.get("source_id") for item in self.sources],
"sources": [dict(item) for item in self.sources],
"evidence": [
{
"source_id": item.source_id,
"excerpt": item.excerpt,
"start": item.start,
"end": item.end,
}
for item in self.evidence
],
}
if self.annotations_unverified:
payload["annotations_unverified"] = [dict(item) for item in self.annotations_unverified]
return payload
def locate_excerpt(text: str, excerpt: str) -> tuple[int, int] | None:
"""Local span from a confirmed literal excerpt. Model offsets are ignored."""
if not excerpt:
return None
start = (text or "").find(excerpt)
if start < 0:
return None
return start, start + len(excerpt)
def texts_from_payload(data: Mapping[str, Any] | None) -> list[str]:
if not isinstance(data, Mapping):
return []
parts: list[str] = []
for item in data.get("sources") or []:
if isinstance(item, Mapping) and item.get("text"):
parts.append(str(item["text"]))
return parts
def verify_evidence(
registry: SourceRegistry,
ref: EvidenceRef,
policy: VerificationPolicy,
) -> VerifiedEvidence:
source_id = (ref.source_id or "").strip()
excerpt = ref.excerpt or ""
if not source_id:
raise ProvenanceError("missing_source_id")
if not excerpt.strip():
raise ProvenanceError("unverified_excerpt", source_id=source_id)
if policy.selected_ids and source_id not in policy.selected_ids:
raise ProvenanceError("unknown_source", source_id=source_id)
source = registry.get(source_id)
if source is None:
raise ProvenanceError("unknown_source", source_id=source_id)
if source.role not in policy.allowed_roles:
raise ProvenanceError("role_not_allowed", source_id=source_id, role=source.role)
located = locate_excerpt(source.text, excerpt)
if located is not None:
return VerifiedEvidence(source_id, excerpt, located[0], located[1])
for other in registry.items():
if other.source_id == source_id or excerpt not in (other.text or ""):
continue
if other.role not in policy.allowed_roles:
raise ProvenanceError("role_not_allowed", source_id=other.source_id, role=other.role)
raise ProvenanceError("wrong_source", source_id=source_id, excerpt=excerpt[:120])
raise ProvenanceError("unverified_excerpt", source_id=source_id, excerpt=excerpt[:120])
def _selected_records(registry: SourceRegistry, policy: VerificationPolicy) -> list[SourceRecord]:
if policy.coverage not in COVERAGE_MODES:
raise ProvenanceError("unknown_coverage", coverage=policy.coverage)
selected = tuple(item for item in policy.selected_ids if item)
if not selected:
raise ProvenanceError("selection_empty")
if len(set(selected)) != len(selected):
raise ProvenanceError("duplicate_source_id", ids=list(selected))
records: list[SourceRecord] = []
for source_id in selected:
source = registry.get(source_id)
if source is None:
raise ProvenanceError("unknown_source", source_id=source_id)
if source.role not in policy.allowed_roles:
raise ProvenanceError("role_not_allowed", source_id=source_id, role=source.role)
records.append(source)
return records
def verify(
registry: SourceRegistry,
*,
policy: VerificationPolicy,
evidence: list[EvidenceRef] | None = None,
annotations: list[dict[str, Any]] | None = None,
proposed_texts: Mapping[str, str] | None = None,
) -> VerifiedArtifact:
"""Materialize a VerifiedArtifact from local sources plus untrusted references."""
selected = _selected_records(registry, policy)
for source in selected:
proposed = (proposed_texts or {}).get(source.source_id)
if proposed is not None and proposed != source.text:
raise ProvenanceError("canonical_overwrite", source_id=source.source_id)
verified: list[VerifiedEvidence] = []
for ref in evidence or []:
verified.append(verify_evidence(registry, ref, policy))
if policy.coverage == COVERAGE_ALL_SELECTED_SOURCES:
sources = tuple(
{"source_id": source.source_id, "role": source.role, "text": source.text} for source in selected
)
else:
grouped: dict[str, list[str]] = {source.source_id: [] for source in selected}
for item in verified:
grouped[item.source_id].append(item.excerpt)
sources = tuple(
{
"source_id": source.source_id,
"role": source.role,
"text": "\n".join(grouped[source.source_id]),
}
for source in selected
)
unverified: tuple[dict[str, Any], ...] = ()
if policy.include_unverified_annotations and annotations:
unverified = tuple(dict(item) for item in annotations)
encounter = tuple(item for item in policy.source_order if item) or tuple(
source.source_id for source in selected
)
return VerifiedArtifact(
coverage=policy.coverage,
source_order=encounter,
sources=sources,
evidence=tuple(verified),
annotations_unverified=unverified,
)