Claude commited on
Commit
6d4b563
·
unverified ·
1 Parent(s): eca43d9

docs: mettre à jour CLAUDE.md, api-stable.md, supprimer architecture-cercles.md

Browse files

- ``CLAUDE.md`` : section Architecture réécrite pour refléter la
structure 3 cercles. Ancien tableau "chantiers post-Sprint 97"
remplacé par un manifeste concis qui pointe vers
``docs/architecture.md``. Section "Moteur narratif" mise à jour
pour refléter la nouvelle location (``measurements/narrative/`` +
``core/facts.py``). Compteurs de tests actualisés.
- ``docs/api-stable.md`` : noms des modules mis à jour pour refléter
les nouveaux chemins (``picarones.measurements.metrics``,
``picarones.measurements.runner``, ``picarones.core.pipeline``,
``picarones.measurements.pipeline_benchmark``,
``picarones.measurements.pipeline_comparison``,
``picarones.measurements.pipeline_spec_loader``,
``picarones.measurements.builtin_metrics``,
``picarones.measurements.alto_metrics``,
``picarones.web.jobs``, ``picarones.core.facts``).
- ``docs/architecture-cercles.md`` supprimé : remplacé par
``docs/architecture.md`` (plus à jour, plus précis).
- ``picarones/extras/__init__.py`` et
``picarones/measurements/__init__.py`` : référence à
``architecture.md`` au lieu de l'ancien ``architecture-cercles.md``.
- ``tests/test_phaseC_migration.py`` supprimé (validait la migration
intermédiaire).

https://claude.ai/code/session_01Hsd7kL8yeCbXn1mA7GQK9L

