"""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()) + _formula_proposals(decisions, linking) def _formula_proposals(decisions: list["DecisionRecord"], linking: "SchemaLinking") -> list[dict]: """Project locally approved F4 formulas without representing them as Evidence. A proposal remains in the persisted session artifact until a separate curator imports, reviews, and publishes it in the workspace repository. """ retracted = { decision.retracts for decision in decisions if decision.type == "decision_retracted" and decision.retracts is not None } latest_f4_decision = {} for decision in sorted(decisions, key=lambda item: item.seq): if decision.seq in retracted or decision.subject != "phase:4" or not decision.detail: continue if decision.type in {"concept_formula_approved", "concept_formula_rejected"}: latest_f4_decision[decision.detail] = decision proposals = [] for formula in linking.concept_formulas: if not isinstance(formula, dict): continue concept = formula.get("concept") sql = formula.get("sql") columns = formula.get("columns") if not isinstance(concept, str) or not isinstance(sql, str) or not isinstance(columns, list): continue # A referenced published Formula Evidence is already represented by its # Evidence receipt/citation, so it must not be reintroduced as a proposal. evidence_id = formula.get("evidence_id", formula.get("id", "")) if isinstance(evidence_id, str) and evidence_id.startswith("evidence:"): continue decision = latest_f4_decision.get(concept) if decision is None or decision.type != "concept_formula_approved": continue proposals.append({ "schema_version": 1, "kind": "formula_proposal", "publication": "session_only", "concept": concept, "columns": columns, "sql": sql, "sources": formula.get("sources", []), "decision_seq": decision.seq, }) return proposals @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"]