Spaces:
Sleeping
docs(s2): Sprint A14-S2 — recadrer le discours, garde-fou install minimal
Browse filesSprint 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 +169 -0
- README.md +51 -28
- docs/roadmap/rewrite-2026.md +185 -0
- tests/test_minimal_install.py +295 -0
|
@@ -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).
|
|
@@ -9,9 +9,17 @@ pinned: false
|
|
| 9 |
|
| 10 |
# Picarones
|
| 11 |
|
| 12 |
-
> **Heritage OCR / HTR / VLM and post-correction benchmarking
|
| 13 |
>
|
| 14 |
-
> **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
[](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml)
|
| 17 |
[](https://www.python.org/downloads/)
|
|
@@ -23,22 +31,25 @@ pinned: false
|
|
| 23 |
|
| 24 |
## What is Picarones?
|
| 25 |
|
| 26 |
-
**Picarones** is an open-source
|
| 27 |
-
|
| 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
|
| 34 |
-
|
| 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**: ~
|
| 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/
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 455 |
-
(
|
| 456 |
-
with the commit SHA used in your benchmark
|
| 457 |
-
embeds the commit and
|
| 458 |
-
|
|
|
|
|
|
|
| 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 |
[](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml)
|
| 25 |
[](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 (S1–S26) 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 |
|
|
@@ -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).
|
|
@@ -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 |
+
)
|