feat(fase2): first-class Pydantic provenance stamp

This commit is contained in:
Kjell Tore Guttormsen 2026-06-24 13:26:55 +02:00
commit a0f263bf41
2 changed files with 104 additions and 0 deletions

View file

@ -0,0 +1,57 @@
"""First-class Pydantic provenance stamp.
Provenance is authoritative framework data **independent** of MAF's ``Annotation`` type,
which silently drops on the Python streaming path (#4316, research 02 Dim 2). A
``ProvenanceStamp`` is a Pydantic model that must carry at least one ``Citation`` (a
``min_length=1`` constraint), the model + role that produced the proposal, the validator's
decision, and the token usage. ``to_annotations()`` maps to MAF ``Annotation`` dicts for
DISPLAY only never the source of truth.
The locator type (``TextSpan``) is owned by ``retrieval.py`` (Step 5) and imported here, so
a citation's span is the same exact object the retriever produced.
"""
from __future__ import annotations
from typing import Literal
from agent_framework import Annotation, TextSpanRegion
from pydantic import BaseModel, Field
from portfolio_optimiser.retrieval import TextSpan
class Citation(BaseModel):
"""One exact-span citation into a source document (locator owned by retrieval.py)."""
file: str
locator: TextSpan
snippet: str
class ProvenanceStamp(BaseModel):
"""Authoritative provenance for one proposal — at least one citation is mandatory."""
citations: list[Citation] = Field(min_length=1)
model: str
role: str
validator_decision: Literal["validated", "rejected"]
token_usage: int
def to_annotations(self) -> list[Annotation]:
"""Map to MAF ``Annotation`` dicts for display only (NOT the source of truth)."""
return [
Annotation(
type="citation",
snippet=c.snippet,
annotated_regions=[
TextSpanRegion(
type="text_span",
start_index=c.locator.start_index,
end_index=c.locator.end_index,
)
],
additional_properties={"file": c.file},
)
for c in self.citations
]

47
tests/test_provenance.py Normal file
View file

@ -0,0 +1,47 @@
"""Step 6 tests — first-class Pydantic provenance stamp.
At least one citation is mandatory (the load-bearing NFR); a valid stamp exposes the
proposal's provenance fields; the annotation adapter yields MAF citation dicts for display.
Pattern: tests/spikes/test_c_validator.py (pydantic raises + field assertions).
"""
import pytest
from pydantic import ValidationError
from portfolio_optimiser.provenance import Citation, ProvenanceStamp
from portfolio_optimiser.retrieval import TextSpan
def _stamp() -> ProvenanceStamp:
return ProvenanceStamp(
citations=[Citation(file="asphalt.txt", locator=TextSpan(0, 10), snippet="0123456789")],
model="qwen3:4b",
role="proposer",
validator_decision="validated",
token_usage=42,
)
def test_zero_citations_is_rejected() -> None:
with pytest.raises(ValidationError):
ProvenanceStamp(
citations=[],
model="m",
role="proposer",
validator_decision="validated",
token_usage=1,
)
def test_valid_stamp_exposes_provenance_fields() -> None:
stamp = _stamp()
assert len(stamp.citations) >= 1
assert stamp.model and stamp.role
assert stamp.validator_decision in {"validated", "rejected"}
assert isinstance(stamp.token_usage, int) and stamp.token_usage > 0
def test_to_annotations_yields_citation_dicts() -> None:
anns = _stamp().to_annotations()
assert anns and all(a["type"] == "citation" for a in anns)
assert anns[0]["annotated_regions"][0]["type"] == "text_span"