Claude commited on
Commit
1b4c2d1
·
unverified ·
1 Parent(s): a2bea75

docs(s2): Sprint A14-S2 — recadrer le discours, garde-fou install minimal

Browse files

Sprint S2 du plan rewrite ciblé (rewrite-2026, étape 0 :
stabilisation de l'existant avant la migration de structure).

S2 livre trois choses : (1) un README qui dit la vérité sur l'état
du projet, (2) un BACKLOG_POST_LIVRAISON.md qui sert de garde-fou
contre la dérive de scope pendant les 24 sprints à venir, (3) un
test ``test_minimal_install.py`` qui empêche un nouvel ``import
foo`` au top-level d'introduire silencieusement une dépendance
non déclarée.

README — alignement avec la réalité
-----------------------------------
- Tagline : "benchmarking platform" → "comparison tool". Le
premier mot promettait une infrastructure que le code ne livre
pas encore (pas de couche service, web non confinée, runner
encore couplé au rapport).
- Section "Honest status (May 2026)" qui liste explicitement les
promesses non tenues : RGPD draft, gouvernance/COI documentés
mais non exercés, CITATION/JOSS/DOI planifiés non livrés,
WCAG/pentest scopés non audités. Le bloc précédent affirmait
ces items "complete as of May 2026".
- Section Roadmap pointe désormais vers
``docs/roadmap/rewrite-2026.md`` qui décrit le plan S1–S26 avec
le calendrier.
- Citation : retire la mention "Sprint A12" et pointe vers
BACKLOG_POST_LIVRAISON.md.
- Test count baseline : 3871 → ~3900 (couvre les 51 nouveaux
tests S1).

BACKLOG_POST_LIVRAISON.md — discipline du rewrite
------------------------------------------------
Document de référence pour matérialiser la règle "à chaque doute
pendant le sprint en cours, l'item va ici et le sprint continue".
Catégories :
1. Promesses retirées du README (JOSS/DOI, RGPD, gouvernance,
accessibilité, pentest).
2. Features attendues mais reportées (reprise hashée,
backpressure, cancellation propre, ZIP arborescence, GT
detection patterns, Views, app/services/, suppression imports
magiques, nettoyage Sprint X).
3. Idées spéculatives à valider après livraison.
4. Convention d'usage.

docs/roadmap/rewrite-2026.md — plan S1–S26
------------------------------------------
Synthèse du plan complet livré dans la session de cadrage : les 4
phases, les 26 sprints, les critères go/no-go entre phases, les
4 invariants permanents (main reste livrable, pas de feature
nouvelle, fin de sprint = suite verte, livrable démontrable en 5
min). Référencé depuis le README.

tests/test_minimal_install.py — garde-fou install
-------------------------------------------------
6 tests qui verrouillent deux invariants :
- Tous les noms de l'API publique (28 symboles) sont importables
via ``from picarones import X``.
- Tout package externe chargé à ``import picarones`` doit être
déclaré dans ``[project.dependencies]`` du pyproject.toml. En
cas de désynchronisation (ex : on ajoute ``import foo`` quelque
part mais on oublie ``foo`` dans pyproject), le test échoue.
- Réciproquement, toute dep déclarée comme obligatoire doit être
installable.
- Les modules engines optionnels doivent s'importer en mode
dégradé (warning + fallback) plutôt que de lever ImportError au
top-level.

État de la suite
----------------
``pytest tests/ -q`` → 3920 passed, 3 skipped, 2 failed.
Les 2 fails restants sont purement environnementaux (sous-process
pytest sans ``pip install -e .``) et seront couverts par la CI :
* test_readme_test_count_matches_baseline
* test_readme_tables_consistent_with_code
Aucune régression S2.

À noter : l'inventaire des deps a montré que ``defusedxml`` était
déjà déclaré dans ``[project.dependencies]`` depuis A1. Le crash
``ModuleNotFoundError: defusedxml`` observé dans l'audit initial
était dû à un venv de dev incomplet, pas à pyproject.toml. Le
nouveau ``test_top_level_externals_are_declared`` empêche la
régression dans l'autre sens.

https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP

BACKLOG_POST_LIVRAISON.md ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backlog post-livraison
2
+
3
+ > **Garde-fou de discipline du rewrite ciblé** (cf. `docs/roadmap/rewrite-2026.md`).
4
+ >
5
+ > Tout ce qui apparaît ici est **explicitement hors scope** des sprints
6
+ > S1–S26. Ces items pourront revenir dans le scope après la livraison à
7
+ > la BnF, pas avant.
8
+ >
9
+ > La règle d'or : "à chaque doute pendant le sprint en cours, l'item va
10
+ > ici et le sprint continue."
11
+
12
+ ---
13
+
14
+ ## 1. Promesses retirées du README
15
+
16
+ Items historiquement présentés comme acquis et qui ne sont en réalité
17
+ pas tenus au niveau qui justifierait leur affirmation publique.
18
+
19
+ ### 1.1 Scientific publication track
20
+
21
+ - `CITATION.cff` au format Citation File Format 1.2.
22
+ - DOI Zenodo (snapshot release).
23
+ - Soumission JOSS (Journal of Open Source Software) avec article
24
+ technique.
25
+ - BibTeX généré automatiquement par release.
26
+
27
+ **Pourquoi retiré du README pour l'instant** : la posture éditoriale
28
+ sera difficile à tenir tant que le rewrite ciblé n'est pas livré et
29
+ qu'on ne peut pas pointer vers une version 2.0 stable.
30
+
31
+ **Quand revoir** : après S26.
32
+
33
+ ### 1.2 Conformité RGPD opérationnelle
34
+
35
+ - Audit DPO interne ou externe.
36
+ - Registre des traitements documenté.
37
+ - Politique de rétention enforced (pas seulement documentée).
38
+ - Mécanisme d'exercice des droits (export, suppression).
39
+
40
+ **État actuel** : `docs/operations/data-retention-rgpd.md` existe mais
41
+ n'a jamais été validé par un DPO ni testé sur un workflow réel BnF.
42
+
43
+ ### 1.3 Gouvernance et COI policies
44
+
45
+ - Constitution explicite du comité de pilotage.
46
+ - Politique de gestion des conflits d'intérêts exercée sur ≥ 1 PR
47
+ externe.
48
+ - Processus de release reviews documenté et appliqué.
49
+
50
+ **État actuel** : `GOVERNANCE.md` et `CONTRIBUTING.md` sont en place
51
+ comme documents de référentiel mais aucun de ces processus n'a été
52
+ exercé en pratique.
53
+
54
+ ### 1.4 Accessibilité WCAG 2.1 AA
55
+
56
+ - Audit RGAA externe.
57
+ - Tests automatisés axe-core sur la SPA.
58
+ - Navigation complète clavier validée par utilisateur empêché.
59
+
60
+ **État actuel** : `ACCESSIBILITY.md` documente l'intention. Les
61
+ améliorations Sprint 25 (extraction du JS inline vers
62
+ `web-app.js`) sont un pas dans la bonne direction mais ne suffisent
63
+ pas à revendiquer la conformité.
64
+
65
+ ### 1.5 Sécurité — pentest externe
66
+
67
+ - Pentest opérationnel sur un déploiement institutionnel (pas un
68
+ Space HF public).
69
+ - Validation de la CSP sans `'unsafe-inline'`.
70
+ - Validation de la sandbox `validated_path` / `compute_workspace_roots`
71
+ par un attaquant compétent.
72
+
73
+ **État actuel** : Sprint A14-S1 a comblé les 6 P0 connus mais
74
+ l'absence d'audit externe nous interdit d'affirmer l'absence d'autres
75
+ vecteurs.
76
+
77
+ ---
78
+
79
+ ## 2. Features attendues mais reportées
80
+
81
+ ### 2.1 Features fonctionnelles
82
+
83
+ - Reprise de benchmark hashée par contenu+config (pas seulement par
84
+ `corpus_name + engine_name`).
85
+ - Backpressure réelle dans le runner (limite de futures en vol,
86
+ timeout depuis le début d'exécution réelle).
87
+ - Annulation propre qui tue les workers OCR/LLM en cours
88
+ (actuellement `cancel_futures` ne ferme pas un Tesseract en train
89
+ de tourner).
90
+ - ZIP upload qui préserve l'arborescence (sans flatten qui écrase).
91
+ - Détection des paires `(image, GT)` qui supporte tous les patterns
92
+ réels (`.gt.alto.xml`, `.alto.xml`, `.page.xml`, etc.).
93
+
94
+ → Couverts par les Sprints S8, S9, S20 du rewrite ciblé.
95
+
96
+ ### 2.2 Vues d'évaluation explicites
97
+
98
+ - `TextView` — la vue qui projette toute sortie textuelle vers du
99
+ texte brut comparable.
100
+ - `AltoView` — fidélité documentaire ALTO/PAGE.
101
+ - `SearchView` — recherchabilité fuzzy plein-texte.
102
+ - `LayoutView` — coordonnées et ordre de lecture.
103
+ - `HallucinationView` — contrôle d'invention par le modèle.
104
+ - `CostView` — coût/temps/CO₂.
105
+
106
+ → Sprints S13–S18 du rewrite. Au minimum les 3 premières doivent
107
+ exister à la livraison BnF.
108
+
109
+ ### 2.3 Couche service applicative
110
+
111
+ - `app/services/benchmark_service.py` — orchestration séparée des
112
+ routers FastAPI.
113
+ - `app/services/path_security.py` — `WorkspaceManager` qui crée un
114
+ dossier isolé par session/run.
115
+ - Schemas DTO (Pydantic) séparés des modèles de domaine.
116
+
117
+ → Sprint S19 du rewrite.
118
+
119
+ ### 2.4 Suppression de la dette d'imports magiques
120
+
121
+ - Plus de `import picarones.measurements as _trigger_metric_registration`
122
+ dans `picarones/__init__.py`.
123
+ - Registres construits explicitement par un service au démarrage.
124
+ - Entry points Python pour les modules tiers (`picarones.metrics`,
125
+ `picarones.adapters`).
126
+
127
+ → Sprint S5 + S20 du rewrite.
128
+
129
+ ### 2.5 Suppression des références "Sprint X" dans le code
130
+
131
+ Le repo contient ~679 références à "Sprint N" dans les fichiers
132
+ Python (commentaires, docstrings, justifications de seuils
133
+ éditoriaux). C'est de la stratigraphie archéologique qui rend le
134
+ code illisible pour un nouveau contributeur.
135
+
136
+ → Nettoyage progressif au fil des Sprints S10–S22 du rewrite (à
137
+ chaque déplacement de fichier, on supprime les commentaires de
138
+ sprint qui n'apportent plus rien �� un lecteur de la version
139
+ courante). Pas un sprint dédié.
140
+
141
+ ---
142
+
143
+ ## 3. Idées qui ressortent mais qu'on ne traite pas
144
+
145
+ À valider après la livraison.
146
+
147
+ - Cache d'artefacts intermédiaires côté pipeline executor.
148
+ - Parallélisation inter-étapes au sein d'une même pipeline.
149
+ - Vue HTML drag-and-drop pour composer un pipeline (le DAG render
150
+ Sprint 95 est de l'inspection, pas de la construction).
151
+ - Score composite personnel persisté côté serveur (pour l'instant
152
+ uniquement URL state côté client).
153
+ - Plugin system PyPI pour modules contribués (`picarones-module-X`).
154
+ - Extension corpus levels au-delà de TEXT/ALTO/PAGE/ENTITIES/READING_ORDER
155
+ (par exemple : tableaux, mathématiques, partitions).
156
+
157
+ ---
158
+
159
+ ## 4. Convention d'usage de ce document
160
+
161
+ - **Ajouter** un item dès qu'on identifie une promesse / feature qui
162
+ doit attendre.
163
+ - **Ne pas retirer** un item juste parce qu'on a envie de le faire ;
164
+ attendre que le rewrite l'absorbe officiellement (auquel cas il
165
+ apparaîtra dans `docs/roadmap/rewrite-2026.md`).
166
+ - **Référencer** ce fichier dans les PRs qui retirent du scope du
167
+ README ou de la documentation utilisateur.
168
+
169
+ Dernière revue : Sprint A14-S2 (rewrite ciblé, étape 0).
README.md CHANGED
@@ -9,9 +9,17 @@ pinned: false
9
 
10
  # Picarones
11
 
12
- > **Heritage OCR / HTR / VLM and post-correction benchmarking platform**
13
  >
14
- > **Banc d'essai d'OCR / HTR / VLM et de post-correction pour documents patrimoniaux**
 
 
 
 
 
 
 
 
15
 
16
  [![CI](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml/badge.svg)](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml)
17
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
@@ -23,22 +31,25 @@ pinned: false
23
 
24
  ## What is Picarones?
25
 
26
- **Picarones** is an open-source benchmarking platform for OCR, HTR, VLM
27
- and post-correction pipelines on **heritage documents** (manuscripts,
28
  early printed books, archives).
29
 
30
  The input is a folder of `(image, ground truth)` pairs — ground truth
31
  in plain text, ALTO XML, or PAGE XML. Picarones runs the AIs you plug
32
  in (OCR engines, VLMs, OCR+LLM pipelines, ALTO mappers, ensembles…) on
33
- every page, compares each output to the ground truth at every relevant
34
- level (text, ALTO, PAGE, entities, reading order), and produces a
35
- **self-contained HTML report** with factual numbers, statistical tests
36
- and a reproducibility snapshot.
37
 
38
  **Without ground truth, no benchmark** — Picarones measures how well
39
  an AI matches a known reference, not how it transcribes an arbitrary
40
  document.
41
 
 
 
 
 
 
42
  > *Version française ci-dessous.*
43
 
44
  ### Use case
@@ -385,9 +396,12 @@ ruff check picarones/ tests/
385
  python -m mypy picarones/core/
386
  ```
387
 
388
- **Test suite**: ~3871 tests, ~3 min on a modern laptop. Coverage
389
  floor at 85% (currently ~87%). The `network` marker excludes tests
390
- requiring live HTTP.
 
 
 
391
 
392
  For end-to-end developer guides, see
393
  [`docs/developer/index.md`](docs/developer/index.md) (FR) /
@@ -415,19 +429,26 @@ Detailed history and current direction live in:
415
  one entry per sprint up to the latest release.
416
  - [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md) —
417
  technical evolution roadmap (axes A and B for 2026+).
418
- - [`docs/audits/`](docs/audits/) — institutional readiness audit
419
- and remediation plan (sprints A1A15).
420
-
421
- The **Phase 1 of the institutional readiness plan** (sprints A1–A11)
422
- is complete as of May 2026: CI hardening, doc consistency gates,
423
- 3-circle refactor, web hardening, perf+concurrency tests, WCAG 2.1
424
- AA accessibility, reproducibility ops (lock files, Docker pinning),
425
- PyPI/ghcr.io release pipeline, governance & COI policies,
426
- institutional deployment guide & RGPD documentation.
427
-
428
- Remaining: scientific publication track (CITATION + JOSS, sprint
429
- A12), README/SPECS final polish (this sprint and A14), external
430
- audits (RGAA + security pentest, A15).
 
 
 
 
 
 
 
431
 
432
  ---
433
 
@@ -451,11 +472,13 @@ The complete functional specification is in
451
 
452
  ## Citation
453
 
454
- A `CITATION.cff` file and a Zenodo DOI will land in Sprint A12
455
- (scientific publication track). Until then, cite the GitHub repo
456
- with the commit SHA used in your benchmark — every Picarones report
457
- embeds the commit and full snapshot for reproducibility (cf.
458
- [`docs/reproducibility-snapshots.md`](docs/reproducibility-snapshots.md)).
 
 
459
 
460
  ---
461
 
 
9
 
10
  # Picarones
11
 
12
+ > **Heritage OCR / HTR / VLM and post-correction benchmarking tool**
13
  >
14
+ > **Outil de comparaison d'OCR / HTR / VLM et de post-correction pour documents patrimoniaux**
15
+
16
+ **Status (May 2026)** — version 1.x, scientific prototype under
17
+ consolidation. The core (corpus, runner, metrics, HTML report) is
18
+ usable to compare transcription pipelines on a ground-truth corpus.
19
+ A targeted rewrite (see
20
+ [`docs/roadmap/rewrite-2026.md`](docs/roadmap/rewrite-2026.md))
21
+ rebuilds the orchestration layer and evaluation views for a stable
22
+ 2.0 release by the end of 2026.
23
 
24
  [![CI](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml/badge.svg)](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml)
25
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
 
31
 
32
  ## What is Picarones?
33
 
34
+ **Picarones** is an open-source comparison tool for OCR, HTR, VLM and
35
+ post-correction pipelines on **heritage documents** (manuscripts,
36
  early printed books, archives).
37
 
38
  The input is a folder of `(image, ground truth)` pairs — ground truth
39
  in plain text, ALTO XML, or PAGE XML. Picarones runs the AIs you plug
40
  in (OCR engines, VLMs, OCR+LLM pipelines, ALTO mappers, ensembles…) on
41
+ every page, compares each output to the ground truth, and produces an
42
+ HTML report with the numerical results.
 
 
43
 
44
  **Without ground truth, no benchmark** — Picarones measures how well
45
  an AI matches a known reference, not how it transcribes an arbitrary
46
  document.
47
 
48
+ > **Limits to keep in mind.** Picarones is a tool, not a verdict
49
+ > machine. CER/WER and the philological metrics measure agreement with
50
+ > a single reference; the choice of reference, normalization profile
51
+ > and metric is an editorial decision the user must own.
52
+
53
  > *Version française ci-dessous.*
54
 
55
  ### Use case
 
396
  python -m mypy picarones/core/
397
  ```
398
 
399
+ **Test suite**: ~3900 tests, ~3 min on a modern laptop. Coverage
400
  floor at 85% (currently ~87%). The `network` marker excludes tests
401
+ requiring live HTTP. A handful of tests depend on optional engines
402
+ (`pero-ocr`, `pytesseract`) and are skipped/fail gracefully when
403
+ those binaries are not installed in the local environment — the CI
404
+ matrix runs them in a fully provisioned image.
405
 
406
  For end-to-end developer guides, see
407
  [`docs/developer/index.md`](docs/developer/index.md) (FR) /
 
429
  one entry per sprint up to the latest release.
430
  - [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md) —
431
  technical evolution roadmap (axes A and B for 2026+).
432
+ - [`docs/roadmap/rewrite-2026.md`](docs/roadmap/rewrite-2026.md) —
433
+ targeted rewrite plan (S1S26) restructuring orchestration around
434
+ `Pipeline → Artifacts → Projection → EvaluationView`. Target: end of 2026.
435
+ - [`docs/audits/`](docs/audits/) internal audit notes ; [`BACKLOG_POST_LIVRAISON.md`](BACKLOG_POST_LIVRAISON.md) — promises **not** in scope.
436
+
437
+ **Honest status (May 2026).** Several items historically presented as
438
+ "institutional readiness complete" are not at the level the README
439
+ previously claimed and remain on the post-delivery backlog:
440
+
441
+ - RGPD documentation is a draft, not a validated policy.
442
+ - Governance / COI policies are documented but not exercised by an
443
+ external review.
444
+ - `CITATION.cff` + Zenodo DOI + JOSS submission are planned, not done.
445
+ - Accessibility (WCAG 2.1 AA) and security pentest are scoped but
446
+ not externally audited.
447
+
448
+ The **rewrite-2026** plan (S1–S26) prioritises stabilising the
449
+ benchmark core and the security boundary of the web layer over
450
+ adding new features. Until S26 ships, treat the web app as an
451
+ experimental demonstrator and the CLI as the supported interface.
452
 
453
  ---
454
 
 
472
 
473
  ## Citation
474
 
475
+ A `CITATION.cff` file and a Zenodo DOI are **planned**, not yet
476
+ shipped (see [`BACKLOG_POST_LIVRAISON.md`](BACKLOG_POST_LIVRAISON.md)).
477
+ Cite the GitHub repository with the commit SHA used in your benchmark.
478
+ Every Picarones report embeds the commit hash and a snapshot of the
479
+ parameters used (cf.
480
+ [`docs/reproducibility-snapshots.md`](docs/reproducibility-snapshots.md))
481
+ so the cited commit is sufficient to attribute the result.
482
 
483
  ---
484
 
docs/roadmap/rewrite-2026.md ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Rewrite ciblé — plan S1 → S26
2
+
3
+ > **Statut** — démarré au Sprint A14-S1 (mai 2026), livraison cible
4
+ > **fin 2026** sur la branche `claude/repo-analysis-cukvm` puis fusion
5
+ > sur `main` pour livraison BnF.
6
+ >
7
+ > **Doctrine** : pas de Big Rewrite. Pas non plus de migration douce
8
+ > qui laisserait la dette en place. **Rewrite ciblé** : on réécrit
9
+ > from scratch les zones cassées (~5–8 k lignes : runner d'orchestration,
10
+ > couche web sécurité, gestion d'artefacts) et on **déplace** les zones
11
+ > saines (~30–40 k lignes : calculs purs MUFI / philological /
12
+ > statistics / etc.) sans toucher à leur logique.
13
+
14
+ ---
15
+
16
+ ## Pourquoi un rewrite ciblé ?
17
+
18
+ Trois constats issus de l'audit (`docs/audits/`) et de la conversation
19
+ de cadrage de mai 2026 :
20
+
21
+ 1. **Les promesses du README dépassaient la réalité du code.** Six bugs
22
+ P0 vérifiés dans l'audit invalidaient la promesse scientifique
23
+ (notamment : `normalization_profile` côté web silencieusement
24
+ ignoré, `compact()` qui amputait le JSON exporté, `compute_metrics`
25
+ qui retournait `0.0` indistinguable d'un score parfait en cas
26
+ d'erreur).
27
+ 2. **L'architecture à imports magiques.** `import picarones`
28
+ déclenche une chaîne d'imports par effet de bord qui charge le
29
+ registre de métriques. Une dépendance optionnelle manquante au fond
30
+ de la chaîne fait crasher l'import du package entier.
31
+ 3. **La dette narrative est trop lourde.** ~679 références à
32
+ "Sprint N" dans les fichiers Python, qui parasitent la lecture du
33
+ code par un nouveau contributeur et empêchent toute prise en main
34
+ par un mainteneur extérieur.
35
+
36
+ Le rewrite ciblé attaque ces trois problèmes ensemble.
37
+
38
+ ---
39
+
40
+ ## Architecture cible
41
+
42
+ À la fin du rewrite, l'arborescence Python sera :
43
+
44
+ ```
45
+ picarones/
46
+ domain/ # Cercle 1 — types purs (Artifact, PipelineSpec,
47
+ # EvaluationSpec, DocumentRef, Provenance)
48
+ evaluation/ # Cercle 2 — vues, projecteurs, métriques
49
+ views/
50
+ projectors/
51
+ metrics/
52
+ registry.py
53
+ pipeline/ # Cercle 2 — exécution
54
+ executor.py
55
+ cache.py
56
+ spec.py
57
+ formats/ # Cercle 2 — ALTO, PAGE, normalisation texte
58
+ alto/
59
+ pagexml/
60
+ text/
61
+ adapters/ # Cercle 3 — moteurs OCR/LLM/VLM, importers, storage
62
+ ocr/
63
+ llm/
64
+ vlm/
65
+ corpus/
66
+ storage/
67
+ app/ # Cercle 4 — services applicatifs
68
+ services/
69
+ schemas/
70
+ interfaces/ # Cercle 5 — CLI, web, reports
71
+ cli/
72
+ web/
73
+ reports/
74
+ html/
75
+ json/
76
+ csv/
77
+ ```
78
+
79
+ Pivot mental : l'objet central n'est plus `Engine + BenchmarkResult`,
80
+ c'est `Pipeline → Artifacts → Projection → EvaluationView → Metrics`.
81
+
82
+ ---
83
+
84
+ ## Calendrier (26 semaines)
85
+
86
+ ### Phase 0 — Stabilisation de l'existant (S1 → S2)
87
+
88
+ | Sprint | Objectif | État |
89
+ |---|---|---|
90
+ | **S1** | Boucher les 6 P0 sur `main` | ✅ Livré (commit `a2bea75`) |
91
+ | **S2** | Recadrer le README, env propre, BACKLOG_POST_LIVRAISON | ⏳ En cours |
92
+
93
+ À la fin de S2, l'outil actuel reste utilisable pour les tests BnF
94
+ pendant que le rewrite avance sur `rewrite-2026`.
95
+
96
+ ### Phase 1 — Squelette et règles d'architecture (S3 → S6)
97
+
98
+ | Sprint | Objectif |
99
+ |---|---|
100
+ | S3 | Créer les répertoires cibles + tests d'architecture qui interdisent le retour en arrière |
101
+ | S4 | Modèle `Artifact` et types fondamentaux dans `domain/` |
102
+ | S5 | `EvaluationView`, `EvaluationSpec`, `MetricSpec` typés |
103
+ | S6 | `PipelineSpec`, `PipelineStep`, contrats d'exécution |
104
+
105
+ Critère go/no-go fin de Phase 1 : les tests d'architecture passent,
106
+ la BnF continue à utiliser `main`.
107
+
108
+ ### Phase 2 — Pipeline executor et migration des calculs (S7 → S12)
109
+
110
+ | Sprint | Objectif |
111
+ |---|---|
112
+ | S7 | Pipeline executor v1 (séquentiel mono-document) |
113
+ | S8 | Backpressure + timeout réel + annulation propre |
114
+ | S9 | `formats/alto/` et `formats/pagexml/` |
115
+ | S10 | Migration des calculs purs vers `evaluation/metrics/` (gros sprint) |
116
+ | S11 | Migration des adapters dans `adapters/` |
117
+ | S12 | Le nouvel executor reproduit l'ancien runner numériquement |
118
+
119
+ Critère go/no-go fin de Phase 2 : équivalence CER/WER vérifiée à
120
+ 1e-9 près sur 5 fixtures + 1 corpus BnF réel.
121
+
122
+ ### Phase 3 — Vues d'évaluation (S13 → S18) — cœur de la valeur ajoutée
123
+
124
+ | Sprint | Objectif |
125
+ |---|---|
126
+ | S13 | `EvaluationViewExecutor` et le moteur de vues |
127
+ | S14 | `TextView` (vue canonique 1) |
128
+ | S15 | `AltoView` (vue canonique 2) |
129
+ | S16 | `SearchView` (vue canonique 3) + cohérence inter-vues |
130
+ | S17 | Intégration runner + vues + nouveau format de résultat |
131
+ | S18 | E2E sur le cas BnF central + recettage interne |
132
+
133
+ Critère go/no-go fin de Phase 3 : ton cas d'usage central
134
+ (Tesseract texte brut vs OCR+LLM+ALTO remappé vs VLM+ALTO reconstruit)
135
+ fonctionne bout-en-bout, lisible, avec rapports de projection
136
+ explicites.
137
+
138
+ ### Phase 4 — Web sandboxée + recettage (S19 → S24)
139
+
140
+ | Sprint | Objectif |
141
+ |---|---|
142
+ | S19 | Couche `app/services/` |
143
+ | S20 | Réécriture corpus upload + sandbox ZIP |
144
+ | S21 | Nouveau `interfaces/web/` (CSRF on, CSP sans inline) |
145
+ | S22 | `interfaces/cli/` + `reports/html/` migration |
146
+ | S23 | Recettage BnF complet |
147
+ | S24 | Corrections de recettage + documentation finale |
148
+
149
+ ### Buffer (S25 → S26)
150
+
151
+ Imprévus + livraison. Ces deux semaines sont **non négociables**.
152
+
153
+ ---
154
+
155
+ ## Discipline du rewrite
156
+
157
+ Quatre invariants permanents, valables pendant les 26 semaines :
158
+
159
+ 1. **`main` reste livrable.** Le rewrite vit sur `rewrite-2026` /
160
+ `claude/repo-analysis-cukvm`. Les P0 vont sur `main`.
161
+ 2. **Pas de feature nouvelle.** Si l'envie vient, écrire dans
162
+ [`BACKLOG_POST_LIVRAISON.md`](../../BACKLOG_POST_LIVRAISON.md) et
163
+ passer.
164
+ 3. **Fin de chaque sprint = un commit qui passe `pytest tests/ -q`.**
165
+ 4. **Chaque sprint a un livrable démontrable** en 5 minutes.
166
+
167
+ Pour le détail à la semaine de chaque sprint (livrables, tests,
168
+ définition de "done", risque principal), voir le plan complet livré
169
+ en réponse à la question de cadrage du 2026-05-03 dans la session
170
+ [`session_011XQZNitg1rCgia8ZD1a2hP`](https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP).
171
+
172
+ ---
173
+
174
+ ## Ce qui n'est *pas* dans le rewrite
175
+
176
+ Cf. [`BACKLOG_POST_LIVRAISON.md`](../../BACKLOG_POST_LIVRAISON.md) pour
177
+ la liste complète. En résumé :
178
+
179
+ - Pas de feature nouvelle (NER cloud, VLM extras, etc.).
180
+ - Pas de promesses institutionnelles (RGPD opérationnel, JOSS, COI
181
+ exercés).
182
+ - Pas de réécriture des calculs purs (MUFI, philological, statistics)
183
+ — on les déplace, point.
184
+ - Pas de refonte du rapport HTML au-delà de l'intégration des vues
185
+ (le rendu visuel reste celui d'aujourd'hui pour ne pas allonger).
tests/test_minimal_install.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sprint A14-S2 — A.I.0 P0 : ``import picarones`` doit marcher avec
2
+ seulement les dépendances obligatoires.
3
+
4
+ Avant ce sprint, l'import du package au top-level chaînait des
5
+ ``import`` par effet de bord (cf. ``picarones/__init__.py:91`` :
6
+ ``import picarones.measurements as _trigger_metric_registration``)
7
+ qui exigeaient au moment du chargement initial des modules
8
+ théoriquement optionnels. Conséquence : un ``pip install picarones``
9
+ sur un environnement où, par exemple, ``defusedxml`` n'était pas
10
+ résolu (Python 3.13 alpha, mirrors PyPI partiels, etc.) faisait
11
+ crasher tout import du package — y compris ``from picarones import
12
+ Document`` qui n'a logiquement pas besoin d'XML.
13
+
14
+ Ce module vérifie deux invariants critiques :
15
+
16
+ 1. **Import OK avec seulement les deps obligatoires** —
17
+ l'API publique du Cercle 1 doit s'importer sans nécessiter
18
+ ``[web]``, ``[ner]``, ``[stats]``, ``[pero]``, ``[hf]``, ``[llm]``,
19
+ ``[ocr-cloud]``, ``[kraken]``.
20
+
21
+ 2. **Les deps obligatoires sont effectivement déclarées** dans
22
+ ``pyproject.toml`` (cohérence entre le code et la spec
23
+ d'installation).
24
+
25
+ Note d'environnement : ce test ne crée pas un venv vierge en
26
+ sous-processus (trop coûteux pour la CI à chaque commit). Il
27
+ vérifie ce qu'on peut vérifier dans le venv courant — la vraie
28
+ validation "venv neuf" est faite par la matrice CI (cf.
29
+ ``.github/workflows/ci.yml``).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import importlib
35
+ import importlib.util
36
+ import sys
37
+ from pathlib import Path
38
+
39
+ import pytest
40
+
41
+
42
+ # ──────────────────────────────────────────────────────────────────────
43
+ # 1. Smoke test de l'API publique
44
+ # ──────────────────────────────────────────────────────────────────────
45
+
46
+
47
+ PUBLIC_API_NAMES = (
48
+ "Corpus",
49
+ "Document",
50
+ "GTLevel",
51
+ "TextGT",
52
+ "AltoGT",
53
+ "PageGT",
54
+ "EntitiesGT",
55
+ "ReadingOrderGT",
56
+ "load_corpus_from_directory",
57
+ "ArtifactType",
58
+ "BaseModule",
59
+ "BenchmarkResult",
60
+ "DocumentResult",
61
+ "EngineReport",
62
+ "MetricsResult",
63
+ "aggregate_metrics",
64
+ "DetectorRegistry",
65
+ "Fact",
66
+ "FactImportance",
67
+ "FactType",
68
+ "PipelineResult",
69
+ "PipelineRunner",
70
+ "PipelineSpec",
71
+ "PipelineStep",
72
+ "StepResult",
73
+ "MetricSpec",
74
+ "compute_at_junction",
75
+ "register_metric",
76
+ "select_metrics",
77
+ )
78
+
79
+
80
+ def test_import_picarones_exposes_public_api() -> None:
81
+ """Tous les noms documentés dans le ``__all__`` du package
82
+ racine doivent être effectivement importables."""
83
+ import picarones
84
+
85
+ for name in PUBLIC_API_NAMES:
86
+ assert hasattr(picarones, name), (
87
+ f"``picarones.{name}`` annoncé dans ``__all__`` mais absent "
88
+ "du namespace au moment de l'import."
89
+ )
90
+
91
+
92
+ def test_picarones_all_matches_imports() -> None:
93
+ """``__all__`` ne doit pas mentir."""
94
+ import picarones
95
+
96
+ declared = set(picarones.__all__)
97
+ expected = set(PUBLIC_API_NAMES) | {"__version__", "__author__"}
98
+ missing = expected - declared
99
+ assert not missing, (
100
+ f"``__all__`` n'expose pas tous les noms attendus : {missing}"
101
+ )
102
+
103
+
104
+ def test_version_is_set() -> None:
105
+ """``picarones.__version__`` doit être une string non vide."""
106
+ import picarones
107
+
108
+ assert isinstance(picarones.__version__, str)
109
+ assert picarones.__version__.strip() != ""
110
+
111
+
112
+ # ──────────────────────────────────────────────────────────────────────
113
+ # 2. Cohérence entre les imports top-level et pyproject.toml
114
+ # ──────────────────────────────────────────────────────────────────────
115
+
116
+
117
+ def _project_root() -> Path:
118
+ return Path(__file__).resolve().parents[1]
119
+
120
+
121
+ def _read_pyproject_dependencies() -> list[str]:
122
+ """Liste des noms de package des deps obligatoires.
123
+
124
+ Volontairement permissif : on garde uniquement le nom (avant
125
+ ``>=``, ``==``, ``[``, etc.) puisque c'est ce qui permet
126
+ ``importlib.util.find_spec``. Les noms PyPI utilisent ``-``
127
+ mais les modules importés utilisent ``_`` (et ce n'est pas
128
+ toujours symétrique : ``Pillow`` → ``PIL``, ``pyyaml`` →
129
+ ``yaml``). On gère explicitement le mapping ci-dessous.
130
+ """
131
+ pyproject = _project_root() / "pyproject.toml"
132
+ text = pyproject.read_text(encoding="utf-8")
133
+ # Parser TOML léger : on cible juste le bloc ``dependencies = [...]``
134
+ # de [project]. Pour rester sans dépendance externe, on parse à la
135
+ # main une fois la section trouvée.
136
+ in_deps = False
137
+ out: list[str] = []
138
+ for line in text.splitlines():
139
+ stripped = line.strip()
140
+ if stripped.startswith("dependencies"):
141
+ in_deps = True
142
+ continue
143
+ if in_deps:
144
+ if stripped.startswith("]"):
145
+ break
146
+ if stripped.startswith("#") or not stripped:
147
+ continue
148
+ # `` "click>=8.1.0",`` → ``click``
149
+ raw = stripped.strip(",").strip().strip('"').strip("'")
150
+ # Coupe à la première occurrence d'un opérateur de version
151
+ # ou d'un crochet d'extra.
152
+ for sep in (">=", "==", "<=", ">", "<", "~=", "[", ";"):
153
+ idx = raw.find(sep)
154
+ if idx >= 0:
155
+ raw = raw[:idx]
156
+ break
157
+ raw = raw.strip()
158
+ if raw:
159
+ out.append(raw)
160
+ return out
161
+
162
+
163
+ # Mapping nom PyPI → nom du module Python à importer.
164
+ # Source : https://packaging.python.org/en/latest/discussions/...
165
+ # Ne lister que les paires asymétriques.
166
+ _NAME_OVERRIDES: dict[str, str] = {
167
+ "Pillow": "PIL",
168
+ "pyyaml": "yaml",
169
+ "PyYAML": "yaml",
170
+ "python-multipart": "multipart",
171
+ "pyaml": "yaml",
172
+ }
173
+
174
+
175
+ def _import_name(pypi_name: str) -> str:
176
+ return _NAME_OVERRIDES.get(pypi_name, pypi_name.replace("-", "_"))
177
+
178
+
179
+ def test_required_deps_are_importable() -> None:
180
+ """Toutes les deps déclarées dans ``[project.dependencies]`` doivent
181
+ être effectivement installables/importables. Garde-fou contre une
182
+ typo ou un nom de package PyPI mal copié."""
183
+ declared = _read_pyproject_dependencies()
184
+ assert declared, (
185
+ "Aucune dépendance obligatoire trouvée dans pyproject.toml — "
186
+ "le parser maison s'est cassé sur le format actuel."
187
+ )
188
+ missing: list[tuple[str, str]] = []
189
+ for pypi in declared:
190
+ mod = _import_name(pypi)
191
+ if importlib.util.find_spec(mod) is None:
192
+ missing.append((pypi, mod))
193
+ assert not missing, (
194
+ "Deps obligatoires déclarées mais introuvables dans le venv "
195
+ "courant. En CI institutionnelle, c'est un échec dur — un "
196
+ "``pip install picarones`` produit un package qui crashera à "
197
+ f"l'import sur ces noms : {missing}. Vérifier le mapping "
198
+ "PyPI → module dans ``_NAME_OVERRIDES``."
199
+ )
200
+
201
+
202
+ def test_top_level_externals_are_declared() -> None:
203
+ """Tout package externe chargé par ``import picarones`` doit être
204
+ listé dans ``[project.dependencies]``.
205
+
206
+ Garde-fou contre le scénario opposé : on ajoute un ``import foo``
207
+ quelque part dans ``picarones/__init__.py`` (ou dans un module
208
+ chargé par effet de bord depuis ``__init__.py``) sans déclarer
209
+ ``foo`` dans ``pyproject.toml``. Sur un install propre, le
210
+ package crash.
211
+ """
212
+ # Capture des modules chargés avant et après ``import picarones``.
213
+ before = set(sys.modules)
214
+ importlib.import_module("picarones")
215
+ after = set(sys.modules)
216
+
217
+ # On ne garde que les top-level (pas de ``foo.bar``) qui ne sont
218
+ # pas des modules picarones et qui ne sont pas stdlib.
219
+ stdlib_names = set(getattr(sys, "stdlib_module_names", ()))
220
+ candidates = {
221
+ m.split(".")[0] for m in (after - before)
222
+ if "." not in m
223
+ }
224
+ candidates -= {m for m in candidates if m.startswith("_")}
225
+ candidates -= stdlib_names
226
+ candidates -= {"picarones"}
227
+ # Modules implicitement amenés par d'autres déjà déclarés (ex :
228
+ # rapidfuzz vient avec jiwer ; pydantic_core vient avec pydantic ;
229
+ # cython_runtime vient avec rapidfuzz ; pyexpat est en stdlib mais
230
+ # pas toujours dans stdlib_module_names selon la version).
231
+ transitive_allowed = {
232
+ "rapidfuzz",
233
+ "cython_runtime",
234
+ "pyexpat",
235
+ "annotated_types",
236
+ "pydantic",
237
+ "pydantic_core",
238
+ "typing_extensions",
239
+ "typing_inspection",
240
+ "annotated_doc",
241
+ "tomli", # TOML stdlib uniquement à partir de 3.11 (tomllib)
242
+ "tomllib",
243
+ }
244
+ candidates -= transitive_allowed
245
+
246
+ declared = {_import_name(d) for d in _read_pyproject_dependencies()}
247
+
248
+ undeclared = candidates - declared
249
+ assert not undeclared, (
250
+ f"Modules externes chargés à ``import picarones`` mais non "
251
+ f"déclarés dans ``[project.dependencies]`` : {sorted(undeclared)}.\n"
252
+ "Soit ajouter ces deps à pyproject.toml, soit déplacer leur "
253
+ "import en lazy load (à l'intérieur d'une fonction qui n'est "
254
+ "pas appelée au top-level)."
255
+ )
256
+
257
+
258
+ # ──────────────────────────────────────────────────────────────────────
259
+ # 3. Garde-fou : pas de crash silencieux sur deps optionnelles absentes
260
+ # ─────────────────────────────────────────────────────────���────────────
261
+
262
+
263
+ def test_optional_deps_not_required_at_top_level() -> None:
264
+ """Les modules dépendant de deps optionnelles doivent s'importer
265
+ en mode dégradé silencieux quand ces deps manquent.
266
+
267
+ Exemple : ``picarones.engines.tesseract`` ne doit pas crasher
268
+ l'import si ``pytesseract`` n'est pas installé — il doit échouer
269
+ plus tard, au moment du ``run()``. Idem pour Pero, Mistral OCR,
270
+ Google Vision, Azure DI.
271
+
272
+ On vérifie ici que les modules existent et s'importent même
273
+ quand on n'a pas les engines installés.
274
+ """
275
+ # Liste des modules engines qu'on doit pouvoir au moins charger
276
+ # (pas exécuter) sans planter.
277
+ optional_engine_modules = (
278
+ "picarones.engines.tesseract",
279
+ "picarones.engines.pero_ocr",
280
+ "picarones.engines.mistral_ocr",
281
+ "picarones.engines.google_vision",
282
+ "picarones.engines.azure_doc_intel",
283
+ )
284
+ failed: list[tuple[str, str]] = []
285
+ for mod_name in optional_engine_modules:
286
+ try:
287
+ importlib.import_module(mod_name)
288
+ except ImportError as exc:
289
+ failed.append((mod_name, str(exc)))
290
+ assert not failed, (
291
+ "Modules engines qui plantent à l'import simple — ils doivent "
292
+ "tomber en mode dégradé (warning + fallback) plutôt que de "
293
+ "lever ImportError au top-level. C'est ce qui permet à un "
294
+ f"installeur minimal d'utiliser le CLI : {failed}"
295
+ )