Files
oto-enterprise-os-dtp/ci/check_readme_claims.sh
T
Claude Code DTP Worker c858e193e0 [DTP-Worker] Sprint 8 · buffer L75 · Gate intégrité chiffres : étendu à la fiche QA (vert-trompeur 21/534)
Fix vert-trompeur hors périmètre du gate README (session précédente) :
- 03_agents/qa/AGENT.md « Verdict agrégé courant » affichait 21 suites·534 tests
  (PASS) en se disant « jamais compté à la main » — figé à la main, périmé vs le
  run réel 22/551. Corrigé 21/534→22/551.
- ci/README.md : exemple 21·534→22·551 + checklist DevOps 6→7 gates statiques.
- Correctif de fond : ci/check_readme_claims.sh RECOMPUTE désormais aussi le compte
  agrégé de la fiche QA depuis regression_run.json (somme des suites commitées) ; un
  claim absent échoue aussi. Portée du gate élargie README + fiche QA (en-tête script
  + ci/README.md §1/§2). Bites : 21/534 → exit1 diff explicite ; claim retiré → exit1.

7 gates verts (dont check_artifacts : aucune dérive out/). Matrice 551/22 PASS inchangée.

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

193 lines
8.7 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
# 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")
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"