"""Phase 10 audit code-quality (2026-05) — chaque appel ``logger.{warning,info,error,debug,critical,exception}(...)`` dans le code source doit commencer par un préfixe ``[module]`` qui identifie la source du log. Convention CLAUDE.md : .. code-block:: python logger.warning("[ner.attach] %s/%s : extraction NER dégradée : %s", ...) logger.info("[job_store] WAL non supporté, fallback rollback") logger.debug("[robustness] cleanup tmp file échoué : %s", exc) Bénéfice : un opérateur qui voit un warning ``"backup failed"`` dans les logs sans préfixe ne sait pas si ça vient de l'OCR, du job store ou d'un détecteur narratif. Avec ``[job_store] backup failed`` la source est immédiate. Stratégie : test **ratchet** — accepter le baseline actuel, refuser toute nouvelle régression. Le nettoyage complet (~30 sites résiduels) peut se faire progressivement. """ from __future__ import annotations import ast import re from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] PRODUCTION = REPO_ROOT / "picarones" _LOG_METHODS = frozenset({ "debug", "info", "warning", "error", "critical", "exception", }) #: Pattern attendu : le 1er argument commence par ``[]`` #: où ```` est un identifiant de module/sous-module. #: Tolère : #: #: - statique : ``[ner.attach]``, ``[job_store]``, ``[friedman_nemenyi]`` #: - placeholder simple : ``[%s]`` (cas des adapters paramétrés type #: ``adapters.llm.base`` qui logguent ``[%s] ...`` où ``%s`` = #: ``self.name``). #: - composé : ``[importers/%s]``, ``[narrative.detector.%s]``, #: ``[pipeline:%s]``, ``[aggregate_%s]``. #: #: Refuse l'absence totale de préfixe (``logger.warning("Erreur ...")``). _PREFIX_RE = re.compile(r"^\[[\w./\-:%()_]+\]") def _scan_unprefixed_logs() -> list[tuple[Path, int, str]]: """``(path, lineno, snippet)`` pour chaque appel ``logger.`` dont le premier argument littéral ne commence pas par ``[]``. """ findings: list[tuple[Path, int, str]] = [] for path in sorted(PRODUCTION.rglob("*.py")): if "__pycache__" in path.parts: continue try: tree = ast.parse(path.read_text(encoding="utf-8")) except SyntaxError: continue for node in ast.walk(tree): if not isinstance(node, ast.Call): continue func = node.func if not isinstance(func, ast.Attribute): continue if func.attr not in _LOG_METHODS: continue # Vérifier que c'est bien ``logger.``. On accepte # aussi ``logging.warning(...)`` (root) et ``self.logger.warning(...)``. if not node.args: continue first = node.args[0] # Extraire la string littérale. msg: str | None = None if isinstance(first, ast.Constant) and isinstance(first.value, str): msg = first.value elif isinstance(first, ast.JoinedStr): # f-string : on prend les morceaux constants au début. parts = [] for v in first.values: if isinstance(v, ast.Constant) and isinstance(v.value, str): parts.append(v.value) else: break if parts: msg = "".join(parts) if msg is None: # Premier argument dynamique (variable, fonction…) — on # ne peut pas vérifier statiquement, skip. continue if not _PREFIX_RE.match(msg): findings.append((path, node.lineno, msg[:60])) return findings #: Baseline du nombre de logs sans préfixe. Phase 10 audit #: code-quality (2026-05) : ~30 sites résiduels acceptés. Test #: ratchet — ne peut que baisser. UNPREFIXED_LOGS_BASELINE = 0 def test_unprefixed_logs_below_baseline() -> None: """Le compteur de logs sans préfixe ``[module]`` ne peut que baisser.""" findings = _scan_unprefixed_logs() count = len(findings) if count > UNPREFIXED_LOGS_BASELINE: sample = "\n".join( f" {p.relative_to(REPO_ROOT)}:{ln} → {msg!r}" for p, ln, msg in findings[:30] ) more = ( f"\n ... ({count - 30} de plus)" if count > 30 else "" ) raise AssertionError( f"Logs sans préfixe ``[module]`` : {count} > baseline " f"{UNPREFIXED_LOGS_BASELINE}.\n\n" f"{sample}{more}\n\n" "Convention CLAUDE.md : chaque log doit commencer par " "``[]`` pour identifier sa source. Exemples : " "``logger.warning(\"[ner.attach] extraction NER dégradée\")``" ) def test_baseline_must_be_tightened_when_progress_made() -> None: """Symétrique : oblige à abaisser ``UNPREFIXED_LOGS_BASELINE`` quand des sites sont corrigés.""" count = len(_scan_unprefixed_logs()) assert count >= UNPREFIXED_LOGS_BASELINE - 5, ( f"Logs sans préfixe : {count} < baseline {UNPREFIXED_LOGS_BASELINE}.\n" f"Abaisser UNPREFIXED_LOGS_BASELINE = {count} pour verrouiller le gain." )