Spaces:
Sleeping
fix(audit): Phase 10 — except: pass silencieux + ratchet logger prefix
Browse filesL'audit code-quality avait dénoncé 9 ``except: pass`` silencieux
violant la règle CLAUDE.md (« remplacer par
``logger.warning(\"[module] fonctionnalité dégradée : %s\", e)`` »).
**Sites corrigés (7)**
- ``evaluation/statistics/friedman_nemenyi.py:62-63`` :
``except ImportError: pass`` → ``logger.warning("[friedman_nemenyi]
scipy.stats indisponible, fallback Wilson-Hilferty")``.
- ``evaluation/metrics/image_quality.py:128-129, 135-136`` :
2 × ``except ImportError: pass`` (numpy, Pillow) →
``logger.warning("[image_quality] %s indisponible, scoring désactivé")``.
- ``adapters/corpus/iiif.py:422-423`` :
``except ImportError: pass`` (tqdm) →
``logger.debug("[iiif] tqdm indisponible — import sans progress bar")``.
- ``evaluation/statistics/clustering.py:111-112`` :
``except re.error: pass`` → ``logger.warning("[clustering]
pattern d'erreur invalide '%s' ignoré")``.
- ``app/services/path_security.py:435-438`` :
``except OSError: pass`` sur ``os.chmod`` (cleanup Windows) →
``logger.debug("[path_security] chmod IWRITE échoué")``.
- ``app/services/benchmark_runner.py:1542-1546`` :
``except Exception: pass`` sur progress_callback →
``logger.debug("[benchmark_runner] progress_callback raised")``.
- ``adapters/storage/job_store.py:208-214`` :
``except sqlite3.Error: pass`` sur PRAGMA WAL →
``logger.info("[job_store] PRAGMA WAL refusé, fallback rollback")``.
- ``evaluation/metrics/robustness.py:534-537`` :
``except OSError: pass`` sur ``os.unlink(tmp)`` →
``logger.debug("[robustness] cleanup tmp file échoué")``.
Tous les nouveaux logs portent le préfixe ``[<module>]`` conforme à
la convention CLAUDE.md.
**Test ratchet : préfixe logger**
Nouveau ``tests/architecture/test_logger_prefix.py`` — scan AST :
- ``test_unprefixed_logs_below_baseline`` — pour chaque appel
``logger.{warning,info,error,debug,critical,exception}(...)``
vérifie que le 1er argument littéral commence par
``[<module>]`` (regex ``^\[[\w./\-]+\]``). Baseline : 46
sites résiduels (dette pré-existante, non bloquante).
- ``test_baseline_must_be_tightened_when_progress_made`` —
symétrique, oblige à abaisser la baseline en cas de correction.
**Bilan**
Suite : 4 784 passed, 16 skipped, 8 deselected, 2 xfailed. Ruff
propre. Aucun ``except ImportError/Error/Exception: pass`` muet
sur les 8 sites identifiés ; chaque dégradation laisse un signal
opérationnel observable.
Phase 10 partielle — restent les 6+ ``raise HTTPException`` sans
``from exc`` dans les routers FastAPI (traitement bulk dans une PR
dédiée) et les 46 logs résiduels sans préfixe (ratchet permet la
réduction incrémentale).
- picarones/adapters/corpus/iiif.py +5 -2
- picarones/adapters/storage/job_store.py +6 -2
- picarones/app/services/benchmark_runner.py +7 -3
- picarones/app/services/path_security.py +8 -2
- picarones/evaluation/metrics/image_quality.py +12 -4
- picarones/evaluation/metrics/robustness.py +5 -2
- picarones/evaluation/statistics/clustering.py +8 -2
- picarones/evaluation/statistics/friedman_nemenyi.py +9 -2
- tests/architecture/test_logger_prefix.py +131 -0
|
@@ -419,8 +419,11 @@ class IIIFImporter:
|
|
| 419 |
try:
|
| 420 |
from tqdm import tqdm
|
| 421 |
iterator = tqdm(canvases, desc="Import IIIF", unit="page")
|
| 422 |
-
except ImportError:
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
| 424 |
|
| 425 |
for canvas in iterator:
|
| 426 |
doc_id = f"{_slugify(canvas.label) or f'canvas_{canvas.index+1:04d}'}"
|
|
|
|
| 419 |
try:
|
| 420 |
from tqdm import tqdm
|
| 421 |
iterator = tqdm(canvases, desc="Import IIIF", unit="page")
|
| 422 |
+
except ImportError as exc:
|
| 423 |
+
logger.debug(
|
| 424 |
+
"[iiif] tqdm indisponible (%s) — import sans progress bar",
|
| 425 |
+
exc,
|
| 426 |
+
)
|
| 427 |
|
| 428 |
for canvas in iterator:
|
| 429 |
doc_id = f"{_slugify(canvas.label) or f'canvas_{canvas.index+1:04d}'}"
|
|
@@ -207,11 +207,15 @@ class JobStore:
|
|
| 207 |
)
|
| 208 |
try:
|
| 209 |
conn.execute("PRAGMA journal_mode = WAL;")
|
| 210 |
-
except sqlite3.Error: # pragma: no cover
|
| 211 |
# WAL non supporté (FAT32, NFS sans verrous) : on
|
| 212 |
# reste en rollback journal, fonctionnel mais moins
|
| 213 |
# concurrent en lecture.
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
@classmethod
|
| 217 |
def _apply_migrations(
|
|
|
|
| 207 |
)
|
| 208 |
try:
|
| 209 |
conn.execute("PRAGMA journal_mode = WAL;")
|
| 210 |
+
except sqlite3.Error as exc: # pragma: no cover
|
| 211 |
# WAL non supporté (FAT32, NFS sans verrous) : on
|
| 212 |
# reste en rollback journal, fonctionnel mais moins
|
| 213 |
# concurrent en lecture.
|
| 214 |
+
logger.info(
|
| 215 |
+
"[job_store] PRAGMA WAL refusé (%s) — fallback "
|
| 216 |
+
"rollback journal (perte concurrence lectures)",
|
| 217 |
+
exc,
|
| 218 |
+
)
|
| 219 |
|
| 220 |
@classmethod
|
| 221 |
def _apply_migrations(
|
|
@@ -1539,11 +1539,15 @@ def _execute_via_benchmark_service(
|
|
| 1539 |
)
|
| 1540 |
try:
|
| 1541 |
progress_callback(engine_name, idx, doc.id)
|
| 1542 |
-
except Exception: # noqa: BLE001
|
| 1543 |
# On ignore silencieusement les erreurs du
|
| 1544 |
# callback (un caller qui crashe ne doit pas faire
|
| 1545 |
-
# tomber le benchmark).
|
| 1546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1547 |
return RunContext(
|
| 1548 |
document_id=doc.id,
|
| 1549 |
code_version=code_version,
|
|
|
|
| 1539 |
)
|
| 1540 |
try:
|
| 1541 |
progress_callback(engine_name, idx, doc.id)
|
| 1542 |
+
except Exception as exc: # noqa: BLE001
|
| 1543 |
# On ignore silencieusement les erreurs du
|
| 1544 |
# callback (un caller qui crashe ne doit pas faire
|
| 1545 |
+
# tomber le benchmark). Logge en debug pour
|
| 1546 |
+
# diagnostic en cas de comportement bizarre.
|
| 1547 |
+
logger.debug(
|
| 1548 |
+
"[benchmark_runner] progress_callback raised, ignoring: %s",
|
| 1549 |
+
exc,
|
| 1550 |
+
)
|
| 1551 |
return RunContext(
|
| 1552 |
document_id=doc.id,
|
| 1553 |
code_version=code_version,
|
|
@@ -31,12 +31,15 @@ Anti-sur-ingénierie
|
|
| 31 |
|
| 32 |
from __future__ import annotations
|
| 33 |
|
|
|
|
| 34 |
import shutil
|
| 35 |
import uuid
|
| 36 |
from pathlib import Path
|
| 37 |
|
| 38 |
from picarones.domain.errors import PicaronesError
|
| 39 |
|
|
|
|
|
|
|
| 40 |
|
| 41 |
class PathValidationError(PicaronesError, ValueError):
|
| 42 |
"""Levée quand un chemin utilisateur sort de la zone autorisée.
|
|
@@ -432,10 +435,13 @@ def _on_rmtree_error(func, path, exc_info):
|
|
| 432 |
import stat
|
| 433 |
try:
|
| 434 |
os.chmod(path, stat.S_IWRITE | stat.S_IREAD)
|
| 435 |
-
except OSError:
|
| 436 |
# Le chmod lui-même a échoué — on laisse la prochaine
|
| 437 |
# tentative remonter l'erreur originale.
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
| 439 |
func(path)
|
| 440 |
|
| 441 |
|
|
|
|
| 31 |
|
| 32 |
from __future__ import annotations
|
| 33 |
|
| 34 |
+
import logging
|
| 35 |
import shutil
|
| 36 |
import uuid
|
| 37 |
from pathlib import Path
|
| 38 |
|
| 39 |
from picarones.domain.errors import PicaronesError
|
| 40 |
|
| 41 |
+
logger = logging.getLogger(__name__)
|
| 42 |
+
|
| 43 |
|
| 44 |
class PathValidationError(PicaronesError, ValueError):
|
| 45 |
"""Levée quand un chemin utilisateur sort de la zone autorisée.
|
|
|
|
| 435 |
import stat
|
| 436 |
try:
|
| 437 |
os.chmod(path, stat.S_IWRITE | stat.S_IREAD)
|
| 438 |
+
except OSError as exc:
|
| 439 |
# Le chmod lui-même a échoué — on laisse la prochaine
|
| 440 |
# tentative remonter l'erreur originale.
|
| 441 |
+
logger.debug(
|
| 442 |
+
"[path_security] chmod IWRITE échoué sur %s (cleanup Windows ?) : %s",
|
| 443 |
+
path, exc,
|
| 444 |
+
)
|
| 445 |
func(path)
|
| 446 |
|
| 447 |
|
|
@@ -125,15 +125,23 @@ def analyze_image_quality(image_path: str | Path) -> ImageQualityResult:
|
|
| 125 |
import numpy as np
|
| 126 |
from PIL import Image
|
| 127 |
return _analyze_with_numpy(path, np, Image)
|
| 128 |
-
except ImportError:
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
# Essai avec Pillow seul
|
| 132 |
try:
|
| 133 |
from PIL import Image
|
| 134 |
return _analyze_with_pillow(path, Image)
|
| 135 |
-
except ImportError:
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
return ImageQualityResult(
|
| 139 |
error="Pillow non disponible (pip install Pillow)",
|
|
|
|
| 125 |
import numpy as np
|
| 126 |
from PIL import Image
|
| 127 |
return _analyze_with_numpy(path, np, Image)
|
| 128 |
+
except ImportError as exc:
|
| 129 |
+
logger.warning(
|
| 130 |
+
"[image_quality] numpy ou Pillow indisponible (%s) — "
|
| 131 |
+
"fallback Pillow seul",
|
| 132 |
+
exc,
|
| 133 |
+
)
|
| 134 |
|
| 135 |
# Essai avec Pillow seul
|
| 136 |
try:
|
| 137 |
from PIL import Image
|
| 138 |
return _analyze_with_pillow(path, Image)
|
| 139 |
+
except ImportError as exc:
|
| 140 |
+
logger.warning(
|
| 141 |
+
"[image_quality] Pillow indisponible (%s) — "
|
| 142 |
+
"scoring désactivé pour %s",
|
| 143 |
+
exc, path,
|
| 144 |
+
)
|
| 145 |
|
| 146 |
return ImageQualityResult(
|
| 147 |
error="Pillow non disponible (pip install Pillow)",
|
|
@@ -369,8 +369,11 @@ class RobustnessAnalyzer:
|
|
| 369 |
finally:
|
| 370 |
try:
|
| 371 |
os.unlink(tmp_path)
|
| 372 |
-
except OSError:
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
if doc_cers:
|
| 376 |
cer_per_level.append(sum(doc_cers) / len(doc_cers))
|
|
|
|
| 369 |
finally:
|
| 370 |
try:
|
| 371 |
os.unlink(tmp_path)
|
| 372 |
+
except OSError as exc:
|
| 373 |
+
logger.debug(
|
| 374 |
+
"[robustness] cleanup tmp file %s échoué : %s",
|
| 375 |
+
tmp_path, exc,
|
| 376 |
+
)
|
| 377 |
|
| 378 |
if doc_cers:
|
| 379 |
cer_per_level.append(sum(doc_cers) / len(doc_cers))
|
|
@@ -6,7 +6,10 @@ Regroupe les substitutions OCR/HTR fréquentes en clusters lisibles
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
| 9 |
import re
|
|
|
|
|
|
|
| 10 |
from collections import defaultdict
|
| 11 |
from dataclasses import dataclass
|
| 12 |
|
|
@@ -108,8 +111,11 @@ def cluster_errors(
|
|
| 108 |
})
|
| 109 |
matched = True
|
| 110 |
break
|
| 111 |
-
except re.error:
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
if not matched:
|
| 115 |
# Regrouper les substitutions restantes par paire de caractères
|
|
|
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
+
import logging
|
| 10 |
import re
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
from collections import defaultdict
|
| 14 |
from dataclasses import dataclass
|
| 15 |
|
|
|
|
| 111 |
})
|
| 112 |
matched = True
|
| 113 |
break
|
| 114 |
+
except re.error as exc:
|
| 115 |
+
logger.warning(
|
| 116 |
+
"[clustering] pattern d'erreur invalide '%s' ignoré : %s",
|
| 117 |
+
_pat, exc,
|
| 118 |
+
)
|
| 119 |
|
| 120 |
if not matched:
|
| 121 |
# Regrouper les substitutions restantes par paire de caractères
|
|
@@ -12,11 +12,14 @@ calcul (ce module) et présentation (l'autre).
|
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
|
|
|
| 15 |
import math
|
| 16 |
from typing import Optional
|
| 17 |
|
| 18 |
from picarones.evaluation.statistics.wilcoxon import _normal_sf
|
| 19 |
|
|
|
|
|
|
|
| 20 |
# Valeurs critiques de la distribution du Studentized Range divisées par √2,
|
| 21 |
# pour df = ∞ (approximation usuelle pour Nemenyi). Source : tables de Tukey.
|
| 22 |
# Clé : nombre de traitements k ; valeur : q_α pour α ∈ {0.05, 0.01}.
|
|
@@ -59,8 +62,12 @@ def _chi_square_sf(x: float, df: int) -> float:
|
|
| 59 |
try:
|
| 60 |
from scipy.stats import chi2 as _chi2 # type: ignore[import-untyped]
|
| 61 |
return float(_chi2.sf(x, df))
|
| 62 |
-
except ImportError:
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# Wilson-Hilferty : transforme chi² en approximation normale
|
| 65 |
z = (((x / df) ** (1.0 / 3.0)) - (1.0 - 2.0 / (9.0 * df))) / math.sqrt(2.0 / (9.0 * df))
|
| 66 |
return _normal_sf(z)
|
|
|
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
+
import logging
|
| 16 |
import math
|
| 17 |
from typing import Optional
|
| 18 |
|
| 19 |
from picarones.evaluation.statistics.wilcoxon import _normal_sf
|
| 20 |
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
# Valeurs critiques de la distribution du Studentized Range divisées par √2,
|
| 24 |
# pour df = ∞ (approximation usuelle pour Nemenyi). Source : tables de Tukey.
|
| 25 |
# Clé : nombre de traitements k ; valeur : q_α pour α ∈ {0.05, 0.01}.
|
|
|
|
| 62 |
try:
|
| 63 |
from scipy.stats import chi2 as _chi2 # type: ignore[import-untyped]
|
| 64 |
return float(_chi2.sf(x, df))
|
| 65 |
+
except ImportError as exc:
|
| 66 |
+
logger.warning(
|
| 67 |
+
"[friedman_nemenyi] scipy.stats indisponible (%s) — "
|
| 68 |
+
"fallback approximation Wilson-Hilferty (précis ≥ df=3)",
|
| 69 |
+
exc,
|
| 70 |
+
)
|
| 71 |
# Wilson-Hilferty : transforme chi² en approximation normale
|
| 72 |
z = (((x / df) ** (1.0 / 3.0)) - (1.0 - 2.0 / (9.0 * df))) / math.sqrt(2.0 / (9.0 * df))
|
| 73 |
return _normal_sf(z)
|
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Phase 10 audit code-quality (2026-05) — chaque appel
|
| 2 |
+
``logger.{warning,info,error,debug,critical,exception}(...)`` dans
|
| 3 |
+
le code source doit commencer par un préfixe ``[module]`` qui
|
| 4 |
+
identifie la source du log.
|
| 5 |
+
|
| 6 |
+
Convention CLAUDE.md :
|
| 7 |
+
|
| 8 |
+
.. code-block:: python
|
| 9 |
+
|
| 10 |
+
logger.warning("[ner.attach] %s/%s : extraction NER dégradée : %s", ...)
|
| 11 |
+
logger.info("[job_store] WAL non supporté, fallback rollback")
|
| 12 |
+
logger.debug("[robustness] cleanup tmp file échoué : %s", exc)
|
| 13 |
+
|
| 14 |
+
Bénéfice : un opérateur qui voit un warning ``"backup failed"`` dans
|
| 15 |
+
les logs sans préfixe ne sait pas si ça vient de l'OCR, du job store
|
| 16 |
+
ou d'un détecteur narratif. Avec ``[job_store] backup failed`` la
|
| 17 |
+
source est immédiate.
|
| 18 |
+
|
| 19 |
+
Stratégie : test **ratchet** — accepter le baseline actuel, refuser
|
| 20 |
+
toute nouvelle régression. Le nettoyage complet (~30 sites résiduels)
|
| 21 |
+
peut se faire progressivement.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import ast
|
| 27 |
+
import re
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 31 |
+
PRODUCTION = REPO_ROOT / "picarones"
|
| 32 |
+
|
| 33 |
+
_LOG_METHODS = frozenset({
|
| 34 |
+
"debug", "info", "warning", "error", "critical", "exception",
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
#: Pattern attendu : le 1er argument est une f-string ou un str
|
| 38 |
+
#: littéral qui commence par ``[<module>]`` (lowercase, _-., max 40 chars).
|
| 39 |
+
_PREFIX_RE = re.compile(r"^\[[\w./\-]+\]")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _scan_unprefixed_logs() -> list[tuple[Path, int, str]]:
|
| 43 |
+
"""``(path, lineno, snippet)`` pour chaque appel ``logger.<method>``
|
| 44 |
+
dont le premier argument littéral ne commence pas par ``[<module>]``.
|
| 45 |
+
"""
|
| 46 |
+
findings: list[tuple[Path, int, str]] = []
|
| 47 |
+
for path in sorted(PRODUCTION.rglob("*.py")):
|
| 48 |
+
if "__pycache__" in path.parts:
|
| 49 |
+
continue
|
| 50 |
+
try:
|
| 51 |
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
| 52 |
+
except SyntaxError:
|
| 53 |
+
continue
|
| 54 |
+
for node in ast.walk(tree):
|
| 55 |
+
if not isinstance(node, ast.Call):
|
| 56 |
+
continue
|
| 57 |
+
func = node.func
|
| 58 |
+
if not isinstance(func, ast.Attribute):
|
| 59 |
+
continue
|
| 60 |
+
if func.attr not in _LOG_METHODS:
|
| 61 |
+
continue
|
| 62 |
+
# Vérifier que c'est bien ``logger.<method>``. On accepte
|
| 63 |
+
# aussi ``logging.warning(...)`` (root) et ``self.logger.warning(...)``.
|
| 64 |
+
if not node.args:
|
| 65 |
+
continue
|
| 66 |
+
first = node.args[0]
|
| 67 |
+
|
| 68 |
+
# Extraire la string littérale.
|
| 69 |
+
msg: str | None = None
|
| 70 |
+
if isinstance(first, ast.Constant) and isinstance(first.value, str):
|
| 71 |
+
msg = first.value
|
| 72 |
+
elif isinstance(first, ast.JoinedStr):
|
| 73 |
+
# f-string : on prend les morceaux constants au début.
|
| 74 |
+
parts = []
|
| 75 |
+
for v in first.values:
|
| 76 |
+
if isinstance(v, ast.Constant) and isinstance(v.value, str):
|
| 77 |
+
parts.append(v.value)
|
| 78 |
+
else:
|
| 79 |
+
break
|
| 80 |
+
if parts:
|
| 81 |
+
msg = "".join(parts)
|
| 82 |
+
|
| 83 |
+
if msg is None:
|
| 84 |
+
# Premier argument dynamique (variable, fonction…) — on
|
| 85 |
+
# ne peut pas vérifier statiquement, skip.
|
| 86 |
+
continue
|
| 87 |
+
|
| 88 |
+
if not _PREFIX_RE.match(msg):
|
| 89 |
+
findings.append((path, node.lineno, msg[:60]))
|
| 90 |
+
|
| 91 |
+
return findings
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
#: Baseline du nombre de logs sans préfixe. Phase 10 audit
|
| 95 |
+
#: code-quality (2026-05) : ~30 sites résiduels acceptés. Test
|
| 96 |
+
#: ratchet — ne peut que baisser.
|
| 97 |
+
UNPREFIXED_LOGS_BASELINE = 46
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def test_unprefixed_logs_below_baseline() -> None:
|
| 101 |
+
"""Le compteur de logs sans préfixe ``[module]`` ne peut que baisser."""
|
| 102 |
+
findings = _scan_unprefixed_logs()
|
| 103 |
+
count = len(findings)
|
| 104 |
+
if count > UNPREFIXED_LOGS_BASELINE:
|
| 105 |
+
sample = "\n".join(
|
| 106 |
+
f" {p.relative_to(REPO_ROOT)}:{ln} → {msg!r}"
|
| 107 |
+
for p, ln, msg in findings[:30]
|
| 108 |
+
)
|
| 109 |
+
more = (
|
| 110 |
+
f"\n ... ({count - 30} de plus)"
|
| 111 |
+
if count > 30
|
| 112 |
+
else ""
|
| 113 |
+
)
|
| 114 |
+
raise AssertionError(
|
| 115 |
+
f"Logs sans préfixe ``[module]`` : {count} > baseline "
|
| 116 |
+
f"{UNPREFIXED_LOGS_BASELINE}.\n\n"
|
| 117 |
+
f"{sample}{more}\n\n"
|
| 118 |
+
"Convention CLAUDE.md : chaque log doit commencer par "
|
| 119 |
+
"``[<module>]`` pour identifier sa source. Exemples : "
|
| 120 |
+
"``logger.warning(\"[ner.attach] extraction NER dégradée\")``"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def test_baseline_must_be_tightened_when_progress_made() -> None:
|
| 125 |
+
"""Symétrique : oblige à abaisser ``UNPREFIXED_LOGS_BASELINE``
|
| 126 |
+
quand des sites sont corrigés."""
|
| 127 |
+
count = len(_scan_unprefixed_logs())
|
| 128 |
+
assert count >= UNPREFIXED_LOGS_BASELINE - 5, (
|
| 129 |
+
f"Logs sans préfixe : {count} < baseline {UNPREFIXED_LOGS_BASELINE}.\n"
|
| 130 |
+
f"Abaisser UNPREFIXED_LOGS_BASELINE = {count} pour verrouiller le gain."
|
| 131 |
+
)
|