File size: 6,228 Bytes
53f68d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162c559
 
53f68d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162c559
 
53f68d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
"""Sprint A14-S3 — geler la fragmentation à plat de ``measurements/``.

Constat de l'audit (cf. ``BACKLOG_POST_LIVRAISON.md`` §2.4) : le
package ``picarones.measurements`` contient ~60 fichiers ``.py`` à
plat, accumulés au fil des Sprints 5-97.  Cette fragmentation rend
le code illisible (60 modules sans hiérarchie) et complique la
migration vers la nouvelle structure ``evaluation/metrics/``.

Cette règle **fige** la liste actuelle (snapshot au Sprint S3) et
**interdit** tout nouveau fichier ``.py`` à plat dans
``measurements/``.  Toute nouvelle métrique / hook / agrégateur
doit aller dans ``picarones/evaluation/metrics/`` (ou un sous-package
approprié).

Comportement attendu en pratique :

- **Nouveau fichier dans evaluation/metrics/** : OK.
- **Nouveau fichier dans measurements/<sous-package>/** (sous-dossier
  comme ``narrative/`` ou ``statistics/`` ou ``runner/``) : OK, le
  test ne regarde que le top-level.
- **Nouveau fichier à plat measurements/<nom>.py** : ÉCHEC.  Soit
  le mettre dans evaluation/metrics/ (préférence forte), soit
  dans un sous-package thématique de measurements/.

La whitelist est intentionnellement gelée à la date du Sprint S3.
Si un fichier de la whitelist est supprimé pendant le rewrite (par
exemple migré vers evaluation/metrics/ au Sprint S10), un autre
test (``test_no_orphaned_whitelist_entries``) le détecte.
"""

from __future__ import annotations

from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
MEASUREMENTS_DIR = REPO_ROOT / "picarones" / "measurements"


#: Snapshot de l'état au Sprint A14-S3 (mai 2026).  59 fichiers
#: ``.py`` à plat.  **Ne pas ajouter d'entrée** sans avoir d'abord
#: tenté de placer le fichier dans evaluation/metrics/ ou dans un
#: sous-package thématique.
WHITELIST_FLAT_FILES_S3: frozenset[str] = frozenset({
    "__init__.py",
    "abbreviations.py",
    "alto_metrics.py",
    "baseline_comparison.py",
    "builtin_hooks.py",
    "builtin_metrics.py",
    "calibration.py",
    "char_scores.py",
    "confusion.py",
    "cost_projection.py",
    "difficulty.py",
    "early_modern_typography.py",
    "equivalence_profile.py",
    "error_absorption.py",
    "hallucination.py",
    "history.py",
    "image_predictive.py",
    "image_quality.py",
    "incremental_comparison.py",
    "inter_engine.py",
    "layout.py",
    "levers.py",
    "lexical_modernization.py",
    "line_metrics.py",
    "longitudinal.py",
    "marginal_cost.py",
    "metrics.py",
    "modern_archives.py",
    "module_policy.py",
    "mufi.py",
    "ner.py",
    "ner_backends.py",
    "normalization.py",
    "numerical_sequences.py",
    "numerical_sequences_hooks.py",
    "philological_hooks.py",
    "pipeline_benchmark.py",
    "pipeline_comparison.py",
    "pipeline_spec_loader.py",
    "pricing.py",
    "rare_tokens.py",
    "readability.py",
    "readability_hooks.py",
    "reading_order.py",
    "reliability.py",
    "robustness.py",
    "robustness_projection.py",
    "roman_numerals.py",
    "searchability.py",
    "searchability_hooks.py",
    "specialization.py",
    "structure.py",
    "taxonomy.py",
    "taxonomy_comparison.py",
    "taxonomy_cooccurrence.py",
    "taxonomy_intra_doc.py",
    "throughput.py",
    "unicode_blocks.py",
    "worst_lines.py",
})


def _flat_python_files() -> set[str]:
    """Liste des fichiers ``.py`` directement dans ``measurements/``.

    Exclut les sous-packages (``narrative/``, ``statistics/``,
    ``runner/``) et les fichiers ``__pycache__``.
    """
    return {
        p.name for p in MEASUREMENTS_DIR.glob("*.py")
        if "__pycache__" not in p.parts
    }


def test_no_new_flat_file_in_measurements() -> None:
    """Toute addition à plat dans ``measurements/`` est interdite.

    Si ce test échoue après l'ajout d'un fichier, deux options :

    1. **Préférée** : déplacer le fichier dans
       ``picarones/evaluation/metrics/`` (ou un sous-package
       approprié).
    2. **Acceptable seulement avec justification** : si le fichier
       *doit* vivre dans ``measurements/`` pendant la transition
       (ex : refactor d'un fichier de la whitelist qui se scinde),
       l'ajouter à WHITELIST_FLAT_FILES_S3 dans ce fichier en
       expliquant pourquoi dans le message de commit.
    """
    actual = _flat_python_files()
    new_files = actual - WHITELIST_FLAT_FILES_S3
    assert not new_files, (
        "\nNouveaux fichiers ``.py`` à plat dans ``picarones/measurements/`` "
        "(plan rewrite-2026 §S3 — fragmentation gelée) :\n"
        + "\n".join(f"  - {f}" for f in sorted(new_files))
        + "\n\nDéplacer ces fichiers vers ``picarones/evaluation/metrics/`` "
        "ou un sous-package approprié.  Voir docs/roadmap/rewrite-2026.md."
    )


def test_no_orphaned_whitelist_entries() -> None:
    """La whitelist ne doit pas contenir d'entrée pointant vers un
    fichier qui n'existe plus.

    Garantit que la migration des fichiers vers ``evaluation/metrics/``
    (Sprint S10) entraîne automatiquement la mise à jour de cette
    whitelist — pas de dette qui s'accumule.
    """
    actual = _flat_python_files()
    orphans = WHITELIST_FLAT_FILES_S3 - actual
    assert not orphans, (
        "\nWhitelist contient des fichiers qui n'existent plus dans "
        "``picarones/measurements/`` :\n"
        + "\n".join(f"  - {f}" for f in sorted(orphans))
        + "\n\nLe fichier a été déplacé/supprimé — retirer l'entrée "
        "de WHITELIST_FLAT_FILES_S3 dans ce fichier."
    )


def test_subpackages_not_affected() -> None:
    """Méta-test : les sous-packages existants de ``measurements/``
    (narrative, statistics, runner) restent intouchés par ce test."""
    expected_subpackages = {"narrative", "statistics", "runner"}
    actual = {
        p.name for p in MEASUREMENTS_DIR.iterdir()
        if p.is_dir() and not p.name.startswith("_") and "__pycache__" not in p.name
    }
    missing = expected_subpackages - actual
    assert not missing, (
        f"Sous-packages attendus dans measurements/ absents : {missing}. "
        "Si l'un d'eux a été migré vers la nouvelle architecture (S10+), "
        "retirer son nom de ``expected_subpackages`` ici."
    )