#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_facilitator_c3.py — Chantier C3 (fusion des révisions ciblées) ==================================================================== Patch strict de facilitator_v9.py : P1. Insère merge_revision() : remplace, dans le dernier YAML complet (project_state.json → dernier_yaml), les slides régénérées par la révision ciblée (appariement par position), puis re-rend le deck ENTIER. Le PPTX partiel « à recoller » disparaît du flux. P2. Branche la fusion dans run_full_pipeline_pass juste avant le rendu (suffix revision_ciblee uniquement). P3. En cas de fusion réussie, l'état du projet est persisté comme un deck complet (dernier_yaml/dernier_pptx à jour). P4. Message utilisateur complété au site révision ciblée. Échec de fusion (pas de dernier_yaml, YAML illisible…) : comportement actuel conservé à l'identique (PPTX partiel + message de recollage). Usage (dossier du pipeline, single-line) : python3 patch_facilitator_c3.py Compatible avant/après les patchs C1 et C2 (ancres indépendantes). Vérifie chaque ancre, écrit .bak-c3, compile, idempotent. """ import py_compile import shutil import sys from pathlib import Path TARGET = Path("facilitator_v9.py") FUNC = ''' def merge_revision(proj: "Project", partial_yaml_path: Path): """Fusion YAML des révisions ciblées (chantier C3). Remplace dans le dernier YAML complet les slides régénérées (appariement par position ; positions inconnues ajoutées en fin). Retourne le chemin du YAML complet fusionné, ou None si fusion impossible (l'appelant conserve alors le flux partiel actuel).""" state = proj.load_state() last = state.get("dernier_yaml") or "" if not last or not Path(last).exists(): warn("Fusion : pas de YAML complet précédent — PPTX partiel " "conservé.") return None try: full = yaml.safe_load(Path(last).read_text(encoding="utf-8")) part = yaml.safe_load( partial_yaml_path.read_text(encoding="utf-8")) except yaml.YAMLError as e: warn(f"Fusion : YAML illisible ({e}).") return None if not isinstance(full, dict) or not full.get("slides"): warn("Fusion : le YAML précédent ne contient pas de slides.") return None news = {} for s in (part or {}).get("slides", []): if isinstance(s, dict) and s.get("position"): news[int(s["position"])] = s if not news: warn("Fusion : aucune slide positionnée dans la révision.") return None merged, replaced = [], 0 for i, s in enumerate(full["slides"]): pos = int(s.get("position", i + 1)) if isinstance(s, dict) \ else i + 1 if pos in news: merged.append(news.pop(pos)) replaced += 1 else: merged.append(s) for pos in sorted(news): merged.append(news[pos]) full["slides"] = merged out = proj.out("revision_fusion", "yaml") out.write_text( yaml.safe_dump(full, allow_unicode=True, sort_keys=False, default_flow_style=False, width=100), encoding="utf-8") ok(f"Fusion : {replaced} slide(s) remplacée(s), " f"{len(merged)} au total → {out.name}") return out ''' PATCHES = [ # P2 — fusion avant le rendu (revision_ciblee) ( ' # Renommer la sortie selon le suffixe demandé\n' ' if suffix != "input" and yaml_path:\n' ' new_path = proj.out(suffix, "yaml")\n' ' yaml_path.rename(new_path)\n' ' yaml_path = new_path\n' '\n' ' manifest.file(yaml_path)\n', ' # Renommer la sortie selon le suffixe demandé\n' ' if suffix != "input" and yaml_path:\n' ' new_path = proj.out(suffix, "yaml")\n' ' yaml_path.rename(new_path)\n' ' yaml_path = new_path\n' '\n' ' merged = False\n' ' if suffix == "revision_ciblee" and yaml_path:\n' ' fused = merge_revision(proj, yaml_path)\n' ' if fused:\n' ' yaml_path, merged = fused, True\n' ' info("Rendu du deck COMPLET fusionné.")\n' '\n' ' manifest.file(yaml_path)\n', ), # P3 — persistance d'état si fusion ( ' elif suffix == "revision_ciblee":\n' ' persist_generation(\n' ' proj, markdown=markdown,\n' ' journal_entry="Révision ciblée — slides ' 'régénérées séparément.")\n', ' elif suffix == "revision_ciblee":\n' ' if merged:\n' ' persist_generation(\n' ' proj, markdown=markdown, yaml_path=yaml_path,\n' ' pptx_path=pptx_path,\n' ' journal_entry="Révision ciblée fusionnée — ' 'deck complet régénéré.")\n' ' else:\n' ' persist_generation(\n' ' proj, markdown=markdown,\n' ' journal_entry="Révision ciblée — slides ' 'régénérées séparément.")\n', ), # P4 — message utilisateur ( ' info("Ouvre ce fichier et copie-colle les slides dans ' 'ton deck maître.")\n', ' info("Ouvre ce fichier et copie-colle les slides dans ' 'ton deck maître.")\n' ' info("(Si la fusion YAML a réussi — voir ci-dessus — ' 'le PPTX est déjà le deck complet.)")\n', ), ] MARKER = "def merge_revision" ANCHOR_SECTION = "# FLUX LIBRE — THE FREE DESIGNER" def fail(msg): print(" ! %s" % msg) sys.exit(1) def main(): if not TARGET.exists(): fail("%s introuvable — lancer depuis le dossier du pipeline." % TARGET) content = TARGET.read_text(encoding="utf-8") if MARKER in content: fail("Déjà patché (merge_revision présent) — rien à faire.") for i, (old, _) in enumerate(PATCHES, 1): n = content.count(old) if n == 0: fail("Ancre du patch %d introuvable — facilitator inattendu." % i) if n > 1: fail("Ancre du patch %d non unique (%d occurrences)." % (i, n)) if content.count(ANCHOR_SECTION) != 1: fail("Ancre de section FREE DESIGNER introuvable ou non unique.") shutil.copy2(TARGET, str(TARGET) + ".bak-fc3") print(" + Sauvegarde : %s.bak-fc3" % TARGET) for old, new in PATCHES: content = content.replace(old, new) lines = content.split("\n") idx = next(i for i, l in enumerate(lines) if ANCHOR_SECTION in l) ins = idx - 1 if lines[idx - 1].startswith("# ───") else idx lines[ins:ins] = FUNC.split("\n") content = "\n".join(lines) TARGET.write_text(content, encoding="utf-8") print(" + 3 patchs + merge_revision appliqués.") try: py_compile.compile(str(TARGET), doraise=True) print(" + Compilation OK.") except py_compile.PyCompileError as e: shutil.copy2(str(TARGET) + ".bak-fc3", TARGET) fail("Erreur de compilation — fichier restauré :\n%s" % e) print("\n La prochaine révision ciblée régénérera le deck complet " "(revision_fusion.yaml).") if __name__ == "__main__": main()