[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>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user