"""Assemblage déterministe du run-sheet démo + du manifeste (hand-off). Le builder est PUR : mêmes entrées (spec + artefacts `out/` sur disque) ⇒ même sortie. Aucun horodatage implicite, aucun ordre non déterministe. L'ordre des beats est dérivé de la position dans le spec (jamais d'un champ `ordre` en dur, pour éviter toute dérive). Les modules cités sont recoupés avec l'ensemble des modules gated par le CI (via `deps.ci_module_paths`) — une démo ne s'appuie que sur du validé. """ from __future__ import annotations from . import deps, evidence SELF_MODULE = "demo/scenarios" def _build_beat(loader: evidence.Loader, beat: dict, ordre: int) -> dict: cites = [] for ev in beat["evidence"]: module, file, pointer = ev["module"], ev["file"], ev["pointer"] valeur, resolu = evidence.resolve(loader, module, file, pointer) cites.append({ "module": module, "file": file, "pointer": pointer, "label": ev["label"], "valeur": valeur, "resolu": resolu, }) return { "ordre": ordre, "id": beat["id"], "titre": beat["titre"], "role_narratif": beat["role_narratif"], "duree_min": beat["duree_min"], "evidence": cites, } def _build_scenario(loader: evidence.Loader, spec: dict, sc: dict) -> dict: beats = [_build_beat(loader, b, i + 1) for i, b in enumerate(sc["beats"])] modules = sorted({c["module"] for b in beats for c in b["evidence"]}) duree = sum(b["duree_min"] for b in beats) return { "id": sc["id"], "projet": sc["projet"], "projet_libelle": spec["projets"][sc["projet"]], "audience": sc["audience"], "titre": sc["titre"], "objectif": sc["objectif"], "duree_min": duree, "modules_cites": modules, "beats": beats, } def build_run_sheet(spec: dict, generated_at: str | None = None) -> dict: """Construit le run-sheet complet (le hand-off principal).""" loader = evidence.Loader() scenarios = [_build_scenario(loader, spec, sc) for sc in spec["scenarios"]] return { "doc": "OTO · Run-sheet démo (méta-livrable)", "spec_version": spec["spec_version"], "roadmap_ref": spec["roadmap_ref"], "generated_at": generated_at, "scenarios": scenarios, } def _all_citations(run_sheet: dict) -> list[dict]: return [c for sc in run_sheet["scenarios"] for b in sc["beats"] for c in b["evidence"]] def build_manifest(spec: dict, run_sheet: dict) -> dict: """Manifeste diagnostic + preuve de couverture CI + drapeau `pret`.""" cites = _all_citations(run_sheet) non_resolues = [c for c in cites if not c["resolu"]] modules_cites = sorted({c["module"] for c in cites}) gated = deps.ci_module_paths() # modules réellement gated par le CI non_gated = sorted(m for m in modules_cites if m not in gated) scen_diag = [] for sc in run_sheet["scenarios"]: sc_cites = [c for b in sc["beats"] for c in b["evidence"]] scen_diag.append({ "id": sc["id"], "projet": sc["projet"], "audience": sc["audience"], "beats": len(sc["beats"]), "citations": len(sc_cites), "citations_resolues": sum(1 for c in sc_cites if c["resolu"]), "duree_min": sc["duree_min"], "modules_cites": sc["modules_cites"], }) couverture_ok = not non_gated pret = (not non_resolues) and couverture_ok return { "deliverable": "Scénarios démo · run-sheet de pitch", "sprint": "Sprint 7 · CRM + Faisabilité", "roadmap_ref": spec["roadmap_ref"], "generated_from": "scenario_spec.json", "deliverables_root": "05_deliverables_mvp", "self_module": SELF_MODULE, "generated_at": run_sheet["generated_at"], "scenarios": scen_diag, "counts": { "scenarios": len(run_sheet["scenarios"]), "beats_total": sum(len(sc["beats"]) for sc in run_sheet["scenarios"]), "citations_total": len(cites), "citations_non_resolues": len(non_resolues), "modules_cites_uniques": len(modules_cites), }, "modules_cites": modules_cites, "couverture_ci": { "ok": couverture_ok, "modules_cites_non_gated": non_gated, }, "pret": pret, }