[DTP-Worker] Sprint 8 · buffer · Faisabilité/generator : les PARAMÈTRES CANONIQUES du générateur 4 volets (« 3 % édition · 8.5 % marketing · 52 % point d'équilibre · USD+DOP · Cardnet · Letter US ») — la garantie anti-invention (#6) imposée à CHAQUE faisabilité — étaient RECOPIÉS dans genlib/model.py::CANONICAL (le dict émis dans chaque rendu, HORS out/ donc invisible à check_artifacts) ET dans la prose README:28, sans AUCUN ancrage à leur source faisant autorité : les contraintes NON-NÉGOCIABLES #9/#10 de CLAUDE.md. Preuve reproduite : changer CLAUDE.md #9 « Point équilibre 52% → 55% » laisse model.py, le test tests/ ET le README à « 52 % » — tous internement cohérents, TOUS contredisant CLAUDE.md → le générateur émettrait 52 % dans chaque faisabilité alors que le mandat dit 55 %, une invention silencieuse de la classe même que #6 interdit. PIÈGE : model.py n'est pas un out/*.json (check_artifacts aveugle) et le SEUL contrôle existant (test_generator.py::test_canonical_params_always_present) assert que le RENDU contient « 3 % »/« 52 % »… mais ces marqueurs sont HARDCODÉS DANS le test — une copie de plus, JAMAIS comparée à CLAUDE.md → aucune suite tests/ (qui teste des FONCTIONS, pas l'ancrage) n'attrape la dérive. Même esprit que le catalogue projets dossier_vente et le roadmap-anchor (INV11) : on ANCRE les consommateurs à leur source. Nouveau bloc dans ci/check_readme_claims.sh : RECOMPUTE les 6 valeurs depuis les lignes #9/#10 de CLAUDE.md (zéro duplication · source unique) et exige qu'elles égalent EXACTEMENT (1) model.py::CANONICAL (% par valeur numérique · textes accent/espace-insensibles via NFKD) et (2) l'énumération de la prose README (set-diff absent ET en trop). Sanity CLAUDE.md en bonus (3 pcts + 3 valeurs en gras lisibles). Un claim absent échoue AUSSI. 7 morsures vérifiées : CLAUDE.md #9 52 %→55 % (model + README stale · model.py DÉRIVE point_equilibre) · model.py marketing 8.5 %→9 % · CLAUDE.md #10 Cardnet→Azul (model stale · paiement) · README retire Letter US (absents=[letterus]) · README ajoute token IBAN (en trop=[iban]) · énumération README supprimée (INTROUVABLE) · model.py USD + DOP → USD (devises) ; restauré = green : CLAUDE.md #9/#10 == model.py CANONICAL (6 valeurs) == prose README · exit 0. État courant : aucune valeur périmée (anti-invention #6, rien à réécrire) — le défaut est la surface ungated. ci/README.md (table + détail « paramètres canoniques ancrés sur CLAUDE.md #9/#10 ») mis à jour · 7 gates re-verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-1
File diff suppressed because one or more lines are too long
@@ -2216,6 +2216,130 @@ if pc_cat is not None:
|
||||
bad(f"Portails catalogue · {PC_RD} — hand-off énumère {sorted(got)} MAIS "
|
||||
f"MANIFEST {sorted(exp)}{detail} → régénérer le README")
|
||||
|
||||
# ============================================================================
|
||||
# Faisabilité/generator · PARAMÈTRES CANONIQUES « ancrés sur CLAUDE.md #9/#10 »
|
||||
# — les 6 valeurs imposées à CHAQUE faisabilité (jamais issues du brief).
|
||||
# ----------------------------------------------------------------------------
|
||||
# Le générateur 4 volets tient sa garantie anti-invention (#6) d'un jeu de
|
||||
# PARAMÈTRES CANONIQUES émis TELS QUELS pour tous les projets : frais d'édition
|
||||
# 3 %, marketing 8.5 %, point d'équilibre 52 % (CLAUDE.md #9) · devises USD+DOP,
|
||||
# format Letter US, paiement Cardnet (CLAUDE.md #10). Ces 6 valeurs sont la
|
||||
# SOURCE FAISANT AUTORITÉ = les contraintes NON-NÉGOCIABLES #9/#10 de CLAUDE.md.
|
||||
# Elles sont RECOPIÉES à deux endroits, tous deux NON gatés :
|
||||
# (a) genlib/model.py `CANONICAL` — le dict que le générateur émet dans CHAQUE
|
||||
# rendu (n'est PAS un out/*.json → invisible à check_artifacts) ;
|
||||
# (b) README:28 « 3 % édition · 8.5 % marketing · 52 % point d'équilibre ·
|
||||
# USD+DOP · Cardnet · Letter US, CLAUDE.md #9/#10 » — la prose du module.
|
||||
# Le SEUL contrôle existant (tests/test_generator.py::test_canonical_params…)
|
||||
# assert que le RENDU contient les marqueurs « 3 % », « 52 % »… — mais ces
|
||||
# marqueurs sont HARDCODÉS DANS LE TEST, une copie de plus, JAMAIS comparée à
|
||||
# CLAUDE.md. Donc si la contrainte #9 change (ex. point d'équilibre 52 %→55 %),
|
||||
# model.py, le test ET le README restent « 52 % » : tous internement cohérents,
|
||||
# TOUS contredisant CLAUDE.md — le générateur émettrait alors 52 % dans chaque
|
||||
# faisabilité alors que le mandat dit 55 % : une invention silencieuse de la
|
||||
# classe même que #6 interdit, qu'aucune suite tests/ (qui teste des FONCTIONS,
|
||||
# pas l'ancrage à CLAUDE.md) n'attrape. Même esprit que le roadmap-anchor (INV11)
|
||||
# et le catalogue projets dossier_vente : on ANCRE les consommateurs à CLAUDE.md.
|
||||
# On RECOMPUTE les 6 valeurs depuis les lignes #9/#10 de CLAUDE.md (zéro
|
||||
# duplication · source unique) et on exige que (1) model.py `CANONICAL` les
|
||||
# égale EXACTEMENT (% par valeur numérique · textes accent/espace-insensibles)
|
||||
# et (2) la prose README les énumère EXACTEMENT (set-diff : absent ET en trop).
|
||||
# Un claim absent échoue AUSSI (traçabilité).
|
||||
# ============================================================================
|
||||
import ast
|
||||
def _ai(s): # accent-insensible, minuscule, sans espaces (comparaison de tokens)
|
||||
return "".join(c for c in unicodedata.normalize("NFKD", s)
|
||||
if unicodedata.category(c) != "Mn").lower().replace(" ", "")
|
||||
def _pnorm(v): # 3.0 → "3%" · 8.5 → "8.5%" (format déterministe)
|
||||
return f"{'%g' % v}%"
|
||||
def _pct(s): # "3 %"/"52%"/… → 3.0/52.0
|
||||
mm = re.search(r"(\d+(?:\.\d+)?)\s*%", s)
|
||||
return float(mm.group(1)) if mm else None
|
||||
try:
|
||||
cm_raw2 = open("CLAUDE.md", encoding="utf-8").read()
|
||||
l9 = re.search(r"^9\.\s+(.+)$", cm_raw2, re.M)
|
||||
l10 = re.search(r"^10\.\s+(.+)$", cm_raw2, re.M)
|
||||
if not l9 or not l10:
|
||||
raise ValueError("contrainte #9 ou #10 INTROUVABLE")
|
||||
# #9 : trois pourcentages, associés par mot-clé (accent-insensible)
|
||||
seg9 = re.split(r"·", l9.group(1))
|
||||
def _pct_kw(kw):
|
||||
for s in seg9:
|
||||
if kw in _ai(s):
|
||||
return _pct(s)
|
||||
return None
|
||||
exp = {
|
||||
"frais_edition_pct": _pct_kw("frais"),
|
||||
"marketing_pct": _pct_kw("marketing"),
|
||||
"point_equilibre_pct": _pct_kw("equilibre"),
|
||||
}
|
||||
# #10 : trois valeurs en gras, associées par leur rôle (devises/format/paiement)
|
||||
seg10 = re.split(r"·", l10.group(1))
|
||||
def _bold_kw(kw):
|
||||
for s in seg10:
|
||||
if kw in _ai(s):
|
||||
mm = re.search(r"\*\*(.+?)\*\*", s)
|
||||
return mm.group(1).strip() if mm else None
|
||||
return None
|
||||
exp["devises"] = _bold_kw("devise")
|
||||
exp["format_doc"] = _bold_kw("format")
|
||||
exp["paiement"] = _bold_kw("paiement")
|
||||
if None in exp.values():
|
||||
raise ValueError(f"valeurs canoniques illisibles dans #9/#10 : {exp}")
|
||||
except (OSError, ValueError) as e:
|
||||
bad(f"Faisabilité canoniques · CLAUDE.md #9/#10 illisible/inattendu : {e}"); exp = None
|
||||
if exp is not None:
|
||||
good(f"Faisabilité canoniques · CLAUDE.md #9/#10 sain : {_pnorm(exp['frais_edition_pct'])} "
|
||||
f"édition · {_pnorm(exp['marketing_pct'])} mkt · {_pnorm(exp['point_equilibre_pct'])} "
|
||||
f"équilibre · {exp['devises']} · {exp['format_doc']} · {exp['paiement']}")
|
||||
_num_keys = ("frais_edition_pct", "marketing_pct", "point_equilibre_pct")
|
||||
_txt_keys = ("devises", "format_doc", "paiement")
|
||||
# 1) ANCRAGE : genlib/model.py CANONICAL == CLAUDE.md #9/#10
|
||||
MP = f"{D}/faisabilite/generator/genlib/model.py"
|
||||
try:
|
||||
msrc = open(MP, encoding="utf-8").read()
|
||||
mm = re.search(r"CANONICAL\s*=\s*(\{.*?\})", msrc, re.S)
|
||||
canon = ast.literal_eval(mm.group(1))
|
||||
except (OSError, ValueError, SyntaxError, AttributeError) as e:
|
||||
bad(f"Faisabilité canoniques · {MP} CANONICAL illisible/inattendu : {e}"); canon = None
|
||||
if canon is not None:
|
||||
drift = []
|
||||
for k in _num_keys:
|
||||
g = _pct(str(canon.get(k, "")))
|
||||
if g is None or g != exp[k]:
|
||||
drift.append(f"{k}={canon.get(k)!r} (attendu {_pnorm(exp[k])})")
|
||||
for k in _txt_keys:
|
||||
if _ai(str(canon.get(k, ""))) != _ai(exp[k]):
|
||||
drift.append(f"{k}={canon.get(k)!r} (attendu {exp[k]!r})")
|
||||
if drift:
|
||||
bad(f"Faisabilité canoniques · model.py CANONICAL DÉRIVE de CLAUDE.md #9/#10 : "
|
||||
f"{' · '.join(drift)} → aligner model.py (ou CLAUDE.md)")
|
||||
else:
|
||||
good("Faisabilité canoniques · model.py CANONICAL == CLAUDE.md #9/#10 (6 valeurs)")
|
||||
# 2) PROSE : README:28 énumère les 6 valeurs EXACTEMENT (absent ET en trop)
|
||||
FG_RD = f"{D}/faisabilite/generator/README.md"
|
||||
fg_rd = read_norm(FG_RD)
|
||||
if fg_rd is not None:
|
||||
mr = re.search(r"param[eè]tres canoniques\*\* \((.+?), CLAUDE\.md", fg_rd)
|
||||
if mr is None:
|
||||
bad(f"Faisabilité canoniques · {FG_RD} — énumération « paramètres canoniques "
|
||||
f"(… CLAUDE.md #9/#10) » INTROUVABLE")
|
||||
else:
|
||||
got = set()
|
||||
for tok in re.split(r"·", mr.group(1)):
|
||||
p = _pct(tok)
|
||||
got.add(_pnorm(p) if p is not None else _ai(tok))
|
||||
want = {_pnorm(exp[k]) for k in _num_keys} | {_ai(exp[k]) for k in _txt_keys}
|
||||
if got == want:
|
||||
good(f"Faisabilité canoniques · {FG_RD} — prose énumère les 6 valeurs == "
|
||||
f"CLAUDE.md #9/#10")
|
||||
else:
|
||||
miss = sorted(want - got); extra = sorted(got - want)
|
||||
detail = (f" · absents={miss}" if miss else "") + \
|
||||
(f" · en trop={extra}" if extra else "")
|
||||
bad(f"Faisabilité canoniques · {FG_RD} — prose énumère {sorted(got)} MAIS "
|
||||
f"CLAUDE.md {sorted(want)}{detail} → régénérer le README")
|
||||
|
||||
sys.exit(1 if FAIL else 0)
|
||||
PY
|
||||
rc=$?
|
||||
|
||||
Reference in New Issue
Block a user