from pathlib import Path from typing import Literal import yaml from pydantic import BaseModel, ValidationError class EvidenceError(Exception): pass class EvidenceDoc(BaseModel): id: str title: str # tier e status sono opzionali: la sola presenza di un documento basta a # vettorizzarlo, quindi l'autore ETL non e' obbligato a compilarli. tier: Literal["structural", "concept"] = "structural" status: Literal["auto", "draft", "reviewed"] = "reviewed" sources: list[str] = [] tables: list[str] = [] concepts: list[str] = [] body: str = "" path: Path | None = None # valorizzato al load, escluso dal dump @classmethod def parse(cls, text: str, path: Path | None = None) -> "EvidenceDoc": if not text.startswith("---\n"): raise EvidenceError(f"frontmatter mancante in {path or ''}") try: _, fm, body = text.split("---\n", 2) except ValueError as e: raise EvidenceError(f"frontmatter malformato in {path or ''}") from e meta = yaml.safe_load(fm) if not isinstance(meta, dict): raise EvidenceError(f"frontmatter non valido in {path or ''}") try: return cls.model_validate({**meta, "body": body.strip("\n"), "path": path}) except ValidationError as e: raise EvidenceError(f"evidence non valida in {path or ''}:\n{e}") from e def dump(self) -> str: meta = self.model_dump(exclude={"body", "path"}, mode="json") fm = yaml.safe_dump(meta, sort_keys=False, allow_unicode=True) return f"---\n{fm}---\n{self.body}\n" def save(self, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(self.dump()) self.path = path def load_evidence_dir(root: Path) -> list[EvidenceDoc]: """Carica ricorsivamente tutte le evidence sotto `root`, preservando la gerarchia per dominio. I file README (di sola navigazione) sono ignorati.""" docs: list[EvidenceDoc] = [] if not root.is_dir(): return docs for f in sorted(root.rglob("*.md")): if f.name.upper().startswith("README"): continue # I file formula (concept->SQL, frontmatter diverso) vivono sotto formulas/ con # estensione .sql.md: non sono EvidenceDoc, li gestisce formula_store (D14b). if f.name.endswith(".sql.md"): continue docs.append(EvidenceDoc.parse(f.read_text(), path=f)) return docs