"""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." )