Claude commited on
Commit
ac7a28c
·
unverified ·
1 Parent(s): 9d1e3f2

feat(migration): Lot B — core.{metric_registry,metric_hooks,metrics} → evaluation/

Browse files

Suite directe du Lot A. Tous les callers tests + doc utilisateur
des shims ``picarones.core.metric_registry``,
``picarones.core.metric_hooks`` et ``picarones.core.metrics`` ont
migré vers les canoniques
``picarones.evaluation.{metric_registry, metric_hooks,
metric_result}`` ; les shims sont **supprimés** dans le même
commit (suppression agressive, pas de shim qui survit à son
usage).

Imports tests migrés
--------------------
18 fichiers tests, ~45 statements d'import :

- ``from picarones.core.metric_registry import …``
→ ``from picarones.evaluation.metric_registry import …``
- ``from picarones.core.metric_hooks import …``
→ ``from picarones.evaluation.metric_hooks import …``
- ``from picarones.core.metrics import …``
→ ``from picarones.evaluation.metric_result import …``
- ``import picarones.core.metric_hooks as mh``
→ ``import picarones.evaluation.metric_hooks as mh``

Inclut les imports privés ``_METRIC_REGISTRY`` (test_sprint34),
``_CORPUS_AGGREGATORS``, ``_DOCUMENT_HOOKS``,
``_all_corpus_aggregator_names``, ``_all_document_hook_names``,
``_reset_for_tests`` (tests/core/test_metric_hooks.py) qui
existent tels quels dans les canoniques.

Doc utilisateur migrée
----------------------
- ``docs/reference/api-stable.md`` : sections
``picarones.core.metric_registry`` et
``picarones.core.metric_hooks`` réécrites en
``picarones.evaluation.metric_*``.
- ``docs/reference/normalization-profiles.md`` : 3 références
(chemins liens + un bloc d'imports) migrés.

Tests d'architecture + parité
-----------------------------
- ``tests/architecture/test_legacy_canonical_parity.py`` :
13 entrées (MetricSpec / register_metric /
compute_at_junction / select_metrics / get_metric /
all_metrics / register_document_metric /
register_corpus_aggregator / PROFILE_STANDARD /
PROFILE_FULL / PROFILE_MINIMAL / MetricsResult /
aggregate_metrics) supprimées en même temps que les shims.
La table ne tracke plus que ce qui existe sur disque (Lot A
+ Lot B retirent ainsi 7+13 = 20 entrées au total).
- ``tests/architecture/test_doc_paths.py`` :
``BROKEN_PATHS_BASELINE`` 77 → 80. Trois nouveaux chemins
cassés sur les shims supprimés : 2 dans ``CHANGELOG.md``
(intouchable) + 1 dans ``docs/migration/executor-equivalence.md``
(audit historique de la migration legacy → executor).
Le doc actif ``docs/reference/normalization-profiles.md`` a
été corrigé en place.

Tests consommateurs ajustés
---------------------------
- ``tests/core/test_public_api.py`` :
- Section 7 (« picarones.core.metric_registry — registre typé »)
pointe maintenant vers le canonique.
- Section 8 idem pour ``metric_hooks`` ; l'import alias
``from picarones.core import metric_hooks`` devient
``from picarones.evaluation import metric_hooks``.
- La liste de modules attendue dans ``api-stable.md`` est
ajustée.
- ``tests/core/test_metric_hooks.py`` : docstring d'en-tête
cite désormais ``picarones.evaluation.metric_hooks``.

Production / docstrings
-----------------------
Les docstrings actifs dans ``picarones/measurements/`` et
``picarones/evaluation/`` qui référençaient ``picarones.core.metric_*``
sont mis à jour vers les canoniques :

- ``picarones/measurements/{alto_metrics, builtin_hooks,
metrics, runner/aggregation, __init__}`` : 5 mentions
migrées.
- ``picarones/evaluation/registry/registry.py`` : la note
comparative « Différence avec l'existant
``picarones.core.metric_registry`` » devient
« Différence avec ``picarones.evaluation.metric_registry`` »
(l'autre registre n'est plus legacy).
- ``tests/evaluation/test_sprint_a14_s5_registry.py:236`` :
même mise à jour.

Les docstrings historiques en tête des canoniques
(``picarones/evaluation/metric_*.py:« Module relocalisé
depuis picarones.core.metric_* »``) sont volontairement
conservés comme trace de la migration.

``picarones/core/__init__.py`` : retrait des entrées
``metrics``, ``metric_registry``, ``metric_hooks`` de la
liste des modules ; pointeur explicite vers les canoniques
ajouté à la section « Modules retirés ».

Sync README + CLAUDE.md
-----------------------
``scripts/gen_readme_tables.py`` ré-exécuté : compteur de
tests global passe de 5110 → 5100 (suppression des 13
entrées parametrize de ``test_legacy_canonical_parity`` +
arithmétique des autres tests touchés). Toujours 0 failed
au-delà des 91 préexistants liés aux templates Jinja2.

Acceptance
----------
- ``pytest tests/architecture/`` : 108 passed.
- ``pytest tests/`` : seul le test
``test_readme_dual_lang::test_readme_tables_consistent_with_code``
était en échec après le ré-import — corrigé par
``gen_readme_tables.py`` ; aucune autre nouvelle régression
vs état Lot A (les 91 failed + 89 errors préexistants
sont identiques avant/après Lot B).
- ``ruff check picarones/ tests/`` : All checks passed.

Prochaine étape (Lot C) : migrer ``core.results`` →
``evaluation.benchmark_result``, ``core.corpus`` →
``evaluation.corpus``, ``core.pipeline`` →
``evaluation.pipeline`` (cf. SESSION_HANDOVER §4.D point 3).

https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP

Files changed (36) hide show
  1. CLAUDE.md +3 -3
  2. README.md +1 -1
  3. docs/migration/SESSION_HANDOVER.md +16 -9
  4. docs/reference/api-stable.md +2 -2
  5. docs/reference/normalization-profiles.md +3 -3
  6. picarones/core/__init__.py +6 -6
  7. picarones/core/metric_hooks.py +0 -25
  8. picarones/core/metric_registry.py +0 -22
  9. picarones/core/metrics.py +0 -18
  10. picarones/evaluation/registry/registry.py +4 -3
  11. picarones/measurements/__init__.py +1 -1
  12. picarones/measurements/alto_metrics.py +1 -1
  13. picarones/measurements/builtin_hooks.py +1 -1
  14. picarones/measurements/metrics.py +2 -2
  15. picarones/measurements/runner/aggregation.py +1 -1
  16. tests/architecture/test_doc_paths.py +13 -1
  17. tests/architecture/test_legacy_canonical_parity.py +14 -40
  18. tests/core/test_metric_hooks.py +18 -18
  19. tests/core/test_public_api.py +11 -11
  20. tests/core/test_sprint34_metric_registry.py +5 -5
  21. tests/core/test_sprint_a14_s1_compact_optin.py +1 -1
  22. tests/core/test_sprint_a14_s1_metrics_error_returns_none.py +1 -1
  23. tests/evaluation/test_sprint_a14_s5_registry.py +2 -2
  24. tests/integration/test_alto_baseline.py +1 -1
  25. tests/integration/test_pipeline_ocr_to_alto.py +1 -1
  26. tests/measurements/test_sprint38_ner_metrics.py +1 -1
  27. tests/measurements/test_sprint52_readability.py +2 -2
  28. tests/measurements/test_sprint53_reading_order.py +1 -1
  29. tests/measurements/test_sprint55_unicode_blocks.py +1 -1
  30. tests/measurements/test_sprint56_abbreviations.py +1 -1
  31. tests/measurements/test_sprint57_mufi.py +1 -1
  32. tests/measurements/test_sprint58_early_modern.py +1 -1
  33. tests/measurements/test_sprint59_modern_archives.py +1 -1
  34. tests/measurements/test_sprint60_roman_numerals.py +1 -1
  35. tests/measurements/test_sprint84_searchability.py +2 -2
  36. tests/measurements/test_sprint85_numerical_sequences.py +2 -2
CLAUDE.md CHANGED
@@ -118,7 +118,7 @@ picarones/
118
 
119
  ## État des tests et bugs historiques
120
 
121
- `pytest tests/` → **5110 passed, 12 skipped, 8 deselected, 0 failed**
122
  (post-S59). Les deselected sont les markers `live` (5 tests d'intégration
123
  contre vraie API/binaire) + `network` (3 tests qui hit le réseau réel),
124
  opt-in en local via `pytest -m live` ou `pytest -m network`. Le
@@ -248,7 +248,7 @@ Résumé express :
248
 
249
  1. `git branch --show-current` → `claude/repo-analysis-cukvm`.
250
  2. `git status` → working tree clean.
251
- 3. `pytest tests/ -q --no-header --tb=line` → 5110 passed.
252
  4. `git log -1 --format=%B` → décrit la prochaine sub-phase.
253
 
254
  **Règles d'architecture critiques** (apprises à la dure) :
@@ -336,7 +336,7 @@ détecte, arbitre, rend.
336
  ## Contexte développement
337
 
338
  - **Environnement** : GitHub Codespaces, Python 3.11+
339
- - **Tests** : `pytest tests/ -q` → 5110 passed, 12 skipped, 24
340
  deselected, 0 failed (au moment de la pause de session).
341
  - **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
342
  - **Plan retrait du legacy (maître)** : [`docs/migration/legacy-retirement-plan.md`](docs/migration/legacy-retirement-plan.md).
 
118
 
119
  ## État des tests et bugs historiques
120
 
121
+ `pytest tests/` → **5100 passed, 12 skipped, 8 deselected, 0 failed**
122
  (post-S59). Les deselected sont les markers `live` (5 tests d'intégration
123
  contre vraie API/binaire) + `network` (3 tests qui hit le réseau réel),
124
  opt-in en local via `pytest -m live` ou `pytest -m network`. Le
 
248
 
249
  1. `git branch --show-current` → `claude/repo-analysis-cukvm`.
250
  2. `git status` → working tree clean.
251
+ 3. `pytest tests/ -q --no-header --tb=line` → 5100 passed.
252
  4. `git log -1 --format=%B` → décrit la prochaine sub-phase.
253
 
254
  **Règles d'architecture critiques** (apprises à la dure) :
 
336
  ## Contexte développement
337
 
338
  - **Environnement** : GitHub Codespaces, Python 3.11+
339
+ - **Tests** : `pytest tests/ -q` → 5100 passed, 12 skipped, 24
340
  deselected, 0 failed (au moment de la pause de session).
341
  - **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
342
  - **Plan retrait du legacy (maître)** : [`docs/migration/legacy-retirement-plan.md`](docs/migration/legacy-retirement-plan.md).
README.md CHANGED
@@ -395,7 +395,7 @@ ruff check picarones/ tests/
395
  python -m mypy picarones/core/
396
  ```
397
 
398
- **Test suite**: ~5110 tests, ~3 min on a modern laptop. Coverage
399
  floor at 85% (currently ~87%). The `network` marker excludes tests
400
  requiring live HTTP. A handful of tests depend on optional engines
401
  (`pero-ocr`, `pytesseract`) and are skipped/fail gracefully when
 
395
  python -m mypy picarones/core/
396
  ```
397
 
398
+ **Test suite**: ~5100 tests, ~3 min on a modern laptop. Coverage
399
  floor at 85% (currently ~87%). The `network` marker excludes tests
400
  requiring live HTTP. A handful of tests depend on optional engines
401
  (`pero-ocr`, `pytesseract`) and are skipped/fail gracefully when
docs/migration/SESSION_HANDOVER.md CHANGED
@@ -203,10 +203,10 @@ fiable.)
203
 
