Picarones / tests /architecture /test_no_flat_files_in_measurements.py
Claude
feat(architecture): Sprint A14-S3 — squelette nouvelle architecture + tests d'invariants
53f68d5 unverified
Raw
History Blame
6.23 kB
"""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, (
f"\nNouveaux fichiers ``.py`` à plat dans ``picarones/measurements/`` "
f"(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, (
f"\nWhitelist contient des fichiers qui n'existent plus dans "
f"``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."
)