File size: 4,536 Bytes
ee86836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e28f42
652752d
 
 
 
 
 
7e28f42
 
 
0b09377
 
 
 
 
 
 
7e28f42
0b09377
7e28f42
0b09377
 
ee86836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"""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."
    )