Spaces:
Running
fix(zero-debt): éliminer toute la dette technique actionnable identifiée
Browse filesAudit récursif à 2 tours sur l'état du repo après le commit 652752d
(découpage statistics.py). 3 agents Explore tour 1 + 2 agents tour 2
+ vérifications personnelles → 8 corrections.
## Tour 1 — corrections du périmètre actionnable
1. **Régénération `scripts/gen_readme_tables.py`** : `test_readme_dual_lang`
échouait depuis le début de la branche. Tables README régénérées,
test enfin vert (3842 → 3843).
2. **Correction massive CLAUDE.md (47 substitutions)** : 39 chemins
`picarones/core/X.py` → `picarones/measurements/X.py` pour les
modules réellement déplacés historiquement (mufi, abbreviations,
modern_archives, calibration, ner, etc.). Plus 5 `*_runner.py`
orphelins remplacés par leur vrai chemin (ex: `core/pipeline_runner.py`
→ `core/pipeline.py`).
3. **6 corrections dans docs vivants** : `docs/profiles.md`,
`docs/cli-workflows.md`, `docs/roadmap/evolution-2026.md`,
`docs/user/writing-a-pipeline-module.md`, `SPECS.md`. Plus de
chemins cassés dans aucun document vivant.
4. **`docs/developer/index.md`** : arbo réécrite avec `core/` et
`measurements/` corrects + mention du sous-package `statistics/`.
5. **`docs/architecture.md`** : référence `statistics.py` →
`statistics/` ; nouvelle section « Convention de découpage des
modules > 400 lignes » qui formalise la convention sous-package
utilisée dans `picarones/measurements/statistics/`.
6. **`picarones/measurements/statistics/clustering.py`** : import
paresseux `from picarones.core.diff_utils import compute_word_diff`
déplacé au top-level. Le commentaire historique « Sprint A3 (B-1) »
référençait une violation de cercle qui n'existe plus.
7. **`picarones/report/levers_render.py`** : `except Exception: continue`
ligne 226 remplacé par `except Exception as exc: logger.warning(...)`
avec contexte (lv_type + payload + exception). Plus de fail
silencieux sur formatter cassé.
8. **`tests/architecture/test_doc_paths.py`** : `BROKEN_PATHS_BASELINE`
122 → 72. Les 72 restants sont **TOUS** dans CHANGELOG.md (67) et
docs/audits/*.md (5) — historiques intouchables. Commentaire
actualisé.
## Tour 2 — corrections sur les corrections du tour 1
9. **CLAUDE.md ligne 195** (Sprint 18) : `core/statistics.py` →
`picarones/measurements/statistics/`. Cette référence n'avait pas
le préfixe `picarones/` et a échappé à la regex de
`test_doc_paths` mais reste factuellement fausse.
10. **CLAUDE.md ligne 197** (Sprint 20) : `core/pricing.py` →
`picarones/measurements/pricing.py` ; `statistics.py` →
`picarones/measurements/statistics/pareto.py` (la fonction
`compute_pareto_front` vit maintenant dans le sous-module
`pareto`).
11. **CLAUDE.md ligne 54** (arbo informelle) : `statistics.py` retiré
de la liste, sous-package `statistics/` ajouté avec son contenu.
12. **`tests/report/test_sprint82_levers.py`** : nouveau test
`test_formatter_exception_logs_warning_and_skips_lever` qui
couvre le `logger.warning` ajouté dans levers_render.py. Évite
qu'une régression future supprime le warning sans CI failure.
## Vérifications finales
- `ruff check picarones/ tests/` : All checks passed!
- `pytest tests/` : **3843 passed, 2 skipped, 4 deselected, 0 failed**.
Le `test_readme_dual_lang` qui échouait depuis le début de la
branche est désormais vert (parité totale).
- `tests/architecture/` : 35 invariants verts.
- `BROKEN_PATHS_BASELINE` : 119 → 122 → **72** sur la branche.
## Ce qui reste comme dette ASSUMÉE (pas actionnable)
- 67 chemins `core/X.py` cassés dans CHANGELOG.md : **historique
versionné**, ne doit jamais être modifié rétroactivement.
- 5 chemins cassés dans `docs/audits/*.md` : audits historiques,
même logique.
- 13 modules de `measurements/` test-only : décision produit,
documentés dans `TEST_ONLY_BASELINE` avec justification par sprint.
- `_normal_sf` couplage `wilcoxon → friedman_nemenyi` : 8 lignes math
partagées, refactor en `_math.py` créerait plus de dette que de
bénéfice.
- 28 `# type: ignore` : tous justifiés (imports optionnels scipy,
spacy, payloads dynamiques).
- 23 `# noqa: F401` : tous des ré-exports rétrocompat documentés.
- CLAUDE.md +0 -0
- README.md +1 -1
- SPECS.md +1 -1
- docs/architecture.md +37 -1
- docs/cli-workflows.md +1 -1
- docs/developer/index.md +10 -2
- docs/profiles.md +1 -1
- docs/roadmap/evolution-2026.md +2 -2
- docs/user/writing-a-pipeline-module.md +1 -1
- picarones/measurements/statistics/clustering.py +8 -3
- picarones/report/levers_render.py +9 -1
- tests/architecture/test_doc_paths.py +13 -9
- tests/report/test_sprint82_levers.py +44 -0
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -385,7 +385,7 @@ ruff check picarones/ tests/
|
|
| 385 |
python -m mypy picarones/core/
|
| 386 |
```
|
| 387 |
|
| 388 |
-
**Test suite**: ~
|
| 389 |
floor at 85% (currently ~87%). The `network` marker excludes tests
|
| 390 |
requiring live HTTP.
|
| 391 |
|
|
|
|
| 385 |
python -m mypy picarones/core/
|
| 386 |
```
|
| 387 |
|
| 388 |
+
**Test suite**: ~3849 tests, ~3 min on a modern laptop. Coverage
|
| 389 |
floor at 85% (currently ~87%). The `network` marker excludes tests
|
| 390 |
requiring live HTTP.
|
| 391 |
|
|
@@ -425,7 +425,7 @@ colonne) et `picarones/report/glossary/{fr,en}.yaml`.
|
|
| 425 |
|
| 426 |
**Note de traçabilité** : les références primaires (Demšar 2006,
|
| 427 |
Wilcoxon 1945, Efron 1979, etc.) sont citées dans les docstrings
|
| 428 |
-
de chaque fonction de `picarones/measurements/statistics
|
| 429 |
Le glossaire contextuel relie chaque métrique à sa publication
|
| 430 |
canonique (champ `reference`).
|
| 431 |
|
|
|
|
| 425 |
|
| 426 |
**Note de traçabilité** : les références primaires (Demšar 2006,
|
| 427 |
Wilcoxon 1945, Efron 1979, etc.) sont citées dans les docstrings
|
| 428 |
+
de chaque fonction de `picarones/measurements/statistics/`.
|
| 429 |
Le glossaire contextuel relie chaque métrique à sa publication
|
| 430 |
canonique (champ `reference`).
|
| 431 |
|
|
@@ -41,7 +41,7 @@ Les implémentations distribuées par défaut dans le package `picarones`.
|
|
| 41 |
|
| 42 |
| Catégorie | Modules |
|
| 43 |
|---|---|
|
| 44 |
-
| Coeur | `metrics.py`, `statistics
|
| 45 |
| Erreurs | `confusion.py`, `taxonomy.py`, `taxonomy_comparison.py`, `taxonomy_cooccurrence.py`, `taxonomy_intra_doc.py` |
|
| 46 |
| Lignes/structure | `line_metrics.py`, `structure.py`, `worst_lines.py`, `char_scores.py` |
|
| 47 |
| Calibration/fiabilité | `calibration.py`, `reliability.py`, `hallucination.py` |
|
|
@@ -141,3 +141,39 @@ Organisés par cercle : `tests/core/`, `tests/measurements/`,
|
|
| 141 |
|
| 142 |
Un test du cercle N **n'importe pas** les implémentations des
|
| 143 |
cercles > N (sauf `tests/integration/`).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
| Catégorie | Modules |
|
| 43 |
|---|---|
|
| 44 |
+
| Coeur | `metrics.py`, `statistics/` (sous-package), `runner.py`, `builtin_hooks.py`, `builtin_metrics.py`, `normalization.py` |
|
| 45 |
| Erreurs | `confusion.py`, `taxonomy.py`, `taxonomy_comparison.py`, `taxonomy_cooccurrence.py`, `taxonomy_intra_doc.py` |
|
| 46 |
| Lignes/structure | `line_metrics.py`, `structure.py`, `worst_lines.py`, `char_scores.py` |
|
| 47 |
| Calibration/fiabilité | `calibration.py`, `reliability.py`, `hallucination.py` |
|
|
|
|
| 141 |
|
| 142 |
Un test du cercle N **n'importe pas** les implémentations des
|
| 143 |
cercles > N (sauf `tests/integration/`).
|
| 144 |
+
|
| 145 |
+
## Convention de découpage des modules > 400 lignes
|
| 146 |
+
|
| 147 |
+
Quand un module Python dépasse 400 lignes ET contient plusieurs
|
| 148 |
+
responsabilités disjointes, le découper en **sous-package** plutôt
|
| 149 |
+
qu'en plusieurs modules à plat. Modèle de référence :
|
| 150 |
+
[`picarones/measurements/statistics/`](../picarones/measurements/statistics/)
|
| 151 |
+
issu du sprint « découpage de statistics.py » (mai 2026).
|
| 152 |
+
|
| 153 |
+
Convention :
|
| 154 |
+
|
| 155 |
+
1. **Renommer** `X.py` en `X/__init__.py` via `git mv` (préserve
|
| 156 |
+
l'historique du fichier original).
|
| 157 |
+
2. **Créer** dans `X/` un sous-module par famille fonctionnelle
|
| 158 |
+
(`bootstrap.py`, `wilcoxon.py`, `friedman_nemenyi.py`, etc.).
|
| 159 |
+
Chaque sous-module doit faire moins de ~400 lignes ; sinon
|
| 160 |
+
re-décomposer.
|
| 161 |
+
3. **`X/__init__.py`** ne contient QUE des ré-exports rétrocompat —
|
| 162 |
+
tous les symboles publics de l'ancien `X.py` doivent rester
|
| 163 |
+
importables via `from picarones.X import …`. Les symboles privés
|
| 164 |
+
ré-exportés doivent être ceux **réellement** consommés par les
|
| 165 |
+
tests (vérifié par grep, pas par supposition).
|
| 166 |
+
4. **`__all__`** explicite dans chaque sous-module et dans le
|
| 167 |
+
`__init__.py`.
|
| 168 |
+
5. **Tests architecture** (`tests/architecture/test_*.py`) doivent
|
| 169 |
+
continuer à passer : si nécessaire, étendre `_measurements_modules()`
|
| 170 |
+
ou `_imports_target_*` pour reconnaître les sous-packages.
|
| 171 |
+
6. **Préfixer les modules de rendu** par leur domaine
|
| 172 |
+
(`cdd_render.py` plutôt que `render_cdd.py`) pour cohérence avec
|
| 173 |
+
`picarones/report/*_render.py`.
|
| 174 |
+
|
| 175 |
+
**Quand NE PAS découper** : si les responsabilités sont fortement
|
| 176 |
+
couplées (ex: un orchestrateur qui appelle 12 sous-fonctions au
|
| 177 |
+
même endroit), le maintien dans un seul fichier > 400 lignes est
|
| 178 |
+
acceptable. Le budget par fichier (`tests/architecture/test_file_budgets.py`)
|
| 179 |
+
documente ces dérogations conscientes.
|
|
@@ -133,7 +133,7 @@ picarones import iiif \
|
|
| 133 |
Télécharge un manifeste IIIF v2/v3 (BnF Gallica, Bodleian, Vatican…) et
|
| 134 |
crée un corpus local avec `.gt.txt` extraits de l'OCR ALTO si présent.
|
| 135 |
Depuis le chantier 4, IIIF et Gallica utilisent les mêmes helpers HTTP
|
| 136 |
-
factorisés ([`picarones/importers/_http.py`](../picarones/importers/_http.py))
|
| 137 |
avec garde-fou `file://`/`ftp://`/`javascript://`.
|
| 138 |
|
| 139 |
## Outils utilitaires
|
|
|
|
| 133 |
Télécharge un manifeste IIIF v2/v3 (BnF Gallica, Bodleian, Vatican…) et
|
| 134 |
crée un corpus local avec `.gt.txt` extraits de l'OCR ALTO si présent.
|
| 135 |
Depuis le chantier 4, IIIF et Gallica utilisent les mêmes helpers HTTP
|
| 136 |
+
factorisés ([`picarones/extras/importers/_http.py`](../picarones/extras/importers/_http.py))
|
| 137 |
avec garde-fou `file://`/`ftp://`/`javascript://`.
|
| 138 |
|
| 139 |
## Outils utilitaires
|
|
@@ -10,10 +10,18 @@ modules. En résumé :
|
|
| 10 |
|
| 11 |
```
|
| 12 |
picarones/
|
| 13 |
-
├── core/ # cœur analytique pur Python
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
│ ├── runner.py # orchestration ThreadPool/ProcessPool
|
| 15 |
│ ├── metrics.py # CER/WER/MER/WIL via jiwer
|
| 16 |
-
│ ├── statistics
|
|
|
|
| 17 |
│ ├── narrative/ # moteur de synthèse factuelle
|
| 18 |
│ ├── pricing.py # modèle de coût pour la vue Pareto
|
| 19 |
│ └── …
|
|
|
|
| 10 |
|
| 11 |
```
|
| 12 |
picarones/
|
| 13 |
+
├── core/ # cœur analytique pur Python (Cercle 1)
|
| 14 |
+
│ ├── pipeline.py # PipelineRunner pour pipelines composées
|
| 15 |
+
│ ├── corpus.py # Document, Corpus, GTLevel
|
| 16 |
+
│ ├── results.py # DocumentResult, EngineReport, BenchmarkResult
|
| 17 |
+
│ ├── modules.py # BaseModule, ArtifactType
|
| 18 |
+
│ ├── facts.py # Fact, FactType, registre narratif
|
| 19 |
+
│ └── …
|
| 20 |
+
├── measurements/ # métriques officielles (Cercle 2)
|
| 21 |
│ ├── runner.py # orchestration ThreadPool/ProcessPool
|
| 22 |
│ ├── metrics.py # CER/WER/MER/WIL via jiwer
|
| 23 |
+
│ ├── statistics/ # Wilcoxon, Friedman, Nemenyi, Pareto
|
| 24 |
+
│ │ (sous-package depuis le sprint « découpage statistics.py »)
|
| 25 |
│ ├── narrative/ # moteur de synthèse factuelle
|
| 26 |
│ ├── pricing.py # modèle de coût pour la vue Pareto
|
| 27 |
│ └── …
|
|
@@ -150,7 +150,7 @@ def my_hook(*, ground_truth, hypothesis, image_path, corpus_lang, ocr_result):
|
|
| 150 |
|
| 151 |
- [`picarones/core/metric_hooks.py`](../picarones/core/metric_hooks.py)
|
| 152 |
— registre, profils, `run_document_hooks()`, `run_corpus_aggregators()`.
|
| 153 |
-
- [`picarones/
|
| 154 |
— les 12 hooks doc + 12 agrégateurs natifs Picarones.
|
| 155 |
- [`tests/test_metric_hooks.py`](../tests/test_metric_hooks.py)
|
| 156 |
— tests unitaires + rétrocompat profil `standard`.
|
|
|
|
| 150 |
|
| 151 |
- [`picarones/core/metric_hooks.py`](../picarones/core/metric_hooks.py)
|
| 152 |
— registre, profils, `run_document_hooks()`, `run_corpus_aggregators()`.
|
| 153 |
+
- [`picarones/measurements/builtin_hooks.py`](../picarones/measurements/builtin_hooks.py)
|
| 154 |
— les 12 hooks doc + 12 agrégateurs natifs Picarones.
|
| 155 |
- [`tests/test_metric_hooks.py`](../tests/test_metric_hooks.py)
|
| 156 |
— tests unitaires + rétrocompat profil `standard`.
|
|
@@ -442,7 +442,7 @@ nouvelle dans le rapport.
|
|
| 442 |
|
| 443 |
**A.II.1.a — Précision sur entités nommées (NER).**
|
| 444 |
|
| 445 |
-
Nouveau module `picarones/
|
| 446 |
Stanza, modèle HIPE pour les corpus historiques. Choix paramétré par
|
| 447 |
profil (`fr_core_news_lg`, `xx_ent_wiki_sm`, `hipe2022`).
|
| 448 |
|
|
@@ -464,7 +464,7 @@ glossaire (entrée `ner_score`).
|
|
| 464 |
|
| 465 |
**A.II.1.b — Score de calibration des moteurs.**
|
| 466 |
|
| 467 |
-
Nouveau module `picarones/
|
| 468 |
fournissent une confidence par token ou par ligne (Tesseract `tsv`
|
| 469 |
output, Pero OCR via `PageLayout`, Mistral OCR via `confidence`, Google
|
| 470 |
Vision via `Word.confidence`). Ajout d'un champ
|
|
|
|
| 442 |
|
| 443 |
**A.II.1.a — Précision sur entités nommées (NER).**
|
| 444 |
|
| 445 |
+
Nouveau module `picarones/measurements/ner.py`. Backends : spaCy multilingue,
|
| 446 |
Stanza, modèle HIPE pour les corpus historiques. Choix paramétré par
|
| 447 |
profil (`fr_core_news_lg`, `xx_ent_wiki_sm`, `hipe2022`).
|
| 448 |
|
|
|
|
| 464 |
|
| 465 |
**A.II.1.b — Score de calibration des moteurs.**
|
| 466 |
|
| 467 |
+
Nouveau module `picarones/measurements/calibration.py`. Tous les moteurs cibles
|
| 468 |
fournissent une confidence par token ou par ligne (Tesseract `tsv`
|
| 469 |
output, Pero OCR via `PageLayout`, Mistral OCR via `confidence`, Google
|
| 470 |
Vision via `Word.confidence`). Ajout d'un champ
|
|
@@ -350,7 +350,7 @@ brancher dans la pipeline et de mesurer.
|
|
| 350 |
### 6.b « Et si je veux juste tester une pipeline OCR seule, sans étapes en aval ? »
|
| 351 |
|
| 352 |
C'est exactement ce que fait le runner OCR historique
|
| 353 |
-
(`run_benchmark` dans `picarones/
|
| 354 |
toujours là, n'a pas changé, et reste la voie recommandée pour
|
| 355 |
les benchmarks d'OCR mono-étage.
|
| 356 |
|
|
|
|
| 350 |
### 6.b « Et si je veux juste tester une pipeline OCR seule, sans étapes en aval ? »
|
| 351 |
|
| 352 |
C'est exactement ce que fait le runner OCR historique
|
| 353 |
+
(`run_benchmark` dans `picarones/measurements/runner.py`) — il est
|
| 354 |
toujours là, n'a pas changé, et reste la voie recommandée pour
|
| 355 |
les benchmarks d'OCR mono-étage.
|
| 356 |
|
|
@@ -10,6 +10,8 @@ import re
|
|
| 10 |
from collections import defaultdict
|
| 11 |
from dataclasses import dataclass
|
| 12 |
|
|
|
|
|
|
|
| 13 |
# Patterns d'erreurs fréquentes (OCR + HTR documents patrimoniaux)
|
| 14 |
_ERROR_PATTERNS = [
|
| 15 |
# (pattern_re, label)
|
|
@@ -27,9 +29,12 @@ _ERROR_PATTERNS = [
|
|
| 27 |
|
| 28 |
|
| 29 |
def _extract_error_pairs(gt: str, hyp: str) -> list[tuple[str, str]]:
|
| 30 |
-
"""Extrait les paires (gt_char_seq, hyp_char_seq) d'erreurs de substitution.
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 33 |
ops = compute_word_diff(gt, hyp)
|
| 34 |
pairs = []
|
| 35 |
for op in ops:
|
|
|
|
| 10 |
from collections import defaultdict
|
| 11 |
from dataclasses import dataclass
|
| 12 |
|
| 13 |
+
from picarones.core.diff_utils import compute_word_diff
|
| 14 |
+
|
| 15 |
# Patterns d'erreurs fréquentes (OCR + HTR documents patrimoniaux)
|
| 16 |
_ERROR_PATTERNS = [
|
| 17 |
# (pattern_re, label)
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
def _extract_error_pairs(gt: str, hyp: str) -> list[tuple[str, str]]:
|
| 32 |
+
"""Extrait les paires (gt_char_seq, hyp_char_seq) d'erreurs de substitution.
|
| 33 |
+
|
| 34 |
+
L'import de ``compute_word_diff`` est au top-level du module
|
| 35 |
+
(cercle 1 → cercle 2, sens autorisé). Il était paresseux historiquement
|
| 36 |
+
pour contourner une violation de cercle (Sprint A3) qui n'existe plus.
|
| 37 |
+
"""
|
| 38 |
ops = compute_word_diff(gt, hyp)
|
| 39 |
pairs = []
|
| 40 |
for op in ops:
|
|
@@ -25,9 +25,12 @@ recommandation : la phrase est purement descriptive.
|
|
| 25 |
|
| 26 |
from __future__ import annotations
|
| 27 |
|
|
|
|
| 28 |
from html import escape as _e
|
| 29 |
from typing import Iterable, Optional
|
| 30 |
|
|
|
|
|
|
|
| 31 |
|
| 32 |
def _lever_label(lever_type: str, labels: dict[str, str]) -> str:
|
| 33 |
return labels.get(f"levers_label_{lever_type}", lever_type)
|
|
@@ -223,7 +226,12 @@ def build_levers_section_html(
|
|
| 223 |
continue
|
| 224 |
try:
|
| 225 |
sentence = formatter(payload, labels)
|
| 226 |
-
except Exception:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
continue
|
| 228 |
if not sentence:
|
| 229 |
continue
|
|
|
|
| 25 |
|
| 26 |
from __future__ import annotations
|
| 27 |
|
| 28 |
+
import logging
|
| 29 |
from html import escape as _e
|
| 30 |
from typing import Iterable, Optional
|
| 31 |
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
|
| 35 |
def _lever_label(lever_type: str, labels: dict[str, str]) -> str:
|
| 36 |
return labels.get(f"levers_label_{lever_type}", lever_type)
|
|
|
|
| 226 |
continue
|
| 227 |
try:
|
| 228 |
sentence = formatter(payload, labels)
|
| 229 |
+
except Exception as exc: # noqa: BLE001 — un formatter cassé ne doit pas casser la section
|
| 230 |
+
logger.warning(
|
| 231 |
+
"[levers_render] formatter %r a échoué sur payload=%r : %s — "
|
| 232 |
+
"ce levier sera omis du rapport",
|
| 233 |
+
lv_type, payload, exc,
|
| 234 |
+
)
|
| 235 |
continue
|
| 236 |
if not sentence:
|
| 237 |
continue
|
|
@@ -28,21 +28,25 @@ from pathlib import Path
|
|
| 28 |
|
| 29 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 30 |
|
| 31 |
-
#: Snapshot
|
| 32 |
#:
|
| 33 |
#: Historique :
|
| 34 |
#: - 119 (initial v1.0.0, dette pré-existante CLAUDE.md/CHANGELOG.md
|
| 35 |
#: qui décrivent des modules sous ``picarones/core/...`` alors qu'ils
|
| 36 |
#: vivent dans ``picarones/measurements/...``).
|
| 37 |
#: - 122 (sprint « découpage de statistics.py », 2026-05-02) : 3 audits
|
| 38 |
-
#: historiques
|
| 39 |
-
#:
|
| 40 |
-
#:
|
| 41 |
-
#:
|
| 42 |
-
#:
|
| 43 |
-
#:
|
| 44 |
-
#:
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
#: Patrons de fichiers de documentation à scanner.
|
| 48 |
DOC_GLOBS: tuple[str, ...] = (
|
|
|
|
| 28 |
|
| 29 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 30 |
|
| 31 |
+
#: Snapshot. Doit baisser, jamais monter.
|
| 32 |
#:
|
| 33 |
#: Historique :
|
| 34 |
#: - 119 (initial v1.0.0, dette pré-existante CLAUDE.md/CHANGELOG.md
|
| 35 |
#: qui décrivent des modules sous ``picarones/core/...`` alors qu'ils
|
| 36 |
#: vivent dans ``picarones/measurements/...``).
|
| 37 |
#: - 122 (sprint « découpage de statistics.py », 2026-05-02) : 3 audits
|
| 38 |
+
#: historiques référencent ``picarones/measurements/statistics.py``
|
| 39 |
+
#: qui est maintenant un sous-package. Baseline relevée.
|
| 40 |
+
#: - 72 (sprint « zéro dette actionnable », 2026-05-02) : 50 chemins
|
| 41 |
+
#: massivement corrigés — 44 dans CLAUDE.md (``core/X.py`` →
|
| 42 |
+
#: ``measurements/X.py`` pour les modules réellement déplacés
|
| 43 |
+
#: historiquement) + 6 dans docs vivants (profiles, cli-workflows,
|
| 44 |
+
#: evolution-2026, user/writing-a-pipeline-module, SPECS).
|
| 45 |
+
#:
|
| 46 |
+
#: Les 72 restants sont **TOUS** dans :
|
| 47 |
+
#: - ``CHANGELOG.md`` (67) : journal historique versionné, intouchable.
|
| 48 |
+
#: - ``docs/audits/*.md`` (5) : audits historiques, intouchables.
|
| 49 |
+
BROKEN_PATHS_BASELINE = 72
|
| 50 |
|
| 51 |
#: Patrons de fichiers de documentation à scanner.
|
| 52 |
DOC_GLOBS: tuple[str, ...] = (
|
|
@@ -423,6 +423,50 @@ class TestRender:
|
|
| 423 |
html = build_levers_section_html([bad], _load_labels("fr"))
|
| 424 |
assert html == ""
|
| 425 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
def test_accepts_dict_input(self) -> None:
|
| 427 |
d = {
|
| 428 |
"type": "complementarity_observation",
|
|
|
|
| 423 |
html = build_levers_section_html([bad], _load_labels("fr"))
|
| 424 |
assert html == ""
|
| 425 |
|
| 426 |
+
def test_formatter_exception_logs_warning_and_skips_lever(
|
| 427 |
+
self, caplog, monkeypatch,
|
| 428 |
+
) -> None:
|
| 429 |
+
"""Si un formatter lève une exception, le levier est omis et un
|
| 430 |
+
``logger.warning`` est émis avec le contexte (type + payload + exc).
|
| 431 |
+
|
| 432 |
+
Garantit que :
|
| 433 |
+
1. La section continue à se rendre malgré le formatter cassé.
|
| 434 |
+
2. Un diagnostic est tracé en logs (pas un fail silencieux).
|
| 435 |
+
"""
|
| 436 |
+
import logging
|
| 437 |
+
|
| 438 |
+
from picarones.report import levers_render
|
| 439 |
+
|
| 440 |
+
# Patche un des formatters pour qu'il lève une exception
|
| 441 |
+
original = levers_render._FORMATTERS.copy()
|
| 442 |
+
|
| 443 |
+
def broken_formatter(payload: dict, labels: dict) -> str:
|
| 444 |
+
raise ValueError("crash test")
|
| 445 |
+
|
| 446 |
+
monkeypatch.setattr(
|
| 447 |
+
levers_render, "_FORMATTERS",
|
| 448 |
+
{**original, "complementarity_observation": broken_formatter},
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
d = {
|
| 452 |
+
"type": "complementarity_observation",
|
| 453 |
+
"importance": 40,
|
| 454 |
+
"payload": {"foo": "bar"},
|
| 455 |
+
}
|
| 456 |
+
with caplog.at_level(logging.WARNING, logger="picarones.report.levers_render"):
|
| 457 |
+
html = build_levers_section_html([d], _load_labels("fr"))
|
| 458 |
+
|
| 459 |
+
# 1. Le levier cassé est omis (HTML ne le contient pas).
|
| 460 |
+
assert "complementarity_observation" not in html
|
| 461 |
+
|
| 462 |
+
# 2. Un warning a été émis avec le contexte attendu.
|
| 463 |
+
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
| 464 |
+
assert any(
|
| 465 |
+
"complementarity_observation" in r.getMessage()
|
| 466 |
+
and "crash test" in r.getMessage()
|
| 467 |
+
for r in warnings
|
| 468 |
+
), f"Expected warning with formatter context, got: {[r.getMessage() for r in warnings]}"
|
| 469 |
+
|
| 470 |
def test_accepts_dict_input(self) -> None:
|
| 471 |
d = {
|
| 472 |
"type": "complementarity_observation",
|