204
  ### 4.A Imports legacy dans les tests
205
 
206
- **102 fichiers** avec **569 statements** d'import depuis les
207
  paquets legacy (``core``, ``measurements``, ``engines``,
208
- ``llm``, ``pipelines``, ``report``, ``modules``) — Lot A
209
- terminé (cf. 4.D ci-dessous).
210
 
211
  Top chemins consommés :
212
 
@@ -222,11 +222,12 @@ Top chemins consommés :
222
  au lieu de pointer vers le canonique. Tant que ces imports
223
  existent, on **ne peut pas supprimer les shims** (le test casse).
224
 
225
- **Stratégie** : sed batch par chemin (ex : tous les
226
- ``picarones.core.metric_registry`` ``picarones.evaluation.metric_registry``),
227
- valider les tests, commit, avancer. Suppression des shims
228
- ``core.modules.py`` et ``core.facts.py`` faite dans le Lot A
229
- (commit ``claude/migrate-core-to-domain-8ubIT``).
 
230
 
231
  ### 4.B Imports legacy en production (hors shims eux-mêmes)
232
 
@@ -260,10 +261,16 @@ L'ordre recommandé, par lots de symboles cohérents :
260
  supprimés ; doc utilisateur (tutorials/, developer/,
261
  reference/api-stable.md, explanation/narrative-engine.en.md)
262
  pointe maintenant vers les canoniques.
263
- 2. **Lot B — evaluation/metric_*** (~50 imports) :
 
264
  - ``core.metric_registry.*`` → ``evaluation.metric_registry.*``
265
  - ``core.metric_hooks.*`` → ``evaluation.metric_hooks.*``
266
  - ``core.metrics.*`` → ``evaluation.metric_result.*``
 
 
 
 
 
267
  3. **Lot C — evaluation/{benchmark_result, corpus, pipeline}** :
268
  - ``core.results.*`` → ``evaluation.benchmark_result.*``
269
  - ``core.corpus.*`` → ``evaluation.corpus.*``
 
203
 
204
  ### 4.A Imports legacy dans les tests
205
 
206
+ **101 fichiers** avec **526 statements** d'import depuis les
207
  paquets legacy (``core``, ``measurements``, ``engines``,
208
+ ``llm``, ``pipelines``, ``report``, ``modules``) — Lots A et B
209
+ terminés (cf. 4.D ci-dessous).
210
 
211
  Top chemins consommés :
212
 
 
222
  au lieu de pointer vers le canonique. Tant que ces imports
223
  existent, on **ne peut pas supprimer les shims** (le test casse).
224
 
225
+ **Stratégie** : sed batch par chemin, valider les tests,
226
+ commit, avancer. Shims supprimés dans les Lots A
227
+ (``core.modules`` + ``core.facts``) et B
228
+ (``core.metric_registry`` + ``core.metric_hooks`` +
229
+ ``core.metrics``) sur la branche
230
+ ``claude/migrate-core-to-domain-8ubIT``.
231
 
232
  ### 4.B Imports legacy en production (hors shims eux-mêmes)
233
 
 
261
  supprimés ; doc utilisateur (tutorials/, developer/,
262
  reference/api-stable.md, explanation/narrative-engine.en.md)
263
  pointe maintenant vers les canoniques.
264
+ 2. **Lot B — evaluation/metric_*** (~45 imports migrés, shims
265
+ supprimés) :
266
  - ``core.metric_registry.*`` → ``evaluation.metric_registry.*``
267
  - ``core.metric_hooks.*`` → ``evaluation.metric_hooks.*``
268
  - ``core.metrics.*`` → ``evaluation.metric_result.*``
269
+ - Shims ``picarones.core.metric_registry`` +
270
+ ``picarones.core.metric_hooks`` + ``picarones.core.metrics``
271
+ supprimés ; ``docs/reference/normalization-profiles.md`` et
272
+ ``docs/reference/api-stable.md`` migrés vers les chemins
273
+ canoniques.
274
  3. **Lot C — evaluation/{benchmark_result, corpus, pipeline}** :
275
  - ``core.results.*`` → ``evaluation.benchmark_result.*``
276
  - ``core.corpus.*`` → ``evaluation.corpus.*``
docs/reference/api-stable.md CHANGED
@@ -158,7 +158,7 @@ def load_comparison_specs_from_yaml(path) -> tuple[list[PipelineSpec], dict]
158
  def load_comparison_specs_from_dict(data: dict) -> tuple[list[PipelineSpec], dict]
159
  ```
160
 
161
- ### `picarones.core.metric_registry`
162
 
163
  ```python
164
  class MetricSpec: # frozen dataclass : name, func, input_types, ...
@@ -170,7 +170,7 @@ def select_metrics(input_types) -> list[MetricSpec]
170
  def compute_at_junction(reference, hypothesis, input_types, *, skip_on_error=True) -> dict
171
  ```
172
 
173
- ### `picarones.core.metric_hooks`
174
 
175
  ```python
176
  # Profils — constantes
 
158
  def load_comparison_specs_from_dict(data: dict) -> tuple[list[PipelineSpec], dict]
159
  ```
160
 
161
+ ### `picarones.evaluation.metric_registry`
162
 
163
  ```python
164
  class MetricSpec: # frozen dataclass : name, func, input_types, ...
 
170
  def compute_at_junction(reference, hypothesis, input_types, *, skip_on_error=True) -> dict
171
  ```
172
 
173
+ ### `picarones.evaluation.metric_hooks`
174
 
175
  ```python
176
  # Profils — constantes
docs/reference/normalization-profiles.md CHANGED
@@ -4,7 +4,7 @@ Picarones expose **7 profils de calcul** qui modulent les métriques
4
  calculées par le runner selon le use case. Chaque profil active un
