Files
oto-enterprise-os-dtp/ci/check_readme_claims.sh
T
Claude Code DTP Worker 504bfb0f10 [DTP-Worker] Sprint 8 · buffer L75 · Chiffre périmé fiche agent : "repo 560 tests" (vert-trompeur ungated) → 558/22 sourcé matrice + gate étendu
Défaut réel (#6 anti-invention) : 03_agents/erpnext_backend/AGENT.md situait les
tests backend par "repo : 560 tests au total" saisi à la main. 560 ne correspond à
aucun compte courant (matrice qa/regression = 558/22 ; repo-wide = 584 méthodes
test_*) — périmé en silence (534→551→558 ; 560 = ancien 534 + 26 harnais self-exclu
INV3). Même classe de vert-trompeur que la fiche QA (21/534), mais ungated.

- Vérif ciblée : toutes les autres bornes chiffrées des 13 AGENT.md exactes
  (crm 81, RBAC 60, e-CF 39, portails 19, chat 31, confotur 44, seo 36).
- Fix : le nombre pointe désormais vers l'artefact gaté (regression_run.json).
- Gate : ci/check_readme_claims.sh recalcule tests+suites+verdict depuis
  regression_run et exige l'égalité avec la fiche backend (comme la fiche QA).
- Preuve morsure : 560 réinjecté ⇒ gate rouge ; restauré ⇒ vert.
- 7 gates verts ; édition doc ⇒ aucune dérive out/ (check_artifacts vert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 04:36:20 +00:00

229 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ============================================================================
# check_readme_claims.sh · OTO Enterprise OS DTP
# ----------------------------------------------------------------------------
# Gate d'INTÉGRITÉ des chiffres du README (Sprint 8 buffer · CLAUDE.md #6 —
# « Zéro invention de chiffres · toujours vérifier sources archives »).
#
# Le README.md est le POINT D'ENTRÉE du mandat. Sa section « État courant
# (sourcé) » affiche des chiffres et déclare, mot pour mot :
# « Chaque chiffre ci-dessous est sourcé vers un artefact commité
# (anti-invention #6) ; ce README n'introduit aucune donnée nouvelle. »
# Cette promesse n'était garantie par AUCUN gate. `check_docs.sh` ne valide que
# les liens (cible existe) — pas la VALEUR des nombres. Résultat : quand un
# module + son job CI sont ajoutés (21→22 modules, 21→22 suites) ou qu'une
# promesse change de statut (14 in_repo + 1 out_of_scope → 15 in_repo), les
# chiffres du README se PÉRIMENT en silence tout en restant « sourcés » vers un
# artefact qui, lui, dit autre chose. Un README qui se contredit avec sa propre
# source est un « vert trompeur » de la même classe que la matrice périmée
# (« demo 18→21 ») ou l'orphan_tests_dirs (INV4) — appliqué à la doc d'entrée.
#
# Ce gate RECOMPUTE chaque chiffre depuis l'artefact cité (jamais une liste à la
# main · #6) et exige l'égalité avec ce qui est ÉCRIT dans les docs d'entrée :
# README.md :
# • Qualité 4Big N/M modules gated à K/100, verdict → qa/audit_4big totals
# • Régression N suites gated → qa/regression plan
# • Recette N promesses (X sprint + Y métriques), Z in_repo, verdict
# → qa/acceptance matrix
# • Agents « 13 agents » (×2 : nav + titre) → 03_agents/*/AGENT.md
# 03_agents/qa/AGENT.md (fiche QA · « Verdict agrégé courant ») :
# • N suites · M tests · M passés · E échec · E erreur · verdict
# → qa/regression run
# 03_agents/erpnext_backend/AGENT.md (« source unique ») :
# • N tests · M suites · verdict (matrice de régression du repo)
# → qa/regression run
# 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é).
#
# Dépendances : bash, git, python3 (stdlib pur · CLAUDE.md #2 — runner sans pip).
# ============================================================================
set -uo pipefail
# shellcheck source=ci/lib.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" || exit 3
cd_repo_root # racine du dépôt, ou ROUGE honnête si hors arbre git (cf. lib.sh)
echo "== Intégrité des chiffres du README (valeur == artefact cité · #6) =="
python3 - <<'PY'
import json, re, subprocess, sys
D = "05_deliverables_mvp"
FAIL = 0
def bad(m):
global FAIL; FAIL = 1; print(f" \033[31m✗\033[0m {m}")
def good(m):
print(f" \033[32m✓\033[0m {m}")
try:
readme = open("README.md", encoding="utf-8").read()
except OSError as e:
print(f" \033[31m✗\033[0m README.md illisible : {e}"); sys.exit(1)
# Texte normalisé (espaces/newlines compressés) pour les claims multi-lignes.
norm = re.sub(r"\s+", " ", readme)
def load(p):
with open(f"{D}/{p}", encoding="utf-8") as f:
return json.load(f)
# --- Vérités-terrain recalculées depuis les artefacts (jamais saisies) -------
q = load("qa/audit_4big/out/quality_report.json")["totals"]
qv = load("qa/audit_4big/out/quality_report.json")["verdict"]
reg = load("qa/regression/out/regression_plan.json")["totals"]
acc = load("qa/acceptance/out/acceptance_matrix.json")
rows = acc["matrix"]
n_sprint = sum(1 for r in rows if r["kind"] == "sprint_deliverable")
n_metric = sum(1 for r in rows if r["kind"] == "mvp_metric")
n_inrepo = sum(1 for r in rows if r["status"] == "in_repo")
acc_verdict = acc["verdict"]
agents = subprocess.run(["git", "ls-files", "03_agents/*/AGENT.md"],
capture_output=True, text=True).stdout.split()
n_agents = len(agents)
# --- Chaque claim : (libellé, regex sur `norm`, groupes attendus) ------------
# Le n-uplet capturé (str) est comparé à l'attendu (str). Un claim non trouvé
# échoue (regex is None) — la disparition d'un chiffre est une régression.
checks = [
("Qualité 4Big · modules gated",
r"\*\*(\d+)/(\d+) modules gated à (\d+)/100\*\*",
(str(q["pass"]), str(q["modules"]), str(q["min_score"])),
f"pass={q['pass']} total={q['modules']} min_score={q['min_score']}"),
("Régression · suites gated",
r"\*\*(\d+) suites gated\*\*",
(str(reg["suites"]),),
f"suites={reg['suites']}"),
("Recette · promesses (sprint + métriques)",
r"\*\*(\d+) promesses\*\* \((\d+) livrables sprint \+ (\d+) métriques",
(str(len(rows)), str(n_sprint), str(n_metric)),
f"total={len(rows)} sprint={n_sprint} metric={n_metric}"),
("Recette · promesses in_repo",
r"\*\*(\d+) `in_repo`",
(str(n_inrepo),),
f"in_repo={n_inrepo}"),
("Agents · fiche nav",
r"Fiches des \*\*(\d+) agents\*\*",
(str(n_agents),),
f"AGENT.md={n_agents}"),
("Agents · titre section",
r"## Les (\d+) agents",
(str(n_agents),),
f"AGENT.md={n_agents}"),
]
for label, pat, expected, src in checks:
m = re.search(pat, norm)
if m is None:
bad(f"{label} — claim INTROUVABLE dans README (attendu {src}) · motif=/{pat}/")
continue
got = m.groups()
if got == expected:
good(f"{label} — README {'/'.join(got)} == artefact ({src})")
else:
bad(f"{label} — README dit {'/'.join(got)} MAIS artefact dit {'/'.join(expected)} ({src}) → régénérer README")
# --- Verdicts textuels cités dans le README (PASS / true) --------------------
if re.search(r"verdict `PASS`", norm):
if qv == "PASS":
good(f"Qualité 4Big · verdict README `PASS` == artefact ({qv})")
else:
bad(f"Qualité 4Big · README dit verdict `PASS` MAIS artefact dit {qv}")
else:
bad("Qualité 4Big · verdict `PASS` non cité dans README (attendu)")
if re.search(r"verdict `true`", norm):
if acc_verdict is True:
good("Recette · verdict README `true` == artefact (True)")
else:
bad(f"Recette · README dit verdict `true` MAIS artefact dit {acc_verdict}")
else:
bad("Recette · verdict `true` non cité dans README (attendu)")
# ============================================================================
# Fiche QA (03_agents/qa/AGENT.md) · même classe de « vert trompeur » —
# ----------------------------------------------------------------------------
# Sa section « Verdict agrégé courant » affiche un compte agrégé (« N suites
# gated · M tests · M passés · 0 échec · 0 erreur → verdict PASS ») en déclarant
# « jamais compté à la main » et en citant qa/regression/out/regression_run.json.
# Ce compte se PÉRIME pourtant en silence quand une suite est ajoutée : la fiche
# affichait « 21 suites · 534 tests » alors que le run dit 22 / 551. Comme pour
# le README, aucun gate ne comparait la VALEUR au run. On la RECALCULE ici depuis
# le compte agrégé faisant autorité (regression_run.json, commité · déterministe).
# ============================================================================
QA = "03_agents/qa/AGENT.md"
try:
qa_txt = re.sub(r"\s+", " ", open(QA, encoding="utf-8").read())
except OSError as e:
bad(f"Fiche QA · {QA} illisible : {e}"); qa_txt = ""
run = load("qa/regression/out/regression_run.json")
suites = run["suites"]
agg = (
str(len(suites)), # suites
str(sum(s["ran"] for s in suites)), # tests exécutés
str(sum(s["passed"] for s in suites)), # passés
str(sum(s["failures"] for s in suites)), # échecs
str(sum(s["errors"] for s in suites)), # erreurs
run["verdict"], # verdict
)
qa_pat = (r"\*\*(\d+) suites gated · (\d+) tests · (\d+) passés · "
r"(\d+) échec · (\d+) erreur → verdict `(PASS|FAIL)`")
qa_src = (f"suites={agg[0]} tests={agg[1]} passés={agg[2]} "
f"échec={agg[3]} erreur={agg[4]} verdict={agg[5]}")
mqa = re.search(qa_pat, qa_txt)
if mqa is None:
bad(f"Fiche QA · verdict agrégé — claim INTROUVABLE dans {QA} (attendu {qa_src})")
elif mqa.groups() == agg:
good(f"Fiche QA · verdict agrégé — {QA} {'/'.join(mqa.groups())} == regression_run ({qa_src})")
else:
bad(f"Fiche QA · {QA} dit {'/'.join(mqa.groups())} MAIS regression_run dit "
f"{'/'.join(agg)} ({qa_src}) → régénérer la fiche")
# ============================================================================
# Fiche ERPNext Backend (03_agents/erpnext_backend/AGENT.md) · même classe.
# ----------------------------------------------------------------------------
# Sa ligne « source unique » situait les tests backend dans le repo par un
# compte agrégé (« repo : 560 tests au total ») saisi à la main. Ce nombre s'est
# PÉRIMÉ en silence (matrice 534 → 551 → 558) : 560 ne correspondait plus ni à la
# matrice (558) ni au repo-wide (584 méthodes test_*). Aucun gate ne le couvrait.
# On le RECALCULE depuis la même matrice faisant autorité (regression_run) et on
# exige tests + suites + verdict — comme la fiche QA.
# ============================================================================
BE = "03_agents/erpnext_backend/AGENT.md"
try:
be_txt = re.sub(r"\s+", " ", open(BE, encoding="utf-8").read())
except OSError as e:
bad(f"Fiche Backend · {BE} illisible : {e}"); be_txt = ""
be_exp = (
str(sum(s["ran"] for s in suites)), # tests exécutés (matrice)
str(len(suites)), # suites
run["verdict"], # verdict
)
be_pat = (r"matrice\s+de régression du repo : \*\*(\d+) tests · (\d+) suites\*\* · "
r"verdict `(PASS|FAIL)`")
be_src = f"tests={be_exp[0]} suites={be_exp[1]} verdict={be_exp[2]}"
mbe = re.search(be_pat, be_txt)
if mbe is None:
bad(f"Fiche Backend · matrice repo — claim INTROUVABLE dans {BE} (attendu {be_src})")
elif mbe.groups() == be_exp:
good(f"Fiche Backend · matrice repo — {BE} {'/'.join(mbe.groups())} == regression_run ({be_src})")
else:
bad(f"Fiche Backend · {BE} dit {'/'.join(mbe.groups())} MAIS regression_run dit "
f"{'/'.join(be_exp)} ({be_src}) → régénérer la fiche")
sys.exit(1 if FAIL else 0)
PY
rc=$?
echo
if [[ "$rc" -eq 0 ]]; then
echo -e "\033[32m✅ Chiffres du README fidèles aux artefacts cités (aucune donnée inventée · #6).\033[0m"
else
echo -e "\033[31m❌ Chiffre(s) du README périmé(s) vs artefacts — corriger README avant merge.\033[0m"
fi
exit "$rc"