Picarones / tests /architecture /test_doc_paths.py
Claude
refactor(measurements): split runner.py (1019 → 6 sub-modules) by concern
0b09377 unverified
Raw
History Blame
4.54 kB
"""Garde-fou contre la dérive doc-vs-code.
Scanne ``CLAUDE.md``, ``README.md``, ``docs/**/*.md`` à la recherche de
chemins de la forme ``picarones/.../X.py`` et vérifie qu'ils existent
dans le repo.
Snapshot v1.0.0 (2026-05-02) : **119 chemins cassés**, presque tous
dans ``CLAUDE.md`` et ``CHANGELOG.md`` qui décrivent systématiquement
des modules sous ``picarones/core/...`` alors qu'ils vivent dans
``picarones/measurements/...``. C'est une dette documentaire connue
qu'il faut résorber par paliers.
Test ratchet : le nombre de chemins cassés ne peut que diminuer. Pour
le faire baisser :
1. Soit corriger le chemin dans la doc.
2. Soit déplacer le module au chemin documenté (rare — la doc se
trompe presque toujours).
3. Soit retirer la référence devenue obsolète.
Puis abaisser :data:`BROKEN_PATHS_BASELINE` du même montant.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
#: Snapshot. Doit baisser, jamais monter.
#:
#: Historique :
#: - 119 (initial v1.0.0, dette pré-existante CLAUDE.md/CHANGELOG.md
#: qui décrivent des modules sous ``picarones/core/...`` alors qu'ils
#: vivent dans ``picarones/measurements/...``).
#: - 122 (sprint « découpage de statistics.py », 2026-05-02) : 3 audits
#: historiques référencent ``picarones/measurements/statistics.py``
#: qui est maintenant un sous-package. Baseline relevée.
#: - 72 (sprint « zéro dette actionnable », 2026-05-02) : 50 chemins
#: massivement corrigés — 44 dans CLAUDE.md + 6 dans docs vivants.
#: - 73 (sprint « découpage de runner.py », 2026-05-03) :
#: ``picarones/measurements/runner.py`` est désormais un sous-package
#: ``runner/``. ``docs/user/writing-a-pipeline-module.md`` a été
#: corrigé en place ; un audit historique
#: (``docs/audits/institutional-readiness-2026-05.md``) référence
#: l'ancien chemin et reste intouché par convention.
#:
#: Les 73 restants sont **TOUS** dans :
#: - ``CHANGELOG.md`` (67) : journal historique versionné, intouchable.
#: - ``docs/audits/*.md`` (6) : audits historiques, intouchables.
BROKEN_PATHS_BASELINE = 73
#: Patrons de fichiers de documentation à scanner.
DOC_GLOBS: tuple[str, ...] = (
"CLAUDE.md",
"README.md",
"CHANGELOG.md",
"SPECS.md",
"docs/**/*.md",
)
#: Pattern minimal d'un chemin Python dans le repo.
PATH_PATTERN: re.Pattern[str] = re.compile(
r"picarones/[a-z_][a-z_0-9]*(?:/[a-z_][a-z_0-9]*)*\.py"
)
def _doc_files() -> list[Path]:
files: list[Path] = []
for glob in DOC_GLOBS:
files.extend(REPO_ROOT.glob(glob))
return sorted({f for f in files if f.is_file()})
def _broken_paths() -> list[tuple[str, str]]:
"""Liste des (doc_relatif, chemin_cassé), dédoublonnée et triée."""
broken: set[tuple[str, str]] = set()
for doc in _doc_files():
try:
text = doc.read_text(encoding="utf-8")
except OSError:
continue
rel_doc = doc.relative_to(REPO_ROOT).as_posix()
for match in PATH_PATTERN.findall(text):
if not (REPO_ROOT / match).exists():
broken.add((rel_doc, match))
return sorted(broken)
def test_broken_doc_paths_below_baseline() -> None:
"""Le nombre de chemins cassés ne peut que diminuer."""
broken = _broken_paths()
if len(broken) > BROKEN_PATHS_BASELINE:
sample = "\n".join(f" {doc}{path}" for doc, path in broken[:30])
more = f"\n ... ({len(broken) - 30} de plus)" if len(broken) > 30 else ""
raise AssertionError(
f"\n{len(broken)} chemins de doc cassés (baseline "
f"{BROKEN_PATHS_BASELINE}).\n"
f"Régression : la doc référence un fichier qui n'existe pas.\n\n"
f"Échantillon :\n{sample}{more}\n\n"
"Soit corrige le chemin, soit le code, soit retire la référence."
)
def test_baseline_must_be_tightened_when_progress_made() -> None:
"""Si on est sous le baseline, mettre à jour :data:`BROKEN_PATHS_BASELINE`.
Verrouille chaque correction de doc pour empêcher une régression
future de glisser sous le seuil obsolète.
"""
broken = _broken_paths()
assert len(broken) >= BROKEN_PATHS_BASELINE, (
f"\nExcellent : {len(broken)} chemins cassés vs baseline "
f"{BROKEN_PATHS_BASELINE}.\n\n"
f"Mets à jour BROKEN_PATHS_BASELINE = {len(broken)} dans "
"tests/architecture/test_doc_paths.py pour verrouiller le gain."
)