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