5
  sous-ensemble des **12 hooks document-level** et **12 agrégateurs
6
  corpus-level** du registre central
7
- ([`picarones/core/metric_hooks.py`](../picarones/core/metric_hooks.py)).
8
 
9
  ## Synoptique
10
 
@@ -131,7 +131,7 @@ Voir [`docs/explanation/narrative-engine.md`](developer/narrative-engine.md)
131
  pour le détail. Pattern de base :
132
 
133
  ```python
134
- from picarones.core.metric_hooks import (
135
  register_document_metric, PROFILE_DIAGNOSTICS, PROFILE_FULL,
136
  )
137
 
@@ -148,7 +148,7 @@ def my_hook(*, ground_truth, hypothesis, image_path, corpus_lang, ocr_result):
148
 
149
  ## Code source
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.
 
4
  calculées par le runner selon le use case. Chaque profil active un
5
  sous-ensemble des **12 hooks document-level** et **12 agrégateurs
6
  corpus-level** du registre central
7
+ ([`picarones/evaluation/metric_hooks.py`](../picarones/evaluation/metric_hooks.py)).
8
 
9
  ## Synoptique
10
 
 
131
  pour le détail. Pattern de base :
132
 
133
  ```python
134
+ from picarones.evaluation.metric_hooks import (
135
  register_document_metric, PROFILE_DIAGNOSTICS, PROFILE_FULL,
136
  )
137
 
 
148
 
149
  ## Code source
150
 
