#!/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"