"""Modèle de données du générateur : constantes canoniques + champs requis. Sépare les VALEURS CANONIQUES (imposées par CLAUDE.md, jamais issues du brief — donc jamais « inventées » projet par projet) de la structure des champs 🔴/🟡 du template v1.0 (§2-§5). """ from __future__ import annotations from typing import Any, Optional # --------------------------------------------------------------------------- # # Version du template canonique consommé (doit rester alignée sur # TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md §8 · version.schema.json pattern). # --------------------------------------------------------------------------- # TEMPLATE_VERSION = "1.0.0" # --------------------------------------------------------------------------- # # Paramètres CANONIQUES CLAUDE.md #9/#10. Émis TELS QUELS par le générateur pour # TOUS les projets. Ne proviennent JAMAIS du brief → aucune invention possible. # --------------------------------------------------------------------------- # CANONICAL = { "frais_edition_pct": "3 %", "marketing_pct": "8.5 %", "point_equilibre_pct": "52 %", "devises": "USD + DOP", "paiement": "Cardnet", "format_doc": "Letter US", } # Ordre canonique des 4 volets (version.schema.json enum + parser). VOLETS = ["masterplan", "architecture", "paysage_experience", "ingenierie"] # --------------------------------------------------------------------------- # # Champs OBLIGATOIRES (🔴) par volet — cf. tableaux §2-§5 du template. # Un 🔴 absent ⇒ champs_manquants ⇒ rétrogradation (jamais « complete »). # Les typologies (§3.2, bloc anti-gap) sont traitées à part par le scorer. # --------------------------------------------------------------------------- # REQUIRED = { "masterplan": [ "localisation", # 1.1 "terrain_m2", # 1.2 "terrain_tareas", # 1.2 "zonage", # 1.3 "nb_unites", # 1.4 "nb_phases", # 1.4 "phasage", # 1.8 ], "architecture": [ "parti_archi", # 2.1 "nb_batiments", # 2.2 "nb_niveaux", # 2.3 ], "paysage_experience": [ "concept_paysage", # 3.1 "amenities", # 3.2 "services_inclus", # 3.3 ], "ingenierie": [ "cout_construction_usd", # 4.4 "revenu_brut_usd", # 4.8 "marge_pct", # 4.9 ], } # Colonnes 🔴 d'une typologie (§3.2). Toutes requises pour lever le blocage prix. TYPO_REQUIRED = [ "nom", "quantite", "surface_interieure_m2", "surface_terrasse_m2", "surface_totale_m2", "prix_usd", "prix_dop", ] # Placeholders signalant l'absence (alignés sur publiciste.parser._ABSENT). _PLACEHOLDER_ABSENT = { "", "—", "-", "n/d", "nd", "non défini", "non defini", "à définir", "a definir", "tbd", "todo", "…", "...", } def is_filled(value: Any) -> bool: """True si `value` est une donnée réelle exploitable (pas un placeholder). - None / cellule vide / « non défini » / `{{…}}` → False - liste vide → False, liste non vide → True - 0 numérique → True (un vrai zéro sourcé est une donnée valide) """ if value is None: return False if isinstance(value, bool): return True if isinstance(value, (int, float)): return True if isinstance(value, (list, tuple)): return len(value) > 0 if isinstance(value, dict): return len(value) > 0 text = str(value).strip().lower() if not text: return False if "{{" in text and "}}" in text: return False return text not in _PLACEHOLDER_ABSENT def get(section: Optional[dict], key: str) -> Any: """Accès défensif à une clé de section de brief (None si absente).""" if not isinstance(section, dict): return None return section.get(key)