Files
ThothII/harness/tht/evidence/session.py
T

92 lines
3.3 KiB
Python

"""Evidence-specific projection into persisted session artifacts."""
import json
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from tht.evidence.corpus.store import CorpusStore
from tht.evidence.search import resolve_citation
if TYPE_CHECKING:
from tht.decisions import DecisionRecord
from tht.session.models import SchemaLinking
def _resolve_session_citation(evidence_root: Path, evidence_id: str) -> str:
# New deployments resolve only immutable materialized files from ACTIVE. Keep the
# curated-tree fallback for sessions created before a canonical corpus exists.
corpus_root = evidence_root.parent.parent / "corpus"
if corpus_root.exists():
return resolve_citation(
CorpusStore(corpus_root),
evidence_id,
materialized_root=evidence_root.parent / ".materialized-evidence",
)
for match in evidence_root.rglob(f"{evidence_id}.md"):
return str(match)
return ""
def project_session(
decisions: list["DecisionRecord"],
linking: "SchemaLinking",
evidence_root: Path,
) -> list[dict]:
"""Project cited and reviewed Evidence into the session artifact shape."""
entries: dict[str, dict] = {}
for candidate in linking.candidates:
for evidence_id in candidate.evidence:
entries.setdefault(
evidence_id,
{
"id": evidence_id,
"file": _resolve_session_citation(evidence_root, evidence_id),
"esito": "usata",
"decision_seq": candidate.decision_seq,
},
)
for decision in decisions:
if decision.type not in ("evidence_accepted", "evidence_rejected"):
continue
entries[decision.subject] = {
"id": decision.subject,
"file": _resolve_session_citation(evidence_root, decision.subject),
"esito": "accettata" if decision.type == "evidence_accepted" else "scartata",
"decision_seq": decision.seq,
}
return list(entries.values())
@dataclass(frozen=True)
class EvidenceReceipt:
stage: str
purpose: str
vector_generation: str
evidence_ids: tuple[str, ...]
def payload(self) -> dict:
return {
"stage": self.stage,
"purpose": self.purpose,
"vector_generation": self.vector_generation,
"evidence_ids": list(self.evidence_ids),
}
def replace_evidence_receipt(repository, session_id: str, receipt: EvidenceReceipt) -> None:
"""Replace the one minimal receipt for a semantic stage; preserve other stages."""
current = repository.read_artifact(session_id, "evidence_receipts")
try:
receipts = json.loads(current) if current else []
except json.JSONDecodeError as error:
raise ValueError("Evidence receipts artifact is malformed") from error
if not isinstance(receipts, list):
raise TypeError("Evidence receipts artifact is malformed")
replaced = [item for item in receipts if isinstance(item, dict) and item.get("stage") != receipt.stage]
replaced.append(receipt.payload())
repository.write_artifact(session_id, "evidence_receipts", json.dumps(replaced, ensure_ascii=False, indent=2) + "\n")
__all__ = ["EvidenceReceipt", "project_session", "replace_evidence_receipt"]