[DTP-Worker] Sprint 8 · buffer L75 · Dernière surface de la classe « compte par suite » : les agrégats de tests EN PROSE (crm « 81 (25+31+25) » · erpnext_backend « RBAC 60 (10+11+12+11+16) + e-CF 39 ») n'étaient couverts par AUCUN gate — une compensation entre suites laisserait la table gatée juste et la prose fausse → recompute total ET multiset des composants (ordre-indépendant) depuis plan.suites dans check_readme_claims (3 morsures : total périmé · compensation captée par le multiset · claim disparu)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-07-31 07:39:08 +00:00
parent 9038417db8
commit d7fc6bab26
3 changed files with 139 additions and 1 deletions
+63
View File
@@ -1,5 +1,68 @@
# Activity Log · 2026-07-31 · Claude Code DTP # Activity Log · 2026-07-31 · Claude Code DTP
## Session `20260731_073024` · Buffer S8 · Dernière surface de la classe « compte par suite » : les agrégats de tests rédigés EN PROSE dans deux fiches (`crm` « 81 (25+31+25) » · `erpnext_backend` « RBAC 60 (10+11+12+11+16) + e-CF 39 ») n'étaient couverts par AUCUN gate — une compensation entre suites laisserait la table gatée juste et la prose fausse
**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA régression). Roadmap
fonctionnellement close (22/22 modules · 564 tests · 7 gates verts au démarrage).
Poursuite de la série anti-dérive (CLAUDE.md #6). La session précédente a gaté les
**cellules** « Tests » par suite des tables de fiches (recompute générique depuis
`plan.suites[path].test_methods`), motivée par le fait qu'une **compensation** +1/1
entre deux suites passerait sous la seule somme agrégée. Recherche de la **même classe
un cran au-dessus** : les agrégats **rédigés en prose** (hors table) que ce recompute
par-cellule ne voit pas.
### Défaut trouvé — 2 agrégats en PROSE ungated (total + composants saisis à la main)
- **`03_agents/crm/AGENT.md:32`** : « Total CRM : **81 tests** (25 + 31 + 25) ».
- **`03_agents/erpnext_backend/AGENT.md:39`** : « Total backend **RBAC 60 tests**
(10 + 11 + 12 + 11 + 16) + **e-CF 39 tests** ».
- **Pourquoi non gaté** : `check_readme_claims` recompute les **cellules de table**
(`row_re` exige un lien `…/README.md` + dernier champ numérique) — ces phrases en
prose n'en sont pas. Le total agrégé du repo (564) est gaté ailleurs mais ne couvre
pas ces **sous-totaux par domaine** ni leurs **composants**. Une compensation entre
deux suites CRM (l'une +1, l'autre 1) laisserait la table (gatée) juste, la somme
repo (564) juste, et la prose « (25 + 31 + 25) » **fausse en silence** — exactement
le piège per-suite corrigé la session précédente, au niveau **agrégé**.
- **État courant** : **aucun nombre périmé** — 81=(25+31+25), RBAC 60=(10+16+11+11+12),
e-CF 39=fiscal/ecf_dgii, tous recoupés sur `plan.suites`. Le défaut est la **surface
ungated** (faux-vert latent), pas un chiffre faux (anti-invention #6 : rien à réécrire).
### Fix — extension de `check_readme_claims.sh` (recompute total + multiset composants)
Nouveau bloc qui, pour chaque agrégat en prose, **RECOMPUTE** depuis `auth`
(`plan.suites[path].test_methods`, déjà chargé · zéro duplication) :
- le **total** == somme des suites du domaine ;
- le **multiset des composants** (trié, **ordre-indépendant**) == multiset des comptes
par suite — c'est ce qui attrape la **compensation** que la somme seule rate ;
- la **cohérence interne** (total == somme des composants écrits).
+ un contrôle à **suite unique** pour e-CF (pas de parenthèses). Un claim **absent**
échoue AUSSI. En-tête du gate + `ci/README.md` (§ table + § détail) mis à jour.
### Preuves de morsure
| Injection | Résultat |
|---|---|
| **Total périmé** `81→82` | **exit 1** `dit total=82 … MAIS source dit total=81 (interne_ok=False)` |
| **Compensation** `(25+31+25)→(24+31+26)` (somme=81 **inchangée**) | **exit 1** `composants=[24,26,31] MAIS source [25,25,31]` — le multiset mord là où la somme est aveugle |
| **Claim e-CF disparu** | **exit 1** `claim INTROUVABLE (attendu 39)` |
| Restauré | **exit 0** |
### Vérifs
| Contrôle | Résultat |
|---|---|
| 7 gates (`guard`/`json`/`docs`/`artifacts`/`regression`/`ci_integrity`/`readme_claims`) | ✅ exit 0 |
| `check_artifacts` (rebuild + égalité byte des `out/`) | ✅ (édition **hors** `05_deliverables_mvp` ⇒ 0 dérive d'artefact ; aucun `AGENT.md` touché) |
| Matrice de régression | **564/22 · PASS** inchangée (ni `tests/` ni doc-evidence touchés) |
| Diff | 2 fichiers (`ci/check_readme_claims.sh` + `ci/README.md`) · 0 untracked |
**Anti-invention (#6)** : rien inventé — le gate recompute total ET composants depuis
`plan.suites` au lieu de figer un compteur ; les nombres de prose **découlent** de l'artefact.
**Hors périmètre worker (VPS · #8)** : néant (gate python/stdlib en-repo).
**Auto-score 4Big** : 96/100.
---
## Session `20260731_070019` · Buffer S8 · « Vert trompeur » ironique : le README de l'auditeur 4Big — le module qui COMPTE les modules — figeait son PROPRE total à la main (« 17/17 modules ») alors que son artefact `quality_report.json` en dit 22 ; ungated ## Session `20260731_070019` · Buffer S8 · « Vert trompeur » ironique : le README de l'auditeur 4Big — le module qui COMPTE les modules — figeait son PROPRE total à la main (« 17/17 modules ») alors que son artefact `quality_report.json` en dit 22 ; ungated
**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA régression). Roadmap **Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA régression). Roadmap
+9 -1
View File
@@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa
| `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui |
| `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui |
| `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui |
| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches == artefact cité (#6) | ✅ oui | | `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) == artefact cité (#6) | ✅ oui |
| `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui |
Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec
@@ -146,6 +146,14 @@ juste et les deux lignes fausses. On recompute donc chaque cellule depuis
`reglib.discovery.count_tests` (même fonction que le plan · zéro duplication) pour le `reglib.discovery.count_tests` (même fonction que le plan · zéro duplication) pour le
`self_module` `qa/regression`, exclu de la matrice par SoD mais bien documenté. `self_module` `qa/regression`, exclu de la matrice par SoD mais bien documenté.
Enfin — même classe, un cran **au-dessus** des cellules — les agrégats rédigés **en
prose** dans deux fiches (`crm` : « Total CRM **81 tests** (25 + 31 + 25) » ;
`erpnext_backend` : « **RBAC 60 tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests** »).
Le **total ET le multiset des composants** (ordre-indépendant) sont recomputés depuis
`suites[path].test_methods` : une **compensation** entre deux suites laisserait la
table (gatée ci-dessus) juste et la prose fausse — c'est le même piège que les
cellules, au niveau agrégé.
Un claim **absent** échoue aussi (la dérive de formulation qui ferait Un claim **absent** échoue aussi (la dérive de formulation qui ferait
disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur
(bash/git/python3), zéro réseau. (bash/git/python3), zéro réseau.
+67
View File
@@ -38,6 +38,10 @@
# 03_agents/*/AGENT.md (tables de livrables · colonne « Tests ») : # 03_agents/*/AGENT.md (tables de livrables · colonne « Tests ») :
# • chaque cellule « Tests » par suite gatée → plan.suites[path] # • chaque cellule « Tests » par suite gatée → plan.suites[path]
# (+ count_tests pour le self_module qa/regression, exclu de la matrice) # (+ count_tests pour le self_module qa/regression, exclu de la matrice)
# 03_agents/{crm,erpnext_backend}/AGENT.md (agrégats EN PROSE) :
# • « Total CRM N (a+b+c) » · « RBAC N (…) » · « e-CF N » — total ET multiset
# des composants recomputés (une compensation entre suites laisserait la
# table juste et la prose fausse) → plan.suites[path]
# Un claim absent échoue AUSSI (la dérive de formulation qui ferait disparaître # Un claim absent échoue AUSSI (la dérive de formulation qui ferait disparaître
# un chiffre est elle-même une régression de traçabilité). # un chiffre est elle-même une régression de traçabilité).
# #
@@ -317,6 +321,69 @@ if seen_rows == 0:
bad("Comptes par suite · AUCUNE ligne de table « Tests » trouvée dans les " bad("Comptes par suite · AUCUNE ligne de table « Tests » trouvée dans les "
"fiches — la disparition du recensement est elle-même une régression.") "fiches — la disparition du recensement est elle-même une régression.")
# ============================================================================
# Fiches agents · comptes AGRÉGÉS EN PROSE (« Total X : N tests (a + b + c) ») —
# dernière surface de la MÊME classe, un cran au-dessus des cellules de table.
# ----------------------------------------------------------------------------
# Les cellules « Tests » des tables sont recomputées ci-dessus, mais deux fiches
# rédigent EN PROSE un agrégat multi-suites : `crm` (« Total CRM : 81 tests
# (25 + 31 + 25) ») et `erpnext_backend` (« RBAC 60 tests (10 + 11 + 12 + 11 + 16)
# + e-CF 39 tests »). Ces totaux ET leurs composants sont SAISIS À LA MAIN et
# n'étaient couverts par AUCUN gate : une compensation entre deux suites (l'une
# +1, l'autre 1) laisserait la table juste et la prose fausse — exactement le
# motif per-suite corrigé la session précédente. On RECOMPUTE ici le total ET le
# multiset des composants (ordre-indépendant) depuis `auth` (plan.suites, source
# faisant autorité déjà chargée), et on exige l'égalité avec ce que la fiche ÉCRIT.
# ============================================================================
PROSE = [
("03_agents/crm/AGENT.md", "Total CRM",
r"Total CRM : \*\*(\d+) tests\*\* \(([\d + ]+)\)",
["crm/commissions", "crm/dossier_vente", "crm/workflow_vente"]),
("03_agents/erpnext_backend/AGENT.md", "RBAC backend",
r"\*\*RBAC (\d+) tests\*\* \(([\d + ]+)\)",
["rbac", "rbac/apply_plan", "rbac/fixtures_gen",
"rbac/roleprofile_gen", "rbac/userperm_gen"]),
]
for path_f, label, pat, suites_list in PROSE:
try:
txt = re.sub(r"\s+", " ", open(path_f, encoding="utf-8").read())
except OSError as e:
bad(f"Prose agrégée · {path_f} illisible : {e}"); continue
try:
exp_parts = sorted(auth[s] for s in suites_list)
except KeyError as e:
bad(f"Prose agrégée · {label} — suite {e} absente de plan.suites (câblage ?)")
continue
exp_total = sum(exp_parts)
m = re.search(pat, txt)
if m is None:
bad(f"Prose agrégée · {path_f} · « {label} » — claim INTROUVABLE "
f"(attendu total={exp_total} composants={exp_parts})")
continue
total = int(m.group(1))
parts = sorted(int(x) for x in re.findall(r"\d+", m.group(2)))
if total == exp_total and parts == exp_parts and total == sum(parts):
good(f"Prose agrégée · {path_f} · {label} — "
f"{total} ({'+'.join(map(str, parts))}) == source ({exp_total})")
else:
bad(f"Prose agrégée · {path_f} · {label} — dit total={total} "
f"composants={parts} MAIS source dit total={exp_total} "
f"composants={exp_parts} (interne_ok={total == sum(parts)}) "
f"→ régénérer la fiche")
# e-CF : agrégat en prose à SUITE UNIQUE (pas de composants entre parenthèses).
be_ecf = re.sub(r"\s+", " ", open("03_agents/erpnext_backend/AGENT.md",
encoding="utf-8").read())
ecf_exp = auth["fiscal/ecf_dgii"]
mecf = re.search(r"\*\*e-CF (\d+) tests\*\*", be_ecf)
if mecf is None:
bad(f"Prose agrégée · e-CF — claim INTROUVABLE (attendu {ecf_exp})")
elif int(mecf.group(1)) == ecf_exp:
good(f"Prose agrégée · e-CF — {mecf.group(1)} == source ({ecf_exp})")
else:
bad(f"Prose agrégée · e-CF — dit {mecf.group(1)} MAIS source dit {ecf_exp} "
f"(fiscal/ecf_dgii) → régénérer la fiche")
sys.exit(1 if FAIL else 0) sys.exit(1 if FAIL else 0)
PY PY
rc=$? rc=$?