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