Files
oto-enterprise-os-dtp/05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py
T
Claude Code DTP Worker fd99362660 [DTP-Worker] hardening correctness · le critère CLI de l'audit 4Big (q4lib/criteria.py) détectait l'entrypoint __main__ par un littéral figé double-quoté → loupait silencieusement les gardes simple-quotées / « Yoda » / sans-espaces (toutes du Python valide) → sous-scorage CLI −20 pts d'un module sain (violation #6)
Même racine que le fix ASCII ac595d0 : une détection assume une graphie là où le langage admet plusieurs formes. Fix = regex de FORME (_MAIN_GUARD_RE : 2 styles de guillemets · ordre inversé · espacement libre) + teeth test_cli_detects_single_quoted_and_yoda_guards (3 fixtures, prouvé mordant : ancien littéral rend False sur les 3, nouvelle regex True). Robustesse pure : les 50 gardes du dépôt sont double-quotées et toujours matchées → aucun score ne bouge, quality_report.json byte-identique (audit_4big s'auto-exclut de son scoring). Cascade régression uniquement : 626/609 → 627/610 (nouveau test) + fiches qa/audit_4big 35→36 & agrégat & backend 626→627 re-dérivés (RED check_readme_claims). Sweep préalable : classe ASCII-vs-Unicode close au niveau CODE (générateurs+libs+gates shell tous ASCII-par-domaine sauf les 2 compteurs test déjà \w).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-11 08:39:51 +00:00

132 lines
5.5 KiB
Python

"""Les 5 critères 4Big — purs, déterministes, calculés depuis le dépôt.
Chaque critère lit UNIQUEMENT le système de fichiers (jamais une valeur saisie)
et renvoie {passed: bool, evidence: str}. C'est le cœur anti-invention (#6) de
l'audit : une note ne peut pas être « écrite pour faire 95 », elle est recomputée
à partir de faits vérifiables (présence d'un fichier, taille, comptage AST léger,
intégrité JSON).
Aucune dépendance pip ; stdlib pure (le runner Gitea tourne sans réseau · #2).
"""
from __future__ import annotations
import json
import os
import re
_TEST_DEF_RE = re.compile(r"^\s*def (test_\w+)\s*\(", re.MULTILINE)
_ADD_PARSER_RE = re.compile(r"add_parser\(|add_subparsers\(")
# Garde `if __name__ == "__main__":` — les DEUX styles de guillemets sont du
# Python valide (PEP 8 n'impose rien), l'ordre des opérandes peut être inversé
# (« Yoda »), et l'espacement autour de `==` est libre. Un littéral figé
# `'__name__ == "__main__"'` louperait un entrypoint valide simple-quoté →
# sous-scorage silencieux du critère CLI. Détecter la forme, pas une graphie.
_MAIN_GUARD_RE = re.compile(
r"""__name__\s*==\s*['"]__main__['"]"""
r"""|['"]__main__['"]\s*==\s*__name__"""
)
def _direct_files(mod_dir: str, suffix: str) -> list[str]:
"""Fichiers se terminant par `suffix` directement dans mod_dir (non récursif)."""
if not os.path.isdir(mod_dir):
return []
return sorted(
os.path.join(mod_dir, n)
for n in os.listdir(mod_dir)
if n.endswith(suffix) and os.path.isfile(os.path.join(mod_dir, n))
)
def crit_doc(mod_dir: str, min_bytes: int) -> dict:
"""DOC — au moins un Markdown (README/SPEC) d'au moins `min_bytes` octets."""
for md in _direct_files(mod_dir, ".md"):
size = os.path.getsize(md)
if size >= min_bytes:
return {"passed": True,
"evidence": f"{os.path.basename(md)} ({size} octets)"}
return {"passed": False,
"evidence": f"aucun *.md ≥ {min_bytes} octets à la racine du module"}
def crit_contrat(mod_dir: str) -> dict:
"""CONTRAT — au moins un contrat de sortie *.schema.json."""
schemas = _direct_files(mod_dir, ".schema.json")
if schemas:
return {"passed": True,
"evidence": ", ".join(os.path.basename(s) for s in schemas)}
return {"passed": False, "evidence": "aucun *.schema.json à la racine du module"}
def crit_tests(mod_dir: str, min_methods: int) -> dict:
"""TESTS — ≥ min_methods méthodes `def test_*` dans tests/."""
tests_dir = os.path.join(mod_dir, "tests")
count = 0
files = 0
if os.path.isdir(tests_dir):
for root, _dirs, names in os.walk(tests_dir):
if "__pycache__" in root:
continue
for n in names:
if n.startswith("test_") and n.endswith(".py"):
files += 1
with open(os.path.join(root, n), encoding="utf-8") as fh:
count += len(_TEST_DEF_RE.findall(fh.read()))
passed = count >= min_methods
return {"passed": passed,
"evidence": f"{count} méthodes test_* dans {files} fichier(s) "
f"(seuil {min_methods})"}
def crit_cli(mod_dir: str) -> dict:
"""CLI — un entrypoint __main__ avec sous-commandes argparse à la racine."""
for py in _direct_files(mod_dir, ".py"):
with open(py, encoding="utf-8") as fh:
src = fh.read()
if _MAIN_GUARD_RE.search(src) and _ADD_PARSER_RE.search(src):
return {"passed": True,
"evidence": f"{os.path.basename(py)} (argparse + __main__)"}
return {"passed": False,
"evidence": "aucun entrypoint __main__ avec add_parser/add_subparsers"}
def crit_handoff(mod_dir: str) -> dict:
"""HANDOFF — out/MANIFEST.json + ≥1 autre artefact, tous JSON non vides."""
out_dir = os.path.join(mod_dir, "out")
manifest = os.path.join(out_dir, "MANIFEST.json")
if not os.path.isfile(manifest):
return {"passed": False, "evidence": "out/MANIFEST.json absent"}
others = [os.path.join(out_dir, n) for n in sorted(os.listdir(out_dir))
if n.endswith(".json") and n != "MANIFEST.json"]
if not others:
return {"passed": False,
"evidence": "out/ ne contient que MANIFEST.json (aucun artefact)"}
for art in [manifest] + others:
try:
with open(art, encoding="utf-8") as fh:
data = json.load(fh)
except (json.JSONDecodeError, OSError) as exc:
return {"passed": False,
"evidence": f"{os.path.basename(art)} illisible : {exc}"}
if data in (None, {}, [], ""):
return {"passed": False,
"evidence": f"{os.path.basename(art)} JSON vide"}
return {"passed": True,
"evidence": f"MANIFEST + {len(others)} artefact(s) JSON valides"}
# id critère → (fonction, kwargs supplémentaires tirés des thresholds)
def evaluate(mod_dir: str, criterion_id: str, thresholds: dict) -> dict:
if criterion_id == "DOC":
return crit_doc(mod_dir, thresholds["min_doc_bytes"])
if criterion_id == "CONTRAT":
return crit_contrat(mod_dir)
if criterion_id == "TESTS":
return crit_tests(mod_dir, thresholds["min_test_methods"])
if criterion_id == "CLI":
return crit_cli(mod_dir)
if criterion_id == "HANDOFF":
return crit_handoff(mod_dir)
raise KeyError(f"Critère inconnu : {criterion_id}")