from datetime import UTC, datetime from pathlib import Path from typing import Literal from pydantic import BaseModel DECISIONS_FILE = "review_decisions.jsonl" # 22 tipi di the reference implementation (verified leggendo session/decisions.py) + 1 nuovo (D15): # `decision_retracted` per il rollback a granularità step (ritira una decisione # senza cancellarne la riga dal log di audit; effective_decisions la onora). DecisionType = Literal[ "concept_clarified", "question_rewritten", "table_promoted", "table_excluded", "column_promoted", "column_excluded", "column_corrected", "join_modified", "evidence_accepted", "evidence_rejected", "ambiguity_open", "memory_rejected", "cte_approved", "cte_corrected", "cte_rejected", "sql_revised", "sql_approved", "sql_rejected", "phase_approved", "phase_auto_approved", "phase_reopened", "phase_skipped", "datamart_requested", "datamart_declined", # F8: promozione memorie riusabili al gate reviewer_memory_promote. subject = # subject della decisione originale, detail = "seq:" (usato da # declined_promotion_seqs per non riproporre i candidati rifiutati). "memory_promoted", "memory_promotion_declined", # D15: marker di ritrazione. subject = "phase:N", retracts = decision_seq ritirata. # Resta nel log di audit (append-only); effective_decisions() la esclude dalla vista. "decision_retracted", # D14a: valore citato nella domanda ancorato a una o piu' colonne. subject = # "phase:4", detail = il valore (es. "ablazione"), rationale = la/e colonna/e scelta/e # dal reviewer (aggregate_lsh_multi le espone tutte senza collassare al miglior match). "value_grounded", # D14b: formula di concetto approvata/rifiutata dal reviewer. subject = "phase:4", # detail = il concetto (es. "fascia pediatrica"), rationale = la/e colonna/e o il motivo. # retrieve_formula restituisce i candidati; queste decisioni registrano la scelta. "concept_formula_approved", "concept_formula_rejected", ] class DecisionRecord(BaseModel): seq: int ts: datetime type: DecisionType subject: str detail: str = "" rationale: str = "" # D15: se type == "decision_retracted", indica quale seq viene ritirata. retracts: int | None = None # D15: la fase corrente al momento della scrittura (high-water-mark). Permette a # effective_decisions() di marcare stale le decisioni il cui subject NON e' "phase:N" # (es. cte_approved usa il nome del CTE) dopo un rollback. None per record storici # (pre-fix) o costruiti a mano: in quel caso si ricade sulla logica basata sul subject. phase: int | None = None def list_decisions(session_dir: Path) -> list[DecisionRecord]: path = session_dir / DECISIONS_FILE if not path.exists(): return [] return [ DecisionRecord.model_validate_json(line) for line in path.read_text().splitlines() if line.strip() ] def append_decision( session_dir: Path, *, type: str, subject: str, detail: str = "", rationale: str = "", retracts: int | None = None, ) -> DecisionRecord: # Fase corrente PRIMA dell'append (high-water-mark D15). Import lazy: phase.py # importa decisions.py (ciclo). Per i marker di fase (subject "phase:N") il valore # e' ridondante col subject; per le decisioni sostanziali con subject "a nome" # (cte_approved, ...) e' l'unico modo per filtrarle dopo un reopen. from tht.phase import current_phase record = DecisionRecord( seq=len(list_decisions(session_dir)) + 1, ts=datetime.now(UTC), type=type, subject=subject, detail=detail, rationale=rationale, retracts=retracts, phase=current_phase(session_dir), ) path = session_dir / DECISIONS_FILE path.parent.mkdir(parents=True, exist_ok=True) with path.open("a") as f: f.write(record.model_dump_json() + "\n") return record