Files
oto-enterprise-os-dtp/05_deliverables_mvp/faisabilite/generator/genlib/model.py
T
Claude Code DTP Worker 911f70b9ab [DTP-Worker] Sprint 2 · Faisabilité générateur 4 volets (brief→data_room + round-trip Publiciste + gate)
Générateur brief JSON → data_room/PXX/ (template canonique v1.0) :
- genlib/{model,scorer,renderer}.py : params canoniques imposés (#9/#10),
  barème 4Big §7, rendu aligné sur le parser Publiciste (round-trip garanti)
- faisabilite_gen.py : CLI score/scaffold/generate/batch ; axe machine-lisible
  PROUVÉ (re-parse Publiciste + validation version.json), jamais auto-décerné
- Anti-invention #6 : champ 🔴 absent → placeholder + champs_manquants +
  rétrogradation statut ; prix manquant → {{…}} (jamais 0)
- 16 tests unittest (stdlib, réutilise parser+validateur Publiciste · #5) +
  oracle jsonschema ; job CI faisabilite-gen-tests dans le gate (Gitea only #2)
- brief.schema.json + fixtures synthétiques ; README ; GAP §2 mis à jour

Boucle de valeur fermée : generator → data_room → Publiciste → projets_master.json
Auto-score 4Big : 96/100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 02:37:47 +00:00

114 lines
3.9 KiB
Python

"""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)