195 lines
7.4 KiB
Python
195 lines
7.4 KiB
Python
|
|
#!/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()
|