#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_layouts_c5a.py — Chantier C5 · lot 1 (catalogue : charts natifs) ====================================================================== Ajoute bar_chart (L42), line_chart (L43) et donut_split (L44) à layouts_v2.yaml par APPEND sécurisé (même mécanique que le patch C4 : parse avant, contrôle de fin de bloc, backup, re-parse, restauration automatique en cas d'échec — le fichier n'est jamais reformaté). Après ce patch, propager : python3 prompt_injection_v2.py && python3 build_gallery.py Usage (dossier du pipeline, single-line) : python3 patch_layouts_c5a.py """ import shutil import sys from pathlib import Path import yaml TARGET = Path("layouts_v2.yaml") FRAGMENT = """ # ── Chantier C5 lot 1 — charts natifs (éditables dans PowerPoint) ── bar_chart: id: L42 famille: Données mode: light champs: [titre, categories, series, unite, source, horizontal] champs_requis: [titre, categories, series] agent_hint: >- Comparaison de valeurs par catégories — graphique NATIF éditable. Max 8 catégories × 3 séries. series = liste de {label, values} ; values = nombres SANS guillemets, alignés sur categories. horizontal: true pour des barres (libellés longs). unite (ex M€) et source optionnels. Couleurs imposées : navy, coral, glacier. line_chart: id: L43 famille: Données mode: light champs: [titre, points_x, series, unite, source] champs_requis: [titre, points_x, series] agent_hint: >- Évolution temporelle — graphique NATIF éditable. Max 12 points × 3 séries. points_x = libellés d'axe (mois, années...) ; series = {label, values}, nombres sans guillemets. Le dernier point de la première série est automatiquement mis en valeur (corail). donut_split: id: L44 famille: Données mode: light champs: [titre, segments, valeur_centrale, source] champs_requis: [titre, segments] agent_hint: >- Répartition d'un tout : anneau à gauche + légende détaillée à droite. 2 à 6 segments = {label, valeur} (nombres sans guillemets). valeur_centrale (optionnelle) s'affiche au centre de l'anneau (ex : total « 120 M€ »). Couleurs = cycle PR imposé. """ 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 ("bar_chart", "line_chart", "donut_split"): 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-c5a") print(" + Sauvegarde : %s.bak-c5a" % 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 ("bar_chart", "line_chart", "donut_split")) assert len(nl) == len(layouts) + 3 except Exception as e: shutil.copy2(str(TARGET) + ".bak-c5a", TARGET) fail("Contrôle post-append échoué (%s) — fichier restauré." % e) print(" + bar_chart (L42), line_chart (L43), donut_split (L44) " "ajoutés (%d layouts au total)." % (len(layouts) + 3)) print("\n Propager : python3 prompt_injection_v2.py && python3 " "build_gallery.py") if __name__ == "__main__": main()