#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_facilitator_c6.py — Chantier C6 (validateur freeform palier 4) ==================================================================== Patch strict de facilitator_v9.py : P1. VALID_BLOCK_TYPES accepte le type « shape ». P2. validate_freeform v2 — deux niveaux : ERREURS (bloquantes) : type de bloc inconnu, shape inconnue, couleur hors charte (hex, token inconnu, dérivé mal formé), bloc hors slide, plus de 15 blocs, mode invalide, background hors charte. AVERTISSEMENTS (non bloquants, affichés) : plus de 10 blocs (densité), alpha hors 0-100 (sera clampé), rotation non multiple de 15° (sera arrondie), radius hors 0-0.5. Le vocabulaire validé : tokens + dérivés token@NN, les 12 shapes curées, full_bleed, border, dash — aligné sur le moteur C6. Usage (dossier du pipeline, single-line) : python3 patch_facilitator_c6.py Vérifie chaque ancre, écrit .bak-fc6, compile, idempotent. """ import py_compile import shutil import sys from pathlib import Path TARGET = Path("facilitator_v9.py") OLD_TYPES = ('VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", ' '"circle",\n' ' "badge", "card", "rect", "line", ' '"image"}\n') NEW_TYPES = ('VALID_BLOCK_TYPES = {"title", "heading", "text", "stat", ' '"circle",\n' ' "badge", "card", "rect", "line", ' '"image", "shape"}\n' 'VALID_FREE_SHAPES = {"chevron", "arrow", "triangle", ' '"pill", "donut",\n' ' "bracket_left", "bracket_right", ' '"oval", "diamond",\n' ' "hexagon", "parallelogram", "moon"}\n' 'FREE_TOKEN_RE = re.compile(\n' ' r"^(navy|navy_light|coral|glacier|slate|card|card_alt' '|white|body|muted)"\n' ' r"(@\\d{1,3})?$")\n') NEW_VALIDATE = '''def validate_freeform(raw: str): """Valide un YAML freeform — palier 4 (C6), deux niveaux. Retourne (ok, message, data). Les avertissements n'empêchent pas le rendu ; ils sont intégrés au message.""" try: data = yaml.safe_load(extract_yaml(raw)) except yaml.YAMLError as e: return False, f"Erreur de syntaxe YAML : {e}", None if not isinstance(data, dict) or "slides" not in data: return False, "Clé 'slides' manquante.", None slides = data["slides"] if not slides: return False, "Aucune slide.", None def color_ok(c): return bool(FREE_TOKEN_RE.match(str(c).strip().lower())) errors, warns = [], [] for i, s in enumerate(slides, 1): if s.get("layout") != "freeform": errors.append(f"Slide {i} : layout doit être 'freeform'") continue if s.get("mode") not in ("light", "dark", None): errors.append(f"Slide {i} : mode doit être 'light' ou " f"'dark'") bg = s.get("background") if bg and not color_ok(bg): errors.append(f"Slide {i} : background '{bg}' hors charte " f"(token ou token@NN)") blocks = s.get("blocks", []) if not blocks: errors.append(f"Slide {i} : aucun bloc") if len(blocks) > 15: errors.append(f"Slide {i} : {len(blocks)} blocs (max 15)") elif len(blocks) > 10: warns.append(f"Slide {i} : {len(blocks)} blocs — pense " f"respiration (10 max conseillé)") for j, b in enumerate(blocks, 1): loc = f"Slide {i} bloc {j}" bt = (b.get("type") or "text").lower() if bt not in VALID_BLOCK_TYPES: errors.append(f"{loc} : type '{bt}' invalide") if bt == "shape": sk = str(b.get("shape") or "").lower() if sk and sk not in VALID_FREE_SHAPES: errors.append( f"{loc} : shape '{sk}' inconnue " f"({', '.join(sorted(VALID_FREE_SHAPES))})") col = float(b.get("col", 0)); w = float(b.get("w", 4)) row = float(b.get("row", 0)); h = float(b.get("h", 1)) if col + w > 12.01: errors.append(f"{loc} : déborde " f"(col+w={col + w:.1f}>12)") if row + h > 12.01: errors.append(f"{loc} : déborde " f"(row+h={row + h:.1f}>12)") for key in ("color", "text_color"): c = b.get(key) if c and not color_ok(c): errors.append(f"{loc} : {key} '{c}' hors charte " f"(token ou token@NN, jamais de hex)") border = b.get("border") if border is not None: if not isinstance(border, dict): errors.append(f"{loc} : border doit être " f"{{color, weight}}") elif border.get("color") and \ not color_ok(border["color"]): errors.append(f"{loc} : border.color " f"'{border['color']}' hors charte") alpha = b.get("alpha") if alpha is not None: try: if not 0 <= float(alpha) <= 100: warns.append(f"{loc} : alpha {alpha} hors " f"0-100 — sera clampé") except (TypeError, ValueError): errors.append(f"{loc} : alpha '{alpha}' non " f"numérique") rot = b.get("rotation") if rot is not None: try: if float(rot) % 15 != 0: warns.append(f"{loc} : rotation {rot}° — sera " f"arrondie au pas de 15°") except (TypeError, ValueError): errors.append(f"{loc} : rotation '{rot}' non " f"numérique") radius = b.get("radius") if radius is not None: try: if not 0 <= float(radius) <= 0.5: warns.append(f"{loc} : radius {radius} hors " f"0-0.5 — sera clampé") except (TypeError, ValueError): errors.append(f"{loc} : radius '{radius}' non " f"numérique") if errors: return False, "\\n ".join(errors), data msg = f"{len(slides)} slides freeform valides." if warns: msg += "\\n ⚠ " + "\\n ⚠ ".join(warns) return True, msg, data ''' MARKER = "VALID_FREE_SHAPES" 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é (VALID_FREE_SHAPES présent) — rien à faire.") if content.count(OLD_TYPES) != 1: fail("Ancre VALID_BLOCK_TYPES introuvable ou non unique — " "appliquer d'abord le patch C4 du facilitator.") # Localiser la fonction validate_freeform complète start = content.find("def validate_freeform(raw: str):") if start == -1: fail("validate_freeform introuvable.") end = content.find("\ndef ", start + 10) if end == -1: fail("Fin de validate_freeform introuvable.") end += 1 # conserver le \\n final avant def suivant shutil.copy2(TARGET, str(TARGET) + ".bak-fc6") print(" + Sauvegarde : %s.bak-fc6" % TARGET) content = content.replace(OLD_TYPES, NEW_TYPES) # recalcul après P1 start = content.find("def validate_freeform(raw: str):") end = content.find("\ndef ", start + 10) + 1 content = content[:start] + NEW_VALIDATE + "\n" + content[end:] TARGET.write_text(content, encoding="utf-8") print(" + Types + shapes + validateur 2 niveaux appliqués.") try: py_compile.compile(str(TARGET), doraise=True) print(" + Compilation OK.") except py_compile.PyCompileError as e: shutil.copy2(str(TARGET) + ".bak-fc6", TARGET) fail("Erreur de compilation — fichier restauré :\n%s" % e) print("\n Prochaine étape : prompt_the_free_designer_v2.md dans " "Mistral Studio (temp 0.7).") if __name__ == "__main__": main()