diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md index 77601d0..3b585a5 100644 --- a/05_activity_log/2026-08-02.md +++ b/05_activity_log/2026-08-02.md @@ -1,5 +1,59 @@ # Activity log · 2026-08-02 · Claude Code DTP Worker +## Sprint 8 · buffer · Legal CONFOTUR — gate d'IDENTITÉ du COMPTE D'INVARIANTS de `_validate` (4ᵉ 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** et **Fiscal e-CF**. Le **4ᵉ générateur** +portant le même nombre magique restait NON gaté : +`legal/confotur/confotur_application_gen.py`. + +Le compte **« 14 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 module (l.24 « schéma + 14 invariants de ») ; +2. docstring `_validate` (l.87 « 14 invariants de cross-coherence ») ; +3. message de succès CLI `validate` « … schéma + 14 invariants verts. » (l.283) ; +4. aide du sous-parseur `validate` (l.295) ; +5. README bloc de commande « schéma + 14 invariants » (l.56) ; +6. README commentaire d'arborescence « CLI build/validate · 14 invariants » (l.82). + +Ajouter un contrôle `# 15 ·` à `_validate` sans toucher les 6 chaînes ⇒ le README +ET les self-reports du `.py` mentent en silence pendant que la CI applique 15 +contrôles. Aucune suite `tests/` (qui teste des FONCTIONS de validation, pas la +prose) n'attrape ce « vert trompeur ». + +**Particularité du registre CONFOTUR** (≠ Fiscal) : le ledger est **PROPRE 1..14** +(pas de schéma-#1 non marqué, pas de split a/b). MAIS le README porte AUSSI un +compte de **SOUS-ENSEMBLE éditorial** — « **2 invariants** refusent tout champ +monétaire » (l.92) — à NE PAS confondre avec le total. Un `\d+ invariants` naïf +mordrait donc l'arbre PROPRE (il verrait `{2, 14}`). Le gate exclut le +sous-ensemble par **lookahead négatif** `(\d+)\s+invariants(?!\s+refusent)`, +appliqué au `.py` comme au README. + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Legal · CONFOTUR · 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-total « N invariants » du `.py` (hors « refusent ») == |ledger| ; +- **(c)** toute mention-total du README (hors « refusent ») == |ledger|. +Une mention ou le registre absent échoue AUSSI (disparition d'un chiffre = +régression de traçabilité #6). + +**Vérif** — 3 checks verts sur l'arbre propre (ledger 1..14 = 14 marqueurs `# N ·`). +**6 morsures** adversariales : M1 self-report `.py` 14→15 (b) · M2 README commande +14→15 (c) · M3 README arborescence 14→13 (c) · **M4 ajout marqueur `# 15 ·` ⇒ +ledger→15 pendant que la prose reste 14 (mord `.py` ET README, 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 no-false-positive** : +sous-ensemble « 2 → 3 invariants refusent » reste VERT (le lookahead exclut bien +le sous-ensemble). Gates re-verts : `check_artifacts` · `check_docs` · +`guard_constraints` · `check_ci_integrity` · `check_regression` · +`check_readme_claims` tous exit 0. + +--- + ## Sprint 8 · buffer · Publiciste — gate d'ANCRAGE des DEVISES du site public sur CLAUDE.md #10 **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 b83f400..8417558 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -1844,6 +1844,103 @@ if ecf_src is not None: bad(f"Fiscal invariants · {ECF_RD_INV} — énumération prose {ecf_enum} " f"≠ 1..{ecf_ninv} (item sauté/dupliqué) → régénérer le README") +# ============================================================================ +# Legal · CONFOTUR · IDENTITÉ du COMPTE D'INVARIANTS de `_validate` — 4ᵉ instance +# de la classe « ledger d'invariants » (après Chat OTOIA · SEO · Fiscal e-CF). +# ---------------------------------------------------------------------------- +# Le générateur confotur_application_gen.py applique un COMPTE d'invariants de +# cross-cohérence 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..14) qui ouvrent chaque contrôle appliqué. +# Le nombre « 14 » est transcrit À LA MAIN à SIX endroits qu'AUCUN gate ne +# couvrait : QUATRE chaînes du .py (docstring module, docstring `_validate`, +# message de succès CLI « … 14 invariants verts. », aide du sous-parseur +# `validate`) + DEUX du README (bloc de commande « schéma + 14 invariants », +# commentaire d'arborescence « CLI build/validate · 14 invariants »). Ajouter un +# contrôle `# 15 ·` à `_validate` sans toucher les chaînes ⇒ le README ET les +# self-reports du .py mentent en silence pendant que la CI applique 15 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 Fiscal : ledger PROPRE 1..N (pas de schéma-#1 non marqué, pas de +# split a/b) MAIS le README porte AUSSI un compte de SOUS-ENSEMBLE éditorial +# (« 2 invariants refusent tout champ monétaire ») À NE PAS confondre avec le total +# — on l'exclut par lookahead négatif `(?!\s+refusent)`, faute de quoi le gate +# mordrait l'arbre propre (le « 2 » n'est pas le compte du ledger). 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-total « N invariants » du .py == |ledger| ; (c) TOUTE +# mention-total du README (hors sous-ensemble « refusent ») == |ledger|. Mention ou +# registre absent = échec (disparition d'un chiffre = régression de traçabilité #6). +# ============================================================================ +CF_GEN = f"{D}/legal/confotur/confotur_application_gen.py" +CF_RD = f"{D}/legal/confotur/README.md" +# mention-total = « N invariants » NON suivi de « refusent » (le sous-ensemble +# éditorial anti-invention du README, ≠ compte total du ledger). +_CF_RX_TOT = r"(\d+)\s+invariants(?!\s+refusent)" +try: + cf_src = open(CF_GEN, encoding="utf-8").read() +except OSError as e: + bad(f"CONFOTUR invariants · générateur {CF_GEN} illisible : {e}"); cf_src = None +if cf_src is not None: + _mfn = re.search(r"def _validate\b", cf_src) + if _mfn is None: + bad(f"CONFOTUR invariants · fonction `_validate` INTROUVABLE dans {CF_GEN} " + "(registre-source du ledger absent → impossible de re-dériver)") + else: + _bs = _mfn.end() + _mn = re.search(r"\ndef ", cf_src[_bs:]) + _body = cf_src[_bs: _bs + _mn.start()] if _mn else cf_src[_bs:] + # marqueurs `# N ·` (le ledger confotur est propre : pas de suffixe a/b, + # mais on garde la détection de doublon nu comme pour Fiscal). + _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"CONFOTUR invariants · aucun marqueur `# N ·` dans `_validate` de " + f"{CF_GEN} (ledger vide → impossible de re-dériver)") + elif _dup_bare: + bad(f"CONFOTUR invariants · marqueur(s) `# N ·` dupliqué(s) sans suffixe " + f"a/b dans `_validate` : {_dup_bare} — vrai doublon de registre") + else: + cf_ninv = len(_top) + if _top != list(range(1, cf_ninv + 1)): + bad(f"CONFOTUR invariants · ledger `_validate` NON contigu : {_top} " + f"(attendu 1..{cf_ninv}) — lacune/doublon dans les marqueurs `# N ·`") + else: + good(f"CONFOTUR invariants · ledger `_validate` contigu 1..{cf_ninv} " + f"({cf_ninv} marqueurs `# N ·`)") + # (b) self-reports du .py « N invariants » (hors sous-ensemble « refusent »). + cf_py_rep = sorted({int(x) for x in re.findall(_CF_RX_TOT, cf_src)}) + if not cf_py_rep: + bad(f"CONFOTUR invariants · aucune mention-total « N invariants » dans " + f"{CF_GEN} (self-report absent → attendu {cf_ninv})") + elif cf_py_rep == [cf_ninv]: + good(f"CONFOTUR invariants · {CF_GEN} — self-report(s) {cf_py_rep} " + f"== ledger ({cf_ninv})") + else: + bad(f"CONFOTUR invariants · {CF_GEN} — self-report(s) « N invariants » " + f"{cf_py_rep} MAIS ledger en compte {cf_ninv} " + "(une chaîne du générateur désynchronisée du registre)") + # (c) README : mentions-total (le sous-ensemble « N invariants refusent » + # est exclu par le lookahead ⇒ ne fausse pas le compte). + try: + cf_rd_raw = open(CF_RD, encoding="utf-8").read() + except OSError as e: + bad(f"CONFOTUR invariants · {CF_RD} illisible : {e}"); cf_rd_raw = None + if cf_rd_raw is not None: + cf_rd_rep = sorted({int(x) for x in re.findall(_CF_RX_TOT, cf_rd_raw)}) + if not cf_rd_rep: + bad(f"CONFOTUR invariants · {CF_RD} — aucune mention-total « N " + f"invariants » (attendu {cf_ninv} · disparition = régression #6)") + elif cf_rd_rep == [cf_ninv]: + good(f"CONFOTUR invariants · {CF_RD} — mention(s)-total {cf_rd_rep} " + "== ledger (aucune dérive · sous-ensemble « refusent » exclu)") + else: + bad(f"CONFOTUR invariants · {CF_RD} — mention(s)-total « N invariants » " + f"{cf_rd_rep} MAIS ledger en compte {cf_ninv} → régénérer le README") + # ============================================================================ # CRM · pipeline vente (états / transitions) — même classe de « vert trompeur », # transcrit À LA MAIN dans DEUX README de module.