[DTP-Worker 20260801_223450] Sprint 8 · buffer · SEO : gate d'IDENTITÉ du COMPTE d'INVARIANTS CI — le compte de contrôles de validation (annoncé « ## Garanties (N invariants) » du README + toutes ses mentions + les 5 self-reports du seo_gen.py : docstrings/aides/succès CLI) recomputé du SEUL registre-source = le LEDGER numéroté « # N · » de la fonction _validate (analogue du registre # Invariant N de Chat OTOIA · source d'autorité NON-artefact, invisible à check_artifacts). Exige : (a) ledger CONTIGU 1..N (comparaison LISTE triée ⇒ mord lacune ET doublon) ; (b) toute mention « N invariants » du .py == |ledger| ; (c) compte annoncé README == |ledger| ; (d) toute mention README == |ledger|. Prose du README RÉSUME (11 puces ≠ énumération 1:1) ⇒ gate du COMPTE, pas set-diff. 5 morsures vérifiées (2 README · 3 source ; doublon ledger attrapé là où l'approche set aurait raté), 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-08-01 22:41:56 +00:00
parent 64b635073a
commit a40bfc51c1
3 changed files with 156 additions and 1 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+95
View File
@@ -1550,6 +1550,101 @@ if seo_exp is not None:
bad(f"SEO · {path_f} — dit {'/'.join(m.groups())} MAIS MANIFEST dit "
f"{'/'.join(seo_exp)} ({seo_src}) → régénérer le doc")
# ============================================================================
# SEO · IDENTITÉ du COMPTE d'INVARIANTS CI — le README annonce « ## Garanties
# (N invariants) » et le générateur se SELF-REPORTE (« schéma + N invariants »)
# ----------------------------------------------------------------------------
# CLASSE DISTINCTE du bloc SEO ci-dessus (qui gate des COMPTES d'ARTEFACT —
# mots-clés total + par langue, recomptés de MANIFEST.counts). Ici la donnée
# gardée est le COMPTE D'INVARIANTS de validation, 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 la fonction `_validate` du générateur :
# les marqueurs `# N ·` (1 · 2 · … · 15) qui ouvrent chaque contrôle appliqué —
# exactement l'analogue du registre `# Invariant N` de Chat OTOIA. Ce compte est
# transcrit À LA MAIN à SEPT endroits qu'AUCUN gate ne couvrait : le titre de
# section du README (« ## Garanties (N invariants) ») + son bloc de commande
# (« schéma de sortie + N invariants »), plus CINQ chaînes du .py lui-même
# (docstring module, docstring `_validate`, aide `build`, message de succès CLI
# « N invariants verts. », aide du sous-parseur `validate`). 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, jamais la prose) ni le
# byte-gate d'artefact (aveugle au .py source) n'attrapent. On RECOMPUTE ici le
# ledger depuis le SEUL registre-source (les `# N ·` de `_validate`) et on exige :
# (a) le ledger est CONTIGU 1..N (lacune/doublon dans les marqueurs = défaut réel
# du registre) ; (b) TOUTE mention « N invariants » du .py (self-reports) == |ledger|
# (un seul chiffre désynchronisé mordu) ; (c) le compte annoncé du README == |ledger| ;
# (d) TOUTE mention « N invariants » du README == |ledger|. La prose du README
# RÉSUME les contrôles (≠ énumération numérotée 1:1) : on gate le COMPTE partout
# où il apparaît, pas un set-diff de la liste. Section/compte absent = échec (la
# disparition d'un chiffre est une régression de traçabilité · #6).
# ============================================================================
SEO_GEN = f"{D}/seo/seo_gen.py"
try:
seo_src = open(SEO_GEN, encoding="utf-8").read()
except OSError as e:
bad(f"SEO invariants · générateur {SEO_GEN} illisible : {e}"); seo_src = None
if seo_src is not None:
# (a) registre-source = marqueurs `# N ·` du LEDGER de `_validate` (autorité).
_mfn = re.search(r"def _validate\b", seo_src)
if _mfn is None:
bad(f"SEO invariants · fonction `_validate` INTROUVABLE dans {SEO_GEN} "
"(registre-source du ledger absent → impossible de re-dériver)")
else:
_bs = _mfn.end()
_mn = re.search(r"\ndef ", seo_src[_bs:])
_body = seo_src[_bs: _bs + _mn.start()] if _mn else seo_src[_bs:]
seo_led = [int(n) for n in re.findall(r"#\s*(\d+)\s*·", _body)]
if not seo_led:
bad(f"SEO invariants · aucun marqueur `# N ·` dans `_validate` de {SEO_GEN} "
"(ledger vide → impossible de re-dériver)")
else:
seo_ninv = len(seo_led)
if sorted(seo_led) != list(range(1, seo_ninv + 1)):
bad(f"SEO invariants · ledger `_validate` NON contigu : {seo_led} "
f"(attendu 1..{seo_ninv}) — lacune/doublon dans les marqueurs `# N ·`")
else:
good(f"SEO invariants · ledger `_validate` contigu 1..{seo_ninv} "
f"({seo_ninv} marqueurs `# N ·`)")
# (b) self-reports du .py « N invariants » (docstrings + aides + succès CLI).
seo_py_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", seo_src)})
if not seo_py_rep:
bad(f"SEO invariants · aucune mention « N invariants » dans {SEO_GEN} "
f"(self-report absent → attendu {seo_ninv})")
elif seo_py_rep == [seo_ninv]:
good(f"SEO invariants · {SEO_GEN} — self-report(s) {seo_py_rep} "
f"== ledger ({seo_ninv})")
else:
bad(f"SEO invariants · {SEO_GEN} — self-report(s) « N invariants » "
f"{seo_py_rep} MAIS ledger en compte {seo_ninv} "
"(une chaîne du générateur désynchronisée du registre)")
# README : titre de section + toute mention « N invariants ».
try:
seo_rd_raw = open(SEO_RD, encoding="utf-8").read()
except OSError as e:
bad(f"SEO invariants · {SEO_RD} illisible : {e}"); seo_rd_raw = None
if seo_rd_raw is not None:
_mh = re.search(r"##\s+Garanties\s+\((\d+)\s+invariants\)", seo_rd_raw)
if _mh is None:
bad(f"SEO invariants · {SEO_RD} — section « ## Garanties (N invariants) » "
f"INTROUVABLE (attendu {seo_ninv})")
else:
# (c) compte annoncé.
if int(_mh.group(1)) == seo_ninv:
good(f"SEO invariants · {SEO_RD} — annonce ({_mh.group(1)}) "
f"== ledger ({seo_ninv})")
else:
bad(f"SEO invariants · {SEO_RD} — annonce ({_mh.group(1)}) MAIS "
f"ledger en compte {seo_ninv} → régénérer le README")
# (d) toute mention « N invariants » du README == ledger.
seo_rd_rep = sorted({int(x) for x in re.findall(r"(\d+)\s+invariants", seo_rd_raw)})
if seo_rd_rep == [seo_ninv]:
good(f"SEO invariants · {SEO_RD} — mention(s) {seo_rd_rep} "
f"== ledger (aucune dérive)")
else:
bad(f"SEO invariants · {SEO_RD} — mention(s) « N invariants » "
f"{seo_rd_rep} MAIS ledger en compte {seo_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.