Spaces:
Sleeping
Sleeping
File size: 5,298 Bytes
ed02e58 40e60ce ed02e58 40e60ce ed02e58 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """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 ``[<identifier>]``
#: où ``<identifier>`` 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.<method>``
dont le premier argument littéral ne commence pas par ``[<module>]``.
"""
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.<method>``. 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 "
"``[<module>]`` 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."
)
|