11f1250b29
Même classe de vert-trompeur que ci/README.md (session 050007) mais dans le code des gates : 3 comptes vivants figés "22 suites · 551 tests · PASS" (matrice 534→551→558). L'ironie : le header de check_regression — le gate anti-péremption de la matrice — s'était lui-même périmé. Fix précédent 050007 : suppression de la surface de dérive (retrait du nombre figé → renvoi à regression_run.json), pas 551→558. Progressions historiques "534→551→558" conservées. 7 gates verts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
230 lines
11 KiB
Bash
Executable File
230 lines
11 KiB
Bash
Executable File
#!/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 agrégé faisant autorité en
|
||
# disait déjà davantage (matrice recomptée ci-dessous, jamais figée ici). 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"
|