#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_layouts_c4.py — Chantier C4 (catalogue : image_split, image_full) ======================================================================= Ajoute les deux layouts images à layouts_v2.yaml par APPEND sécurisé (le fichier n'est jamais réécrit ni reformaté — protection des commentaires et de la mise en forme, leçon de l'incident Le Chat) : 1. Parse le fichier actuel, vérifie l'absence des deux clés. 2. Vérifie que la dernière ligne significative appartient bien au bloc layouts: (ligne indentée) — sinon refus. 3. Sauvegarde .bak-c4, append du fragment, re-parse de contrôle. 4. En cas d'échec du re-parse : restauration automatique. Après ce patch, propager : python3 prompt_injection_v2.py && python3 build_gallery.py (le Designer voit les nouveaux layouts, la galerie et le bloc skill sont régénérés). Usage (dossier du pipeline, single-line) : python3 patch_layouts_c4.py """ import shutil import sys from pathlib import Path import yaml TARGET = Path("layouts_v2.yaml") FRAGMENT = """ # ── Chantier C4 — layouts images ────────────────────────────────── image_split: id: L50 famille: Visuel mode: light champs: [titre, image, bullets, side, legende] champs_requis: [titre, image, bullets] agent_hint: >- Image d'appui sur 40 % de la slide (side: left par défaut, right possible) + titre et points clés (max 4 bullets). À utiliser quand une image du dossier assets/ du projet illustre le propos ; image = nom de fichier exact tel que listé par /lire. legende (optionnelle) s'affiche sur un bandeau navy sous l'image. image_full: id: L51 famille: Visuel mode: dark champs: [titre, image, sous_titre] champs_requis: [titre, image] agent_hint: >- Ouverture de chapitre visuelle : image plein cadre + voile navy + titre display blanc. Alternative à section_divider quand un asset du projet s'y prête. image = nom de fichier exact de assets/. """ 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) raw = TARGET.read_text(encoding="utf-8") try: data = yaml.safe_load(raw) except yaml.YAMLError as e: fail("layouts_v2.yaml actuel illisible : %s" % e) layouts = (data or {}).get("layouts") if not isinstance(layouts, dict): fail("Bloc layouts: introuvable dans le fichier.") for key in ("image_split", "image_full"): if key in layouts: fail("'%s' déjà présent — rien à faire." % key) # La dernière ligne significative doit être DANS le bloc layouts: last = next((l for l in reversed(raw.splitlines()) if l.strip() and not l.strip().startswith("#")), "") if not last.startswith(" "): fail("Le fichier ne se termine pas dans le bloc layouts: " "(dernière ligne non indentée : %r) — fusion manuelle " "requise." % last[:40]) shutil.copy2(TARGET, str(TARGET) + ".bak-c4") print(" + Sauvegarde : %s.bak-c4" % TARGET) with open(TARGET, "a", encoding="utf-8") as f: if not raw.endswith("\n"): f.write("\n") f.write(FRAGMENT) try: check = yaml.safe_load(TARGET.read_text(encoding="utf-8")) nl = check["layouts"] assert "image_split" in nl and "image_full" in nl assert len(nl) == len(layouts) + 2 except Exception as e: shutil.copy2(str(TARGET) + ".bak-c4", TARGET) fail("Contrôle post-append échoué (%s) — fichier restauré." % e) print(" + image_split (L50) et image_full (L51) ajoutés " "(%d layouts au total)." % (len(layouts) + 2)) print("\n Propager : python3 prompt_injection_v2.py && python3 " "build_gallery.py") if __name__ == "__main__": main()