haiku.rag/tests/capabilities/test_evidence_ledger.py
Yiorgis Gozadinos 85594a1fd1
Record what each evidence capability retrieved and cited
`CapabilityEvidenceRecord` holds the relationships a transcript cannot express:
which chunks a capability retrieved, which it cited, in which questions, and at
which point in the conversation. RAG and analysis each own one in their own state
namespace. Nothing is co-written: the host's state is JSON storage, so a shared
record would be overwritten by whichever capability synced last, and merging
happens in transient per-request views instead.

Both clocks are derived from the conversation rather than counted locally, so
every participant computes the same values without sharing a counter. Question
identity is the message count when the question arrived; epoch is the message
count at an outcome. Epochs are therefore globally comparable, which is what
lets `citation_status` require a declaration to follow the newest evidence of
every capability, and what makes equal epochs mean one request.

A declaration is written only after `resolve_citations` succeeds, so a call
naming only unresolvable ids is not a citation. Status is derived, never stored,
so refs and status cannot contradict.

Resuming a question requires the host to carry the capability state from the run
being resumed. Without it the identity of the question in progress is unknowable,
and adopting the current message count would relabel that question as a new one
and judge every declaration in it against the wrong identity.

Nothing reads the records yet and no wire behaviour changes.
2026-08-13 13:00:01 +03:00

150 lines
5.1 KiB
Python

from haiku.rag.capabilities.ledger import (
CapabilityEvidenceRecord,
CitationDeclaration,
EvidenceRef,
citation_status,
)
def rag_ref(chunk_id: str = "c1") -> EvidenceRef:
return EvidenceRef(capability="rag", chunk_id=chunk_id)
def test_a_record_survives_the_state_round_trip():
"""Capability state is persisted as JSON, so the schema must survive it.
A dict keyed by ``(capability, chunk_id)`` does not: the key serialises to
``"rag,c1"`` and fails revalidation as a tuple.
"""
record = CapabilityEvidenceRecord(question=4)
record.note_evidence(5)
record.declare([rag_ref()], epoch=7, retrieved_now={"c1"})
restored = CapabilityEvidenceRecord.model_validate(record.model_dump(mode="json"))
assert restored == record
assert citation_status([restored], question=4) == "grounded"
assert restored.occurrences["c1"].cited_in_questions == [4]
assert restored.occurrences["c1"].retrieved_in_questions == [4]
def test_no_declaration_reads_as_missing():
assert citation_status([CapabilityEvidenceRecord()], question=0) == "missing"
assert citation_status([], question=0) == "missing"
def test_refs_make_it_grounded_and_no_refs_make_it_ungrounded():
grounded = CapabilityEvidenceRecord()
grounded.declare([rag_ref()], epoch=1)
ungrounded = CapabilityEvidenceRecord()
ungrounded.declare([], epoch=1)
assert citation_status([grounded], question=0) == "grounded"
assert citation_status([ungrounded], question=0) == "ungrounded"
def test_an_earlier_questions_declaration_is_never_current():
"""Epochs outlive a question, so the epoch alone would inherit it."""
record = CapabilityEvidenceRecord(question=2)
record.declare([rag_ref()], epoch=3)
assert citation_status([record], question=2) == "grounded"
record.question = 8
assert record.declaration is not None
assert citation_status([record], question=8) == "missing"
def test_a_citation_in_the_same_request_as_the_evidence_is_not_current():
"""Citing must follow seeing: equal epochs mean one request."""
record = CapabilityEvidenceRecord()
record.note_evidence(5)
record.declare([rag_ref()], epoch=5)
assert citation_status([record], question=0) == "missing"
record.declare([rag_ref()], epoch=7)
assert citation_status([record], question=0) == "grounded"
def test_evidence_from_another_capability_after_citing_makes_it_uncited():
"""Currency spans capabilities, which only works because epochs are global."""
cited = CapabilityEvidenceRecord()
cited.note_evidence(3)
cited.declare([rag_ref()], epoch=5)
searched_after = CapabilityEvidenceRecord()
searched_after.note_evidence(7)
assert citation_status([cited], question=0) == "grounded"
assert citation_status([cited, searched_after], question=0) == "missing"
def test_declarations_at_the_same_epoch_merge():
record = CapabilityEvidenceRecord()
record.declare([rag_ref("c1")], epoch=3)
record.declare([rag_ref("c2")], epoch=3)
assert record.declaration is not None
assert [ref.chunk_id for ref in record.declaration.refs] == ["c1", "c2"]
def test_repeating_a_ref_at_the_same_epoch_does_not_duplicate_it():
record = CapabilityEvidenceRecord()
record.declare([rag_ref()], epoch=3)
record.declare([rag_ref()], epoch=3)
assert record.declaration is not None
assert len(record.declaration.refs) == 1
def test_neither_cite_order_downgrades_a_grounded_declaration():
grounded_then_empty = CapabilityEvidenceRecord()
grounded_then_empty.declare([rag_ref()], epoch=3)
grounded_then_empty.declare([], epoch=3)
empty_then_grounded = CapabilityEvidenceRecord()
empty_then_grounded.declare([], epoch=3)
empty_then_grounded.declare([rag_ref()], epoch=3)
assert citation_status([grounded_then_empty], question=0) == "grounded"
assert citation_status([empty_then_grounded], question=0) == "grounded"
def test_the_same_chunk_id_under_two_capabilities_stays_separate():
rag = CapabilityEvidenceRecord()
rag.declare([EvidenceRef(capability="rag", chunk_id="shared")], epoch=3)
analysis = CapabilityEvidenceRecord()
analysis.declare([EvidenceRef(capability="analysis", chunk_id="shared")], epoch=3)
assert rag.occurrences["shared"].capability == "rag"
assert analysis.occurrences["shared"].capability == "analysis"
def test_an_evidence_epoch_never_moves_backwards():
record = CapabilityEvidenceRecord()
record.note_evidence(9)
record.note_evidence(4)
assert record.latest_evidence_epoch == 9
def test_citing_the_same_chunk_in_two_questions_records_both():
record = CapabilityEvidenceRecord(question=2)
record.declare([rag_ref()], epoch=3, retrieved_now={"c1"})
record.question = 8
record.declare([rag_ref()], epoch=9)
occurrence = record.occurrences["c1"]
assert occurrence.cited_in_questions == [2, 8]
assert occurrence.retrieved_in_questions == [2]
def test_a_declaration_records_the_question_and_epoch_it_was_made_at():
record = CapabilityEvidenceRecord(question=6)
record.declare([rag_ref()], epoch=11)
assert record.declaration == CitationDeclaration(
question=6, epoch=11, refs=[rag_ref()]
)