[DTP-Worker 20260802_023503] Sprint 8 · buffer · CRM Commissions : gate d'IDENTITÉ du COMPTE D'INVARIANTS de _validate (7ᵉ instance du ledger · série close)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-02 02:39:14 +00:00
parent bf11db6d85
commit aab89a6032
2 changed files with 178 additions and 0 deletions
+121
View File
@@ -7890,6 +7890,127 @@ if a5i_src is not None:
bad(f"Audit 5D invariants · {A5I_RD} — mention(s) « N invariants » "
f"{a5i_rd_rep} MAIS ledger en compte {a5i_ninv} → régénérer le README")
# ============================================================================
# CRM · Commissions · IDENTITÉ du COMPTE D'INVARIANTS de `_validate` — 7ᵉ
# instance de la classe « ledger d'invariants » (après Chat OTOIA · SEO · Fiscal
# e-CF · Legal CONFOTUR · CRM Dossier Vente · QA Audit 5D), appliquée au
# générateur du barème de commissions vendeurs (roadmap ligne 51).
# ----------------------------------------------------------------------------
# Le générateur commissions_gen.py applique un COMPTE d'invariants de cross-
# cohérence barème ↔ workflow ↔ DocType ↔ RBAC (les 4 contrats) dont la source
# d'autorité n'est PAS un artefact JSON (invisible à check_artifacts qui byte-gate
# les out/*.json) mais le LEDGER NUMÉROTÉ de `_validate` : les marqueurs `# N ·`
# (1..10) qui ouvrent chaque contrôle. Le nombre « 10 » est transcrit À LA MAIN à
# CINQ endroits qu'AUCUN gate ne couvrait : DEUX chaînes du .py (message de succès
# CLI « … schéma + 10 invariants verts. », aide du sous-parseur `validate`) + TROIS
# du README (bloc de commande « schéma + 10 invariants », titre de section « ## Les
# 10 invariants », prose de score « 10 invariants de cross-cohérence »). Ajouter un
# contrôle `# 11 ·` à `_validate` sans toucher les chaînes ⇒ le README ET les
# self-reports du .py mentent en silence pendant que la CI applique 11 contrôles —
# « vert trompeur » (#6) que ni les suites tests/ (FONCTIONS de validation, jamais
# la prose) ni le byte-gate d'artefact (aveugle au .py source) n'attrapent.
# PARTICULARITÉ (≈ Dossier Vente) : ledger PROPRE 1..N compté À PART du schéma
# (phrasé « schéma + N invariants » — pas de schéma-#1 non marqué, pas de split
# a/b ; on garde la détection de doublon nu comme filet). Le module porte AUSSI un
# SECOND nombre — « 25 tests » (le compte de la suite unittest, déjà gaté par
# ailleurs), présent dans le .py absent mais dans le README : l'ancre sur le mot
# « invariants » l'exclut nativement (« 25 tests » ≠ mot « invariants »), donc
# AUCUN faux positif. Comme Dossier Vente, ce README A un titre « ## Les N
# invariants » ET une énumération numérotée du ledger → on gate (a)(b)(c)(d)(e).
# On RECOMPUTE le ledger depuis le SEUL registre-source (les `# N ·` de `_validate`)
# et on exige : (a) ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de
# registre, mordu) ; (b) TOUTE mention « N invariants » du .py == |ledger| ; (c) le
# compte annoncé du titre README « ## Les N invariants » == |ledger| ; (d) TOUTE
# mention « N invariants » du README == |ledger| ; (e) l'ÉNUMÉRATION numérotée de
# la section (« 1. … 2. … N. ») elle-même contiguë 1..N (item ni sauté ni dupliqué).
# Mention ou registre absent = échec (disparition d'un chiffre = régression #6).
# ============================================================================
CMI_GEN = f"{D}/crm/commissions/commissions_gen.py"
CMI_RD = f"{D}/crm/commissions/README.md"
try:
cmi_src = open(CMI_GEN, encoding="utf-8").read()
except OSError as e:
bad(f"Commissions invariants · générateur {CMI_GEN} illisible : {e}"); cmi_src = None
if cmi_src is not None:
_mfn = re.search(r"def _validate\b", cmi_src)
if _mfn is None:
bad(f"Commissions invariants · fonction `_validate` INTROUVABLE dans {CMI_GEN} "
"(registre-source du ledger absent → impossible de re-dériver)")
else:
_bs = _mfn.end()
_mn = re.search(r"\ndef ", cmi_src[_bs:])
_body = cmi_src[_bs: _bs + _mn.start()] if _mn else cmi_src[_bs:]
_pairs = re.findall(r"#\s*(\d+)([a-z]?)\s*·", _body)
_dup_bare = sorted({n for n in {p[0] for p in _pairs}
if [s for num, s in _pairs if num == n and s == ""]
and sum(1 for num, _ in _pairs if num == n) > 1})
_top = sorted({int(n) for n, _ in _pairs})
if not _top:
bad(f"Commissions invariants · aucun marqueur `# N ·` dans `_validate` de "
f"{CMI_GEN} (ledger vide → impossible de re-dériver)")
elif _dup_bare:
bad(f"Commissions invariants · marqueur(s) `# N ·` dupliqué(s) sans suffixe "
f"a/b dans `_validate` : {_dup_bare} — vrai doublon de registre")
else:
cmi_ninv = len(_top)
if _top != list(range(1, cmi_ninv + 1)):
bad(f"Commissions invariants · ledger `_validate` NON contigu : {_top} "
f"(attendu 1..{cmi_ninv}) — lacune/doublon dans les marqueurs `# N ·`")
else:
good(f"Commissions invariants · ledger `_validate` contigu 1..{cmi_ninv} "
f"({cmi_ninv} marqueurs `# N ·` · schéma compté à part)")
# (b) self-reports du .py « N invariants » (le distinct « N tests » est
# exclu nativement par l'ancre sur le mot « invariants »).
cmi_py_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", cmi_src)})
if not cmi_py_rep:
bad(f"Commissions invariants · aucune mention « N invariants » dans {CMI_GEN} "
f"(self-report absent → attendu {cmi_ninv})")
elif cmi_py_rep == [cmi_ninv]:
good(f"Commissions invariants · {CMI_GEN} — self-report(s) {cmi_py_rep} "
f"== ledger ({cmi_ninv})")
else:
bad(f"Commissions invariants · {CMI_GEN} — self-report(s) « N invariants » "
f"{cmi_py_rep} MAIS ledger en compte {cmi_ninv} "
"(une chaîne du générateur désynchronisée du registre)")
# README : titre de section + toute mention + énumération numérotée.
try:
cmi_rd_raw = open(CMI_RD, encoding="utf-8").read()
except OSError as e:
bad(f"Commissions invariants · {CMI_RD} illisible : {e}"); cmi_rd_raw = None
if cmi_rd_raw is not None:
_mh = re.search(r"##\s+Les\s+(\d+)\s+invariants", cmi_rd_raw)
if _mh is None:
bad(f"Commissions invariants · {CMI_RD} — section « ## Les N invariants » "
f"INTROUVABLE (attendu {cmi_ninv})")
else:
# (c) compte annoncé du titre.
if int(_mh.group(1)) == cmi_ninv:
good(f"Commissions invariants · {CMI_RD} — annonce ({_mh.group(1)}) "
f"== ledger ({cmi_ninv})")
else:
bad(f"Commissions invariants · {CMI_RD} — annonce ({_mh.group(1)}) MAIS "
f"ledger en compte {cmi_ninv} → régénérer le README")
# (d) toute mention « N invariants » du README == ledger.
cmi_rd_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", cmi_rd_raw)})
if cmi_rd_rep == [cmi_ninv]:
good(f"Commissions invariants · {CMI_RD} — mention(s) {cmi_rd_rep} "
f"== ledger (aucune dérive)")
else:
bad(f"Commissions invariants · {CMI_RD} — mention(s) « N invariants » "
f"{cmi_rd_rep} MAIS ledger en compte {cmi_ninv} → régénérer le README")
# (e) énumération numérotée de la section == contiguë 1..N.
_msec = re.search(r"##\s+Les\s+\d+\s+invariants.*?(?=\n##\s|\Z)",
cmi_rd_raw, re.S)
cmi_enum = ([int(x) for x in
re.findall(r"(?<![\d.])(\d+)\.\s", _msec.group(0))]
if _msec else [])
if cmi_enum == list(range(1, cmi_ninv + 1)):
good(f"Commissions invariants · {CMI_RD} — énumération prose "
f"contiguë 1..{cmi_ninv} == ledger")
else:
bad(f"Commissions invariants · {CMI_RD} — énumération prose {cmi_enum} "
f"≠ 1..{cmi_ninv} (item sauté/dupliqué) → régénérer le README")
sys.exit(1 if FAIL else 0)
PY
rc=$?