Picarones / tests /architecture /test_doc_paths.py
Claude
chore: finir le retrait execution_mode (audit du commit precedent)
a23e336 unverified
"""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 initial (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.
Note : l'historique de baseline ci-dessous utilise les dénominations
de version telles qu'elles existaient à chaque sprint (« v1.0.0 »,
« v2.0 », etc.). Ces étiquettes ne sont pas mises à jour
rétroactivement après le repositionnement SemVer pré-1.0 — voir
``docs/explanation/versioning.md``.
"""
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.
#: - 77 (sprint « Lot A — core.{modules,facts} → domain », 2026-05-07) :
#: suppression des shims ``picarones/core/modules.py`` et
#: ``picarones/core/facts.py``. Deux références demeurent dans
#: ``CHANGELOG.md`` (journal versionné) et
#: ``docs/roadmap/evolution-2026.md`` (plan stratégique historique
#: décrivant la création initiale du module).
#: - 80 (sprint « Lot B — core.metric_* → evaluation », 2026-05-07) :
#: suppression des shims ``picarones/core/metric_registry.py``,
#: ``picarones/core/metric_hooks.py`` et
#: ``picarones/core/metrics.py``. Trois nouvelles références
#: héritées : deux dans ``CHANGELOG.md`` (intouchable) + une
#: dans ``docs/migration/executor-equivalence.md`` (audit
#: historique de la migration legacy → executor). Le doc actif
#: ``docs/reference/normalization-profiles.md`` a été corrigé
#: en place vers ``picarones/evaluation/metric_hooks.py``.
#: - 83 (sprint « Lot C — core.{results,corpus,pipeline} → evaluation »,
#: 2026-05-07) : suppression des shims ``picarones/core/results.py``,
#: ``picarones/core/corpus.py`` et ``picarones/core/pipeline.py``.
#: Trois nouvelles références héritées : deux dans ``CHANGELOG.md``
#: (intouchable) + une dans ``docs/roadmap/evolution-2026.md``
#: (plan stratégique historique). Le doc actif
#: ``docs/reference/api-stable.md`` a été migré vers les chemins
#: canoniques ``picarones.evaluation.{benchmark_result, corpus,
#: pipeline}``.
#: - 88 (sprint « Lot D — measurements/X (34 shims) → evaluation/metrics »,
#: 2026-05-07) : suppression des 34 shims plats de ``measurements/``.
#: Cinq nouveaux chemins cassés héritage : 4 dans ``docs/audits/*.md``
#: (intouchable) + 1 dans ``docs/roadmap/evolution-2026.md``
#: (plan stratégique historique). Les docs actifs ``CLAUDE.md``,
#: ``README.md`` et ``SPECS.md`` ont été corrigés en place vers
#: ``picarones/formats/text/normalization.py``.
#: - 94 (sprint « Lot E — engines/ + modules/ → adapters/legacy_* »,
#: 2026-05-07) : suppression des 8 shims ``picarones/engines/`` et
#: ``picarones/modules/``. Six nouveaux chemins cassés héritage :
#: 5 dans ``CHANGELOG.md`` (intouchable) + 1 dans
#: ``docs/audits/remediation-plan-2026-05.md`` (intouchable) — les
#: audits citant ``aws_textract`` / ``kraken`` étaient déjà cassés
#: avant la migration (ces moteurs n'ont jamais été implémentés).
#: ``SPECS.md`` a été corrigé en place vers
#: ``picarones/adapters/legacy_engines/base.py``.
#: - 132 (sprint « Lot F — report/ → reports/ », 2026-05-07) :
#: suppression des 37 shims ``picarones/report/`` (29 *_render.py,
#: 2 helpers, 6 modules + glossary). 38 nouveaux chemins cassés
#: héritage : 29 dans ``CHANGELOG.md`` + 8 dans ``docs/audits/*.md``
#: et ``docs/migration/legacy-retirement-plan.md`` — tous
#: intouchables. Le doc actif ``docs/reference/views.md`` a été
#: corrigé en place vers les chemins ``picarones/reports/html/{views,
#: generator, renderers, templates}``.
#: - 134 (sprint « Lot G — core/{diff_utils, xml_utils} », 2026-05-07) :
#: suppression des 2 derniers shims de ``picarones/core/``. Le
#: sous-paquet ``core/`` n'existe plus du tout. Deux nouveaux
#: chemins cassés héritage dans ``CHANGELOG.md`` (intouchable).
#: - 138 (sprints « Lots H + I », 2026-05-07) : suppression du
#: sous-paquet ``measurements/statistics/`` (Lot H, 9 shims) et
#: des 3 shims ``extras/importers/{htr_united, huggingface,
#: _fallback_log}`` (Lot I). Quatre nouveaux chemins cassés
#: héritage répartis dans ``docs/audits/*.md`` (intouchables).
#:
#: Les chemins cassés restants sont **TOUS** dans :
#: - ``CHANGELOG.md`` : journal historique versionné, intouchable.
#: - ``docs/audits/*.md`` : audits historiques, intouchables.
#: - ``docs/roadmap/evolution-2026.md`` : plan stratégique historique.
#: - ``docs/migration/{executor-equivalence, legacy-retirement-plan}.md`` :
#: audits/plans historiques (citent des chemins legacy à des fins
#: de comparaison).
# Phase 7.B.2 : +3 broken paths — la doc référence
# ``picarones.evaluation.pipeline_benchmark`` /
# ``pipeline_comparison`` / ``pipeline`` qui ont migré vers
# ``picarones.pipeline.legacy_*``. Les docs concernées
# (CHANGELOG.md, audits, sub-plans) gardent volontairement les
# anciens chemins pour la traçabilité historique.
# Sprint H.5 : -11 broken paths — fix des refs actives dans
# docs/how-to/cli-workflows.md, narrative-engine, normalization-profiles,
# doc-consistency, SESSION_HANDOVER.
# Sprint H.2.d : +1 — suppression de ``adapters/legacy_engines/``.
# Sprint H.4 : +3 — suppression des stubs canoniques
# ``interfaces/cli/{run,report,import_corpus}.py`` et promotion de
# ``interfaces/{cli,web}/_legacy/`` vers ``interfaces/{cli,web}/``
# (les anciens chemins ``_legacy/`` apparaissent dans CHANGELOG +
# audits historiques, intouchables).
# Sprint H.6 : +2 — la section CHANGELOG 2.0 référence
# ``picarones/cli/__init__.py`` et ``picarones/engines/tesseract.py``
# dans le bloc « Migration depuis 1.x » (intouchables : c'est
# justement le but du bloc).
# Sprint H.8 : +2 — la suppression du shim ``picarones/i18n.py`` et
# la migration de l'exemple de docstring dans ``robustness.py``
# vers ``adapters.ocr.tesseract`` cassent quelques refs dans des
# audits historiques (CHANGELOG, docs/audits/).
# Sprint H.9 : -7 — nettoyage massif de ~17 fichiers ``docs/`` qui
# pointaient vers les paquets supprimés (``picarones.measurements.*``
# / ``picarones.core.*`` / ``picarones.engines.*`` / ``picarones.cli.*``
# / ``picarones.web.*`` / ``picarones.report.*`` / etc.) re-pointent
# désormais vers les chemins canoniques. ``docs/migration/`` archivée
# vers ``docs/archives/migration/`` (les refs internes y restent
# intentionnellement, c'est de l'historique).
# Sprint S7 : +1 — suppression du shim ``picarones/pipeline/spec.py``
# (deprecation period 1.x → 2.0 expirée). Quelques refs historiques
# au shim subsistent dans ``CHANGELOG.md`` / ``docs/audits/``.
#
# Phase B3-final (mai 2026) : +1 ref dans entrée historique
# Sprint H.4 du CHANGELOG qui mentionne le chemin de
# ``benchmark_runner.py`` (module supprimé). La mention documente
# l'historique du module et n'est pas corrigée volontairement.
#: Phase 1 D3 (juin 2026) : 164 → 41. L'archivage du CHANGELOG
#: pré-v2.0 vers ``docs/archive/changelog-pre-v2.md`` retire 123
#: chemins cassés historiques du périmètre actif.
#: Phase 1 D4 (juin 2026) : 41 → 6. Les dossiers ``docs/audits/``,
#: ``docs/migration/``, ``docs/archives/migration/``, et la roadmap
#: pré-v2.0 ont été consolidés sous ``docs/archive/``. Les 35
#: chemins cassés qu'ils portaient sortent du périmètre actif (cf.
#: ``EXCLUDED_PATH_PREFIXES`` ci-dessous).
#: Retrait ``execution_mode`` (mai 2026) : 6 → 5. La section 4.1 de
#: ``docs/reference/specification.md`` ne pointe plus vers
#: ``picarones/adapters/legacy_engines/base.py`` (path supprimé).
#:
#: Les 5 chemins restants sont dans la doc active :
#: - CHANGELOG.md (4) : refs Sprint H.4/H.6 dans la section
#: migration v2.0 (intouchables sans réécrire l'historique 2.0) ;
#: - docs/explanation/architecture.md (1) : ref historique au shim
#: ``picarones/pipeline/spec.py`` supprimé en Sprint S7.
BROKEN_PATHS_BASELINE = 5
#: Patrons de fichiers de documentation à scanner.
#: Phase 1 D5 : SPECS.md déplacé vers docs/reference/specification.md
#: — couvert par le glob ``docs/**/*.md``.
DOC_GLOBS: tuple[str, ...] = (
"CLAUDE.md",
"README.md",
"CHANGELOG.md",
"docs/**/*.md",
)
#: Sous-dossiers exclus du scan : les archives historiques peuvent
#: contenir des chemins vers du code supprimé (c'est précisément ce
#: pour quoi elles sont archivées). Compter ces chemins dans le
#: ratchet empêcherait la décroissance — toute migration ne ferait
#: que déplacer la dette, pas la résorber. L'exclusion est explicite
#: pour qu'un mainteneur futur sache que les archives ne sont PAS un
#: échappatoire silencieux pour la doc active.
EXCLUDED_PATH_PREFIXES: tuple[str, ...] = (
"docs/archive/", # nouveau (Phase 1 D3 / D4)
"docs/archives/", # ancien nom (sera consolidé en D4)
)
#: 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"
)
#: Fichiers générés à l'install (setuptools-scm) — réels et présents
#: dans tout environnement installé/CI, mais absents et git-ignorés
#: d'un checkout source vierge. Sans cette liste, le résultat du
#: test dépendrait de la présence ou non de ``pip install -e`` :
#: 164 (installé) vs 165 (checkout frais). Les neutraliser rend le
#: compte *déterministe* — c'est le contraire d'institutionnaliser la
#: poussière : on retire une fausse entrée environnement-dépendante,
#: le baseline (164) reste inchangé.
GENERATED_OK: frozenset[str] = frozenset({"picarones/_version.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()
and not any(
f.relative_to(REPO_ROOT).as_posix().startswith(prefix)
for prefix in EXCLUDED_PATH_PREFIXES
)
})
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 match in GENERATED_OK:
continue
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."
)