"""Garde-fou contre la croissance silencieuse des fichiers. Chaque fichier listé dans :data:`FILE_BUDGETS` a un budget en lignes. Si un fichier dépasse son budget, le test échoue et la PR est forcée à choisir entre : 1. **Refactor** pour rentrer dans le budget (extraire un sous-module, factoriser, supprimer du code mort). 2. **Relever le budget délibérément** : modifier la valeur dans ce fichier en l'expliquant dans le message de commit. La hausse devient un acte conscient, plus une dérive silencieuse. Calibration : snapshot v1.0.0 (2026-05-02), ``current + ~15 %`` de marge pour l'évolution naturelle. Les god-modules historiques (statistics, generator, runner) gardent un budget proche de leur taille actuelle ; le choix de les dégonfler est une décision dédiée à un sprint de refactor, pas un sous-produit de l'invariant. Re-calibrer à chaque release tag. """ from __future__ import annotations from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[2] # Format : chemin relatif → max_lines. # Seuls les fichiers ≥ 400 lignes sont surveillés (les petits fichiers # n'ont pas besoin de budget — leur croissance est gérée par les tests # de couverture, pas par un seuil dur). FILE_BUDGETS: dict[str, int] = { # Phase B3-final (mai 2026) — ``benchmark_runner.py`` (entry # point legacy) supprimé après la migration complète vers # ``RunOrchestrator``. Les 8 modules ``_benchmark_*.py`` # extraits du god-module historique (Phase 6 audit code-quality) # qui restent utiles (helpers de production consommés par le # converter ou par les call sites Python) ne dépassent pas le # seuil de 400 LOC et n'ont donc pas besoin d'entrée budget ici. # --- God-modules : budget actuel + 15 % de marge. # Le rétrécissement sera l'objet d'un sprint de refactor dédié. # Phase 4.6 audit code-quality (2026-05) — commentaires retirés : # ils décrivaient des modules supprimés en v2.0 (``measurements/``, # ``core/``, ``report/``, ``pipelines/legacy_*``) qui ne sont plus # référencés ailleurs. L'historique reste accessible via git log # + CHANGELOG. "picarones/reports/html/generator.py": 550, # actuel 471 # Audit prod P1 — ranking/stratification/homogénéité extraits vers # ``benchmark_result_ranking.py`` (284 LOC, sous seuil 400 : pas # d'entrée). Budget resserré 1058 → 845 (actuel ~733 + 15 %). "picarones/evaluation/benchmark_result.py": 845, # actuel ~733 "picarones/evaluation/statistics/friedman_nemenyi.py": 560, # actuel 486 (audit F5 : F d'Iman-Davenport + betainc natif) "picarones/evaluation/metrics/image_quality.py": 470, # actuel 408 (audit F17 : branche image vide qui pose error) "picarones/reports/html/renderers/philological.py": 700, # actuel 601 "picarones/evaluation/metrics/modern_archives.py": 700, # actuel 599 "picarones/evaluation/metrics/builtin_hooks.py": 700, # actuel 590 "picarones/evaluation/metrics/history.py": 720, # actuel 615 # Phase 3.1 audit code-quality (2026-05) : retrait des 5 helpers # pure-Python ``_apply_*`` + stub ``_degrade_pure_python`` (~300 LOC # mortes) → budget dégonflé de 850 à 650. "picarones/evaluation/metrics/robustness.py": 650, # actuel 578 "picarones/interfaces/web/benchmark_utils.py": 600, # actuel 520 # Importers IIIF / Gallica. "picarones/adapters/corpus/iiif.py": 675, # actuel 567 "picarones/adapters/corpus/gallica.py": 675, # actuel 563 "picarones/evaluation/metrics/levers.py": 675, # actuel 561 "picarones/evaluation/metrics/inter_engine.py": 575, # actuel 484 "picarones/adapters/corpus/escriptorium.py": 650, # actuel 553 # Sprint A14-S1 — A.I.0 P0 : ajout de validated_path, # validated_prompt_filename, safe_report_name et compute_workspace_roots. # Audit prod P1.2 — clusters uploads/rate_limit/csp/public_mode/ # paths extraits vers security_*.py (chacun < 400 : pas d'entrée). # Budget resserré 850 → 480 (actuel ~397) : ne restent que le bloc # CSRF (état mutable) + secure_cookies/deployment + la façade. "picarones/interfaces/web/security.py": 480, # actuel ~397 # Sprint A14-S8 — CorpusRunner introduit pour orchestrer les # pipelines composées sur un corpus avec backpressure / timeout # réel / annulation propre. Budget stable, l'extension # ProcessPoolExecutor (S11) restera dans cette enveloppe. "picarones/pipeline/runner.py": 550, # actuel 462 # Sprint A14-S28 — PipelineExecutor refondu pour consommer un # ExecutionPlan (run_plan) tout en gardant run(spec) comme sucre. # PipelinePlanner introduit pour transformer une PipelineSpec en # plan immuable (validation + bindings + jonctions de métriques). # Sprint A14-S47 — branchement ArtifactStore : +60 lignes (lookup # cache avant exec, persistance après succès, helpers privés). "picarones/pipeline/executor.py": 600, # actuel 541 "picarones/pipeline/planner.py": 465, # actuel 403 # Sprint A14-S29 — ArtifactStore (ABC + 2 implémentations) avec # hash multi-paramètres pour adresser la critique d'audit n° 14 # « hash multi-paramètres + reprise par hash ». "picarones/adapters/storage/artifact_store.py": 580, # actuel 504 # Sprint A14-S37 + S52 + S56 — JobStore SQLite : POST/GET/DELETE, # JobStoreError, schema_version table (S56) + busy_timeout 30s + # WAL mode pour les jobs concurrents. "picarones/adapters/storage/job_store.py": 500, # actuel 421 # Sprint A14-S41 — artifacts_index.jsonl séparé. "picarones/app/services/benchmark_service.py": 470, # actuel 400 # ``BaseLLMAdapter`` implémente le contrat ``StepExecutor`` # (input_types, output_types, execute) en plus de complete(). "picarones/adapters/llm/base.py": 520, # actuel ~440 "picarones/evaluation/corpus.py": 600, # actuel 533 "picarones/evaluation/synthetic.py": 600, # actuel 510 "picarones/evaluation/metrics/roman_numerals.py": 575, # actuel 484 "picarones/adapters/corpus/htr_united.py": 575, # actuel 473 "picarones/adapters/corpus/huggingface.py": 550, # actuel 464 # Phase 3.3 audit code-quality (2026-05) — option # ``--normalization-profile`` + résolution builtin/YAML (~30 LOC). "picarones/interfaces/cli/_workflows.py": 1000, # actuel 877 — Phase D1 audit B3-final : decorator ``_b3_final_options`` + plumbing diagnose/economics/edition "picarones/interfaces/web/jobs.py": 625, # actuel 541 — fix race UNIQUE constraint + transaction context manager + atomic append_event_and_update_progress # ``__init__.py`` du CLI : commandes ``info``, ``engines``, # ``metrics``, ``report``, ``demo`` regroupées. "picarones/interfaces/cli/__init__.py": 500, # actuel 396 "picarones/evaluation/metric_hooks.py": 500, # actuel 427 "picarones/evaluation/metrics/numerical_sequences.py": 500, # actuel 428 "picarones/formats/text/normalization.py": 500, # actuel 420 "picarones/reports/html/comparison.py": 500, # actuel 414 # Renderers HTML — helpers couleur + format mutualisés. "picarones/reports/_helpers/render_helpers.py": 480, # actuel 428 # --- Services applicatifs (couche 6). Budgets ``current + 15 %``. "picarones/app/services/corpus_service.py": 625, # actuel 541 "picarones/app/services/path_security.py": 470, # actuel 410 # Audit prod — dégonflage du god-module, terminé : helpers # extraits → sous-package (P1.1), 4 builders @staticmethod → # builders.py (Phase A), gros bloc stateful _execute_with_partial # (~283 l) → run_orchestrator_execution.py (Phase B). OBJECTIF # AUDIT ATTEINT : 1316 → 496 lignes (<500). Budget verrouillé # serré (496 + ~5 %) — toute reprise au-dessus de 500 échoue. "picarones/app/services/run_orchestrator.py": 520, # actuel ~496 "picarones/adapters/ocr/tesseract.py": 560, # actuel 479 — Phase B5 migration Option B (+ ALTO_XML expose) "picarones/app/schemas/run_spec.py": 620, # actuel 530 — Phase B1 migration Option B (+90 LOC : 7 nouveaux champs + 2 validators) "picarones/reports/html/render.py": 700, # actuel 615 } def _line_count(path: Path) -> int: """Compte les lignes physiques (y compris vides).""" return len(path.read_text(encoding="utf-8").splitlines()) @pytest.mark.parametrize( ("rel_path", "budget"), sorted(FILE_BUDGETS.items()), ) def test_file_size_within_budget(rel_path: str, budget: int) -> None: """Chaque fichier surveillé doit rester ≤ budget.""" path = REPO_ROOT / rel_path assert path.exists(), ( f"Fichier disparu : {rel_path}. " "Retire l'entrée de FILE_BUDGETS dans " "tests/architecture/test_file_budgets.py." ) actual = _line_count(path) assert actual <= budget, ( f"\n{rel_path} a {actual} lignes (budget {budget}).\n\n" "Soit refactor pour rentrer dans le budget, soit relève le budget " "consciemment dans tests/architecture/test_file_budgets.py " "avec une justification dans le message de commit." ) def test_no_orphaned_budget_entries() -> None: """Toute entrée de FILE_BUDGETS doit pointer vers un fichier existant.""" missing = [p for p in FILE_BUDGETS if not (REPO_ROOT / p).exists()] assert not missing, ( f"Entrées orphelines dans FILE_BUDGETS : {missing}. " "Le fichier a été déplacé/supprimé — retire l'entrée." ) def test_budget_table_covers_all_large_files() -> None: """Tout fichier ≥ 400 lignes doit avoir une entrée dans FILE_BUDGETS. Empêche un fichier nouveau ou subitement gros d'échapper à la surveillance. Si un fichier dépasse 400 lignes, ajoute-le à FILE_BUDGETS avec son budget (current + 15 %). """ threshold = 400 untracked: list[tuple[str, int]] = [] for path in (REPO_ROOT / "picarones").rglob("*.py"): rel = path.relative_to(REPO_ROOT).as_posix() if rel in FILE_BUDGETS: continue count = _line_count(path) if count >= threshold: untracked.append((rel, count)) assert not untracked, ( f"\nFichiers ≥ {threshold} lignes non surveillés :\n" + "\n".join(f" {p} ({n} lignes)" for p, n in sorted(untracked)) + "\n\nAjoute-les à FILE_BUDGETS dans " "tests/architecture/test_file_budgets.py avec budget = current + ~15 %." )