diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index 4285a80..0c8539d 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,62 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## Sprint 8 · buffer · CRM Commissions — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (7ᵉ instance du ledger) + +**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de +chiffres »). Le pattern « ledger d'invariants » (recompute le nombre de contrôles +CI depuis le registre-source `# N ·` de `_validate`, jamais une liste à la main) +a déjà couvert **Chat OTOIA**, **SEO**, **Fiscal e-CF**, **Legal CONFOTUR**, +**CRM Dossier Vente** et **QA Audit 5D**. Le **7ᵉ générateur** — dernier candidat +restant recensé au ledger (`crm/commissions` ·10) — était NON gaté : +`crm/commissions/commissions_gen.py` (barème de commissions vendeurs · roadmap l.51). + +Le compte **« 10 invariants »** y est transcrit **à la main à CINQ endroits**, +aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un +`out/*.json`) : +1. message de succès CLI `validate` « … schéma + 10 invariants verts. » (l.209) ; +2. aide du sous-parseur `validate` « schéma + 10 invariants » (l.221) ; +3. README bloc de commande « schéma + 10 invariants » (l.60) ; +4. README titre de section « ## Les 10 invariants » (l.64) ; +5. README prose de score « 10 invariants de cross-cohérence » (l.92). + +Ajouter un contrôle `# 11 ·` à `_validate` sans toucher les 5 chaînes ⇒ le README +ET les self-reports du `.py` mentent en silence pendant que la CI applique 11 +contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la +prose) n'attrape ce « vert trompeur ». + +**Particularité du registre Commissions** (≈ Dossier Vente) : le ledger est +**PROPRE 1..10** — le **schéma est compté À PART** (phrasé « schéma + N +invariants »), donc pas de schéma-#1 non marqué et pas de split a/b ; on garde la +détection de doublon nu comme filet. Le module porte AUSSI un **SECOND nombre** — +« **25 tests** » (la suite unittest, déjà gatée par ailleurs) présent dans le +README : l'ancre sur le mot « invariants » l'exclut nativement (« 25 tests » ≠ +« invariants ») ⇒ 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). + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « CRM · Commissions · IDENTITÉ +du COMPTE D'INVARIANTS ») — RECOMPUTE le ledger depuis le SEUL registre-source +(`# N ·` de `_validate`) et exige : +- **(a)** ledger CONTIGU 1..N (lacune OU doublon nu = vrai défaut de registre) ; +- **(b)** toute mention « N invariants » du `.py` == |ledger| ; +- **(c)** l'annonce 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. ») contiguë 1..N. +Une mention ou le registre absent échoue AUSSI (traçabilité #6 · anti-évaporation). + +**Vérif** — 5 checks verts sur l'arbre propre (ledger 1..10 = 10 marqueurs +`# N ·`, schéma compté à part). **7 morsures** adversariales : M1 self-report +`.py` 10→11 (b) · M2 titre README 10→11 (c+d) · M3 prose README 10→11 (d) · **M4 +ajout marqueur `# 11 ·` ⇒ ledger→11 pendant que la prose reste 10 (mord `.py` ET +README titre/mention ET l'énumération, preuve que l'autorité est le CODE)** · M5 +suppression `# 7 ·` ⇒ gap non-contigu (a) · M6 doublon nu `# 5 ·` ⇒ détection de +vrai doublon (a) · M7 énumération README 7.→8. ⇒ deux 8/pas de 7 (e). Le distinct +« 25 tests » reste INVISIBLE au gate (ancre « invariants »). Gates re-verts : +`check_artifacts` · `check_docs` · `guard_constraints` · `check_ci_integrity` · +`check_regression` · `check_readme_claims` tous exit 0. **Série ledger close : les +7 générateurs portant le nombre magique d'invariants sont désormais tous gatés.** + +--- + ## Sprint 8 · buffer · QA Audit 5D — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (6ᵉ instance du ledger) **Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 2bb7b18..fbe207c 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -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"(?