diff --git a/05_activity_log/2026-07-31.md b/05_activity_log/2026-07-31.md index 17faee7..45ecce3 100644 --- a/05_activity_log/2026-07-31.md +++ b/05_activity_log/2026-07-31.md @@ -1,5 +1,68 @@ # 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 **Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA régression). Roadmap diff --git a/ci/README.md b/ci/README.md index 320abf2..872cf46 100644 --- a/ci/README.md +++ b/ci/README.md @@ -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-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-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 | 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 `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 disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 2e40994..4d2706f 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -38,6 +38,10 @@ # 03_agents/*/AGENT.md (tables de livrables · colonne « Tests ») : # • chaque cellule « Tests » par suite gatée → plan.suites[path] # (+ 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 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 " "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) PY rc=$?