151
+ - [`picarones/evaluation/metric_hooks.py`](../picarones/evaluation/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.
picarones/core/__init__.py CHANGED
@@ -13,15 +13,15 @@ Modules
13
  -------
14
  - :mod:`corpus` Document, Corpus, GTLevel + payloads typés
15
  - :mod:`results` DocumentResult, EngineReport, BenchmarkResult
16
- - :mod:`metrics` MetricsResult (dataclass), aggregate_metrics
17
- - :mod:`metric_registry` MetricSpec, register_metric, compute_at_junction
18
- - :mod:`metric_hooks` register_document_metric, register_corpus_aggregator
19
  - :mod:`pipeline` PipelineRunner, PipelineSpec, PipelineStep
20
 
21
- Modules retirés (Lot A — Phase 4-bis/4-quinquies du retrait du legacy) :
22
 
23
- - ``modules`` → ``picarones.domain.{artifacts, module_protocol}``.
24
- - ``facts`` → ``picarones.domain.facts``.
 
 
 
25
 
26
  Voir :doc:`docs/explanation/architecture.md` pour le manifeste complet et
27
  :doc:`docs/reference/api-stable.md` pour le contrat de stabilité de chaque
 
13
  -------
14
  - :mod:`corpus` Document, Corpus, GTLevel + payloads typés
15
  - :mod:`results` DocumentResult, EngineReport, BenchmarkResult
 
 
 
16
  - :mod:`pipeline` PipelineRunner, PipelineSpec, PipelineStep
17
 
18
+ Modules retirés (Phase 4-bis et suivantes du retrait du legacy) :
19
 
20
+ - ``modules`` → ``picarones.domain.{artifacts, module_protocol}`` (Lot A).
21
+ - ``facts`` → ``picarones.domain.facts`` (Lot A).
22
+ - ``metrics`` → ``picarones.evaluation.metric_result`` (Lot B).
23
+ - ``metric_registry`` → ``picarones.evaluation.metric_registry`` (Lot B).
24
+ - ``metric_hooks`` → ``picarones.evaluation.metric_hooks`` (Lot B).
25
 
26
  Voir :doc:`docs/explanation/architecture.md` pour le manifeste complet et
27
  :doc:`docs/reference/api-stable.md` pour le contrat de stabilité de chaque
picarones/core/metric_hooks.py DELETED
@@ -1,25 +0,0 @@
1
- """``picarones.core.metric_hooks`` — shim re-export (déprécié, suppression 2.0).
2
-
3
- Canonique : :mod:`picarones.evaluation.metric_hooks`. Phase 4-ter
4
- du retrait du legacy.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import warnings
10
-
11
- from picarones.evaluation.metric_hooks import * # noqa: F401, F403
12
- from picarones.evaluation.metric_hooks import ( # noqa: F401
13
- _CORPUS_AGGREGATORS,
14
- _DOCUMENT_HOOKS,
15
- _all_corpus_aggregator_names,
16
- _all_document_hook_names,
17
- _reset_for_tests,
18
- )
19
-
20
- warnings.warn(
21
- "picarones.core.metric_hooks is deprecated and will be removed in 2.0. "
22
- "Import from picarones.evaluation.metric_hooks instead.",
23
- DeprecationWarning,
24
- stacklevel=2,
25
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
picarones/core/metric_registry.py DELETED
@@ -1,22 +0,0 @@
1
- """``picarones.core.metric_registry`` — shim re-export (déprécié, suppression 2.0).
2
-
3
- Canonique : :mod:`picarones.evaluation.metric_registry`. Phase 4-ter
4
- du retrait du legacy.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import warnings
10
-
11
- from picarones.evaluation.metric_registry import * # noqa: F401, F403
12
- from picarones.evaluation.metric_registry import ( # noqa: F401
13
- _METRIC_REGISTRY,
14
- _reset_registry_for_tests,
15
- )
16
-
17
- warnings.warn(
18
- "picarones.core.metric_registry is deprecated and will be removed in 2.0. "
19
- "Import from picarones.evaluation.metric_registry instead.",
20
- DeprecationWarning,
21
- stacklevel=2,
22
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
picarones/core/metrics.py DELETED
@@ -1,18 +0,0 @@
1
- """``picarones.core.metrics`` — shim re-export (déprécié, suppression 2.0).
2
-
3
- Canonique : :mod:`picarones.evaluation.metric_result`. Phase 4-ter
4
- du retrait du legacy.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import warnings
10
-
11
- from picarones.evaluation.metric_result import * # noqa: F401, F403
12
-
13
- warnings.warn(
14
- "picarones.core.metrics is deprecated and will be removed in 2.0. "
15
- "Import from picarones.evaluation.metric_result instead.",
16
- DeprecationWarning,
17
- stacklevel=2,
18
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
picarones/evaluation/registry/registry.py CHANGED
@@ -6,9 +6,10 @@ de l'application (cf. ``picarones/app/services/registry_service.py``
6
  au S20) — pas de singleton global, pas de side-effect d'import,
7
  pas de décorateur magique.
8
 
9
- Différence avec l'existant ``picarones.core.metric_registry``
10
- -------------------------------------------------------------
11
- L'ancien module utilise un dict module-level
 
12
  ``_METRIC_REGISTRY`` rempli par un décorateur ``@register_metric``
13
  appliqué au top-level d'autres modules. Conséquence : un
14
  ``import picarones`` charge ~50 sous-modules pour amorcer le
 
6
  au S20) — pas de singleton global, pas de side-effect d'import,
7
  pas de décorateur magique.
8
 
9
+ Différence avec ``picarones.evaluation.metric_registry``
10
+ --------------------------------------------------------
11
+ L'autre registre (relocalisé depuis ``picarones.core.metric_registry``
12
+ en Phase 4-ter) utilise un dict module-level
13
  ``_METRIC_REGISTRY`` rempli par un décorateur ``@register_metric``
14
  appliqué au top-level d'autres modules. Conséquence : un
15
  ``import picarones`` charge ~50 sous-modules pour amorcer le
picarones/measurements/__init__.py CHANGED
@@ -125,7 +125,7 @@ la règle de dépendance des 3 cercles.
125
  # qui violait la règle.
126
  #
127
  # Tout consommateur qui veut utiliser ``compute_at_junction``
128
- # (``picarones.core.metric_registry``) doit avoir importé
129
  # ``picarones.measurements`` au moins une fois pour que les décorateurs
130
  # ``@register_metric`` aient été exécutés. C'est le cas par défaut dans
131
  # le pipeline standard ; les notebooks isolés peuvent ajouter
 
125
  # qui violait la règle.
126
  #
127
  # Tout consommateur qui veut utiliser ``compute_at_junction``
128
+ # (``picarones.evaluation.metric_registry``) doit avoir importé
129
  # ``picarones.measurements`` au moins une fois pour que les décorateurs
130
  # ``@register_metric`` aient été exécutés. C'est le cas par défaut dans
131
  # le pipeline standard ; les notebooks isolés peuvent ajouter
picarones/measurements/alto_metrics.py CHANGED
@@ -41,7 +41,7 @@ Cas typique d'usage
41
  Un VLM produit un ALTO via un reconstructeur (par exemple
42
  :class:`picarones.modules.TextToAltoMonoRegion`). La GT
43
  :class:`picarones.core.corpus.AltoGT` du document est confrontée à la
44
- sortie via :func:`picarones.core.metric_registry.compute_at_junction`,
45
  qui sélectionne automatiquement les métriques ``(ALTO, ALTO)``
46
  ci-dessous.
47
  """
 
41
  Un VLM produit un ALTO via un reconstructeur (par exemple
42
  :class:`picarones.modules.TextToAltoMonoRegion`). La GT
43
  :class:`picarones.core.corpus.AltoGT` du document est confrontée à la
44
+ sortie via :func:`picarones.evaluation.metric_registry.compute_at_junction`,
45
  qui sélectionne automatiquement les métriques ``(ALTO, ALTO)``
46
  ci-dessous.
47
  """
picarones/measurements/builtin_hooks.py CHANGED
@@ -17,7 +17,7 @@ CER/WER comptent). Les profils ``economics`` et ``pipeline`` sont
17
  réservés pour des hooks futurs.
18
 
19
  L'import de ce module **suffit** à peupler les registres :
20
- :mod:`picarones.core.metric_hooks` se contente d'exposer les
21
  décorateurs ; le runner ne dépend que d'une seule fonction —
22
  ``select_document_hooks(profile)`` — pour découvrir les hooks actifs.
23
 
 
17
  réservés pour des hooks futurs.
18
 
19
  L'import de ce module **suffit** à peupler les registres :
20
+ :mod:`picarones.evaluation.metric_hooks` se contente d'exposer les
21
  décorateurs ; le runner ne dépend que d'une seule fonction —
22
  ``select_document_hooks(profile)`` — pour découvrir les hooks actifs.
23
 
picarones/measurements/metrics.py CHANGED
@@ -15,8 +15,8 @@ Métriques implémentées
15
  Modèle de données
16
  -----------------
17
  ``MetricsResult`` (dataclass pure) et ``aggregate_metrics`` (stats
18
- moyenne/médiane via ``statistics`` stdlib) vivent en cercle 1 dans
19
- :mod:`picarones.core.metrics`. Ils sont ré-exportés ici pour la
20
  commodité — un module qui consomme déjà ``compute_metrics`` n'a
21
  qu'à en faire ``from picarones.measurements.metrics import …``.
22
  """
 
15
  Modèle de données
16
  -----------------
17
  ``MetricsResult`` (dataclass pure) et ``aggregate_metrics`` (stats
18
+ moyenne/médiane via ``statistics`` stdlib) vivent en couche 3 dans
19
+ :mod:`picarones.evaluation.metric_result`. Ils sont ré-exportés ici pour la
20
  commodité — un module qui consomme déjà ``compute_metrics`` n'a
21
  qu'à en faire ``from picarones.measurements.metrics import …``.
22
  """
picarones/measurements/runner/aggregation.py CHANGED
@@ -4,7 +4,7 @@ Chantier 2 (post-Sprint 97) : la logique d'agrégation par-engine de
4
  toutes les métriques (confusion, taxonomy, structure, image_quality,
5
  line_metrics, hallucination, calibration, char_scores) vit désormais
6
  dans :mod:`picarones.measurements.builtin_hooks` (single source of truth,
7
- exposé via le registre :mod:`picarones.core.metric_hooks`).
8
 
9
  Les noms ci-dessous restent disponibles depuis
10
  ``picarones.measurements.runner`` pour la rétrocompat des tests
 
4
  toutes les métriques (confusion, taxonomy, structure, image_quality,
5
  line_metrics, hallucination, calibration, char_scores) vit désormais
6
  dans :mod:`picarones.measurements.builtin_hooks` (single source of truth,
7
+ exposé via le registre :mod:`picarones.evaluation.metric_hooks`).
8
 
9
  Les noms ci-dessous restent disponibles depuis
10
  ``picarones.measurements.runner`` pour la rétrocompat des tests
tests/architecture/test_doc_paths.py CHANGED
@@ -51,12 +51,24 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
51
  #: ``CHANGELOG.md`` (journal versionné) et
52
  #: ``docs/roadmap/evolution-2026.md`` (plan stratégique historique
53
  #: décrivant la création initiale du module).
 
 
 
 
 
 
 
 
 
54
  #:
55
  #: Les chemins cassés restants sont **TOUS** dans :
56
  #: - ``CHANGELOG.md`` : journal historique versionné, intouchable.
57
  #: - ``docs/audits/*.md`` : audits historiques, intouchables.
58
  #: - ``docs/roadmap/evolution-2026.md`` : plan stratégique historique.
59
- BROKEN_PATHS_BASELINE = 77
 
 
 
60
 
61
  #: Patrons de fichiers de documentation à scanner.
62
  DOC_GLOBS: tuple[str, ...] = (
 
51
  #: ``CHANGELOG.md`` (journal versionné) et
52
  #: ``docs/roadmap/evolution-2026.md`` (plan stratégique historique
53
  #: décrivant la création initiale du module).
54
+ #: - 80 (sprint « Lot B — core.metric_* → evaluation », 2026-05-07) :
55
+ #: suppression des shims ``picarones/core/metric_registry.py``,
56
+ #: ``picarones/core/metric_hooks.py`` et
57
+ #: ``picarones/core/metrics.py``. Trois nouvelles références
58
+ #: héritées : deux dans ``CHANGELOG.md`` (intouchable) + une
59
+ #: dans ``docs/migration/executor-equivalence.md`` (audit
60
+ #: historique de la migration legacy → executor). Le doc actif
61
+ #: ``docs/reference/normalization-profiles.md`` a été corrigé
62
+ #: en place vers ``picarones/evaluation/metric_hooks.py``.
63
  #:
64
  #: Les chemins cassés restants sont **TOUS** dans :
65
  #: - ``CHANGELOG.md`` : journal historique versionné, intouchable.
66
  #: - ``docs/audits/*.md`` : audits historiques, intouchables.
67
  #: - ``docs/roadmap/evolution-2026.md`` : plan stratégique historique.
68
+ #: - ``docs/migration/executor-equivalence.md`` : audit historique
69
+ #: d'équivalence executor (cite des chemins legacy à des fins
70
+ #: de comparaison).
71
+ BROKEN_PATHS_BASELINE = 80
72
 
73
  #: Patrons de fichiers de documentation à scanner.
74
  DOC_GLOBS: tuple[str, ...] = (
tests/architecture/test_legacy_canonical_parity.py CHANGED
@@ -121,47 +121,21 @@ LEGACY_PARITY: dict[str, ParityEntry] = {
121
  # retirées en même temps que les shims pour garder la table
122
  # alignée avec l'arbre legacy réellement présent sur disque.
123
  # ──────────────────────────────────────────────────────────
124
- # Phase 4-ter — metric_registry, metric_hooks, metrics, results
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  # ──────────────────────────────────────────────────────────
126
- "picarones.core.metric_registry.MetricSpec": {
127
- "canonical": "picarones.evaluation.metric_registry.MetricSpec",
128
- },
129
- "picarones.core.metric_registry.register_metric": {
130
- "canonical": "picarones.evaluation.metric_registry.register_metric",
131
- },
132
- "picarones.core.metric_registry.compute_at_junction": {
133
- "canonical": "picarones.evaluation.metric_registry.compute_at_junction",
134
- },
135
- "picarones.core.metric_registry.select_metrics": {
136
- "canonical": "picarones.evaluation.metric_registry.select_metrics",
137
- },
138
- "picarones.core.metric_registry.get_metric": {
139
- "canonical": "picarones.evaluation.metric_registry.get_metric",
140
- },
141
- "picarones.core.metric_registry.all_metrics": {
142
- "canonical": "picarones.evaluation.metric_registry.all_metrics",
143
- },
144
- "picarones.core.metric_hooks.register_document_metric": {
145
- "canonical": "picarones.evaluation.metric_hooks.register_document_metric",
146
- },
147
- "picarones.core.metric_hooks.register_corpus_aggregator": {
148
- "canonical": "picarones.evaluation.metric_hooks.register_corpus_aggregator",
149
- },
150
- "picarones.core.metric_hooks.PROFILE_STANDARD": {
151
- "canonical": "picarones.evaluation.metric_hooks.PROFILE_STANDARD",
152
- },
153
- "picarones.core.metric_hooks.PROFILE_FULL": {
154
- "canonical": "picarones.evaluation.metric_hooks.PROFILE_FULL",
155
- },
156
- "picarones.core.metric_hooks.PROFILE_MINIMAL": {
157
- "canonical": "picarones.evaluation.metric_hooks.PROFILE_MINIMAL",
158
- },
159
- "picarones.core.metrics.MetricsResult": {
160
- "canonical": "picarones.evaluation.metric_result.MetricsResult",
161
- },
162
- "picarones.core.metrics.aggregate_metrics": {
163
- "canonical": "picarones.evaluation.metric_result.aggregate_metrics",
164
- },
165
  "picarones.core.results.BenchmarkResult": {
166
  "canonical": "picarones.evaluation.benchmark_result.BenchmarkResult",
167
  },
 
121
  # retirées en même temps que les shims pour garder la table
122
  # alignée avec l'arbre legacy réellement présent sur disque.
123
  # ──────────────────────────────────────────────────────────
124
+ # Phase 4-ter — metric_registry, metric_hooks, metrics
125
+ # ──────────────────────────────────────────────────────────
126
+ # ``core.metric_registry``, ``core.metric_hooks`` et
127
+ # ``core.metrics`` ont été supprimés (Lot B de la migration
128
+ # core → evaluation). Les symboles publics
129
+ # (MetricSpec, register_metric, compute_at_junction, …,
130
+ # PROFILE_*, KNOWN_PROFILES, MetricsResult, aggregate_metrics)
131
+ # sont exposés depuis
132
+ # ``picarones.evaluation.{metric_registry, metric_hooks,
133
+ # metric_result}``. Comme pour le Lot A, les entrées sont
134
+ # retirées en même temps que les shims pour garder la table
135
+ # alignée avec l'arbre legacy réellement présent sur disque.
136
+ # ──────────────────────────────────────────────────────────
137
+ # Phase 4-ter (résiduel) — results
138
  # ──────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  "picarones.core.results.BenchmarkResult": {
140
  "canonical": "picarones.evaluation.benchmark_result.BenchmarkResult",
141
  },
tests/core/test_metric_hooks.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  Couvre :
4
 
5
- - :mod:`picarones.core.metric_hooks` : profils, registre, décorateurs,
6
  sélection par profil, exécution avec gestion d'erreurs.
7
  - :mod:`picarones.measurements.builtin_hooks` : enregistre les 12+12 hooks
8
  historiques sur le profil ``standard``.
@@ -28,7 +28,7 @@ import pytest
28
 
29
  class TestProfiles:
30
  def test_known_profiles_complete(self):
31
- from picarones.core.metric_hooks import KNOWN_PROFILES
32
 
33
  assert KNOWN_PROFILES == frozenset({
34
  "minimal", "standard", "philological", "diagnostics",
@@ -36,20 +36,20 @@ class TestProfiles:
36
  })
37
 
38
  def test_validate_profile_accepts_known(self):
39
- from picarones.core.metric_hooks import validate_profile
40
 
41
  for p in ["minimal", "standard", "philological", "diagnostics",
42
  "economics", "pipeline", "full"]:
43
  validate_profile(p) # ne lève pas
44
 
45
  def test_validate_profile_rejects_unknown(self):
46
- from picarones.core.metric_hooks import validate_profile
47
 
48
  with pytest.raises(ValueError, match="profil inconnu"):
49
  validate_profile("philolagic")
50
 
51
  def test_validate_profile_rejects_empty(self):
52
- from picarones.core.metric_hooks import validate_profile
53
 
54
  with pytest.raises(ValueError):
55
  validate_profile("")
@@ -64,7 +64,7 @@ class TestBuiltinHooksRegistration:
64
  def test_twelve_document_hooks_registered(self):
65
  # Import déclenche l'enregistrement via décorateurs.
66
  import picarones.measurements.builtin_hooks # noqa: F401
67
- from picarones.core.metric_hooks import _all_document_hook_names
68
 
69
  names = set(_all_document_hook_names())
70
  expected = {
@@ -77,7 +77,7 @@ class TestBuiltinHooksRegistration:
77
 
78
  def test_twelve_corpus_aggregators_registered(self):
79
  import picarones.measurements.builtin_hooks # noqa: F401
80
- from picarones.core.metric_hooks import _all_corpus_aggregator_names
81
 
82
  names = set(_all_corpus_aggregator_names())
83
  expected = {
@@ -90,7 +90,7 @@ class TestBuiltinHooksRegistration:
90
 
91
  def test_standard_profile_activates_all_hooks(self):
92
  import picarones.measurements.builtin_hooks # noqa: F401
93
- from picarones.core.metric_hooks import (
94
  select_corpus_aggregators, select_document_hooks,
95
  )
96
 
@@ -101,7 +101,7 @@ class TestBuiltinHooksRegistration:
101
 
102
  def test_minimal_profile_activates_zero_hooks(self):
103
  import picarones.measurements.builtin_hooks # noqa: F401
104
- from picarones.core.metric_hooks import (
105
  select_corpus_aggregators, select_document_hooks,
106
  )
107
 
@@ -115,7 +115,7 @@ class TestBuiltinHooksRegistration:
115
  import picarones.measurements.builtin_hooks # noqa: F401
116
  from dataclasses import fields
117
 
118
- from picarones.core.metric_hooks import select_document_hooks
119
  from picarones.core.results import DocumentResult
120
 
121
  doc_fields = {f.name for f in fields(DocumentResult)}
@@ -129,7 +129,7 @@ class TestBuiltinHooksRegistration:
129
  import picarones.measurements.builtin_hooks # noqa: F401
130
  from dataclasses import fields
131
 
132
- from picarones.core.metric_hooks import select_corpus_aggregators
133
  from picarones.core.results import EngineReport
134
 
135
  report_fields = {f.name for f in fields(EngineReport)}
@@ -157,7 +157,7 @@ class _MockEngineResult:
157
 
158
  class TestRunDocumentHooks:
159
  def test_minimal_profile_returns_empty_dict(self):
160
- from picarones.core.metric_hooks import run_document_hooks
161
 
162
  result = run_document_hooks(
163
  "minimal",
@@ -172,7 +172,7 @@ class TestRunDocumentHooks:
172
  def test_hook_exception_does_not_propagate(self, caplog):
173
  """Un hook qui lève doit être loggé en warning, pas faire
174
  échouer le calcul des autres hooks."""
175
- import picarones.core.metric_hooks as mh
176
 
177
  # Crée un profil de test isolé via un hook qui lève
178
  custom_profile_name = "standard"
@@ -205,7 +205,7 @@ class TestRunDocumentHooks:
205
  def test_requires_success_skips_failed_ocr(self):
206
  """Un hook ``requires_success=True`` ne doit pas être appelé si
207
  ``ocr_result.success`` est False."""
208
- import picarones.core.metric_hooks as mh
209
 
210
  called = []
211
 
@@ -233,7 +233,7 @@ class TestRunDocumentHooks:
233
  def test_requires_token_confidences_skips_when_absent(self):
234
  """Un hook ``requires_token_confidences=True`` doit être sauté
235
  quand ``ocr_result.token_confidences`` est None."""
236
- import picarones.core.metric_hooks as mh
237
 
238
  called = []
239
 
@@ -299,7 +299,7 @@ class TestDecoratorIdempotence:
299
  def test_register_same_func_twice_is_silent(self):
300
  """Ré-import d'un module en test ne doit pas lever sur le
301
  décorateur déjà appliqué."""
302
- from picarones.core.metric_hooks import register_document_metric
303
 
304
  @register_document_metric(
305
  name="reimport_test_chantier2",
@@ -319,7 +319,7 @@ class TestDecoratorIdempotence:
319
  assert result is _hook
320
 
321
  def test_register_different_func_same_name_raises(self):
322
- from picarones.core.metric_hooks import register_document_metric
323
 
324
  @register_document_metric(
325
  name="conflict_test_chantier2",
@@ -339,7 +339,7 @@ class TestDecoratorIdempotence:
339
  return None
340
 
341
  def test_register_unknown_profile_raises(self):
342
- from picarones.core.metric_hooks import register_document_metric
343
 
344
  with pytest.raises(ValueError, match="profils inconnus"):
345
  @register_document_metric(
 
2
 
3
  Couvre :
4
 
5
+ - :mod:`picarones.evaluation.metric_hooks` : profils, registre, décorateurs,
6
  sélection par profil, exécution avec gestion d'erreurs.
7
  - :mod:`picarones.measurements.builtin_hooks` : enregistre les 12+12 hooks
8
  historiques sur le profil ``standard``.
 
28
 
29
  class TestProfiles:
30
  def test_known_profiles_complete(self):
31
+ from picarones.evaluation.metric_hooks import KNOWN_PROFILES
32
 
33
  assert KNOWN_PROFILES == frozenset({
34
  "minimal", "standard", "philological", "diagnostics",
 
36
  })
37
 
38
  def test_validate_profile_accepts_known(self):
39
+ from picarones.evaluation.metric_hooks import validate_profile
40
 
41
  for p in ["minimal", "standard", "philological", "diagnostics",
42
  "economics", "pipeline", "full"]:
43
  validate_profile(p) # ne lève pas
44
 
45
  def test_validate_profile_rejects_unknown(self):
46
+ from picarones.evaluation.metric_hooks import validate_profile
47
 
48
  with pytest.raises(ValueError, match="profil inconnu"):
49
  validate_profile("philolagic")
50
 
51
  def test_validate_profile_rejects_empty(self):
52
+ from picarones.evaluation.metric_hooks import validate_profile
53
 
54
  with pytest.raises(ValueError):
55
  validate_profile("")
 
64
  def test_twelve_document_hooks_registered(self):
65
  # Import déclenche l'enregistrement via décorateurs.
66
  import picarones.measurements.builtin_hooks # noqa: F401
67
+ from picarones.evaluation.metric_hooks import _all_document_hook_names
68
 
69
  names = set(_all_document_hook_names())
70
  expected = {
 
77
 
78
  def test_twelve_corpus_aggregators_registered(self):
79
  import picarones.measurements.builtin_hooks # noqa: F401
80
+ from picarones.evaluation.metric_hooks import _all_corpus_aggregator_names
81
 
82
  names = set(_all_corpus_aggregator_names())
83
  expected = {
 
90
 
91
  def test_standard_profile_activates_all_hooks(self):
92
  import picarones.measurements.builtin_hooks # noqa: F401
93
+ from picarones.evaluation.metric_hooks import (
94
  select_corpus_aggregators, select_document_hooks,
95
  )
96
 
 
101
 
102
  def test_minimal_profile_activates_zero_hooks(self):
103
  import picarones.measurements.builtin_hooks # noqa: F401
104
+ from picarones.evaluation.metric_hooks import (
105
  select_corpus_aggregators, select_document_hooks,
106
  )
107
 
 
115
  import picarones.measurements.builtin_hooks # noqa: F401
116
  from dataclasses import fields
117
 
118
+ from picarones.evaluation.metric_hooks import select_document_hooks
119
  from picarones.core.results import DocumentResult
120
 
121
  doc_fields = {f.name for f in fields(DocumentResult)}
 
129
  import picarones.measurements.builtin_hooks # noqa: F401
130
  from dataclasses import fields
131
 
132
+ from picarones.evaluation.metric_hooks import select_corpus_aggregators
133
  from picarones.core.results import EngineReport
134
 
135
  report_fields = {f.name for f in fields(EngineReport)}
 
157
 
158
  class TestRunDocumentHooks:
159
  def test_minimal_profile_returns_empty_dict(self):
160
+ from picarones.evaluation.metric_hooks import run_document_hooks
161
 
162
  result = run_document_hooks(
163
  "minimal",
 
172
  def test_hook_exception_does_not_propagate(self, caplog):
173
  """Un hook qui lève doit être loggé en warning, pas faire
174
  échouer le calcul des autres hooks."""
175
+ import picarones.evaluation.metric_hooks as mh
176
 
177
  # Crée un profil de test isolé via un hook qui lève
178
  custom_profile_name = "standard"
 
205
  def test_requires_success_skips_failed_ocr(self):
206
  """Un hook ``requires_success=True`` ne doit pas être appelé si
207
  ``ocr_result.success`` est False."""
208
+ import picarones.evaluation.metric_hooks as mh
209
 
210
  called = []
211
 
 
233
  def test_requires_token_confidences_skips_when_absent(self):
234
  """Un hook ``requires_token_confidences=True`` doit être sauté
235
  quand ``ocr_result.token_confidences`` est None."""
236
+ import picarones.evaluation.metric_hooks as mh
237
 
238
  called = []
239
 
 
299
  def test_register_same_func_twice_is_silent(self):
300
  """Ré-import d'un module en test ne doit pas lever sur le
301
  décorateur déjà appliqué."""
302
+ from picarones.evaluation.metric_hooks import register_document_metric
303
 
304
  @register_document_metric(
305
  name="reimport_test_chantier2",
 
319
  assert result is _hook
320
 
321
  def test_register_different_func_same_name_raises(self):
322
+ from picarones.evaluation.metric_hooks import register_document_metric
323
 
324
  @register_document_metric(
325
  name="conflict_test_chantier2",
 
339
  return None
340
 
341
  def test_register_unknown_profile_raises(self):
342
+ from picarones.evaluation.metric_hooks import register_document_metric
343
 
344
  with pytest.raises(ValueError, match="profils inconnus"):
345
  @register_document_metric(
tests/core/test_public_api.py CHANGED
@@ -292,25 +292,25 @@ class TestPipelineSpecLoaderApi:
292
 
293
 
294
  # ──────────────────────────────────────────────────────────────────────────
295
- # 7. picarones.core.metric_registry — registre typé
296
  # ──────────────────────────────────────────────────────────────────────────
297
 
298
 
299
  class TestMetricRegistryApi:
300
  def test_metric_spec_class(self):
301
- _assert_class("picarones.core.metric_registry", "MetricSpec")
302
 
303
  @pytest.mark.parametrize("name", [
304
  "register_metric", "get_metric", "all_metrics",
305
  "select_metrics", "compute_at_junction",
306
  ])
307
  def test_function_exists(self, name):
308
- _assert_function("picarones.core.metric_registry", name)
309
 
310
  def test_register_metric_keyword_only(self):
311
  """``register_metric`` est exclusivement keyword-only sur ``name``,
312
  ``input_types`` etc. — décorateur factory."""
313
- from picarones.core.metric_registry import register_metric
314
  sig = inspect.signature(register_metric)
315
  for name in ["name", "input_types", "description"]:
316
  assert name in sig.parameters, (
@@ -319,7 +319,7 @@ class TestMetricRegistryApi:
319
 
320
 
321
  # ──────────────────────────────────────────────────────────────────────────
322
- # 8. picarones.core.metric_hooks — profils + registre de hooks
323
  # ──────────────────────────────────────────────────────────────────────────
324
 
325
 
@@ -330,14 +330,14 @@ class TestMetricHooksApi:
330
  "PROFILE_FULL",
331
  ])
332
  def test_profile_constant_exists(self, profile_name):
333
- from picarones.core import metric_hooks
334
  assert hasattr(metric_hooks, profile_name), (
335
  f"Profil {profile_name} disparu"
336
  )
337
  assert isinstance(getattr(metric_hooks, profile_name), str)
338
 
339
  def test_known_profiles_set(self):
340
- from picarones.core.metric_hooks import KNOWN_PROFILES
341
 
342
  assert isinstance(KNOWN_PROFILES, frozenset)
343
  # Les 7 profils contractuels
@@ -347,7 +347,7 @@ class TestMetricHooksApi:
347
  "DocumentMetricHook", "CorpusMetricAggregator",
348
  ])
349
  def test_class_exists(self, name):
350
- _assert_class("picarones.core.metric_hooks", name)
351
 
352
  @pytest.mark.parametrize("name", [
353
  "validate_profile",
@@ -356,7 +356,7 @@ class TestMetricHooksApi:
356
  "run_document_hooks", "run_corpus_aggregators",
357
  ])
358
  def test_function_exists(self, name):
359
- _assert_function("picarones.core.metric_hooks", name)
360
 
361
 
362
  # ──────────────────────────────────────────────────────────────────────────
@@ -502,8 +502,8 @@ class TestApiStableDoc:
502
  "picarones.measurements.pipeline_benchmark",
503
  "picarones.measurements.pipeline_comparison",
504
  "picarones.measurements.pipeline_spec_loader",
505
- "picarones.core.metric_registry",
506
- "picarones.core.metric_hooks",
507
  "picarones.measurements.builtin_metrics",
508
  "picarones.measurements.alto_metrics",
509
  "picarones.web.jobs",
 
292
 
293
 
294
  # ──────────────────────────────────────────────────────────────────────────
295
+ # 7. picarones.evaluation.metric_registry — registre typé (canonique)
296
  # ──────────────────────────────────────────────────────────────────────────
297
 
298
 
299
  class TestMetricRegistryApi:
300
  def test_metric_spec_class(self):
301
+ _assert_class("picarones.evaluation.metric_registry", "MetricSpec")
302
 
303
  @pytest.mark.parametrize("name", [
304
  "register_metric", "get_metric", "all_metrics",
305
  "select_metrics", "compute_at_junction",
306
  ])
307
  def test_function_exists(self, name):
308
+ _assert_function("picarones.evaluation.metric_registry", name)
309
 
310
  def test_register_metric_keyword_only(self):
311
  """``register_metric`` est exclusivement keyword-only sur ``name``,
312
  ``input_types`` etc. — décorateur factory."""
313
+ from picarones.evaluation.metric_registry import register_metric
314
  sig = inspect.signature(register_metric)
315
  for name in ["name", "input_types", "description"]:
316
  assert name in sig.parameters, (
 
319
 
320
 
321
  # ──────────────────────────────────────────────────────────────────────────
322
+ # 8. picarones.evaluation.metric_hooks — profils + registre de hooks (canonique)
323
  # ──────────────────────────────────────────────────────────────────────────
324
 
325
 
 
330
  "PROFILE_FULL",
331
  ])
332
  def test_profile_constant_exists(self, profile_name):
333
+ from picarones.evaluation import metric_hooks
334
  assert hasattr(metric_hooks, profile_name), (
335
  f"Profil {profile_name} disparu"
336
  )
337
  assert isinstance(getattr(metric_hooks, profile_name), str)
338
 
339
  def test_known_profiles_set(self):
340
+ from picarones.evaluation.metric_hooks import KNOWN_PROFILES
341
 
342
  assert isinstance(KNOWN_PROFILES, frozenset)
343
  # Les 7 profils contractuels
 
347
  "DocumentMetricHook", "CorpusMetricAggregator",
348
  ])
349
  def test_class_exists(self, name):
350
+ _assert_class("picarones.evaluation.metric_hooks", name)
351
 
352
  @pytest.mark.parametrize("name", [
353
  "validate_profile",
 
356
  "run_document_hooks", "run_corpus_aggregators",
357
  ])
358
  def test_function_exists(self, name):
359
+ _assert_function("picarones.evaluation.metric_hooks", name)
360
 
361
 
362
  # ──────────────────────────────────────────────────────────────────────────
 
502
  "picarones.measurements.pipeline_benchmark",
503
  "picarones.measurements.pipeline_comparison",
504
  "picarones.measurements.pipeline_spec_loader",
505
+ "picarones.evaluation.metric_registry",
506
+ "picarones.evaluation.metric_hooks",
507
  "picarones.measurements.builtin_metrics",
508
  "picarones.measurements.alto_metrics",
509
  "picarones.web.jobs",
tests/core/test_sprint34_metric_registry.py CHANGED
@@ -19,7 +19,7 @@ from __future__ import annotations
19
 
20
  import pytest
21
 
22
- from picarones.core.metric_registry import (
23
  MetricSpec,
24
  all_metrics,
25
  compute_at_junction,
@@ -126,7 +126,7 @@ class TestComputeAtJunction:
126
  assert "cer" in out
127
  finally:
128
  # Nettoyage manuel — pas d'API publique, on écrit dans le dict.
129
- from picarones.core.metric_registry import _METRIC_REGISTRY
130
 
131
  _METRIC_REGISTRY.pop("_test_always_raises", None)
132
 
@@ -146,7 +146,7 @@ class TestComputeAtJunction:
146
  skip_on_error=False,
147
  )
148
  finally:
149
- from picarones.core.metric_registry import _METRIC_REGISTRY
150
 
151
  _METRIC_REGISTRY.pop("_test_propagates", None)
152
 
@@ -211,7 +211,7 @@ class TestRegistrationGuards:
211
  def _second(ref: str, hyp: str) -> float:
212
  return 1.0
213
  finally:
214
- from picarones.core.metric_registry import _METRIC_REGISTRY
215
 
216
  _METRIC_REGISTRY.pop("_test_duplicate", None)
217
 
@@ -232,7 +232,7 @@ class TestRegistrationGuards:
232
  input_types=(ArtifactType.TEXT, ArtifactType.TEXT),
233
  )(_func)
234
 
235
- from picarones.core.metric_registry import _METRIC_REGISTRY
236
 
237
  _METRIC_REGISTRY.pop("_test_idempotent", None)
238
 
 
19
 
20
  import pytest
21
 
22
+ from picarones.evaluation.metric_registry import (
23
  MetricSpec,
24
  all_metrics,
25
  compute_at_junction,
 
126
  assert "cer" in out
127
  finally:
128
  # Nettoyage manuel — pas d'API publique, on écrit dans le dict.
129
+ from picarones.evaluation.metric_registry import _METRIC_REGISTRY
130
 
131
  _METRIC_REGISTRY.pop("_test_always_raises", None)
132
 
 
146
  skip_on_error=False,
147
  )
148
  finally:
149
+ from picarones.evaluation.metric_registry import _METRIC_REGISTRY
150
 
151
  _METRIC_REGISTRY.pop("_test_propagates", None)
152
 
 
211
  def _second(ref: str, hyp: str) -> float:
212
  return 1.0
213
  finally:
214
+ from picarones.evaluation.metric_registry import _METRIC_REGISTRY
215
 
216
  _METRIC_REGISTRY.pop("_test_duplicate", None)
217
 
 
232
  input_types=(ArtifactType.TEXT, ArtifactType.TEXT),
233
  )(_func)
234
 
235
+ from picarones.evaluation.metric_registry import _METRIC_REGISTRY
236
 
237
  _METRIC_REGISTRY.pop("_test_idempotent", None)
238
 
tests/core/test_sprint_a14_s1_compact_optin.py CHANGED
@@ -19,7 +19,7 @@ suppression des analyses via ``drop_analyses=True``.
19
 
20
  from __future__ import annotations
21
 
22
- from picarones.core.metrics import MetricsResult
23
  from picarones.core.results import DocumentResult
24
 
25
 
 
19
 
20
  from __future__ import annotations
21
 
22
+ from picarones.evaluation.metric_result import MetricsResult
23
  from picarones.core.results import DocumentResult
24
 
25
 
tests/core/test_sprint_a14_s1_metrics_error_returns_none.py CHANGED
@@ -19,7 +19,7 @@ from __future__ import annotations
19
  from unittest import mock
20
 
21
 
22
- from picarones.core.metrics import MetricsResult, aggregate_metrics
23
  from picarones.measurements import metrics as metrics_module
24
  from picarones.measurements.metrics import compute_metrics
25
 
 
19
  from unittest import mock
20
 
21
 
22
+ from picarones.evaluation.metric_result import MetricsResult, aggregate_metrics
23
  from picarones.measurements import metrics as metrics_module
24
  from picarones.measurements.metrics import compute_metrics
25
 
tests/evaluation/test_sprint_a14_s5_registry.py CHANGED
@@ -232,8 +232,8 @@ class TestCompute:
232
 
233
  class TestNoGlobalSingleton:
234
  def test_two_registries_are_independent(self) -> None:
235
- """Différence cruciale avec l'ancien
236
- ``picarones.core.metric_registry`` qui a un dict global :
237
  deux ``MetricRegistry()`` ne se partagent rien."""
238
  reg_a = MetricRegistry()
239
  reg_b = MetricRegistry()
 
232
 
233
  class TestNoGlobalSingleton:
234
  def test_two_registries_are_independent(self) -> None:
235
+ """Différence cruciale avec
236
+ ``picarones.evaluation.metric_registry`` qui a un dict global :
237
  deux ``MetricRegistry()`` ne se partagent rien."""
238
  reg_a = MetricRegistry()
239
  reg_b = MetricRegistry()
tests/integration/test_alto_baseline.py CHANGED
@@ -27,7 +27,7 @@ from picarones.measurements.alto_metrics import (
27
  extract_text_from_alto,
28
  )
29
  from picarones.core.corpus import AltoGT, Document, GTLevel, TextGT
30
- from picarones.core.metric_registry import compute_at_junction, select_metrics
31
  from picarones.domain.artifacts import ArtifactType
32
  from picarones.domain.module_protocol import BaseModule
33
  from picarones.evaluation.pipeline import (
 
27
  extract_text_from_alto,
28
  )
29
  from picarones.core.corpus import AltoGT, Document, GTLevel, TextGT
30
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
31
  from picarones.domain.artifacts import ArtifactType
32
  from picarones.domain.module_protocol import BaseModule
33
  from picarones.evaluation.pipeline import (
tests/integration/test_pipeline_ocr_to_alto.py CHANGED
@@ -32,7 +32,7 @@ from typing import Any
32
  import pytest
33
 
34
  from picarones.core.corpus import AltoGT, Document, GTLevel, TextGT
35
- from picarones.core.metric_registry import select_metrics
36
  from picarones.domain.artifacts import ArtifactType
37
  from picarones.domain.module_protocol import BaseModule
38
  from picarones.evaluation.pipeline import (
 
32
  import pytest
33
 
34
  from picarones.core.corpus import AltoGT, Document, GTLevel, TextGT
35
+ from picarones.evaluation.metric_registry import select_metrics
36
  from picarones.domain.artifacts import ArtifactType
37
  from picarones.domain.module_protocol import BaseModule
38
  from picarones.evaluation.pipeline import (
tests/measurements/test_sprint38_ner_metrics.py CHANGED
@@ -31,7 +31,7 @@ from __future__ import annotations
31
 
32
  import pytest
33
 
34
- from picarones.core.metric_registry import compute_at_junction, select_metrics
35
  from picarones.domain.artifacts import ArtifactType
36
  from picarones.measurements.ner import Entity, compute_ner_metrics, ner_f1
37
 
 
31
 
32
  import pytest
33
 
34
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
35
  from picarones.domain.artifacts import ArtifactType
36
  from picarones.measurements.ner import Entity, compute_ner_metrics, ner_f1
37
 
tests/measurements/test_sprint52_readability.py CHANGED
@@ -28,7 +28,7 @@ from __future__ import annotations
28
 
29
  import pytest
30
 
31
- from picarones.core.metric_registry import select_metrics
32
  from picarones.domain.artifacts import ArtifactType
33
  from picarones.measurements.readability import (
34
  count_sentences,
@@ -236,7 +236,7 @@ class TestRegistryIntegration:
236
  assert "flesch_delta_en" in names
237
 
238
  def test_registered_function_returns_same_as_direct_call(self) -> None:
239
- from picarones.core.metric_registry import compute_at_junction
240
 
241
  gt = "Je vous envoie cette missive afin de vous informer."
242
  ocr = "Je vous écris une lettre. Voici la situation."
 
28
 
29
  import pytest
30
 
31
+ from picarones.evaluation.metric_registry import select_metrics
32
  from picarones.domain.artifacts import ArtifactType
33
  from picarones.measurements.readability import (
34
  count_sentences,
 
236
  assert "flesch_delta_en" in names
237
 
238
  def test_registered_function_returns_same_as_direct_call(self) -> None:
239
+ from picarones.evaluation.metric_registry import compute_at_junction
240
 
241
  gt = "Je vous envoie cette missive afin de vous informer."
242
  ocr = "Je vous écris une lettre. Voici la situation."
tests/measurements/test_sprint53_reading_order.py CHANGED
@@ -26,7 +26,7 @@ from __future__ import annotations
26
 
27
  import pytest
28
 
29
- from picarones.core.metric_registry import compute_at_junction, select_metrics
30
  from picarones.domain.artifacts import ArtifactType
31
  from picarones.measurements.reading_order import (
32
  compute_reading_order_metrics,
 
26
 
27
  import pytest
28
 
29
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
30
  from picarones.domain.artifacts import ArtifactType
31
  from picarones.measurements.reading_order import (
32
  compute_reading_order_metrics,
tests/measurements/test_sprint55_unicode_blocks.py CHANGED
@@ -23,7 +23,7 @@ from __future__ import annotations
23
 
24
  import pytest
25
 
26
- from picarones.core.metric_registry import compute_at_junction, select_metrics
27
  from picarones.domain.artifacts import ArtifactType
28
  from picarones.measurements.unicode_blocks import (
29
  compute_unicode_block_accuracy,
 
23
 
24
  import pytest
25
 
26
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
27
  from picarones.domain.artifacts import ArtifactType
28
  from picarones.measurements.unicode_blocks import (
29
  compute_unicode_block_accuracy,
tests/measurements/test_sprint56_abbreviations.py CHANGED
@@ -34,7 +34,7 @@ from picarones.measurements.abbreviations import (
34
  compute_abbreviation_metrics,
35
  detect_abbreviations,
36
  )
37
- from picarones.core.metric_registry import compute_at_junction, select_metrics
38
  from picarones.domain.artifacts import ArtifactType
39
 
40
 
 
34
  compute_abbreviation_metrics,
35
  detect_abbreviations,
36
  )
37
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
38
  from picarones.domain.artifacts import ArtifactType
39
 
40
 
tests/measurements/test_sprint57_mufi.py CHANGED
@@ -31,7 +31,7 @@ from __future__ import annotations
31
 
32
  import pytest
33
 
34
- from picarones.core.metric_registry import compute_at_junction, select_metrics
35
  from picarones.domain.artifacts import ArtifactType
36
  from picarones.measurements.mufi import (
37
  compute_mufi_coverage,
 
31
 
32
  import pytest
33
 
34
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
35
  from picarones.domain.artifacts import ArtifactType
36
  from picarones.measurements.mufi import (
37
  compute_mufi_coverage,
tests/measurements/test_sprint58_early_modern.py CHANGED
@@ -38,7 +38,7 @@ from picarones.measurements.early_modern_typography import (
38
  early_modern_preservation,
39
  get_category,
40
  )
41
- from picarones.core.metric_registry import compute_at_junction, select_metrics
42
  from picarones.domain.artifacts import ArtifactType
43
 
44
 
 
38
  early_modern_preservation,
39
  get_category,
40
  )
41
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
42
  from picarones.domain.artifacts import ArtifactType
43
 
44
 
tests/measurements/test_sprint59_modern_archives.py CHANGED
@@ -35,7 +35,7 @@ from __future__ import annotations
35
 
36
  import pytest
37
 
38
- from picarones.core.metric_registry import compute_at_junction, select_metrics
39
  from picarones.measurements.modern_archives import (
40
  ADDRESS,
41
  ADMINISTRATIVE,
 
35
 
36
  import pytest
37
 
38
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
39
  from picarones.measurements.modern_archives import (
40
  ADDRESS,
41
  ADMINISTRATIVE,
tests/measurements/test_sprint60_roman_numerals.py CHANGED
@@ -21,7 +21,7 @@ from __future__ import annotations
21
 
22
  import pytest
23
 
24
- from picarones.core.metric_registry import compute_at_junction, select_metrics
25
  from picarones.domain.artifacts import ArtifactType
26
  from picarones.evaluation.metrics.roman_numerals import (
27
  ALL_STATUSES,
 
21
 
22
  import pytest
23
 
24
+ from picarones.evaluation.metric_registry import compute_at_junction, select_metrics
25
  from picarones.domain.artifacts import ArtifactType
26
  from picarones.evaluation.metrics.roman_numerals import (
27
  ALL_STATUSES,
tests/measurements/test_sprint84_searchability.py CHANGED
@@ -179,7 +179,7 @@ class TestRealisticCase:
179
 
180
  class TestRegistry:
181
  def test_metric_registered(self) -> None:
182
- from picarones.core.metric_registry import select_metrics
183
  from picarones.domain.artifacts import ArtifactType
184
 
185
  metrics = select_metrics(
@@ -198,7 +198,7 @@ class TestRegistry:
198
  assert v == 0.0
199
 
200
  def test_metric_via_compute_at_junction(self) -> None:
201
- from picarones.core.metric_registry import compute_at_junction
202
  from picarones.domain.artifacts import ArtifactType
203
 
204
  results = compute_at_junction(
 
179
 
180
  class TestRegistry:
181
  def test_metric_registered(self) -> None:
182
+ from picarones.evaluation.metric_registry import select_metrics
183
  from picarones.domain.artifacts import ArtifactType
184
 
185
  metrics = select_metrics(
 
198
  assert v == 0.0
199
 
200
  def test_metric_via_compute_at_junction(self) -> None:
201
+ from picarones.evaluation.metric_registry import compute_at_junction
202
  from picarones.domain.artifacts import ArtifactType
203
 
204
  results = compute_at_junction(
tests/measurements/test_sprint85_numerical_sequences.py CHANGED
@@ -223,7 +223,7 @@ class TestRealistic:
223
 
224
  class TestRegistry:
225
  def test_strict_and_value_metrics_registered(self) -> None:
226
- from picarones.core.metric_registry import select_metrics
227
  from picarones.domain.artifacts import ArtifactType
228
 
229
  metrics = select_metrics((ArtifactType.TEXT, ArtifactType.TEXT))
@@ -243,7 +243,7 @@ class TestRegistry:
243
  assert value == 1.0
244
 
245
  def test_metric_via_compute_at_junction(self) -> None:
246
- from picarones.core.metric_registry import compute_at_junction
247
  from picarones.domain.artifacts import ArtifactType
248
 
249
  results = compute_at_junction(
 
223
 
224
  class TestRegistry:
225
  def test_strict_and_value_metrics_registered(self) -> None:
226
+ from picarones.evaluation.metric_registry import select_metrics
227
  from picarones.domain.artifacts import ArtifactType
228
 
229
  metrics = select_metrics((ArtifactType.TEXT, ArtifactType.TEXT))
 
243
  assert value == 1.0
244
 
245
  def test_metric_via_compute_at_junction(self) -> None:
246
+ from picarones.evaluation.metric_registry import compute_at_junction
247
  from picarones.domain.artifacts import ArtifactType
248
 
249
  results = compute_at_junction(