[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:
Claude Code DTP Worker
2026-07-31 20:08:05 +00:00
parent 7934081bc2
commit 342b571b29
3 changed files with 208 additions and 1 deletions
+22 -1
View File
File diff suppressed because one or more lines are too long
+124
View File
@@ -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=$?