#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_layouts_c5b.py — Chantier C5 lot 2 (catalogue : waterfall, heatmap_table, funnel) ================================================================ Append sécurisé à layouts_v2.yaml (même mécanique verrouillée que C4 : parse avant, contrôle du bloc layouts:, .bak, re-parse, restauration auto). Après ce patch, propager : python3 prompt_injection_v2.py && python3 build_gallery.py. Usage (dossier du pipeline, single-line) : python3 patch_layouts_c5b.py """ import shutil import sys from pathlib import Path import yaml TARGET = Path("layouts_v2.yaml") NEW_KEYS = ("waterfall", "heatmap_table", "funnel") FRAGMENT = """ # ── Chantier C5 lot 2 — data & structure ────────────────────────── waterfall: id: L45 famille: Données mode: light champs: [titre, depart, marches, arrivee, unite, source] champs_requis: [titre, depart, marches, arrivee] agent_hint: >- Pont de valeur (waterfall) : expliquer un écart entre deux montants par des variations successives. depart et arrivee = {label, valeur} ; marches = liste de {label, delta} SIGNÉ (positif ou négatif, max 8). Le moteur calcule les cumuls — ne jamais fournir de cumul. Idéal pour : évolution de budget, pont d'effectifs, décomposition d'un résultat. heatmap_table: id: L46 famille: Comparaison mode: light champs: [titre, headers, rows, legende] champs_requis: [titre, headers, rows] agent_hint: >- Tableau à intensité : évaluer plusieurs items sur plusieurs critères. headers = colonnes (max 6) ; rows = {label, scores} avec score ENTIER de 0 (faible) à 4 (fort), max 8 lignes. Le moteur traduit chaque score en teinte de navy — aucune autre donnée. Idéal pour : maturité, couverture fonctionnelle, cartographie de risques. funnel: id: L47 famille: Process mode: light champs: [titre, etapes, source] champs_requis: [titre, etapes] agent_hint: >- Entonnoir de conversion : volumes décroissants d'étape en étape. etapes = {label, valeur, description?} du haut vers le bas, 3 à 5 étages. Largeurs proportionnelles aux valeurs (plancher de lisibilité), dernier étage corail. Idéal pour : pipeline commercial, adoption, qualification progressive. """ 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 NEW_KEYS: if key in layouts: fail("'%s' déjà présent — rien à faire." % key) 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-c5b") print(" + Sauvegarde : %s.bak-c5b" % 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 all(k in nl for k in NEW_KEYS) assert len(nl) == len(layouts) + len(NEW_KEYS) except Exception as e: shutil.copy2(str(TARGET) + ".bak-c5b", TARGET) fail("Contrôle post-append échoué (%s) — fichier restauré." % e) print(" + waterfall (L45), heatmap_table (L46), funnel (L47) " "ajoutés (%d layouts au total)." % (len(layouts) + len(NEW_KEYS))) print("\n Propager : python3 prompt_injection_v2.py && python3 " "build_gallery.py") if __name__ == "__main__": main()