diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index f39fbea..4285a80 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,61 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## 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 +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** et +**CRM Dossier Vente**. Le **6ᵉ générateur** portant le même nombre magique restait +NON gaté : `qa/audit_5d/audit_5d_gen.py` (candidat restant recensé : `crm/commissions` +·10, ledger propre non gaté). + +Le compte **« 15 invariants »** y est transcrit **à la main à SIX endroits**, +aucun gaté, tous invisibles à `check_artifacts` (le `.py` source n'est pas un +`out/*.json`) : +1. docstring sous-commande module (l.19 « valide schéma + 15 invariants ») ; +2. docstring `_validate` (l.80 « 15 invariants de cohérence audit/contrôles ») ; +3. message de succès CLI `validate` « … 15 invariants — verdict … » (l.218) ; +4. aide du sous-parseur `validate` « valide schéma + 15 invariants » (l.229) ; +5. README bloc de commande « schéma + 15 invariants » (l.66) ; +6. README prose d'architecture « CLI build/validate + **15 invariants** » (l.81). + +Ajouter un contrôle `# 16 ·` à `_validate` sans toucher les 6 chaînes ⇒ le README +ET les self-reports du `.py` mentent en silence pendant que la CI applique 16 +contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la +prose) n'attrape ce « vert trompeur ». + +**Particularité du registre Audit 5D** (≈ Dossier Vente, ≠ Fiscal) : le ledger est +**PROPRE 1..15** — 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. MAIS le module porte un **SECOND nombre distinct** — +« **17 contrôles** » (le compte de fonctions-contrôle de `qalib/controls.py`, déjà +gaté par ailleurs), présent dans le `.py` ET le README : un `\d+ invariants` ancré +sur le mot « invariants » l'**exclut nativement** (« 17 contrôles » ≠ mot +« invariants ») ⇒ **aucun faux positif** sur l'arbre propre. Contrairement à +Dossier Vente/Fiscal, ce README n'a **NI titre « ## Les N invariants » NI +énumération numérotée** du ledger → on ne gate que (a)(b)(d). + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « QA · Audit 5D · 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| ; +- **(d)** toute mention « N invariants » du README == |ledger|. +Une mention ou le registre absent échoue AUSSI (traçabilité #6 · anti-évaporation). + +**Vérif** — 3 checks verts sur l'arbre propre (ledger 1..15 = 15 marqueurs `# N ·`, +schéma compté à part). **6 morsures** adversariales : M1 self-report `.py` 15→16 +(b) · M2 mention README 15→16 (d) · **M3 ajout marqueur `# 16 ·` ⇒ ledger→16 pendant +que la prose reste 15 (mord `.py` ET README, preuve que l'autorité est le CODE)** · +M4 suppression `# 7 ·` ⇒ gap non-contigu (a) · M5 doublon nu `# 5 ·` ⇒ détection de +vrai doublon (a) · **M6 no-false-positive** : le distinct « 17 contrôles » reste +INVISIBLE au gate (l'ancre « invariants » l'exclut, arbre propre VERT). Gates +re-verts : `check_artifacts` · `check_docs` · `guard_constraints` · +`check_ci_integrity` · `check_regression` · `check_readme_claims` tous exit 0. + +--- + ## Sprint 8 · buffer · CRM Dossier Vente — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (5ᵉ 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 eea0bac..2bb7b18 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -7792,6 +7792,104 @@ if dvv_src is not None: bad(f"Dossier Vente invariants · {DVV_RD} — énumération prose {dvv_enum} " f"≠ 1..{dvv_ninv} (item sauté/dupliqué) → régénérer le README") +# ============================================================================ +# QA · Audit 5D · IDENTITÉ du COMPTE D'INVARIANTS de `_validate` — 6ᵉ instance +# de la classe « ledger d'invariants » (après Chat OTOIA · SEO · Fiscal e-CF · +# Legal CONFOTUR · CRM Dossier Vente), appliquée au générateur de l'audit QA. +# ---------------------------------------------------------------------------- +# Le générateur audit_5d_gen.py applique un COMPTE d'invariants de cohérence +# audit/contrôles 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..15) qui ouvrent chaque contrôle. Le +# nombre « 15 » est transcrit À LA MAIN à SIX endroits qu'AUCUN gate ne couvrait : +# QUATRE chaînes du .py (docstring sous-commande « valide schéma + 15 invariants », +# docstring `_validate`, message de succès CLI « … 15 invariants — verdict … », +# aide du sous-parseur `validate`) + DEUX du README (bloc de commande « schéma + +# 15 invariants », prose d'architecture « CLI build/validate + **15 invariants** »). +# Ajouter un contrôle `# 16 ·` à `_validate` sans toucher les chaînes ⇒ le README +# ET les self-reports du .py mentent en silence pendant que la CI applique 16 +# 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É vs 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). MAIS le module porte +# un SECOND nombre distinct — « 17 contrôles » (le compte de fonctions-contrôle, +# déjà gaté par ailleurs) — présent dans le .py ET le README : un `\d+ invariants` +# ancré sur le mot « invariants » l'exclut nativement (« 17 contrôles » ≠ mot +# « invariants »), donc AUCUN faux positif sur l'arbre propre. Contrairement à +# Dossier Vente/Fiscal, ce README n'a NI titre « ## Les N invariants » NI +# énumération numérotée du ledger → on ne gate que (a)(b)(d). 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| ; (d) TOUTE mention « N +# invariants » du README == |ledger|. Mention ou registre absent = échec +# (disparition d'un chiffre = régression de traçabilité #6). +# ============================================================================ +A5I_GEN = f"{D}/qa/audit_5d/audit_5d_gen.py" +A5I_RD = f"{D}/qa/audit_5d/README.md" +try: + a5i_src = open(A5I_GEN, encoding="utf-8").read() +except OSError as e: + bad(f"Audit 5D invariants · générateur {A5I_GEN} illisible : {e}"); a5i_src = None +if a5i_src is not None: + _mfn = re.search(r"def _validate\b", a5i_src) + if _mfn is None: + bad(f"Audit 5D invariants · fonction `_validate` INTROUVABLE dans {A5I_GEN} " + "(registre-source du ledger absent → impossible de re-dériver)") + else: + _bs = _mfn.end() + _mn = re.search(r"\ndef ", a5i_src[_bs:]) + _body = a5i_src[_bs: _bs + _mn.start()] if _mn else a5i_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"Audit 5D invariants · aucun marqueur `# N ·` dans `_validate` de " + f"{A5I_GEN} (ledger vide → impossible de re-dériver)") + elif _dup_bare: + bad(f"Audit 5D invariants · marqueur(s) `# N ·` dupliqué(s) sans suffixe " + f"a/b dans `_validate` : {_dup_bare} — vrai doublon de registre") + else: + a5i_ninv = len(_top) + if _top != list(range(1, a5i_ninv + 1)): + bad(f"Audit 5D invariants · ledger `_validate` NON contigu : {_top} " + f"(attendu 1..{a5i_ninv}) — lacune/doublon dans les marqueurs `# N ·`") + else: + good(f"Audit 5D invariants · ledger `_validate` contigu 1..{a5i_ninv} " + f"({a5i_ninv} marqueurs `# N ·` · schéma compté à part)") + # (b) self-reports du .py « N invariants » (le distinct « N contrôles » + # est exclu nativement par l'ancre sur le mot « invariants »). + a5i_py_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", a5i_src)}) + if not a5i_py_rep: + bad(f"Audit 5D invariants · aucune mention « N invariants » dans {A5I_GEN} " + f"(self-report absent → attendu {a5i_ninv})") + elif a5i_py_rep == [a5i_ninv]: + good(f"Audit 5D invariants · {A5I_GEN} — self-report(s) {a5i_py_rep} " + f"== ledger ({a5i_ninv})") + else: + bad(f"Audit 5D invariants · {A5I_GEN} — self-report(s) « N invariants » " + f"{a5i_py_rep} MAIS ledger en compte {a5i_ninv} " + "(une chaîne du générateur désynchronisée du registre)") + # (d) toute mention « N invariants » du README == ledger. + try: + a5i_rd_raw = open(A5I_RD, encoding="utf-8").read() + except OSError as e: + bad(f"Audit 5D invariants · {A5I_RD} illisible : {e}"); a5i_rd_raw = None + if a5i_rd_raw is not None: + a5i_rd_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", a5i_rd_raw)}) + if not a5i_rd_rep: + bad(f"Audit 5D invariants · aucune mention « N invariants » dans {A5I_RD} " + f"(self-report README absent → attendu {a5i_ninv})") + elif a5i_rd_rep == [a5i_ninv]: + good(f"Audit 5D invariants · {A5I_RD} — mention(s) {a5i_rd_rep} " + f"== ledger (aucune dérive)") + else: + 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") + sys.exit(1 if FAIL else 0) PY rc=$?