CLAUDE.md CHANGED
@@ -6,121 +6,89 @@ HuggingFace Space : huggingface.co/spaces/Ma-Ri-Ba-Ku/Picarones (Docker, port 78
6
 
7
  ---
8
 
9
- ## Lecture rapide chantiers post-Sprint 97
10
 
11
- 5 chantiers ont consolidé l'architecture sans suppression :
12
 
13
- | # | Livré | Effet | Doc |
14
- |---|---|---|---|
15
- | 1 | `TextToAltoMonoRegion` + refonte `BaseOCREngine` | Valide l'axe B bout-en-bout (BaseModule réel TEXT→ALTO + 5 engines factorisés) | [`docs/architecture.md`](docs/architecture.md) |
16
- | 2 | Profils + registre de hooks | `runner.py` allégé de 303 lignes ; 7 profils pour moduler le calcul ; `--profile` ajouté | [`docs/profiles.md`](docs/profiles.md) |
17
- | 3 | 5 vues HTML thématiques | 16 renderers orphelins regroupés en `economics`, `advanced_taxonomy`, `diagnostics`, `pipeline`, `robustness` | [`docs/views.md`](docs/views.md) |
18
- | 4 | Workflows CLI + LLM Sprint 15 + Gallica/IIIF | 3 commandes `diagnose`/`economics`/`edition` ; Sprint 15 propagé aux 4 LLM ; `_http.py` factorisé | [`docs/cli-workflows.md`](docs/cli-workflows.md) |
19
- | 5 | Découpage monolithes | `cli.py` 1519L → 7 fichiers ; `narrative/detectors.py` 1229L → 8 fichiers (6 familles + helpers) | [`docs/architecture.md`](docs/architecture.md) |
20
-
21
- Branche : `claude/code-quality-audit-ACnhK`. Voir [`CHANGELOG.md`](CHANGELOG.md)
22
- section `[post-Sprint 97]` pour le détail.
23
 
24
- **18 détecteurs narratifs** (et non « 12 » comme annoncé dans les sprints
25
- historiques)voir
26
- [`picarones/core/narrative/detectors/`](picarones/core/narrative/detectors/).
27
 
28
  ---
29
 
30
  ## Setup
31
 
32
  ```bash
33
- pip install -e ".[dev,web]" # IMPORTANT : toujours inclure [web] pour les tests
34
  pytest tests/ -q --tb=short # lancer les tests
35
  picarones demo --output rapport.html # rapport démo sans moteur installé
36
  picarones serve --port 8080 # interface web locale
37
  ```
38
 
39
- Mise à jour Codespace complète :
40
- ```bash
41
- git pull && pip install -e ".[dev,web]" && picarones demo --output rapport_demo.html && picarones serve --port 8080
42
- ```
43
-
44
  ---
45
 
46
- ## Architecture
47
 
48
  ```
49
  picarones/
50
- ├── cli/ # (chantier 5) Package CLI Click — 7 fichiers
51
- │ ├── __init__.py # Groupe `cli` + helpers + commandes simples
52
- │ ├── _workflows.py # run, diagnose, economics, edition, compare
53
- │ ├── _pipeline.py # pipeline run + compare
54
- │ ├── _imports.py, _serve.py, _history.py, _robustness.py
55
- ├── fixtures.py # Données de test fictives (documents médiévaux)
56
- ├── modules/ # (chantier 1) Modules BaseModule de référence
57
- │ └── alto_text_to_mono_region.py # Reconstructeur ALTO baseline
58
- ├── core/
59
- ├── corpus.py # Chargement corpus (dossier local, ALTO XML, PAGE XML)
60
- │ ├── metrics.py # CER, WER, MER, WIL (via jiwer)
61
- │ ├── normalization.py # Profils : nfc, caseless, minimal, medieval_french, early_modern_french,
62
- │ # medieval_latin, early_modern_english, medieval_english
63
- │ ├── statistics.py # Bootstrap CI 95%, Wilcoxon (scipy optionnel), corrélations
64
- │ ├── runner.py # Orchestrateur benchmark (ThreadPool IO-bound, ProcessPool CPU-bound)
65
- │ ├── results.py # Modèles de données DocumentResult, BenchmarkResults + export JSON
66
- ── confusion.py # Matrice de confusion unicode
67
- ├── char_scores.py # Scores ligatures (fi, fl, œ, æ, ꝑ…) et diacritiques
68
- ├── taxonomy.py # Taxonomie erreurs 9 classes (confusion visuelle, abréviation…)
69
- ├── structure.py # Analyse structurelle (blocs, lignes, mots)
70
- ├── image_quality.py # Métriques qualité image (contraste, bruit, résolution…)
71
- │ ├── difficulty.py # Score difficulté intrinsèque par document
72
- │ ├── hallucination.py # Détection hallucinations VLM (score ancrage, ratio longueur)
73
- │ ├── line_metrics.py # Distribution erreurs par ligne (Gini, percentiles)
74
- ├── history.py # Suivi longitudinal SQLite
75
- ├── robustness.py # Analyse robustesse (bruit, flou, rotation, résolution)
76
- ── narrative/ # Moteur narratif factuel (Sprint 16) — modèle Fact + registre
77
- ── facts.py # Fact, FactType (12 types), FactImportance, DetectorRegistry
78
- └── detectors.py # Stubs des 12 détecteurs, implémentations par sprint
79
- ├── engines/
80
- │ ├── base.py # BaseEngine avec execution_mode ("io" ou "cpu")
81
- ├── tesseract.py # execution_mode = "cpu"
82
- ├── pero_ocr.py # execution_mode = "cpu"
83
- ── mistral_ocr.py # endpoint /v1/ocr dédié (pas chat/completions)
84
- ├── google_vision.py
85
- │ └── azure_doc_intel.py
86
- ── llm/
87
- ├── base.py
88
- ├── mistral_adapter.py
89
- │ ├── openai_adapter.py
90
- │ ├── anthropic_adapter.py
91
- ── ollama_adapter.py
92
- ── pipelines/
93
- ├── base.py # OCRLLMPipeline (interface BaseOCREngine)
94
- │ └── over_normalization.py
95
- ├── prompts/ # 8 fichiers .txt FR+EN
96
- ├── medieval_french.txt
97
- ├── medieval_french_zero_shot.txt
98
- │ ├── early_modern_french.txt
99
- │ ├── early_modern_french_zero_shot.txt
100
- │ ├── medieval_english.txt
101
- │ ├── early_modern_english.txt
102
- │ ├── medieval_latin.txt
103
- │ └── zero_shot.txt
104
- ├── report/
105
- │ ├── generator.py # Orchestration Jinja2 (617 lignes depuis Sprint 17)
106
- │ ├── diff_utils.py
107
- │ ├── templates/ # Templates Jinja2 (Sprint 17)
108
- │ │ ├── base.html.j2 # assemble tout via {% include %}
109
- │ │ ├── _header.html, _footer.html, _styles.css, _app.js
110
- │ │ └── view_ranking.html, view_gallery.html, view_document.html,
111
- │ │ view_analyses.html, view_characters.html
112
- │ ├── i18n/ # Traductions FR/EN (Sprint 17 — extraites de i18n.py)
113
- │ │ ├── fr.json
114
- │ │ └── en.json
115
- │ └── vendor/ # Chart.js vendorisé
116
- ├── web/
117
- │ └── app.py # FastAPI, SSE, upload corpus ZIP, endpoints modèles dynamiques
118
- └── importers/
119
- ├── iiif.py
120
- ├── htr_united.py
121
- ├── huggingface.py
122
- ├── gallica.py
123
- └── escriptorium.py
124
  ```
125
 
126
  ---
@@ -160,7 +128,7 @@ correspondants (`test_sprint15_llm_pipeline_bugs.py`, `test_sprint8_escriptorium
160
  CI, Makefile et invocation directe produisent le même résultat. Le job
161
  `lint` du CI est bloquant — un F401 (import inutilisé) ou un E741
162
  (variable ambiguë) fait échouer la PR, par design.
163
- - **Les profils de normalisation** sont dans `picarones/core/normalization.py` — l'endpoint
164
  `/api/normalization/profiles` doit les lire dynamiquement depuis ce fichier, pas depuis une
165
  liste statique.
166
 
@@ -299,58 +267,52 @@ AZURE_DOC_INTEL_KEY=...
299
 
300
  ---
301
 
302
- ## Moteur narratif (Sprint 16)
303
 
304
- Fondations en place dans `picarones/core/narrative/` :
 
 
 
305
 
306
  ```
307
- core/narrative/
308
- ├── __init__.py # API publique + pipeline build_synthesis
309
- ├── facts.py # Modèle Fact, FactType (12 types), FactImportance, DetectorRegistry
310
- ├── detectors.py # 10 détecteurs implémentés (Sprint 19) + 2 stubs (Sprint 5)
311
- ├── arbiter.py # Tri par importance, non-redondance, anti-contradiction
312
- ├── renderer.py # Rendu templates YAML par str.format_map (déterministe)
313
- └── templates/
314
- ├── fr.yaml # 10 templates français
315
- └── en.yaml # 10 templates anglais
 
 
 
 
 
 
 
 
316
  ```
317
 
318
- **Principe anti-hallucination** : chaque valeur numérique ou nom d'entité dans le
319
- `payload` d'un `Fact` doit provenir du JSON d'entrée. Test `test_sprint19_narrative_engine.py`
320
- parse la synthèse rendue et vérifie que chaque nombre est traçable au payload
321
- (via `_numbers_in_payload`) augmenté d'une liste blanche limitative de constantes
322
- de template (`95`, `100`).
323
-
324
- **Détecteurs activés dans le registre par défaut (Sprint 20)** — les 12 sont opérationnels :
325
- - Sprint 3 : `statistical_tie`
326
- - Sprint 4 : `global_leader_cer`, `significant_gap`, `stratum_winner`, `stratum_collapse`,
327
- `error_profile_outlier`, `llm_hallucination_flag`, `robustness_fragile`,
328
- `speed_winner`, `confidence_warning`
329
- - Sprint 5 : `pareto_alternative`, `cost_outlier`
330
 
331
- **Règle anti-contradiction** (arbitre) : si `SIGNIFICANT_GAP` (Wilcoxon non corrigé)
332
- et `STATISTICAL_TIE` (Nemenyi corrigé) concernent les mêmes moteurs, Nemenyi
333
- l'emporte — on ne veut pas dire en même temps "A bat B significativement" ET
334
- "A et B sont indiscernables".
335
 
336
- **Pipeline** : `build_synthesis(benchmark_data, lang, max_facts=5)` détecte,
337
- arbitre, rend. Le `ReportGenerator.generate` l'appelle et passe le résultat
338
- au template `_narrative_summary.html` (placé entre `_header.html` et `_critical_difference.html`).
339
 
340
  ---
341
 
342
  ## Contexte développement
343
 
344
- - **Environnement** : GitHub Codespaces (`/workspaces/Picarones`), Python 3.12
345
- - **Tests** : 3098+ passed, 2 skipped (cf. CHANGELOG section `[post-Sprint 97]`
346
- pour les nouveaux tests des chantiers 1-5 qui ajoutent ~1500 lignes
347
- de validation : `test_alto_baseline.py`, `test_metric_hooks.py`,
348
- `test_views.py`, `test_chantier4.py`, `test_chantier5.py`).
349
  - **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
350
- - **Documentation post-chantiers** : [`docs/architecture.md`](docs/architecture.md),
351
- [`docs/profiles.md`](docs/profiles.md), [`docs/cli-workflows.md`](docs/cli-workflows.md),
352
- [`docs/views.md`](docs/views.md).
353
  - **Branche active** : `claude/code-quality-audit-ACnhK`.
354
- - **Détecteurs narratifs** : 18 (et non 12 comme indiqué historiquement),
355
- organisés en 6 familles dans
356
- [`picarones/core/narrative/detectors/`](picarones/core/narrative/detectors/).
 
6
 
7
  ---
8
 
9
+ ## Architecture en 3 cercles
10
 
11
+ Voir le manifeste complet dans [`docs/architecture.md`](docs/architecture.md).
12
 
13
+ ```
14
+ Cercle 3 (extras, report, cli, web)
15
+
16
+
17
+ Cercle 2 (measurements, engines, llm, pipelines, modules)
18
+
19
+
20
+ Cercle 1 (core)
21
+ ```
 
22
 
23
+ Règle de dépendance stricte : les imports vont uniquement de l'extérieur
24
+ vers l'intérieur. **Aucun shim** un module a un seul emplacement.
 
25
 
26
  ---
27
 
28
  ## Setup
29
 
30
  ```bash
31
+ pip install -e ".[dev,web]" # toujours inclure [web] pour les tests
32
  pytest tests/ -q --tb=short # lancer les tests
33
  picarones demo --output rapport.html # rapport démo sans moteur installé
34
  picarones serve --port 8080 # interface web locale
35
  ```
36
 
 
 
 
 
 
37
  ---
38
 
39
+ ## Structure
40
 
41
  ```
42
  picarones/
43
+ ├── core/ Cercle 1 abstractions pures (7 modules)
44
+ │ ├── modules.py BaseModule, ArtifactType
45
+ │ ├── corpus.py Document, Corpus, GTLevel, payloads typés
46
+ │ ├── results.py DocumentResult, EngineReport, BenchmarkResult
47
+ │ ├── metric_registry.py MetricSpec, register_metric, compute_at_junction
48
+ ├── metric_hooks.py register_document_metric, register_corpus_aggregator
49
+ ├── pipeline.py PipelineRunner, PipelineSpec, PipelineStep
50
+ │ └── facts.py Fact, FactType, FactImportance, DetectorRegistry
51
+
52
+ ├── measurements/ Cercle 2 métriques officielles (~55 modules)
53
+ │ ├── runner.py run_benchmark (orchestration)
54
+ │ ├── metrics.py / statistics.py / normalization.py / builtin_hooks.py
55
+ ├── confusion.py / taxonomy.py / calibration.py / line_metrics.py / ...
56
+ │ ├── readability.py / reliability.py / searchability.py / ner.py / ...
57
+ │ ├── mufi.py / abbreviations.py / unicode_blocks.py / roman_numerals.py
58
+ │ ├── pipeline_benchmark.py / pipeline_comparison.py / pipeline_spec_loader.py
59
+ ── narrative/ moteur narratif (arbiter, renderer, registry,
60
+ 18 détecteurs en 6 familles : ranking, pareto,
61
+ stratum, quality, history, ensemble)
62
+
63
+ ├── engines/ Cercle 2 adapters OCR (5)
64
+ │ ├── base.py BaseOCREngine (hérite de BaseModule)
65
+ │ ├── tesseract.py / pero_ocr.py
66
+ │ ├── mistral_ocr.py / google_vision.py / azure_doc_intel.py
67
+
68
+ ├── llm/ Cercle 2 adapters LLM (4)
69
+ ── base.py / mistral_adapter.py / openai_adapter.py
70
+ ── anthropic_adapter.py / ollama_adapter.py
71
+
72
+ ├── pipelines/ Cercle 2 — pipelines OCR+LLM intégrés
73
+ │ ├── base.py (OCRLLMPipeline) / over_normalization.py
74
+
75
+ ├── modules/ Cercle 2 modules BaseModule officiels
76
+ ── alto_text_to_mono_region.py
77
+
78
+ ── extras/ Cercle 3 — plugins / extensions
79
+ │ └── importers/ IIIF, Gallica, HTR-United, HuggingFace, eScriptorium
80
+
81
+ ├── report/ Cercle 3 — rendu HTML
82
+ │ ├── generator.py / colors.py / diff_utils.py
83
+ │ ├── views/ 5 vues thématiques
84
+ ── templates/ / i18n/ / glossary/ / vendor/
85
+ │ └── *_render.py ~22 renderers (calibration, NER, Pareto, etc.)
86
+
87
+ ── cli/ Cercle 3 — Click (7 fichiers)
88
+ ├── web/ Cercle 3 FastAPI (app.py, jobs.py)
89
+ ├── prompts/ 8 fichiers .txt FR+EN
90
+ ├── data/ Tables indicatives (pricing.yaml)
91
+ ── fixtures.py Corpus de test fictifs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  ```
93
 
94
  ---
 
128
  CI, Makefile et invocation directe produisent le même résultat. Le job
129
  `lint` du CI est bloquant — un F401 (import inutilisé) ou un E741
130
  (variable ambiguë) fait échouer la PR, par design.
131
+ - **Les profils de normalisation** sont dans `picarones/measurements/normalization.py` — l'endpoint
132
  `/api/normalization/profiles` doit les lire dynamiquement depuis ce fichier, pas depuis une
133
  liste statique.
134
 
 
267
 
268
  ---
269
 
270
+ ## Moteur narratif
271
 
272
+ Le modèle de données (`Fact`, `FactType`, `FactImportance`,
273
+ `DetectorRegistry`) vit en cercle 1 dans
274
+ [`picarones/core/facts.py`](picarones/core/facts.py). Les détecteurs et
275
+ le rendu vivent en cercle 2 :
276
 
277
  ```
278
+ picarones/measurements/narrative/
279
+ ├── __init__.py API publique + pipeline build_synthesis
280
+ ├── arbiter.py Tri par importance, non-redondance, anti-contradiction
281
+ ├── renderer.py Rendu templates YAML par str.format_map (déterministe)
282
+ ├── registry.py Registre par défaut des détecteurs
283
+ ├── templates/{fr,en}.yaml 18 templates × 2 langues
284
+ └── detectors/ 18 détecteurs en 6 familles
285
+ ├── ranking.py 5 (global_leader, statistical_tie, significant_gap,
286
+ │ speed_winner, median_mean_gap_warning)
287
+ ├── pareto.py 2 (pareto_alternative, cost_outlier)
288
+ ├── stratum.py 3 (stratum_winner, stratum_collapse,
289
+ │ stratification_recommended)
290
+ ├── quality.py 4 (error_profile_outlier, llm_hallucination_flag,
291
+ │ robustness_fragile, confidence_warning)
292
+ ├── history.py 3 (engine_off_baseline, engine_unstable,
293
+ │ regression_in_history)
294
+ └── ensemble.py 1 (ensemble_opportunity)
295
  ```
296
 
297
+ **Principe anti-hallucination** : chaque valeur numérique ou nom d'entité
298
+ dans le `payload` d'un `Fact` provient du JSON d'entrée. Le test
299
+ `test_sprint19_narrative_engine.py` parse la synthèse rendue et vérifie
300
+ la traçabilité.
 
 
 
 
 
 
 
 
301
 
302
+ **Règle anti-contradiction** (arbitre) : si `SIGNIFICANT_GAP` (Wilcoxon
303
+ non corrigé) et `STATISTICAL_TIE` (Nemenyi corrigé) concernent les mêmes
304
+ moteurs, Nemenyi l'emporte.
 
305
 
306
+ **Pipeline** : `build_synthesis(benchmark_data, lang, max_facts=5)`
307
+ détecte, arbitre, rend.
 
308
 
309
  ---
310
 
311
  ## Contexte développement
312
 
313
+ - **Environnement** : GitHub Codespaces, Python 3.11+
314
+ - **Tests** : `pytest tests/ -q` → ~3354 passed, 2 skipped, 0 failed.
 
 
 
315
  - **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
316
+ - **Manifeste architecture** : [`docs/architecture.md`](docs/architecture.md).
317
+ - **API publique stable** : [`docs/api-stable.md`](docs/api-stable.md).
 
318
  - **Branche active** : `claude/code-quality-audit-ACnhK`.
 
 
 
docs/api-stable.md CHANGED
@@ -79,7 +79,7 @@ class EngineReport: # agrégat moteur sur tout le corpus
79
  class BenchmarkResult: # résultat global multi-moteurs
80
  ```
81
 
82
- ### `picarones.core.metrics`
83
 
84
  ```python
85
  class MetricsResult: # CER, WER, MER, WIL + variantes diplomatique/caseless
@@ -87,7 +87,7 @@ def compute_metrics(reference, hypothesis, char_exclude=None) -> MetricsResult
87
  def aggregate_metrics(results: list) -> dict
88
  ```
89
 
90
- ### `picarones.core.runner`
91
 
92
  ```python
93
  def run_benchmark(
@@ -105,7 +105,7 @@ def run_benchmark(
105
  ) -> BenchmarkResult
106
  ```
107
 
108
- ### `picarones.core.pipeline_runner`
109
 
110
  ```python
111
  class PipelineStep:
@@ -115,7 +115,7 @@ class PipelineResult:
115
  class PipelineRunner:
116
  ```
117
 
118
- ### `picarones.core.pipeline_benchmark`
119
 
120
  ```python
121
  class StepAggregate:
@@ -125,7 +125,7 @@ def default_initial_inputs(doc) -> dict
125
  def run_pipeline_benchmark(spec, corpus, factory=...) -> PipelineBenchmarkResult
126
  ```
127
 
128
- ### `picarones.core.pipeline_comparison`
129
 
130
  ```python
131
  class PipelineComparisonResult:
@@ -133,7 +133,7 @@ class PipelineComparisonResult:
133
  def compare_pipelines(specs, corpus, factories=None) -> PipelineComparisonResult
134
  ```
135
 
136
- ### `picarones.core.pipeline_spec_loader`
137
 
138
  ```python
139
  class PipelineSpecLoadError(ValueError):
@@ -183,7 +183,7 @@ def run_document_hooks(profile, *, ground_truth, hypothesis, image_path, corpus_
183
  def run_corpus_aggregators(profile, document_results) -> dict
184
  ```
185
 
186
- ### `picarones.core.builtin_metrics`
187
 
188
  Métriques scalaires natives, enregistrées dans le registre typé :
189
 
@@ -197,7 +197,7 @@ def wil(reference, hypothesis) -> float
197
  def text_preservation_after_reconstruction(reference_text, hypothesis_alto) -> float
198
  ```
199
 
200
- ### `picarones.core.alto_metrics`
201
 
202
  Métriques (ALTO, ALTO) + helper :
203
 
@@ -210,7 +210,7 @@ def alto_text_mer(reference_alto, hypothesis_alto) -> float
210
  def alto_text_wil(reference_alto, hypothesis_alto) -> float
211
  ```
212
 
213
- ### `picarones.core.jobs`
214
 
215
  Persistance des jobs benchmark (utilisé par l'interface web) :
216
 
@@ -241,7 +241,7 @@ def reset_default_store(...)
241
  reflètent ces changements.
242
  - **Modules `picarones.extras/`** : statut variable selon le
243
  sous-package (academic / governance / historical / importers).
244
- Voir `docs/architecture-cercles.md`.
245
  - **Comportement des renderers HTML** : la structure des fichiers HTML
246
  peut évoluer entre versions mineures. Nous gardons les noms des
247
  vues principales.
@@ -268,15 +268,15 @@ version mineure si une RFC le justifie.
268
 
269
  ```python
270
  # Mesures (déplacées vers picarones.measurements/)
271
- from picarones.core.confusion import build_confusion_matrix
272
- from picarones.core.taxonomy import classify_errors
273
- from picarones.core.calibration import compute_calibration_metrics
274
  # ... ~40 modules métriques ...
275
 
276
  # Moteur narratif (déplacé vers picarones.measurements.narrative/)
277
- from picarones.core.narrative import build_synthesis
278
- from picarones.core.narrative.facts import Fact
279
- from picarones.core.narrative.detectors import detect_global_leader_cer
280
 
281
  # Plugins (déplacés vers picarones.extras/)
282
  from picarones.core.taxonomy_intra_doc import compute_taxonomy_position_heatmap
@@ -296,7 +296,7 @@ Pour les **nouvelles** intégrations, préférer les chemins canoniques :
296
 
297
  ## Voir aussi
298
 
299
- - [`docs/architecture-cercles.md`](architecture-cercles.md) — cartographie
300
  des 3 cercles + critères d'assignation.
301
  - [`docs/architecture.md`](architecture.md) — vue d'ensemble post-chantiers.
302
  - [`tests/test_public_api.py`](../tests/test_public_api.py) — test
 
79
  class BenchmarkResult: # résultat global multi-moteurs
80
  ```
81
 
82
+ ### `picarones.measurements.metrics`
83
 
84
  ```python
85
  class MetricsResult: # CER, WER, MER, WIL + variantes diplomatique/caseless
 
87
  def aggregate_metrics(results: list) -> dict
88
  ```
89
 
90
+ ### `picarones.measurements.runner`
91
 
92
  ```python
93
  def run_benchmark(
 
105
  ) -> BenchmarkResult
106
  ```
107
 
108
+ ### `picarones.core.pipeline`
109
 
110
  ```python
111
  class PipelineStep:
 
115
  class PipelineRunner:
116
  ```
117
 
118
+ ### `picarones.measurements.pipeline_benchmark`
119
 
120
  ```python
121
  class StepAggregate:
 
125
  def run_pipeline_benchmark(spec, corpus, factory=...) -> PipelineBenchmarkResult
126
  ```
127
 
128
+ ### `picarones.measurements.pipeline_comparison`
129
 
130
  ```python
131
  class PipelineComparisonResult:
 
133
  def compare_pipelines(specs, corpus, factories=None) -> PipelineComparisonResult
134
  ```
135
 
136
+ ### `picarones.measurements.pipeline_spec_loader`
137
 
138
  ```python
139
  class PipelineSpecLoadError(ValueError):
 
183
  def run_corpus_aggregators(profile, document_results) -> dict
184
  ```
185
 
186
+ ### `picarones.measurements.builtin_metrics`
187
 
188
  Métriques scalaires natives, enregistrées dans le registre typé :
189
 
 
197
  def text_preservation_after_reconstruction(reference_text, hypothesis_alto) -> float
198
  ```
199
 
200
+ ### `picarones.measurements.alto_metrics`
201
 
202
  Métriques (ALTO, ALTO) + helper :
203
 
 
210
  def alto_text_wil(reference_alto, hypothesis_alto) -> float
211
  ```
212
 
213
+ ### `picarones.web.jobs`
214
 
215
  Persistance des jobs benchmark (utilisé par l'interface web) :
216
 
 
241
  reflètent ces changements.
242
  - **Modules `picarones.extras/`** : statut variable selon le
243
  sous-package (academic / governance / historical / importers).
244
+ Voir `docs/architecture.md`.
245
  - **Comportement des renderers HTML** : la structure des fichiers HTML
246
  peut évoluer entre versions mineures. Nous gardons les noms des
247
  vues principales.
 
268
 
269
  ```python
270
  # Mesures (déplacées vers picarones.measurements/)
271
+ from picarones.measurements.confusion import build_confusion_matrix
272
+ from picarones.measurements.taxonomy import classify_errors
273
+ from picarones.measurements.calibration import compute_calibration_metrics
274
  # ... ~40 modules métriques ...
275
 
276
  # Moteur narratif (déplacé vers picarones.measurements.narrative/)
277
+ from picarones.measurements.narrative import build_synthesis
278
+ from picarones.core.facts import Fact, FactType, FactImportance
279
+ from picarones.measurements.narrative.detectors import detect_global_leader_cer
280
 
281
  # Plugins (déplacés vers picarones.extras/)
282
  from picarones.core.taxonomy_intra_doc import compute_taxonomy_position_heatmap
 
296
 
297
  ## Voir aussi
298
 
299
+ - [`docs/architecture.md`](architecture.md) — cartographie
300
  des 3 cercles + critères d'assignation.
301
  - [`docs/architecture.md`](architecture.md) — vue d'ensemble post-chantiers.
302
  - [`tests/test_public_api.py`](../tests/test_public_api.py) — test
docs/architecture-cercles.md DELETED
@@ -1,229 +0,0 @@
1
- # Architecture en 3 cercles — chantier de refonte post-chantier 6
2
-
3
- Ce document **fige la cartographie** de chaque module Picarones dans son
4
- cercle d'appartenance. Il sert de référence stable pour les
5
- contributions futures : avant d'ajouter un module, consulter ce
6
- document pour identifier dans quel cercle il doit aller.
7
-
8
- ## Principe — 3 cercles concentriques
9
-
10
- ```
11
- ┌─────────────────────────────────────────────────────────────┐
12
- │ Cercle 3 — Plugins (extras/) │
13
- │ ┌─────────────────────────────────────────────────────┐ │
14
- │ │ Cercle 2 — Modules officiels │ │
15
- │ │ ┌──────────────────────────────────────────┐ │ │
16
- │ │ │ Cercle 1 — Noyau invariant (core/) │ │ │
17
- │ │ │ API publique stable, ~15 modules │ │ │
18
- │ │ └──────────────────────────────────────────┘ │ │
19
- │ │ Adapters, mesures, rapport, CLI, web │ │
20
- │ │ ~30 modules métriques + ~15 adapters/UI │ │
21
- │ └─────────────────────────────────────────────────────┘ │
22
- │ Modules niche, gouvernance préventive, importers exotiques │
23
- │ Distribués via extras pip ou packages séparés à terme │
24
- └─────────────────────────────────────────────────────────────┘
25
- ```
26
-
27
- Plus on s'éloigne du cœur, plus c'est optionnel et plus c'est facile
28
- à supprimer/remplacer/externaliser.
29
-
30
- ## Cercle 1 — Noyau invariant
31
-
32
- **Critères** : ce qui définit *ce qu'est* Picarones. API publique
33
- stable. Ne casse pas entre versions mineures.
34
-
35
- **Localisation** : `picarones/core/` (après phase E) — strictement
36
- ~15 modules.
37
-
38
- **Contenu** :
39
-
40
- | Module | Rôle |
41
- |---|---|
42
- | `corpus.py` | Document, Corpus, GTLevel multi-niveaux |
43
- | `modules.py` | BaseModule, ArtifactType (contrat unique pour modules tiers) |
44
- | `results.py` | BenchmarkResult, EngineReport, DocumentResult |
45
- | `metrics.py` | CER/WER/MER/WIL via jiwer (métriques de base) |
46
- | `runner.py` | Orchestrateur (parallélisation, reprise, timeout) |
47
- | `pipeline_runner.py` | Banc d'essai mono-doc des pipelines composées |
48
- | `pipeline_benchmark.py` | Orchestration corpus-wide |
49
- | `pipeline_comparison.py` | Comparaison de N pipelines |
50
- | `pipeline_spec_loader.py` | Chargement YAML déclaratif |
51
- | `metric_registry.py` | Registre typé `(input_type, output_type) → metric` |
52
- | `metric_hooks.py` | Profils + registre de hooks document/corpus |
53
- | `builtin_metrics.py` | CER/WER/MER/WIL enregistrés sur registre typé |
54
- | `alto_metrics.py` | Métriques `(ALTO, ALTO)` (chantier 1) |
55
-
56
- **Discipline** :
57
- - Toute modification non rétrocompatible exige une **RFC** et bump majeur.
58
- - Test `test_public_api.py` (à créer en phase D) qui échoue si un nom disparaît.
59
- - Aucun import direct depuis `extras/` ou de modules optionnels.
60
-
61
- ## Cercle 2 — Modules officiels
62
-
63
- **Critères** : maintenu par les mainteneurs Picarones, livré par
64
- défaut, mais peut techniquement vivre ailleurs (un fork peut le
65
- remplacer par un équivalent).
66
-
67
- **Localisation** :
68
- - `picarones/measurements/` (après phase E) — métriques au-delà du CER de base.
69
- - `picarones/engines/` — adapters OCR.
70
- - `picarones/llm/` — adapters LLM.
71
- - `picarones/modules/` — modules `BaseModule` de référence (chantier 1).
72
- - `picarones/report/` — génération HTML.
73
- - `picarones/cli/` — interface CLI.
74
- - `picarones/web/` — interface web FastAPI.
75
- - `picarones/pipelines/` — pipelines OCR+LLM legacy (à statuer en phase D).
76
-
77
- **Métriques officielles** (futur `picarones/measurements/`) :
78
-
79
- | Catégorie | Modules |
80
- |---|---|
81
- | Texte | `confusion`, `char_scores`, `taxonomy`, `structure`, `taxonomy_comparison` |
82
- | Lignes | `line_metrics`, `hallucination` |
83
- | Fiabilité | `calibration`, `reliability`, `robustness`, `robustness_projection` |
84
- | Structure ALTO/PAGE | `reading_order`, `layout`, `error_absorption` |
85
- | Recherche | `searchability`, `numerical_sequences`, `rare_tokens` |
86
- | Lisibilité | `readability` (Flesch), `specialization` |
87
- | Inter-moteurs | `inter_engine`, `worst_lines` |
88
- | Économie | `throughput`, `cost_projection`, `marginal_cost`, `pricing` |
89
- | Comparaison | `incremental_comparison` |
90
- | Narrative | `narrative/` (engine + 6 familles de détecteurs) |
91
- | Hooks | `builtin_hooks` |
92
- | Contexte corpus | `history`, `difficulty`, `image_quality`, `normalization` |
93
- | Statistiques | `statistics` |
94
- | Levers | `levers` |
95
-
96
- **Discipline** :
97
- - Modification libre sans RFC.
98
- - Nouveau module doit s'enregistrer via `@register_metric` ou
99
- `@register_document_metric` plutôt qu'imports directs depuis `runner.py`.
100
- - Couvre les 4 axes du produit : viabilité prod, hallucinations VLM,
101
- pipelines composées, projection coût/vitesse.
102
-
103
- ## Cercle 3 — Plugins
104
-
105
- **Critères** : ne sert pas tout le monde, peut être désactivé sans
106
- amputer le produit principal.
107
-
108
- **Localisation** : `picarones/extras/` (sous-package interne pour
109
- l'instant ; packages PyPI séparés possibles à terme).
110
-
111
- **Sous-packages** :
112
-
113
- ### `extras/academic/` — modules techniques sans cas d'usage prod
114
-
115
- | Module | Pourquoi en plugin |
116
- |---|---|
117
- | `taxonomy_intra_doc.py` | Heatmap classe×position. Question rare, peu actionnable |
118
- | `taxonomy_cooccurrence.py` | Jaccard inter-classes. Académique, info rare |
119
- | `image_predictive.py` | Score combiné avec poids éditoriaux arbitraires |
120
-
121
- ### `extras/governance/` — gouvernance préventive
122
-
123
- | Module | Pourquoi en plugin |
124
- |---|---|
125
- | `module_policy.py` | Manifest + audit pour modules contribués externes. Inutile tant qu'il n'y a pas 5+ modules tiers réels |
126
-
127
- ### `extras/historical/` — métriques philologiques (phase B)
128
-
129
- | Module | Public spécifique |
130
- |---|---|
131
- | `unicode_blocks.py` | Tous périodes |
132
- | `abbreviations.py` | Médiéval (Capelli) |
133
- | `mufi.py` | Médiéval (PUA) |
134
- | `early_modern_typography.py` | XVIᵉ-XVIIIᵉ siècles |
135
- | `modern_archives.py` | XIXᵉ-XXᵉ siècles |
136
- | `roman_numerals.py` | Toutes périodes |
137
- | `lexical_modernization.py` | Édition critique |
138
- | `philological_runner.py` | Orchestration des 6 modules ci-dessus |
139
-
140
- ### `extras/importers/` — imports externes (phase C)
141
-
142
- | Module | Statut |
143
- |---|---|
144
- | `_http.py` | Helpers HTTP partagés (chantier 4) |
145
- | `iiif.py` | Maintenu |
146
- | `htr_united.py` | Maintenu |
147
- | `gallica.py` | Maintenu |
148
- | `huggingface.py` | Expérimental (à finir ou marqué unstable) |
149
- | `escriptorium.py` | Expérimental (à finir ou marqué unstable) |
150
-
151
- ### `extras/render/` — renderers correspondants
152
-
153
- Renderers atomiques pour les modules `extras/`. Importés
154
- conditionnellement par les vues thématiques du chantier 3 (qui sont
155
- elles-mêmes dans `report/views/`, donc Cercle 2).
156
-
157
- ## Distinguer un module Cercle 1 vs Cercle 2
158
-
159
- Critère **corrigé** (alignement architecture hexagonale / DDD) :
160
-
161
- > **Cercle 1 = abstractions et logique métier du domaine,
162
- > indépendantes de l'interface utilisateur. Stables entre versions
163
- > mineures.**
164
- >
165
- > **Cercle 2 = adapters concrets (engines, LLM, modules de référence),
166
- > couches d'interface (report, cli, web), et mesures au-delà du noyau
167
- > (measurements). Maintenus mais peuvent évoluer.**
168
-
169
- Le critère « si on supprime ce module, le produit reste viable »
170
- mélange deux questions distinctes (« est-ce indispensable ? » et
171
- « est-ce une abstraction stable ? »). On préfère le critère DDD :
172
-
173
- - **Cercle 1** : abstractions et orchestration qui définissent ce
174
- que Picarones *est* logiquement (corpus, BaseModule, registres,
175
- runner). Indépendant de l'interface utilisateur.
176
- - **Cercle 2** : ce qui rend le domaine utilisable concrètement
177
- (adapters, mesures, présentation HTML, CLI).
178
-
179
- Exemple :
180
- - `corpus.py` → Cercle 1 (abstraction du domaine).
181
- - `runner.py` → Cercle 1 (orchestration du domaine).
182
- - `confusion.py` → Cercle 2 (mesure au-delà du noyau, dans
183
- ``measurements/``).
184
- - `report/generator.py` → Cercle 2 (couche de présentation, même si
185
- essentielle à l'usage pratique).
186
- - `engines/tesseract.py` → Cercle 2 (adapter concret).
187
-
188
- > Note : la convention « `base.py` dans le dossier du concept »
189
- > (`engines/base.py`, `llm/base.py`) reste dans son dossier d'origine.
190
- > Ces contrats sont logiquement Cercle 1 (API publique stable) mais
191
- > physiquement co-localisés avec leurs implémentations, comme dans
192
- > Django, SQLAlchemy, FastAPI. Convention universelle Python.
193
- - Sans `taxonomy_intra_doc.py` : on a toujours un bench complet et
194
- utile → Cercle 3.
195
-
196
- ## Distinguer un module Cercle 2 vs Cercle 3
197
-
198
- Test concret : ce module sert-il à répondre à la question
199
- *« peut-on déployer ce moteur en prod sur ce corpus dans nos
200
- contraintes ? »* — soit en mesurant un risque (hallucinations,
201
- stabilité), soit en projetant un coût (throughput, pricing), soit
202
- en évaluant la qualité (CER, calibration, structure) ?
203
-
204
- - **Oui** → Cercle 2.
205
- - **Non** → Cercle 3.
206
-
207
- Exemple :
208
- - `hallucination.py` : mesure un risque pour la prod VLM → Cercle 2.
209
- - `throughput.py` : projette un coût opérationnel → Cercle 2.
210
- - `taxonomy_intra_doc.py` : décrit une distribution sans implication
211
- de décision → Cercle 3.
212
-
213
- ## Disclaimer
214
-
215
- Cette cartographie est **une décision produit**, pas une vérité
216
- absolue. Elle peut évoluer si les usages réels d'institutions
217
- révèlent qu'un module Cercle 3 est en fait essentiel, ou
218
- inversement.
219
-
220
- Toute remise en cause doit passer par une RFC documentée, pas par
221
- une PR silencieuse.
222
-
223
- ## Voir aussi
224
-
225
- - [`docs/architecture.md`](architecture.md) — vue d'ensemble post-chantiers 1-6.
226
- - [`docs/profiles.md`](profiles.md) — profils de calcul (chantier 2).
227
- - [`docs/views.md`](views.md) — vues HTML du rapport.
228
- - [`docs/cli-workflows.md`](cli-workflows.md) — commandes CLI.
229
- - `docs/api-stable.md` — *à créer en phase D* — engagement API publique du Cercle 1.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
picarones/extras/__init__.py CHANGED
@@ -15,9 +15,9 @@ Convention de rétrocompat
15
  Pour chaque module déplacé depuis ``picarones/core/`` ou
16
  ``picarones/report/`` vers ``picarones/extras/``, un fichier-shim est
17
  laissé à l'ancien emplacement qui réexporte les noms publics. Les
18
- imports historiques (``from picarones.core.taxonomy_intra_doc import
19
  ...``) continuent à fonctionner sans modification.
20
 
21
- Voir :doc:`docs/architecture-cercles.md` pour la cartographie complète
22
  et les critères d'assignation au Cercle 3.
23
  """
 
15
  Pour chaque module déplacé depuis ``picarones/core/`` ou
16
  ``picarones/report/`` vers ``picarones/extras/``, un fichier-shim est
17
  laissé à l'ancien emplacement qui réexporte les noms publics. Les
18
+ imports historiques (``from picarones.measurements.taxonomy_intra_doc import
19
  ...``) continuent à fonctionner sans modification.
20
 
21
+ Voir :doc:`docs/architecture.md` pour la cartographie complète
22
  et les critères d'assignation au Cercle 3.
23
  """
picarones/measurements/__init__.py CHANGED
@@ -82,6 +82,6 @@ Tous les modules historiquement dans ``picarones.core.X`` restent
82
  accessibles via des fichiers-shims qui les redirigent vers le nouvel
83
  emplacement. Aucun import existant ne casse.
84
 
85
- Voir :doc:`docs/architecture-cercles.md` et la phase E du plan de
86
  refonte.
87
  """
 
82
  accessibles via des fichiers-shims qui les redirigent vers le nouvel
83
  emplacement. Aucun import existant ne casse.
84
 
85
+ Voir :doc:`docs/architecture.md` et la phase E du plan de
86
  refonte.
87
  """
tests/test_phaseC_migration.py DELETED
@@ -1,229 +0,0 @@
1
- """Tests de la phase C — extras/importers/ (importers vers Cercle 3).
2
-
3
- Couvre :
4
-
5
- - 6 importers (``_http``, ``iiif``, ``htr_united``, ``gallica``,
6
- ``huggingface``, ``escriptorium``) déplacés vers
7
- ``picarones/extras/importers/``.
8
- - Identité préservée à travers les shims.
9
- - ``huggingface`` et ``escriptorium`` émettent un ``UserWarning``
10
- ``experimental`` à l'import.
11
- - ``picarones.importers/__init__.py`` continue à réexporter les
12
- noms historiques.
13
- - ``cli/_imports.py`` continue à fonctionner.
14
- - pyproject.toml déclare ``[importers]``.
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import importlib
20
- import sys
21
- import warnings
22
- from pathlib import Path
23
-
24
- import pytest
25
-
26
-
27
- # ──────────────────────────────────────────────────────────────────────────
28
- # 1. Imports historiques rétrocompat via shims
29
- # ──────────────────────────────────────────────────────────────────────────
30
-
31
-
32
- class TestImportersRetrocompat:
33
- @pytest.mark.parametrize("module_path, attribute", [
34
- ("picarones.importers.iiif", "IIIFImporter"),
35
- ("picarones.importers.iiif", "import_iiif_manifest"),
36
- ("picarones.importers.htr_united", "HTRUnitedEntry"),
37
- ("picarones.importers.htr_united", "HTRUnitedCatalogue"),
38
- ("picarones.importers.htr_united", "import_htr_united_corpus"),
39
- ("picarones.importers.gallica", "GallicaClient"),
40
- ("picarones.importers.gallica", "GallicaRecord"),
41
- ("picarones.importers.gallica", "search_gallica"),
42
- ("picarones.importers.gallica", "import_gallica_document"),
43
- ("picarones.importers._http", "validate_http_url"),
44
- ("picarones.importers._http", "download_url"),
45
- ])
46
- def test_legacy_path_works(self, module_path: str, attribute: str):
47
- with warnings.catch_warnings():
48
- warnings.simplefilter("ignore")
49
- mod = importlib.import_module(module_path)
50
- assert hasattr(mod, attribute)
51
-
52
-
53
- # ──────────────────────────────────────────────────────────────────────────
54
- # 2. Imports via le nouveau chemin extras/importers/
55
- # ──────────────────────────────────────────────────────────────────────────
56
-
57
-
58
- class TestExtrasImportersPath:
59
- @pytest.mark.parametrize("new_path, attribute", [
60
- ("picarones.extras.importers._http", "validate_http_url"),
61
- ("picarones.extras.importers._http", "download_url"),
62
- ("picarones.extras.importers.iiif", "IIIFImporter"),
63
- ("picarones.extras.importers.iiif", "import_iiif_manifest"),
64
- ("picarones.extras.importers.htr_united", "HTRUnitedCatalogue"),
65
- ("picarones.extras.importers.gallica", "GallicaClient"),
66
- ("picarones.extras.importers.huggingface", "HuggingFaceImporter"),
67
- ("picarones.extras.importers.escriptorium", "EScriptoriumClient"),
68
- ])
69
- def test_extras_path_works(self, new_path: str, attribute: str):
70
- with warnings.catch_warnings():
71
- warnings.simplefilter("ignore")
72
- mod = importlib.import_module(new_path)
73
- assert hasattr(mod, attribute)
74
-
75
-
76
- # ──────────────────────────────────────────────────────────────────────────
77
- # 3. Identité préservée
78
- # ──────────────────────────────────────────────────────────────────────────
79
-
80
-
81
- class TestIdentityThroughShim:
82
- def test_iiif_identity(self):
83
- with warnings.catch_warnings():
84
- warnings.simplefilter("ignore")
85
- from picarones.extras.importers.iiif import IIIFImporter as via_new
86
- from picarones.importers.iiif import IIIFImporter as via_old
87
- assert via_old is via_new
88
-
89
- def test_gallica_identity(self):
90
- with warnings.catch_warnings():
91
- warnings.simplefilter("ignore")
92
- from picarones.extras.importers.gallica import GallicaClient as via_new
93
- from picarones.importers.gallica import GallicaClient as via_old
94
- assert via_old is via_new
95
-
96
- def test_http_helpers_identity(self):
97
- with warnings.catch_warnings():
98
- warnings.simplefilter("ignore")
99
- from picarones.extras.importers._http import (
100
- validate_http_url as via_new,
101
- )
102
- from picarones.importers._http import (
103
- validate_http_url as via_old,
104
- )
105
- assert via_old is via_new
106
-
107
-
108
- # ──────────────────────────────────────────────────────────────────────────
109
- # 4. Modules expérimentaux : UserWarning à l'import
110
- # ──────────────────────────────────────────────────────────────────────────
111
-
112
-
113
- def _force_reimport(module_name_substring: str) -> None:
114
- """Vide le cache d'import pour pouvoir capturer le UserWarning."""
115
- for name in list(sys.modules.keys()):
116
- if module_name_substring in name:
117
- del sys.modules[name]
118
-
119
-
120
- class TestExperimentalImporters:
121
- def test_huggingface_emits_userwarning(self):
122
- _force_reimport("huggingface")
123
- with warnings.catch_warnings(record=True) as w:
124
- warnings.simplefilter("always")
125
- import picarones.extras.importers.huggingface # noqa: F401
126
- msgs = [str(x.message) for x in w if issubclass(x.category, UserWarning)]
127
- assert any("experimental" in m for m in msgs), (
128
- f"huggingface n'a pas émis de UserWarning experimental — "
129
- f"warnings reçus : {[str(x.message) for x in w]}"
130
- )
131
-
132
- def test_escriptorium_emits_userwarning(self):
133
- _force_reimport("escriptorium")
134
- with warnings.catch_warnings(record=True) as w:
135
- warnings.simplefilter("always")
136
- import picarones.extras.importers.escriptorium # noqa: F401
137
- msgs = [str(x.message) for x in w if issubclass(x.category, UserWarning)]
138
- assert any("experimental" in m for m in msgs)
139
-
140
- def test_iiif_does_not_emit_warning(self):
141
- """Les importers maintenus ne doivent PAS émettre de warning."""
142
- _force_reimport("iiif")
143
- with warnings.catch_warnings(record=True) as w:
144
- warnings.simplefilter("always")
145
- import picarones.extras.importers.iiif # noqa: F401
146
- msgs = [str(x.message) for x in w if issubclass(x.category, UserWarning)]
147
- # Il peut y avoir d'autres warnings (deprecation Python, etc.)
148
- # mais pas de "experimental" sur iiif
149
- assert not any(
150
- "iiif" in m and "experimental" in m for m in msgs
151
- ), "iiif ne doit pas être marqué experimental"
152
-
153
-
154
- # ──────────────────────────────────────────────────────────────────────────
155
- # 5. picarones.importers/__init__.py — réexports historiques
156
- # ──────────────────────────────────────────────────────────────────────────
157
-
158
-
159
- class TestImportersInitReexports:
160
- def test_reexports_work(self):
161
- """Le ``__init__`` réexporte des symboles via les shims, eux-mêmes
162
- chargeant depuis extras."""
163
- with warnings.catch_warnings():
164
- warnings.simplefilter("ignore")
165
- from picarones.importers import (
166
- EScriptoriumClient,
167
- GallicaClient,
168
- IIIFImporter,
169
- )
170
- assert IIIFImporter is not None
171
- assert GallicaClient is not None
172
- assert EScriptoriumClient is not None
173
-
174
-
175
- # ──────────────────────────────────────────────────────────────────────────
176
- # 6. cli/_imports.py — toujours fonctionnel
177
- # ──────────────────────────────────────────────────────────────────────────
178
-
179
-
180
- class TestCliImportsCommand:
181
- def test_cli_imports_module_loads(self):
182
- """``picarones.cli._imports`` importe IIIFImporter depuis
183
- ``picarones.importers.iiif`` — doit fonctionner via shim."""
184
- try:
185
- with warnings.catch_warnings():
186
- warnings.simplefilter("ignore")
187
- import picarones.cli._imports # noqa: F401
188
- except ImportError as exc:
189
- if "click" in str(exc):
190
- pytest.skip("click absent")
191
- raise
192
-
193
-
194
- # ──────────────────────────────────────────────────────────────────────────
195
- # 7. pyproject.toml — extra [importers]
196
- # ─────────────────────────────────────────────────────���────────────────────
197
-
198
-
199
- class TestPyprojectExtra:
200
- def test_importers_extra_declared(self):
201
- path = Path(__file__).parent.parent / "pyproject.toml"
202
- content = path.read_text(encoding="utf-8")
203
- assert "importers = []" in content or 'importers = [' in content
204
- assert "extras/importers" in content
205
- assert "Cercle 3" in content
206
-
207
-
208
- # ──────────────────────────────────────────────────────────────────────────
209
- # 8. Originaux sont des shims minces
210
- # ──────────────────────────────────────────────────────────────────────────
211
-
212
-
213
- class TestOriginalsAreShims:
214
- @pytest.mark.parametrize("path", [
215
- "picarones/importers/_http.py",
216
- "picarones/importers/iiif.py",
217
- "picarones/importers/htr_united.py",
218
- "picarones/importers/gallica.py",
219
- "picarones/importers/huggingface.py",
220
- "picarones/importers/escriptorium.py",
221
- ])
222
- def test_is_thin_shim(self, path):
223
- repo_root = Path(__file__).parent.parent
224
- content = (repo_root / path).read_text(encoding="utf-8")
225
- n_lines = len([line for line in content.splitlines() if line.strip()])
226
- assert n_lines < 30, (
227
- f"{path} fait {n_lines} lignes — devrait être un shim mince"
228
- )
229
- assert "déplacé" in content or "extras" in content