Marcel Bautista-Kuljevan commited on
Commit
2ac9751
·
unverified ·
2 Parent(s): adcd7656857b1f

Merge pull request #57 from maribakulj/claude/repo-analysis-cukvm

Browse files

Migration legacy → rewrite : 165+ shims supprimés (Lots A à J + fix templates)

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/workflows/ci.yml +44 -11
  2. .gitignore +11 -1
  3. .well-known/security.txt +18 -0
  4. CHANGELOG.md +25 -25
  5. CLAUDE.md +0 -0
  6. GOVERNANCE.md +1 -1
  7. NOTICE +28 -0
  8. README.md +28 -29
  9. SECURITY.en.md +109 -0
  10. SPECS.md +99 -63
  11. docs/api/adapters.md +82 -0
  12. docs/api/app.md +39 -0
  13. docs/api/domain.md +30 -0
  14. docs/api/evaluation.md +47 -0
  15. docs/api/index.md +51 -0
  16. docs/api/pipeline.md +25 -0
  17. docs/architecture.md +0 -179
  18. docs/developer/extending-i18n.md +1 -1
  19. docs/developer/index.en.md +1 -1
  20. docs/developer/index.md +36 -20
  21. docs/developer/module-policy.md +4 -3
  22. docs/explanation/architecture.md +190 -0
  23. docs/{developer → explanation}/narrative-engine.en.md +2 -2
  24. docs/{developer → explanation}/narrative-engine.md +0 -0
  25. docs/{cli-workflows.md → how-to/cli-workflows.md} +2 -2
  26. INSTALL.md → docs/how-to/install.md +8 -19
  27. docs/index.md +160 -0
  28. docs/legal/THIRD_PARTY_LICENSES.md +155 -0
  29. docs/legal/dpa-template.md +218 -0
  30. docs/migration/SESSION_HANDOVER.md +508 -0
  31. docs/migration/legacy-retirement-plan.md +1239 -0
  32. docs/migration/pipeline-convergence-plan.md +410 -0
  33. docs/migration/regression-tolerances.md +178 -0
  34. docs/operations/deployment-institutional.md +1 -1
  35. docs/operations/observability.md +208 -0
  36. docs/operations/runbook.md +374 -0
  37. docs/operations/supply-chain.md +125 -0
  38. docs/{views → reference}/alto-view.md +0 -0
  39. docs/{api-stable.md → reference/api-stable.md} +45 -31
  40. docs/{views → reference}/comparing-views.md +0 -0
  41. docs/{profiles.md → reference/normalization-profiles.md} +5 -5
  42. docs/{reproducibility-snapshots.md → reference/reproducibility-snapshots.md} +0 -0
  43. docs/{views → reference}/text-view.md +0 -0
  44. docs/{views.md → reference/views.md} +9 -9
  45. BACKLOG_POST_LIVRAISON.md → docs/roadmap/backlog.md +0 -0
  46. docs/roadmap/rewrite-2026.md +14 -13
  47. docs/security/threat-model.md +148 -0
  48. docs/{user → tutorials}/reading-a-report.en.md +2 -2
  49. docs/{user → tutorials}/reading-a-report.md +2 -2
  50. docs/{user → tutorials}/writing-a-pipeline-module.md +9 -8
.github/workflows/ci.yml CHANGED
@@ -5,7 +5,7 @@
5
  # - Linux, macOS, Windows
6
  # - Couverture exigée >= 85 % (--cov-fail-under, plancher 2 pts sous baseline 87 %)
7
  # - Timeout pytest 5 min par test individuel (pytest-timeout, mode thread)
8
- # - Type-check mypy (strict sur picarones/core/, lax ailleurs — durci en A11)
9
  # - Scanners sécurité : bandit (statique) + pip-audit (CVE deps) + trivy (image)
10
  # - Build de la distribution Python
11
  # - Vérification de l'exécutable demo
@@ -92,22 +92,55 @@ jobs:
92
  # ── Tests ───────────────────────────────────────────────────
93
  # Sprint A1 : --cov-fail-under=85 (baseline mesuré 87 %, marge 2 pts).
94
  # pytest-timeout est configuré dans pyproject.toml [tool.pytest.ini_options].
95
- # ``timeout-minutes`` au niveau step : le job ne hang JAMAIS plus de
96
- # 15 min sur les tests, même si pytest-timeout (par-test) échoue à
97
- # cleanup un thread daemon.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  - name: Run tests
99
  # Sur Python 3.13, on continue malgré une erreur pour ne pas bloquer
100
  # le merge pendant la fenêtre informationnelle de 6 mois (m-8).
101
  continue-on-error: ${{ matrix.python-version == '3.13' }}
102
- timeout-minutes: 15
103
  shell: bash
104
  run: |
105
- pytest tests/ -q --tb=short --no-header \
106
- --cov=picarones --cov-report=xml --cov-report=term-missing \
107
- --cov-fail-under=85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  env:
109
  PYTHONIOENCODING: utf-8
110
  PYTHONUTF8: "1"
 
111
 
112
  # ── Couverture ──────────────────────────────────────────────
113
  # Conditions :
@@ -245,7 +278,7 @@ jobs:
245
  # Job 5 : Type-checking — Sprint A1 (item M-4)
246
  #
247
  # mypy est configuré dans pyproject.toml [tool.mypy] :
248
- # - strict sur picarones.core.* (10 modules)
249
  # - lax ailleurs (follow_imports=silent)
250
  # Deux checks pré-existants désactivés (disallow_any_generics et
251
  # warn_return_any), à ré-activer en Sprint A11 après fix des
@@ -270,8 +303,8 @@ jobs:
270
  python -m pip install --upgrade pip setuptools wheel
271
  pip install -e ".[dev,web,stats]"
272
 
273
- - name: Run mypy on picarones/core (strict)
274
- run: python -m mypy picarones/core/
275
 
276
  # ──────────────────────────────────────────────────────────────────
277
  # Job 6 : Sécurité — Sprint A1 (item B-7)
 
5
  # - Linux, macOS, Windows
6
  # - Couverture exigée >= 85 % (--cov-fail-under, plancher 2 pts sous baseline 87 %)
7
  # - Timeout pytest 5 min par test individuel (pytest-timeout, mode thread)
8
+ # - Type-check mypy (strict sur picarones/domain/, lax ailleurs — durci en A11)
9
  # - Scanners sécurité : bandit (statique) + pip-audit (CVE deps) + trivy (image)
10
  # - Build de la distribution Python
11
  # - Vérification de l'exécutable demo
 
92
  # ── Tests ───────────────────────────────────────────────────
93
  # Sprint A1 : --cov-fail-under=85 (baseline mesuré 87 %, marge 2 pts).
94
  # pytest-timeout est configuré dans pyproject.toml [tool.pytest.ini_options].
95
+ #
96
+ # Garde-fous anti-hang :
97
+ #
98
+ # 1. ``timeout-minutes: 12`` au niveau step : cap dur GitHub si
99
+ # tout le reste échoue.
100
+ # 2. ``timeout`` GNU autour de pytest : SIGTERM à 9 minutes,
101
+ # SIGKILL 30s après si Python n'a pas obéi. Couvre
102
+ # spécifiquement le cas d'un hang de SHUTDOWN de
103
+ # l'interpréteur Python 3.12+ (threads non-daemon, connexions
104
+ # sqlite non fermées, ResourceWarnings — observé sur ubuntu
105
+ # 3.12 où pytest finit en 3:21 et l'interpréteur reste 12 min
106
+ # avant de rendre la main).
107
+ # 3. ``-X faulthandler`` : si le hang revient, on aura les stack
108
+ # traces de tous les threads dans le log avant le SIGKILL.
109
+ # 4. ``PYTHONFAULTHANDLER=1`` redondance ceinture-bretelles.
110
+ #
111
+ # Le code de retour 124 (SIGTERM par GNU timeout) ou 137 (SIGKILL)
112
+ # est traité comme un échec normal — on perd l'info pytest mais
113
+ # on préserve la latence de la CI.
114
  - name: Run tests
115
  # Sur Python 3.13, on continue malgré une erreur pour ne pas bloquer
116
  # le merge pendant la fenêtre informationnelle de 6 mois (m-8).
117
  continue-on-error: ${{ matrix.python-version == '3.13' }}
118
+ timeout-minutes: 12
119
  shell: bash
120
  run: |
121
+ # ``timeout`` n'est pas standard sur macOS (BSD vs GNU) — on
122
+ # détecte et on adapte. Sur Windows, le shell bash de
123
+ # Git-Bash n'a pas timeout : on retombe sur python direct.
124
+ if command -v timeout >/dev/null 2>&1; then
125
+ timeout --signal=SIGTERM --kill-after=30 540 \
126
+ python -X faulthandler -m pytest tests/ -q --tb=short --no-header \
127
+ --cov=picarones --cov-report=xml --cov-report=term-missing \
128
+ --cov-fail-under=85
129
+ elif command -v gtimeout >/dev/null 2>&1; then
130
+ # macOS Homebrew coreutils.
131
+ gtimeout --signal=SIGTERM --kill-after=30 540 \
132
+ python -X faulthandler -m pytest tests/ -q --tb=short --no-header \
133
+ --cov=picarones --cov-report=xml --cov-report=term-missing \
134
+ --cov-fail-under=85
135
+ else
136
+ python -X faulthandler -m pytest tests/ -q --tb=short --no-header \
137
+ --cov=picarones --cov-report=xml --cov-report=term-missing \
138
+ --cov-fail-under=85
139
+ fi
140
  env:
141
  PYTHONIOENCODING: utf-8
142
  PYTHONUTF8: "1"
143
+ PYTHONFAULTHANDLER: "1"
144
 
145
  # ── Couverture ──────────────────────────────────────────────
146
  # Conditions :
 
278
  # Job 5 : Type-checking — Sprint A1 (item M-4)
279
  #
280
  # mypy est configuré dans pyproject.toml [tool.mypy] :
281
+ # - strict sur picarones.domain.* (couche 1 du rewrite, ex-picarones.core)
282
  # - lax ailleurs (follow_imports=silent)
283
  # Deux checks pré-existants désactivés (disallow_any_generics et
284
  # warn_return_any), à ré-activer en Sprint A11 après fix des
 
303
  python -m pip install --upgrade pip setuptools wheel
304
  pip install -e ".[dev,web,stats]"
305
 
306
+ - name: Run mypy on picarones/domain (strict)
307
+ run: python -m mypy picarones/domain/
308
 
309
  # ──────────────────────────────────────────────────────────────────
310
  # Job 6 : Sécurité — Sprint A1 (item B-7)
.gitignore CHANGED
@@ -28,9 +28,19 @@ jobs.db-shm
28
  jobs.db-wal
29
 
30
  # Exceptions : fichiers HTML sources du package (templates Jinja2, pas rapports)
31
- !picarones/report/templates/*.html
32
  !picarones/web/templates/*.html
 
 
 
 
 
 
 
 
33
  # Sprint A14-S3 — sous-package du code (homonyme de corpus/ data ignoré ligne 21)
34
  !picarones/adapters/corpus/
35
  !picarones/adapters/corpus/**
 
 
 
36
  _version.py
 
28
  jobs.db-wal
29
 
30
  # Exceptions : fichiers HTML sources du package (templates Jinja2, pas rapports)
 
31
  !picarones/web/templates/*.html
32
+ # Lot G fix (mai 2026) — Phase 5.E avait migré les templates de
33
+ # picarones/report/templates/ vers picarones/reports_v2/html/templates/
34
+ # mais oublié l'exception .gitignore correspondante : les 10 .html
35
+ # avaient donc été silencieusement ignorés par git lors du commit
36
+ # cc53ead, faisant échouer ~91 tests (TemplateNotFound _header.html
37
+ # etc.). Cette nouvelle exception remplace l'ancienne (plus en
38
+ # vigueur depuis la suppression de picarones/report/ au Lot F).
39
+ !picarones/reports_v2/html/templates/*.html
40
  # Sprint A14-S3 — sous-package du code (homonyme de corpus/ data ignoré ligne 21)
41
  !picarones/adapters/corpus/
42
  !picarones/adapters/corpus/**
43
+ # Phase 4-quater (cleanup) : ré-ignorer __pycache__/ dans ce sous-package
44
+ # (la négation ci-dessus est trop large et casse la règle ligne 1).
45
+ picarones/adapters/corpus/**/__pycache__/
46
  _version.py
.well-known/security.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Picarones — security.txt (RFC 9116)
2
+ #
3
+ # This file is meant to be served at:
4
+ # https://<deployment-host>/.well-known/security.txt
5
+ #
6
+ # Institutional deployments (BnF, LoC, BL) MUST update the values
7
+ # below before going live — the canonical contact for the upstream
8
+ # project is the GitHub Security Advisories endpoint, but each
9
+ # deployment SHOULD designate its own security contact.
10
+
11
+ Contact: https://github.com/maribakulj/Picarones/security/advisories/new
12
+ Expires: 2027-05-31T23:59:59.000Z
13
+ Encryption: https://github.com/maribakulj/Picarones/security/advisories/new
14
+ Acknowledgments: https://github.com/maribakulj/Picarones/security/advisories
15
+ Preferred-Languages: fr, en
16
+ Canonical: https://github.com/maribakulj/Picarones/blob/main/.well-known/security.txt
17
+ Policy: https://github.com/maribakulj/Picarones/blob/main/SECURITY.md
18
+ Hiring: https://github.com/maribakulj/Picarones/issues
CHANGELOG.md CHANGED
@@ -7,9 +7,15 @@ La numérotation de version suit [Semantic Versioning](https://semver.org/lang/f
7
 
8
  ---
9
 
10
- ## [Unreleased] — fix CI Windows + cap timeout — 2026-05
11
 
12
- ### Bug Windows : `:` dans les clés du store
 
 
 
 
 
 
13
 
14
  Le ``FilesystemArtifactStore`` produisait des filenames de la forme
15
  ``<step_hash>:<output_type>.json`` (séparateur ``:``). ``:`` est un
@@ -29,7 +35,7 @@ les ``ArtifactType`` et tous les caractères Windows réservés.
29
  acceptable. Aucun impact sur les artefacts persistés (l'index
30
  ``index.jsonl`` est régénéré automatiquement).
31
 
32
- ### CI : exclusion des tests live + timeout codecov
33
 
34
  Voir commit `ce30e80` :
35
 
@@ -40,11 +46,9 @@ Voir commit `ce30e80` :
40
  ``timeout-minutes: 5`` sur ``Upload coverage to Codecov`` ;
41
  ``fail_ci_if_error: false`` sur codecov.
42
 
43
- ---
44
-
45
- ## [Unreleased] — audit institutionnel S58-S59 (post-S57) — 2026-05
46
 
47
- ### ⚠️ BREAKING CHANGES (déprécations en cours, suppression en 2.0)
48
 
49
  Trois symboles supprimés au S57 sont **restaurés en S59** comme alias
50
  dépréciés avec `DeprecationWarning` à l'accès. Ils seront supprimés
@@ -142,17 +146,15 @@ réseau (TimeoutError, ConnectionError, URLError).
142
  - `tests/architecture/test_manifest_reproducibility.py` : 4 tests.
143
  - `tests/interfaces/web/test_rate_limit_xff.py` : 7 tests.
144
 
145
- ---
146
-
147
- ## [Unreleased] — rewrite A14 (S27-S46) + audit remediation (S47-S57) — 2026-05
148
 
149
- > Cette section couvre la phase **rewrite ciblé** (S27-S46) puis les
150
- > **6 vagues de remédiation** des dettes identifiées en audit
151
- > *institutional readiness 2026-05* (S47-S57). Détail complet dans
152
- > `docs/migration/rewrite-status-s46.md` et
153
- > `docs/audits/remediation-plan-2026-05.md`.
154
 
155
- ### Phase rewrite (S27-S46) — partial rewrite
156
 
157
  20 sprints sur la directive *« rewrite tout, le plus solide, sans dette
158
  technique »*. Stratégie : **rewrite parallèle**, pas full rewrite — le
@@ -177,7 +179,7 @@ benchmark, jobs), JobStore SQLite, UI Jinja2 + i18n FR/EN.
177
  SearchView). Vues thématiques legacy (Pareto, narrative, glossary,
178
  case-studies) à porter une à une post-livraison.
179
 
180
- ### Phase remédiation (S47-S57) — 30 dettes adressées en 6 vagues
181
 
182
  | Vague | Sprint | Issues | Thème |
183
  |-------|--------|--------|-------|
@@ -191,7 +193,7 @@ case-studies) à porter une à une post-livraison.
191
 
192
  **Tous les 30 issues sont adressés au S57**.
193
 
194
- ### S57 — détail des rectifications
195
 
196
  - **#15 Lazy imports SDK tiers** : confirmé intentionnel — `mistralai`,
197
  `anthropic`, `openai`, `ollama` sont importés à l'intérieur des
@@ -254,11 +256,9 @@ case-studies) à porter une à une post-livraison.
254
  qualité d'image, présence de notes marginales). Un seuil à 10
255
  points faisait échouer la CI sur du bruit légitime.
256
 
257
- ---
258
-
259
- ## [Unreleased] — fix CI perf_regression — 2026-05
260
 
261
- ### ⚠️ BREAKING CHANGE — sémantique `--fail-if-cer-above`
262
 
263
  L'option `picarones run --fail-if-cer-above` interprétait sa valeur
264
  comme un **pourcentage** (ex : `15.0` = 15 %). Désormais elle attend
@@ -1889,7 +1889,7 @@ sur des monolithes globaux.
1889
  ingénieur qui veut **brancher son propre module** dans Picarones
1890
  (correcteur LLM, reconstructeur ALTO, classifieur d'entités,
1891
  re-segmenteur…) trouve maintenant un guide complet bout-en-bout.
1892
- - Nouveau document `docs/user/writing-a-pipeline-module.md` :
1893
  - **TL;DR** avec un exemple `MyCorrector` minimal en 25 lignes.
1894
  - Section **« Le contrat ``BaseModule`` »** : tableau des
1895
  champs obligatoires (``input_types``, ``output_types``,
@@ -3437,7 +3437,7 @@ sur des monolithes globaux.
3437
  `CONFIDENCE_WARNING`, `cost_unit_pages=1000` propagé dans
3438
  `PARETO_ALTERNATIVE`/`COST_OUTLIER`, paramètre `select_facts(..., type_order=...)`,
3439
  test stabilité bootstrap (±0,5 pp inter-seeds), test E2E synthèse EN.
3440
- Doc « Politique éditoriale » dans `docs/developer/narrative-engine.md`.
3441
  - **Sprint 24** — durcissement sécurité institutionnelle : mode public
3442
  (`PICARONES_PUBLIC_MODE=1`), `PICARONES_BROWSE_ROOTS`, validation Pillow
3443
  sur upload (CVE-2023-50447), rate limit + sémaphore concurrence,
@@ -3520,7 +3520,7 @@ sur des monolithes globaux.
3520
  vue opt-in « score composite personnel » avec curseurs à 0 par défaut
3521
  et formule visible. État persisté en URL. +21 tests.
3522
  - **Sprint 22** — études de cas (`docs/case-studies/`),
3523
- `docs/user/reading-a-report.md`, trois guides développeur dans
3524
  `docs/developer/`. Garde-fou « pas de fausses études prétendant
3525
  être réelles ». +18 tests.
3526
 
 
7
 
8
  ---
9
 
10
+ ## [Unreleased] — towards 1.3.0 (release institutionnelle BnF) — 2026-05
11
 
12
+ > Section unique conforme à Keep-a-Changelog. Les chantiers actifs
13
+ > sont regroupés ci-dessous par thème ; chaque thème reflète un audit
14
+ > ou un fix livré sur la branche ``claude/repo-analysis-cukvm``.
15
+
16
+ ### Fix CI : Windows + cap timeout (S59)
17
+
18
+ #### Bug Windows : `:` dans les clés du store
19
 
20
  Le ``FilesystemArtifactStore`` produisait des filenames de la forme
21
  ``<step_hash>:<output_type>.json`` (séparateur ``:``). ``:`` est un
 
35
  acceptable. Aucun impact sur les artefacts persistés (l'index
36
  ``index.jsonl`` est régénéré automatiquement).
37
 
38
+ #### CI : exclusion des tests live + timeout codecov
39
 
40
  Voir commit `ce30e80` :
41
 
 
46
  ``timeout-minutes: 5`` sur ``Upload coverage to Codecov`` ;
47
  ``fail_ci_if_error: false`` sur codecov.
48
 
49
+ ### Audit institutionnel S58-S59 (post-S57)
 
 
50
 
51
+ #### ⚠️ BREAKING CHANGES (déprécations en cours, suppression en 2.0)
52
 
53
  Trois symboles supprimés au S57 sont **restaurés en S59** comme alias
54
  dépréciés avec `DeprecationWarning` à l'accès. Ils seront supprimés
 
146
  - `tests/architecture/test_manifest_reproducibility.py` : 4 tests.
147
  - `tests/interfaces/web/test_rate_limit_xff.py` : 7 tests.
148
 
149
+ ### Rewrite A14 (S27-S46) + audit remediation (S47-S57)
 
 
150
 
151
+ Cette section couvre la phase **rewrite ciblé** (S27-S46) puis les
152
+ **6 vagues de remédiation** des dettes identifiées en audit
153
+ *institutional readiness 2026-05* (S47-S57). Détail complet dans
154
+ `docs/migration/rewrite-status-s46.md` et
155
+ `docs/audits/remediation-plan-2026-05.md`.
156
 
157
+ #### Phase rewrite (S27-S46) — partial rewrite
158
 
159
  20 sprints sur la directive *« rewrite tout, le plus solide, sans dette
160
  technique »*. Stratégie : **rewrite parallèle**, pas full rewrite — le
 
179
  SearchView). Vues thématiques legacy (Pareto, narrative, glossary,
180
  case-studies) à porter une à une post-livraison.
181
 
182
+ #### Phase remédiation (S47-S57) — 30 dettes adressées en 6 vagues
183
 
184
  | Vague | Sprint | Issues | Thème |
185
  |-------|--------|--------|-------|
 
193
 
194
  **Tous les 30 issues sont adressés au S57**.
195
 
196
+ #### S57 — détail des rectifications
197
 
198
  - **#15 Lazy imports SDK tiers** : confirmé intentionnel — `mistralai`,
199
  `anthropic`, `openai`, `ollama` sont importés à l'intérieur des
 
256
  qualité d'image, présence de notes marginales). Un seuil à 10
257
  points faisait échouer la CI sur du bruit légitime.
258
 
259
+ ### Fix CI perf_regression
 
 
260
 
261
+ #### ⚠️ BREAKING CHANGE — sémantique `--fail-if-cer-above`
262
 
263
  L'option `picarones run --fail-if-cer-above` interprétait sa valeur
264
  comme un **pourcentage** (ex : `15.0` = 15 %). Désormais elle attend
 
1889
  ingénieur qui veut **brancher son propre module** dans Picarones
1890
  (correcteur LLM, reconstructeur ALTO, classifieur d'entités,
1891
  re-segmenteur…) trouve maintenant un guide complet bout-en-bout.
1892
+ - Nouveau document `docs/tutorials/writing-a-pipeline-module.md` :
1893
  - **TL;DR** avec un exemple `MyCorrector` minimal en 25 lignes.
1894
  - Section **« Le contrat ``BaseModule`` »** : tableau des
1895
  champs obligatoires (``input_types``, ``output_types``,
 
3437
  `CONFIDENCE_WARNING`, `cost_unit_pages=1000` propagé dans
3438
  `PARETO_ALTERNATIVE`/`COST_OUTLIER`, paramètre `select_facts(..., type_order=...)`,
3439
  test stabilité bootstrap (±0,5 pp inter-seeds), test E2E synthèse EN.
3440
+ Doc « Politique éditoriale » dans `docs/explanation/narrative-engine.md`.
3441
  - **Sprint 24** — durcissement sécurité institutionnelle : mode public
3442
  (`PICARONES_PUBLIC_MODE=1`), `PICARONES_BROWSE_ROOTS`, validation Pillow
3443
  sur upload (CVE-2023-50447), rate limit + sémaphore concurrence,
 
3520
  vue opt-in « score composite personnel » avec curseurs à 0 par défaut
3521
  et formule visible. État persisté en URL. +21 tests.
3522
  - **Sprint 22** — études de cas (`docs/case-studies/`),
3523
+ `docs/tutorials/reading-a-report.md`, trois guides développeur dans
3524
  `docs/developer/`. Garde-fou « pas de fausses études prétendant
3525
  être réelles ». +18 tests.
3526
 
CLAUDE.md CHANGED
The diff for this file is too large to render. See raw diff
 
GOVERNANCE.md CHANGED
@@ -97,7 +97,7 @@ prestation (cf. modalités à définir au cas par cas).
97
  ## Politique de breaking changes
98
 
99
  L'API publique de Picarones est définie par
100
- [`docs/api-stable.md`](docs/api-stable.md). Elle inclut :
101
 
102
  - les symboles ré-exportés depuis `picarones/__init__.py` ;
103
  - les commandes CLI `picarones X` documentées dans le README ;
 
97
  ## Politique de breaking changes
98
 
99
  L'API publique de Picarones est définie par
100
+ [`docs/reference/api-stable.md`](docs/reference/api-stable.md). Elle inclut :
101
 
102
  - les symboles ré-exportés depuis `picarones/__init__.py` ;
103
  - les commandes CLI `picarones X` documentées dans le README ;
NOTICE ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Picarones
2
+ Copyright 2025-2026 the Picarones contributors
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License"); you
5
+ may not use this software except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
+ implied. See the License for the specific language governing
14
+ permissions and limitations under the License.
15
+
16
+ ────────────────────────────────────────────────────────────────────
17
+ Third-party software
18
+ ────────────────────────────────────────────────────────────────────
19
+
20
+ This product includes software developed by third parties. The
21
+ authoritative list of third-party dependencies, their licenses and
22
+ their copyright notices is maintained in:
23
+
24
+ docs/legal/THIRD_PARTY_LICENSES.md
25
+
26
+ That file is regenerated by ``scripts/gen_third_party_licenses.py``
27
+ on every release. In case of discrepancy, the file in the
28
+ ``docs/legal/`` directory at the time of release prevails.
README.md CHANGED
@@ -102,7 +102,7 @@ Three families of metrics calibrated for historical documents:
102
  trend with change-point detection; controlled per-slot ANOVA-like
103
  comparison.
104
 
105
- For the full list with definitions, see [`docs/views.md`](docs/views.md)
106
  and the contextual glossary embedded in every report (25 bilingual
107
  entries).
108
 
@@ -189,7 +189,7 @@ picarones serve --port 8080
189
  ```
190
 
191
  For Docker, institutional deployment, or HuggingFace Spaces, see
192
- [`INSTALL.md`](INSTALL.md) and
193
  [`docs/operations/deployment-institutional.md`](docs/operations/deployment-institutional.md).
194
 
195
  ---
@@ -210,12 +210,12 @@ For Docker, institutional deployment, or HuggingFace Spaces, see
210
 
211
  LLM/VLM adapters (used through pipelines, not as standalone OCR
212
  engines): GPT-4o, Claude, Mistral Large, Ollama (local). See
213
- [`docs/cli-workflows.md`](docs/cli-workflows.md).
214
 
215
  The `Engine` table is regenerated automatically by
216
  `scripts/gen_readme_tables.py` — adding a new adapter under
217
- `picarones/engines/` makes the next CI run update this table or
218
- fail.
219
 
220
  ---
221
 
@@ -244,7 +244,7 @@ fail.
244
  <!-- /generated:cli -->
245
 
246
  Each command supports `--help` for full options. See
247
- [`docs/cli-workflows.md`](docs/cli-workflows.md) for end-to-end
248
  examples.
249
 
250
  ---
@@ -299,7 +299,7 @@ client generation.
299
 
300
  Picarones ships **11 built-in normalization profiles** for historical
301
  text comparison (defined in
302
- [`picarones/measurements/normalization.py`](picarones/measurements/normalization.py),
303
  exposed via `/api/normalization/profiles`):
304
 
305
  `nfc`, `caseless`, `minimal`, `medieval_french`,
@@ -309,7 +309,7 @@ exposed via `/api/normalization/profiles`):
309
 
310
  Custom profiles can be loaded from YAML files with user-defined
311
  diplomatic tables and `exclude_chars` sets. See
312
- [`docs/profiles.md`](docs/profiles.md).
313
 
314
  A traceability table mapping each profile to its source standard
315
  (MUFI v4.0, TEI P5, DEAF) will ship in Sprint A12 (B-6).
@@ -320,24 +320,23 @@ A traceability table mapping each profile to its source standard
320
 
321
  ```
322
  picarones/
323
- ├── core/ Cercle 1 — pure abstractions (7 modules)
324
- ├── measurements/ Cercle 2 — official metrics (~70 modules + narrative engine)
325
- ├── engines/ Cercle 25 OCR adapters
326
- ├── llm/ Cercle 24 LLM adapters
327
- ├── pipelines/ Cercle 2 — OCR+LLM pipelines
328
- ├── modules/ Cercle 2official BaseModule modules
329
- ├── extras/ Cercle 3plugins (importers, historical)
330
- ── report/ Cercle 3HTML rendering
331
- ├── cli/ Cercle 3 — Click CLI (15 commands)
332
- ├── web/ Cercle 3 — FastAPI app + 11 routers
333
- ├── prompts/ 8 versioned prompt templates
334
- └── data/ Indicative tables (pricing.yaml)
335
  ```
336
 
337
- Strict 3-circle architecture: imports flow only from outer to inner.
338
- Enforced by `tests/core/test_circle_dependencies.py` (Sprint A3).
339
- See [`docs/architecture.md`](docs/architecture.md) for the full
340
- manifesto.
 
 
 
341
 
342
  ---
343
 
@@ -396,7 +395,7 @@ ruff check picarones/ tests/
396
  python -m mypy picarones/core/
397
  ```
398
 
399
- **Test suite**: ~5030 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
@@ -456,11 +455,11 @@ experimental demonstrator and the CLI as the supported interface.
456
 
457
  | Audience | Entry point |
458
  |----------|-------------|
459
- | **End user** | [`docs/user/reading-a-report.md`](docs/user/reading-a-report.md) ([EN](docs/user/reading-a-report.en.md)) |
460
  | **Developer** | [`docs/developer/index.md`](docs/developer/index.md) ([EN](docs/developer/index.en.md)) |
461
  | **Operations / DSI** | [`docs/operations/deployment-institutional.md`](docs/operations/deployment-institutional.md), [`docs/operations/data-retention-rgpd.md`](docs/operations/data-retention-rgpd.md), [`docs/operations/release-process.md`](docs/operations/release-process.md) |
462
- | **Architect** | [`docs/architecture.md`](docs/architecture.md), [`docs/api-stable.md`](docs/api-stable.md) |
463
- | **Researcher** | [`docs/case-studies/`](docs/case-studies/), [`docs/reproducibility-snapshots.md`](docs/reproducibility-snapshots.md) |
464
  | **Contributor** | [`CONTRIBUTING.md`](CONTRIBUTING.md), [`GOVERNANCE.md`](GOVERNANCE.md), [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) |
465
  | **Security** | [`SECURITY.md`](SECURITY.md) |
466
  | **Accessibility** | [`ACCESSIBILITY.md`](ACCESSIBILITY.md) |
@@ -477,7 +476,7 @@ 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
  ---
 
102
  trend with change-point detection; controlled per-slot ANOVA-like
103
  comparison.
104
 
105
+ For the full list with definitions, see [`docs/reference/views.md`](docs/reference/views.md)
106
  and the contextual glossary embedded in every report (25 bilingual
107
  entries).
108
 
 
189
  ```
190
 
191
  For Docker, institutional deployment, or HuggingFace Spaces, see
192
+ [`docs/how-to/install.md`](docs/how-to/install.md) and
193
  [`docs/operations/deployment-institutional.md`](docs/operations/deployment-institutional.md).
194
 
195
  ---
 
210
 
211
  LLM/VLM adapters (used through pipelines, not as standalone OCR
212
  engines): GPT-4o, Claude, Mistral Large, Ollama (local). See
213
+ [`docs/how-to/cli-workflows.md`](docs/how-to/cli-workflows.md).
214
 
215
  The `Engine` table is regenerated automatically by
216
  `scripts/gen_readme_tables.py` — adding a new adapter under
217
+ `picarones/adapters/legacy_engines/` makes the next CI run update
218
+ this table or fail.
219
 
220
  ---
221
 
 
244
  <!-- /generated:cli -->
245
 
246
  Each command supports `--help` for full options. See
247
+ [`docs/how-to/cli-workflows.md`](docs/how-to/cli-workflows.md) for end-to-end
248
  examples.
249
 
250
  ---
 
299
 
300
  Picarones ships **11 built-in normalization profiles** for historical
301
  text comparison (defined in
302
+ [`picarones/formats/text/normalization.py`](picarones/formats/text/normalization.py),
303
  exposed via `/api/normalization/profiles`):
304
 
305
  `nfc`, `caseless`, `minimal`, `medieval_french`,
 
309
 
310
  Custom profiles can be loaded from YAML files with user-defined
311
  diplomatic tables and `exclude_chars` sets. See
312
+ [`docs/reference/normalization-profiles.md`](docs/reference/normalization-profiles.md).
313
 
314
  A traceability table mapping each profile to its source standard
315
  (MUFI v4.0, TEI P5, DEAF) will ship in Sprint A12 (B-6).
 
320
 
321
  ```
322
  picarones/
323
+ ├── domain/ Layer 1 — pure types (Pydantic, stdlib only)
324
+ ├── formats/ Layer 2 — parsing/serialization (ALTO, PAGE XML)
325
+ ├── evaluation/ Layer 3metrics & analyses
326
+ ├── pipeline/ Layer 4canonical pipeline executor
327
+ ├── adapters/ Layer 5external libs (OCR, LLM, VLM, corpus)
328
+ ├── app/ Layer 6application services
329
+ ├── reports_v2/ Layer 7HTML / JSON / CSV report renderers
330
+ ── interfaces/ Layer 8CLI Click, Web FastAPI
 
 
 
 
331
  ```
332
 
333
+ Legacy paths (`core/, measurements/, engines/, llm/, pipelines/,
334
+ report/, modules/`) still present as shims, in active retirement
335
+ (see `docs/migration/`). Strict 8-layer architecture: imports flow
336
+ outer → inner. Enforced by
337
+ `tests/architecture/test_layer_dependencies.py`. See
338
+ [`docs/explanation/architecture.md`](docs/explanation/architecture.md)
339
+ for the full manifesto.
340
 
341
  ---
342
 
 
395
  python -m mypy picarones/core/
396
  ```
397
 
398
+ **Test suite**: ~5000 tests, ~3 min on a modern laptop. Coverage
399
  floor at 85% (currently ~87%). The `network` marker excludes tests
400
  requiring live HTTP. A handful of tests depend on optional engines
401
  (`pero-ocr`, `pytesseract`) and are skipped/fail gracefully when
 
455
 
456
  | Audience | Entry point |
457
  |----------|-------------|
458
+ | **End user** | [`docs/tutorials/reading-a-report.md`](docs/tutorials/reading-a-report.md) ([EN](docs/tutorials/reading-a-report.en.md)) |
459
  | **Developer** | [`docs/developer/index.md`](docs/developer/index.md) ([EN](docs/developer/index.en.md)) |
460
  | **Operations / DSI** | [`docs/operations/deployment-institutional.md`](docs/operations/deployment-institutional.md), [`docs/operations/data-retention-rgpd.md`](docs/operations/data-retention-rgpd.md), [`docs/operations/release-process.md`](docs/operations/release-process.md) |
461
+ | **Architect** | [`docs/explanation/architecture.md`](docs/explanation/architecture.md), [`docs/reference/api-stable.md`](docs/reference/api-stable.md) |
462
+ | **Researcher** | [`docs/case-studies/`](docs/case-studies/), [`docs/reference/reproducibility-snapshots.md`](docs/reference/reproducibility-snapshots.md) |
463
  | **Contributor** | [`CONTRIBUTING.md`](CONTRIBUTING.md), [`GOVERNANCE.md`](GOVERNANCE.md), [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) |
464
  | **Security** | [`SECURITY.md`](SECURITY.md) |
465
  | **Accessibility** | [`ACCESSIBILITY.md`](ACCESSIBILITY.md) |
 
476
  Cite the GitHub repository with the commit SHA used in your benchmark.
477
  Every Picarones report embeds the commit hash and a snapshot of the
478
  parameters used (cf.
479
+ [`docs/reference/reproducibility-snapshots.md`](docs/reference/reproducibility-snapshots.md))
480
  so the cited commit is sufficient to attribute the result.
481
 
482
  ---
SECURITY.en.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- translation: machine + human review pending -->
2
+
3
+ # SECURITY — Picarones (English)
4
+
5
+ > French version: [`SECURITY.md`](SECURITY.md) (canonical).
6
+ > Detailed threat model: [`docs/security/threat-model.md`](docs/security/threat-model.md).
7
+ >
8
+ > This is a summary translation focused on what an English-speaking
9
+ > auditor needs. The canonical FR version remains authoritative
10
+ > for institutional sign-off. Full alignment scheduled for a
11
+ > dedicated human-review sprint.
12
+
13
+ ## Reporting a vulnerability
14
+
15
+ If you discover a security vulnerability in Picarones, please **do
16
+ not file a public GitHub issue**. Instead, use one of the following
17
+ private channels:
18
+
19
+ - **GitHub Security Advisories** (preferred):
20
+ https://github.com/maribakulj/Picarones/security/advisories/new
21
+ - **`/.well-known/security.txt`** on any institutional deployment
22
+ (RFC 9116) — the contact address is documented there.
23
+
24
+ We acknowledge reports within **72 hours** and aim to ship a fix
25
+ within **30 days** for HIGH severity issues, **90 days** for MEDIUM.
26
+ A coordinated disclosure agreement is offered for non-trivial issues.
27
+
28
+ ## Supported versions
29
+
30
+ | Version | Status | Security fixes |
31
+ |---------|--------|----------------|
32
+ | 1.x (current) | Active | Yes |
33
+ | 0.x | End of life | No — please upgrade |
34
+ | Pre-release branches | Best effort | On request |
35
+
36
+ ## Deployment contexts
37
+
38
+ Picarones is designed for three deployment contexts:
39
+
40
+ 1. **Developer machine** (Codespaces, laptop) — local access only,
41
+ relaxed defaults to keep iteration fast.
42
+ 2. **Institutional server** (intranet, scientific cluster) —
43
+ authenticated internal access, with cost guards (rate limit, body
44
+ size limit, max concurrent jobs).
45
+ 3. **Public space** (HuggingFace Space, online demo) — anyone can
46
+ reach the API; cloud API keys (OpenAI, Anthropic, Mistral, Azure…)
47
+ must NOT be exposed to financial DoS.
48
+
49
+ ## Security controls — quick reference
50
+
51
+ | Variable | Default | Effect |
52
+ |----------|---------|--------|
53
+ | `PICARONES_PUBLIC_MODE` | off | If `1`/`true`, refuses cloud OCR/LLM with shared keys and enables rate limit |
54
+ | `PICARONES_MAX_UPLOAD_MB` | `100` | Max upload size in MiB |
55
+ | `PICARONES_MAX_CONCURRENT_JOBS` | `2` | Max parallel benchmark jobs (in-process semaphore) |
56
+ | `PICARONES_RATE_LIMIT_PER_HOUR` | `5` (public mode) | Max jobs per IP per hour, `0` disables |
57
+ | `PICARONES_CSP` | hardened policy | Override Content-Security-Policy |
58
+ | `PICARONES_CSRF_REQUIRED` | off | If `1`/`true`, enables CSRF protection (double-submit cookie + HMAC) |
59
+ | `PICARONES_CSRF_SECRET` | auto | HMAC secret for CSRF tokens; **must be set in production** |
60
+
61
+ ## In-process middlewares
62
+
63
+ The `picarones.interfaces.web.security` module provides four
64
+ middlewares that institutional operators wire via `create_app(...)`:
65
+
66
+ - `SecurityHeadersMiddleware` — adds CSP, X-Frame-Options,
67
+ X-Content-Type-Options, Referrer-Policy, Permissions-Policy to
68
+ every response.
69
+ - `BodySizeLimitMiddleware` — rejects requests where
70
+ `Content-Length` exceeds a threshold. **Known limitation**: does
71
+ not catch `Transfer-Encoding: chunked`; nginx
72
+ `client_max_body_size` is recommended in front.
73
+ - `RateLimitMiddleware` — sliding window, in-memory,
74
+ `trust_proxy_count: int` for safe `X-Forwarded-For` parsing,
75
+ LRU eviction on `max_clients=10000` to bound memory.
76
+ - `AuthenticationMiddleware` — opt-in wrapper around an
77
+ `AuthenticationBackend` Protocol; the institution plugs in its
78
+ SSO/LDAP/JWT scheme.
79
+
80
+ ## Audit trail
81
+
82
+ Sensitive job mutations (`POST /api/jobs`, `DELETE /api/jobs/{id}`)
83
+ emit a structured `[audit]` log line at INFO level with the source
84
+ IP, ready to be ingested by the institution's SIEM.
85
+
86
+ ## Reproducibility and integrity
87
+
88
+ `RunManifest` is byte-deterministic (`model_dump_json` with ordered
89
+ fields). The SHA-256 hash of a manifest can be cited in a scientific
90
+ publication to anchor the run. Cryptographic signing of manifests
91
+ (Sigstore) is on the roadmap.
92
+
93
+ ## Cloud API key management
94
+
95
+ Cloud keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `MISTRAL_API_KEY`,
96
+ `GOOGLE_APPLICATION_CREDENTIALS`, `AZURE_DOC_INTEL_*`) are read from
97
+ environment variables only. Adapters never log keys. For
98
+ institutional deployments, source the env from a secrets vault
99
+ (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, etc.) at
100
+ process startup.
101
+
102
+ See also [`docs/operations/runbook.md`](docs/operations/runbook.md)
103
+ for incident response and [`docs/legal/dpa-template.md`](docs/legal/dpa-template.md)
104
+ for the data processing agreement template covering cloud
105
+ sub-processors.
106
+
107
+ ## Last revised
108
+
109
+ 2026-05. This document is reviewed at every major release.
SPECS.md CHANGED
@@ -23,7 +23,7 @@
23
  ## Table des matières
24
 
25
  1. [Vision et positionnement](#1-vision-et-positionnement)
26
- 2. [Architecture en 3 cercles](#2-architecture-en-3-cercles)
27
  3. [Module 1 — Corpus et imports](#3-module-1--corpus-et-imports)
28
  4. [Module 2 — Adaptateurs OCR / HTR](#4-module-2--adaptateurs-ocr--htr)
29
  5. [Module 3 — Pipelines OCR+LLM et pipelines composables](#5-module-3--pipelines-ocrllm-et-pipelines-composables)
@@ -125,72 +125,108 @@ plusieurs briques nouvelles dans l'écosystème OCR/HTR open-source :
125
 
126
  ---
127
 
128
- ## 2. Architecture en 3 cercles
129
 
130
  ```
131
- Cercle 3 (extras, report, cli, web)
132
-
133
-
134
- Cercle 2 (measurements, engines, llm, pipelines, modules)
135
-
136
-
137
- Cercle 1 (core)
138
  ```
139
 
140
  **Règle de dépendance** : les imports vont uniquement de
141
- l'extérieur vers l'intérieur. Aucun shim un module a un seul
142
- emplacement. La règle est appliquée par
143
- `tests/core/test_circle_dependencies.py` (Sprint A3) qui parse
144
  l'AST de chaque fichier et bloque toute violation au merge.
145
 
146
- ### 2.1 Cercle 1 abstractions pures
147
-
148
- 7 modules dans `picarones/core/` :
149
-
150
- - `corpus.py` `Document`, `Corpus`, `GTLevel.{TEXT,ALTO,PAGE,ENTITIES,READING_ORDER}`,
151
- payloads typés, loader auto-détectant les fichiers `.gt.alto.xml`,
152
- `.gt.page.xml`, `.gt.entities.json`, `.gt.reading_order.json`.
153
- - `modules.py` `BaseModule`, `ArtifactType`. Interface commune
154
- à OCR, mappeurs, rewriters, classifieurs.
155
- - `metric_registry.py` — `MetricSpec`, `@register_metric`,
156
- `select_metrics`, `compute_at_junction`. Sélection par signature
157
- de types exacte (pas de coercion).
158
- - `metric_hooks.py` — registre legacy compatible (Sprint 16-).
159
- - `metrics.py` `MetricsResult`, `aggregate_metrics`.
160
- - `results.py` — `DocumentResult`, `EngineReport`, `BenchmarkResult`,
161
- sérialisation JSON.
162
- - `facts.py` `Fact`, `FactType` (20 entrées), `FactImportance`,
163
- `DetectorRegistry`. Modèle de données du moteur narratif.
164
- - `diff_utils.py` `compute_word_diff`, `compute_char_diff`,
165
- `diff_stats` (déplacé Cercle 3 → Cercle 1 en A3).
166
- - `pipeline.py` `PipelineRunner`, `PipelineSpec`, `PipelineStep`.
167
- - `xml_utils.py` `safe_parse_xml` (defusedxml).
168
-
169
- ### 2.2 Cercle 2 — logique métier
170
-
171
- 5 sous-packages :
172
-
173
- - `measurements/` ~70 modules de calcul de métriques + le
174
- moteur narratif (`narrative/` avec arbiter, registry, renderer,
175
- 20 détecteurs en 6 familles).
176
- - `engines/` — adaptateurs OCR : Tesseract, Pero OCR, Mistral OCR,
177
- Google Vision, Azure Document Intelligence (5 adapters).
178
- - `llm/` adaptateurs LLM : OpenAI, Anthropic, Mistral, Ollama
179
- (4 adapters).
180
- - `pipelines/` — orchestration OCR+LLM (3 modes historiques).
181
- - `modules/` — modules `BaseModule` officiels (ALTO text→region
182
- mappers).
183
-
184
- ### 2.3 Cercle 3 — entrées et rendu
185
-
186
- - `report/` générateur HTML, ~25 modules de rendu, vendor
187
- Chart.js, templates Jinja2 (10 partials), i18n FR/EN, glossaire
188
- contextuel (25 entrées bilingues).
189
- - `cli/` Click CLI (15 commandes) en package `picarones/cli/`.
190
- - `web/` FastAPI (app + 11 routers + sécurité + jobs SQLite +
191
- maintenance auto-purge).
192
- - `extras/` plugins : importers (IIIF, Gallica, HTR-United, HF
193
- Datasets, eScriptorium), modules historiques.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  ---
196
 
@@ -263,7 +299,7 @@ Endpoint `POST /api/corpus/upload`. Validation Pillow
263
  ### 4.1 Architecture des adaptateurs
264
 
265
  Chaque moteur OCR est une classe Python qui hérite de
266
- `BaseOCREngine` (`picarones/engines/base.py`), elle-même héritière
267
  de `BaseModule` (Sprint 33). Une instance déclare son
268
  `execution_mode` (`"io"` ou `"cpu"`) que le runner utilise pour
269
  choisir entre `ThreadPoolExecutor` (cloud APIs) et
@@ -431,7 +467,7 @@ canonique (champ `reference`).
431
 
432
  ### 6.2 Profils de normalisation
433
 
434
- 11 profils livrés (`picarones/measurements/normalization.py`,
435
  exposés via `/api/normalization/profiles`) : `nfc`, `caseless`,
436
  `minimal`, `medieval_french`, `early_modern_french`,
437
  `medieval_latin`, `medieval_english`, `early_modern_english`,
@@ -649,7 +685,7 @@ qui contient :
649
  git, paquets installés (top 200).
650
 
651
  Procédure complète de re-jeu d'un benchmark à 5 ans d'écart :
652
- [`docs/reproducibility-snapshots.md`](docs/reproducibility-snapshots.md)
653
  (Sprint A8 / M-12).
654
 
655
  ### 9.2 Reproductibilité des builds
 
23
  ## Table des matières
24
 
25
  1. [Vision et positionnement](#1-vision-et-positionnement)
26
+ 2. [Architecture en 8 couches concentriques](#2-architecture-en-8-couches-concentriques)
27
  3. [Module 1 — Corpus et imports](#3-module-1--corpus-et-imports)
28
  4. [Module 2 — Adaptateurs OCR / HTR](#4-module-2--adaptateurs-ocr--htr)
29
  5. [Module 3 — Pipelines OCR+LLM et pipelines composables](#5-module-3--pipelines-ocrllm-et-pipelines-composables)
 
125
 
126
  ---
127
 
128
+ ## 2. Architecture en 8 couches concentriques
129
 
130
  ```
131
+ domain formats evaluation → pipeline → adapters → app → reports_v2 → interfaces
 
 
 
 
 
 
132
  ```
133
 
134
  **Règle de dépendance** : les imports vont uniquement de
135
+ l'extérieur vers l'intérieur (de gauche à droite dans le
136
+ diagramme). La règle est appliquée par
137
+ `tests/architecture/test_layer_dependencies.py` qui parse
138
  l'AST de chaque fichier et bloque toute violation au merge.
139
 
140
+ > **Note sur le legacy** : le projet est en cours de retrait
141
+ > du legacy. Une arborescence historique
142
+ > (``picarones/{core,measurements,engines,llm,pipelines,
143
+ > report,modules}``) cohabite encore et est en train de
144
+ > disparaître phase par phase. Cf.
145
+ > [`docs/migration/legacy-retirement-plan.md`](docs/migration/legacy-retirement-plan.md)
146
+ > pour le statut et le calendrier. Tout nouveau code va
147
+ > dans l'arborescence canonique ; les chemins legacy
148
+ > existants sont des shims minimaux destinés à être
149
+ > supprimés.
150
+
151
+ ### 2.1 `picarones/domain/` types purs
152
+
153
+ Cercle le plus interne. Stdlib + Pydantic uniquement, aucune
154
+ I/O, aucun framework, aucun module legacy.
155
+
156
+ | Module | Contenu |
157
+ |---|---|
158
+ | `artifacts.py` | `Artifact`, `ArtifactType` (10 types : IMAGE, RAW_TEXT, CORRECTED_TEXT, ALTO_XML, PAGE_XML, CANONICAL_DOCUMENT, ENTITIES, READING_ORDER, ALIGNMENT, CONFIDENCES) |
159
+ | `corpus.py` | `CorpusSpec` |
160
+ | `documents.py` | `DocumentRef` |
161
+ | `evaluation_spec.py` | `MetricSpec`, `EvaluationView`, `EvaluationSpec` |
162
+ | `pipeline_spec.py` | `PipelineSpec`, `PipelineStep`, `INITIAL_STEP_ID` |
163
+ | `projection_spec.py` | `ProjectionSpec` |
164
+ | `provenance.py` | `ProvenanceRecord` |
165
+ | `run_manifest.py` | `RunManifest` |
166
+ | `module_protocol.py` | `BaseModule` (ABC, voie de retrait au profit de `StepExecutor`) |
167
+ | `facts.py` | `Fact`, `FactType`, `FactImportance`, `DetectorRegistry` |
168
+ | `errors.py` | Hiérarchie d'exceptions (`PicaronesError`, `AdapterStepError`, …) |
169
+
170
+ ### 2.2 `picarones/formats/` — parsing / sérialisation
171
+
172
+ ALTO 4, PAGE XML, JSON, XML utilitaires. Stdlib + lxml +
173
+ defusedxml. Pas de logique métier.
174
+
175
+ ### 2.3 `picarones/evaluation/` — métriques et calcul
176
+
177
+ Cœur de la valeur ajoutée. Stdlib + numpy + scipy + jiwer +
178
+ spacy + rapidfuzz.
179
+
180
+ | Sous-paquet | Contenu |
181
+ |---|---|
182
+ | `metrics/` | ~30 métriques (CER, WER, MUFI, philological, NER, calibration, taxonomy, …) |
183
+ | `statistics/` | Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto, clustering, CDD |
184
+ | `views/`, `projectors/` | EvaluationView (Sprint S13+), projecteurs `AltoToText`, `PageToText`, `CanonicalToText` |
185
+ | `corpus.py` | `Document`, `Corpus`, `GTLevel`, payloads (legacy en cours de retrait) |
186
+ | `metric_registry.py`, `metric_hooks.py`, `metric_result.py` | Registres typés + hooks + dataclasses résultats |
187
+ | `pipeline.py`, `pipeline_benchmark.py`, `pipeline_comparison.py` | `PipelineRunner` legacy + orchestration corpus-wide (en cours de convergence vers `pipeline.executor`) |
188
+ | `benchmark_result.py` | `BenchmarkResult`, `EngineReport`, `DocumentResult`, sérialisation JSON |
189
+ | `engines/` | OCR engines legacy (`BaseOCREngine`-based) — temporairement avant suppression complète |
190
+ | `_diff_utils.py` | `compute_word_diff`, `compute_char_diff`, `diff_stats` |
191
+
192
+ ### 2.4 `picarones/pipeline/` — orchestration canonique
193
+
194
+ `PipelineExecutor` instance-based, `StepExecutor` Protocol,
195
+ `ExecutionPlan` immuable. Cible canonique pour le bench
196
+ d'axe B (pipelines composées).
197
+
198
+ ### 2.5 `picarones/adapters/` — adapters externes
199
+
200
+ Adapters OCR / LLM / VLM consommant des libs externes
201
+ (pytesseract, mistralai, openai, anthropic, google.cloud,
202
+ azure.*, pero_ocr, ollama). Implémentent `StepExecutor`.
203
+
204
+ | Sous-paquet | Contenu |
205
+ |---|---|
206
+ | `ocr/` | `TesseractAdapter`, `PeroOCRAdapter`, `MistralOCRAdapter`, `GoogleVisionAdapter`, `AzureDocIntelAdapter`, `PrecomputedAdapter` |
207
+ | `llm/` | `BaseLLMAdapter` + Mistral / OpenAI / Anthropic / Ollama |
208
+ | `vlm/` | Adapters VLM (zero-shot OCR via vision-language models) |
209
+ | `corpus/` | Loaders externes : IIIF, Gallica, HTR-United, HuggingFace |
210
+ | `storage/` | `ArtifactStore`, `JobStore` (S29 + S47) |
211
+ | `legacy_engines/`, `legacy_modules/` | Engines + modules legacy `BaseModule`-based (en cours de retrait, cf. Phase 7.A) |
212
+
213
+ ### 2.6 `picarones/app/` — services applicatifs
214
+
215
+ `BenchmarkService`, `CorpusRunner`, `RunOrchestrator`.
216
+ Orchestrent les pipelines canoniques sur corpus.
217
+
218
+ ### 2.7 `picarones/reports_v2/` — rendu HTML / JSON / CSV
219
+
220
+ Rapport final consommant un `BenchmarkResult` ou `RunResult`.
221
+ 22 renderers thématiques + 5 vues (advanced_taxonomy,
222
+ diagnostics, economics, pipeline, robustness) +
223
+ `ReportGenerator` orchestrateur + templates Jinja2 +
224
+ glossaire bilingue (25 entrées) + i18n FR/EN.
225
+
226
+ ### 2.8 `picarones/interfaces/` — entrées utilisateur
227
+
228
+ CLI Click, Web FastAPI, IIIF/Gallica/eScriptorium importers
229
+ exposés en interface.
230
 
231
  ---
232
 
 
299
  ### 4.1 Architecture des adaptateurs
300
 
301
  Chaque moteur OCR est une classe Python qui hérite de
302
+ `BaseOCREngine` (`picarones/adapters/legacy_engines/base.py`), elle-même héritière
303
  de `BaseModule` (Sprint 33). Une instance déclare son
304
  `execution_mode` (`"io"` ou `"cpu"`) que le runner utilise pour
305
  choisir entre `ThreadPoolExecutor` (cloud APIs) et
 
467
 
468
  ### 6.2 Profils de normalisation
469
 
470
+ 11 profils livrés (`picarones/formats/text/normalization.py`,
471
  exposés via `/api/normalization/profiles`) : `nfc`, `caseless`,
472
  `minimal`, `medieval_french`, `early_modern_french`,
473
  `medieval_latin`, `medieval_english`, `early_modern_english`,
 
685
  git, paquets installés (top 200).
686
 
687
  Procédure complète de re-jeu d'un benchmark à 5 ans d'écart :
688
+ [`docs/reference/reproducibility-snapshots.md`](docs/reference/reproducibility-snapshots.md)
689
  (Sprint A8 / M-12).
690
 
691
  ### 9.2 Reproductibilité des builds
docs/api/adapters.md ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `picarones.adapters` — implémentations concrètes
2
+
3
+ ## OCR
4
+
5
+ ::: picarones.adapters.ocr.base
6
+ options:
7
+ show_root_heading: true
8
+
9
+ ::: picarones.adapters.ocr.tesseract
10
+ options:
11
+ show_root_heading: true
12
+ members: ["TesseractAdapter"]
13
+
14
+ ::: picarones.adapters.ocr.pero_ocr
15
+ options:
16
+ show_root_heading: true
17
+ members: ["PeroOCRAdapter"]
18
+
19
+ ::: picarones.adapters.ocr.mistral_ocr
20
+ options:
21
+ show_root_heading: true
22
+ members: ["MistralOCRAdapter"]
23
+
24
+ ::: picarones.adapters.ocr.google_vision
25
+ options:
26
+ show_root_heading: true
27
+ members: ["GoogleVisionAdapter"]
28
+
29
+ ::: picarones.adapters.ocr.azure_doc_intel
30
+ options:
31
+ show_root_heading: true
32
+ members: ["AzureDocIntelAdapter"]
33
+
34
+ ## LLM
35
+
36
+ ::: picarones.adapters.llm.base
37
+ options:
38
+ show_root_heading: true
39
+ members: ["BaseLLMAdapter", "LLMAdapterError", "LLMResult", "normalize_llm_content"]
40
+
41
+ ::: picarones.adapters.llm.anthropic_adapter
42
+ options:
43
+ show_root_heading: true
44
+
45
+ ::: picarones.adapters.llm.openai_adapter
46
+ options:
47
+ show_root_heading: true
48
+
49
+ ::: picarones.adapters.llm.mistral_adapter
50
+ options:
51
+ show_root_heading: true
52
+
53
+ ::: picarones.adapters.llm.ollama_adapter
54
+ options:
55
+ show_root_heading: true
56
+
57
+ ## VLM
58
+
59
+ ::: picarones.adapters.vlm.base
60
+ options:
61
+ show_root_heading: true
62
+ members: ["BaseVLMAdapter", "VLMAdapterError"]
63
+
64
+ ## Storage
65
+
66
+ ::: picarones.adapters.storage.artifact_store
67
+ options:
68
+ show_root_heading: true
69
+
70
+ ::: picarones.adapters.storage.job_store
71
+ options:
72
+ show_root_heading: true
73
+
74
+ ## Helpers
75
+
76
+ ::: picarones.adapters.output_paths
77
+ options:
78
+ show_root_heading: true
79
+
80
+ ::: picarones.adapters._retry
81
+ options:
82
+ show_root_heading: true
docs/api/app.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `picarones.app` — services applicatifs
2
+
3
+ ## Schémas
4
+
5
+ ::: picarones.app.schemas.run_spec
6
+ options:
7
+ show_root_heading: true
8
+
9
+ ## Services
10
+
11
+ ::: picarones.app.services.run_orchestrator
12
+ options:
13
+ show_root_heading: true
14
+
15
+ ::: picarones.app.services.benchmark_service
16
+ options:
17
+ show_root_heading: true
18
+
19
+ ::: picarones.app.services.job_runner
20
+ options:
21
+ show_root_heading: true
22
+
23
+ ::: picarones.app.services.dependencies
24
+ options:
25
+ show_root_heading: true
26
+
27
+ ::: picarones.app.services.path_security
28
+ options:
29
+ show_root_heading: true
30
+
31
+ ::: picarones.app.services.registry_service
32
+ options:
33
+ show_root_heading: true
34
+
35
+ ## Résultats
36
+
37
+ ::: picarones.app.results
38
+ options:
39
+ show_root_heading: true
docs/api/domain.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `picarones.domain` — types purs
2
+
3
+ ::: picarones.domain.artifacts
4
+ options:
5
+ show_root_heading: true
6
+ members_order: source
7
+
8
+ ::: picarones.domain.documents
9
+ options:
10
+ show_root_heading: true
11
+
12
+ ::: picarones.domain.corpus
13
+ options:
14
+ show_root_heading: true
15
+
16
+ ::: picarones.domain.evaluation_spec
17
+ options:
18
+ show_root_heading: true
19
+
20
+ ::: picarones.domain.pipeline_spec
21
+ options:
22
+ show_root_heading: true
23
+
24
+ ::: picarones.domain.run_manifest
25
+ options:
26
+ show_root_heading: true
27
+
28
+ ::: picarones.domain.errors
29
+ options:
30
+ show_root_heading: true
docs/api/evaluation.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `picarones.evaluation` — métriques et vues
2
+
3
+ ## Vues
4
+
5
+ ::: picarones.evaluation.views.base
6
+ options:
7
+ show_root_heading: true
8
+
9
+ ::: picarones.evaluation.views.executor
10
+ options:
11
+ show_root_heading: true
12
+
13
+ ::: picarones.evaluation.views.text_view
14
+ options:
15
+ show_root_heading: true
16
+
17
+ ::: picarones.evaluation.views.alto_view
18
+ options:
19
+ show_root_heading: true
20
+
21
+ ::: picarones.evaluation.views.search_view
22
+ options:
23
+ show_root_heading: true
24
+
25
+ ## Registre
26
+
27
+ ::: picarones.evaluation.registry.registry
28
+ options:
29
+ show_root_heading: true
30
+
31
+ ## Projecteurs
32
+
33
+ ::: picarones.evaluation.projectors.base
34
+ options:
35
+ show_root_heading: true
36
+
37
+ ::: picarones.evaluation.projectors.alto
38
+ options:
39
+ show_root_heading: true
40
+
41
+ ::: picarones.evaluation.projectors.canonical
42
+ options:
43
+ show_root_heading: true
44
+
45
+ ::: picarones.evaluation.projectors.pagexml
46
+ options:
47
+ show_root_heading: true
docs/api/index.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Reference (auto-générée)
2
+
3
+ > **Audience** : développeur tiers, contributeur, mainteneur. Cette
4
+ > référence est **générée automatiquement** depuis les docstrings du
5
+ > code par [mkdocstrings](https://mkdocstrings.github.io/), au build
6
+ > du site de documentation.
7
+ >
8
+ > Pour la **politique de stabilité** de l'API publique (semver,
9
+ > deprecation periods, symboles cibles), voir
10
+ > [`../reference/api-stable.md`](../reference/api-stable.md).
11
+ >
12
+ > Pour l'**architecture** et le **pourquoi** des choix de design,
13
+ > voir [`../explanation/architecture.md`](../explanation/architecture.md).
14
+
15
+ ## Build local
16
+
17
+ ```bash
18
+ pip install -e ".[docs]"
19
+ mkdocs serve # hot-reload sur http://localhost:8000
20
+ ```
21
+
22
+ ou
23
+
24
+ ```bash
25
+ mkdocs build # site statique dans site/
26
+ ```
27
+
28
+ ## Structure
29
+
30
+ L'API publique est groupée par cercle architectural :
31
+
32
+ | Cercle | Référence |
33
+ |--------|-----------|
34
+ | Domain (types purs) | [`domain.md`](domain.md) |
35
+ | Pipeline (orchestration) | [`pipeline.md`](pipeline.md) |
36
+ | Evaluation (métriques + vues) | [`evaluation.md`](evaluation.md) |
37
+ | Adapters (OCR/LLM/VLM) | [`adapters.md`](adapters.md) |
38
+ | App services (orchestrateur, jobs) | [`app.md`](app.md) |
39
+
40
+ ## Stabilité
41
+
42
+ Tous les symboles documentés ici sont de l'**API publique** ce qui
43
+ signifie :
44
+
45
+ - Suivent semver — un retrait nécessite une release majeure et une
46
+ deprecation period d'au moins une release mineure (`DeprecationWarning`
47
+ émis depuis la version N, suppression en N+2 majeure).
48
+ - Sont vérifiés par `tests/core/test_public_api_signatures.py`.
49
+
50
+ Les symboles **privés** (préfixe `_` ou non listés dans `__all__`)
51
+ peuvent changer sans préavis.
docs/api/pipeline.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # `picarones.pipeline` — orchestration mono-document
2
+
3
+ ::: picarones.pipeline.executor
4
+ options:
5
+ show_root_heading: true
6
+
7
+ ::: picarones.pipeline.planner
8
+ options:
9
+ show_root_heading: true
10
+
11
+ ::: picarones.pipeline.runner
12
+ options:
13
+ show_root_heading: true
14
+
15
+ ::: picarones.pipeline.validation
16
+ options:
17
+ show_root_heading: true
18
+
19
+ ::: picarones.pipeline.types
20
+ options:
21
+ show_root_heading: true
22
+
23
+ ::: picarones.pipeline.protocols
24
+ options:
25
+ show_root_heading: true
docs/architecture.md DELETED
@@ -1,179 +0,0 @@
1
- # Architecture Picarones — manifeste
2
-
3
- Picarones est un **banc d'essai** pour pipelines OCR/HTR sur documents
4
- patrimoniaux. Le code est organisé en **3 cercles concentriques** avec
5
- une règle de dépendance stricte : les flèches d'import vont uniquement
6
- de l'extérieur vers l'intérieur.
7
-
8
- ```
9
- Cercle 3 (extras, report, cli, web)
10
-
11
-
12
- Cercle 2 (measurements, engines, llm, pipelines, modules)
13
-
14
-
15
- Cercle 1 (core)
16
- ```
17
-
18
- ## Cercle 1 — `picarones/core/` : abstractions de domaine
19
-
20
- Pas de logique métier, pas d'I/O. Uniquement des **contrats** que les
21
- cercles supérieurs implémentent.
22
-
23
- | Module | Contenu |
24
- |---|---|
25
- | `modules.py` | `BaseModule`, `ArtifactType`, `validate_inputs`/`validate_outputs` |
26
- | `corpus.py` | `Document`, `Corpus`, `GTLevel`, payloads typés (`TextGT`, `AltoGT`, `PageGT`, `EntitiesGT`, `ReadingOrderGT`) |
27
- | `results.py` | `DocumentResult`, `EngineReport`, `BenchmarkResult` |
28
- | `metric_registry.py` | `MetricSpec`, `register_metric`, `select_metrics`, `compute_at_junction` |
29
- | `metric_hooks.py` | `register_document_metric`, `register_corpus_aggregator`, profils de calcul |
30
- | `pipeline.py` | `PipelineRunner`, `PipelineSpec`, `PipelineStep` (DAG de modules) |
31
- | `facts.py` | `Fact`, `FactType`, `FactImportance`, `DetectorRegistry` |
32
-
33
- **Règle** : un module du cercle 1 peut importer un autre module du
34
- cercle 1. Il ne peut **rien** importer des cercles 2 ou 3.
35
-
36
- ## Cercle 2 — implémentations officielles
37
-
38
- Les implémentations distribuées par défaut dans le package `picarones`.
39
-
40
- ### `picarones/measurements/` — métriques (~50 modules)
41
-
42
- | Catégorie | Modules |
43
- |---|---|
44
- | Coeur | `metrics.py`, `statistics/` (sous-package), `runner.py`, `builtin_hooks.py`, `builtin_metrics.py`, `normalization.py` |
45
- | Erreurs | `confusion.py`, `taxonomy.py`, `taxonomy_comparison.py`, `taxonomy_cooccurrence.py`, `taxonomy_intra_doc.py` |
46
- | Lignes/structure | `line_metrics.py`, `structure.py`, `worst_lines.py`, `char_scores.py` |
47
- | Calibration/fiabilité | `calibration.py`, `reliability.py`, `hallucination.py` |
48
- | Image | `image_quality.py`, `image_predictive.py`, `difficulty.py` |
49
- | Robustesse | `robustness.py`, `robustness_projection.py` |
50
- | Inter-moteurs | `inter_engine.py`, `specialization.py` |
51
- | Statistique avancée | `baseline_comparison.py`, `longitudinal.py`, `incremental_comparison.py` |
52
- | Contenu | `searchability.py`, `numerical_sequences.py`, `rare_tokens.py`, `readability.py` |
53
- | Structure ALTO | `layout.py`, `reading_order.py`, `ner.py`, `ner_backends.py`, `error_absorption.py` |
54
- | Économie | `cost_projection.py`, `marginal_cost.py`, `pricing.py`, `throughput.py` |
55
- | Philologie historique | `mufi.py`, `abbreviations.py`, `unicode_blocks.py`, `early_modern_typography.py`, `modern_archives.py`, `roman_numerals.py`, `lexical_modernization.py`, `philological_runner.py` |
56
- | Pipelines composées | `pipeline_benchmark.py`, `pipeline_comparison.py`, `pipeline_spec_loader.py`, `alto_metrics.py` |
57
- | Divers | `equivalence_profile.py`, `levers.py`, `module_policy.py`, `history.py` |
58
- | Runners adaptifs | `readability_runner.py`, `searchability_runner.py`, `numerical_sequences_runner.py` |
59
- | Narratif | `narrative/` (arbiter, renderer, registry, 18 détecteurs en 6 familles) |
60
-
61
- ### `picarones/engines/` — adapters OCR (5)
62
-
63
- `tesseract.py`, `pero_ocr.py`, `mistral_ocr.py`, `google_vision.py`,
64
- `azure_doc_intel.py`. Tous héritent de `picarones.core.engine.BaseOCREngine`
65
- (qui vit dans `engines/base.py` pour la lisibilité).
66
-
67
- ### `picarones/llm/` — adapters LLM (4)
68
-
69
- `mistral_adapter.py`, `openai_adapter.py`, `anthropic_adapter.py`,
70
- `ollama_adapter.py`. Interface commune dans `base.py`.
71
-
72
- ### `picarones/pipelines/` — pipelines OCR+LLM intégrés
73
-
74
- `base.py` (`OCRLLMPipeline`, qui hérite de `BaseOCREngine`),
75
- `over_normalization.py`.
76
-
77
- ### `picarones/modules/` — modules `BaseModule` officiels
78
-
79
- Démonstrateurs qui prouvent l'axe B du plan d'évolution :
80
- `alto_text_to_mono_region.py`.
81
-
82
- ## Cercle 3 — extensions et présentation
83
-
84
- ### `picarones/extras/importers/` — connecteurs corpus
85
-
86
- `iiif.py`, `gallica.py`, `htr_united.py`, `huggingface.py`,
87
- `escriptorium.py`, `_http.py`. Plugins pluggable, certains expérimentaux.
88
-
89
- ### `picarones/report/` — rendu HTML
90
-
91
- | Sous-dossier | Contenu |
92
- |---|---|
93
- | `generator.py` | Orchestration Jinja2 |
94
- | `views/` | 5 vues thématiques (economics, advanced_taxonomy, diagnostics, pipeline, robustness) |
95
- | `templates/` | Jinja2 (base, header, footer, vues, partials) |
96
- | `i18n/` | FR/EN |
97
- | `glossary/` | 25 entrées bilingues |
98
- | `vendor/` | Chart.js |
99
- | `*_render.py` | ~22 renderers (calibration, NER, Pareto, Sankey, etc.) |
100
-
101
- Pas de sous-dossier `extras/render/` — tout le rendu est ici.
102
-
103
- ### `picarones/cli/` — Click (7 fichiers)
104
-
105
- Point d'entrée `picarones.cli:cli` (référencé dans `pyproject.toml`).
106
- 15 sous-commandes : `run`, `diagnose`, `economics`, `edition`,
107
- `compare`, `metrics`, `engines`, `info`, `report`, `demo`, `serve`,
108
- `history`, `robustness`, `pipeline run/compare`, `import`.
109
-
110
- ### `picarones/web/` — FastAPI
111
-
112
- Interface web (`app.py`).
113
-
114
- ## Données
115
-
116
- | Dossier | Rôle |
117
- |---|---|
118
- | `picarones/prompts/` | Prompts LLM versionnés (8 fichiers, FR + EN) |
119
- | `picarones/data/` | Tables indicatives (pricing, etc.) |
120
- | `picarones/fixtures.py` | Corpus de démonstration |
121
-
122
- ## Règles de migration
123
-
124
- 1. **Pas de shim** : un module a un seul emplacement physique. Les
125
- imports pointent directement vers la vraie source.
126
- 2. **Pas de double API** : une fonction a un seul nom canonique. Les
127
- alias historiques sont supprimés et les tests mis à jour.
128
- 3. **Frontières strictes** : si un module Y du cercle N importe le
129
- module X, alors le cercle de X est ≤ N. Une exception
130
- pragmatique : `engines/base.py` est conceptuellement cercle 1
131
- mais physiquement dans `engines/` pour rester avec ses
132
- implémentations.
133
- 4. **Les dépendances optionnelles** (`scipy`, `spacy`, etc.) sont
134
- gérées par try/except à l'import — pas par shim.
135
-
136
- ## Tests
137
-
138
- Organisés par cercle : `tests/core/`, `tests/measurements/`,
139
- `tests/engines/`, `tests/extras/`, `tests/report/`,
140
- `tests/integration/` (tests E2E croisant plusieurs cercles).
141
-
142
- Un test du cercle N **n'importe pas** les implémentations des
143
- cercles > N (sauf `tests/integration/`).
144
-
145
- ## Convention de découpage des modules > 400 lignes
146
-
147
- Quand un module Python dépasse 400 lignes ET contient plusieurs
148
- responsabilités disjointes, le découper en **sous-package** plutôt
149
- qu'en plusieurs modules à plat. Modèle de référence :
150
- [`picarones/measurements/statistics/`](../picarones/measurements/statistics/)
151
- issu du sprint « découpage de statistics.py » (mai 2026).
152
-
153
- Convention :
154
-
155
- 1. **Renommer** `X.py` en `X/__init__.py` via `git mv` (préserve
156
- l'historique du fichier original).
157
- 2. **Créer** dans `X/` un sous-module par famille fonctionnelle
158
- (`bootstrap.py`, `wilcoxon.py`, `friedman_nemenyi.py`, etc.).
159
- Chaque sous-module doit faire moins de ~400 lignes ; sinon
160
- re-décomposer.
161
- 3. **`X/__init__.py`** ne contient QUE des ré-exports rétrocompat —
162
- tous les symboles publics de l'ancien `X.py` doivent rester
163
- importables via `from picarones.X import …`. Les symboles privés
164
- ré-exportés doivent être ceux **réellement** consommés par les
165
- tests (vérifié par grep, pas par supposition).
166
- 4. **`__all__`** explicite dans chaque sous-module et dans le
167
- `__init__.py`.
168
- 5. **Tests architecture** (`tests/architecture/test_*.py`) doivent
169
- continuer à passer : si nécessaire, étendre `_measurements_modules()`
170
- ou `_imports_target_*` pour reconnaître les sous-packages.
171
- 6. **Préfixer les modules de rendu** par leur domaine
172
- (`cdd_render.py` plutôt que `render_cdd.py`) pour cohérence avec
173
- `picarones/report/*_render.py`.
174
-
175
- **Quand NE PAS découper** : si les responsabilités sont fortement
176
- couplées (ex: un orchestrateur qui appelle 12 sous-fonctions au
177
- même endroit), le maintien dans un seul fichier > 400 lignes est
178
- acceptable. Le budget par fichier (`tests/architecture/test_file_budgets.py`)
179
- documente ces dérogations conscientes.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/developer/extending-i18n.md CHANGED
@@ -48,7 +48,7 @@ automatiquement sur `fr` si une langue manque.
48
 
49
  ## Format YAML pour les templates narratifs
50
 
51
- Voir `docs/developer/narrative-engine.md` pour le détail. En bref :
52
 
53
  ```yaml
54
  fact_type_value: >-
 
48
 
49
  ## Format YAML pour les templates narratifs
50
 
51
+ Voir `docs/explanation/narrative-engine.md` pour le détail. En bref :
52
 
53
  ```yaml
54
  fact_type_value: >-
docs/developer/index.en.md CHANGED
@@ -13,7 +13,7 @@ module.
13
  ## Architecture
14
 
15
  Picarones uses a **3-circle architecture** (manifesto in
16
- [`docs/architecture.md`](../architecture.md)):
17
 
18
  ```
19
  Circle 3 (extras, report, cli, web)
 
13
  ## Architecture
14
 
15
  Picarones uses a **3-circle architecture** (manifesto in
16
+ [`docs/explanation/architecture.md`](../architecture.md)):
17
 
18
  ```
19
  Circle 3 (extras, report, cli, web)
docs/developer/index.md CHANGED
@@ -5,33 +5,49 @@ fondamentaux du projet.
5
 
6
  ## Architecture
7
 
8
- Voir [CLAUDE.md](../../CLAUDE.md) pour la cartographie complète des
9
- modules. En résumé :
 
 
10
 
11
  ```
12
  picarones/
13
- ├── core/ # cœur analytique pur Python (Cercle 1)
14
- │ ├── pipeline.py # PipelineRunner pour pipelines composées
15
- │ ├── corpus.py # Document, Corpus, GTLevel
16
- │ ├── results.py # DocumentResult, EngineReport, BenchmarkResult
17
- │ ├── modules.py # BaseModule, ArtifactType
 
18
  │ ├── facts.py # Fact, FactType, registre narratif
19
  │ └── …
20
- ├── measurements/ # métriques officielles (Cercle 2)
21
- ├── runner.py # orchestration ThreadPool/ProcessPool
22
- │ ├── metrics.py # CER/WER/MER/WIL via jiwer
23
- │ ├── statistics/ # Wilcoxon, Friedman, Nemenyi, Pareto
24
- │ (sous-package depuis le sprint « découpage statistics.py »)
25
- │ ├── narrative/ # moteur de synthèse factuelle
26
- │ ├── pricing.py # modèle de coût pour la vue Pareto
27
- │ └──
28
- ├── engines/ # adaptateurs OCR (Tesseract, Pero, Mistral OCR…)
29
- ├── llm/ # adaptateurs LLM (OpenAI, Anthropic, Mistral, Ollama)
30
- ├── pipelines/ # OCRLLMPipeline (3 modes)
31
- ├── report/ # générateur HTML + templates Jinja2 + i18n + glossaire
32
- ── web/ # FastAPI + SPA vanilla JS
 
 
 
 
 
 
 
 
 
33
  ```
34
 
 
 
 
 
35
  ## Guides d'extension
36
 
37
  - [Étendre le moteur narratif](narrative-engine.md) — ajouter un type
 
5
 
6
  ## Architecture
7
 
8
+ Voir [CLAUDE.md](../../CLAUDE.md) et
9
+ [`docs/explanation/architecture.md`](../explanation/architecture.md)
10
+ pour la cartographie complète. En résumé : architecture **8
11
+ couches concentriques** (post-rewrite, canonique) :
12
 
13
  ```
14
  picarones/
15
+ ├── domain/ # Layer 1 types purs (Pydantic, stdlib only)
16
+ │ ├── artifacts.py # Artifact, ArtifactType (10 types)
17
+ │ ├── corpus.py # CorpusSpec
18
+ │ ├── documents.py # DocumentRef
19
+ │ ├── pipeline_spec.py # PipelineSpec, PipelineStep (Pydantic immutable)
20
+ │ ├── module_protocol.py # BaseModule (ABC, en cours de retrait au profit de StepExecutor)
21
  │ ├── facts.py # Fact, FactType, registre narratif
22
  │ └── …
23
+ ├── formats/ # Layer 2 — parsing/serialization (ALTO 4, PAGE XML, JSON)
24
+ ├── evaluation/ # Layer 3 — métriques et calcul
25
+ │ ├── metrics/ # ~30 métriques (CER/WER, MUFI, philological, NER, …)
26
+ │ ├── statistics/ # Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto
27
+ ├── views/, projectors/ # EvaluationView (S13+), projecteurs Alto/Page/CanonicalToText
28
+ │ ├── corpus.py # Document, Corpus, GTLevel (legacy en cours de retrait)
29
+ │ ├── pipeline.py # PipelineRunner legacy (en cours de retrait)
30
+ │ └── benchmark_result.py # BenchmarkResult, EngineReport, DocumentResult
31
+ ├── pipeline/ # Layer 4 PipelineExecutor canonique (instance-based)
32
+ ├── adapters/ # Layer 5 adapters externes (libs externes autorisées)
33
+ ├── ocr/ # Tesseract, Pero, Mistral OCR, Google Vision, Azure DI
34
+ ├── llm/ # OpenAI, Anthropic, Mistral, Ollama
35
+ │ ├── vlm/ # Adapters VLM (zero-shot)
36
+ │ ├── corpus/ # IIIF, Gallica, HTR-United, HuggingFace
37
+ │ ├── storage/ # ArtifactStore, JobStore
38
+ │ └── legacy_engines/, legacy_modules/ # legacy BaseModule-based, en retrait
39
+ ├── app/ # Layer 6 — services applicatifs (BenchmarkService, …)
40
+ ├── reports_v2/ # Layer 7 — rendu HTML / JSON / CSV (22 renderers + 5 vues)
41
+ └── interfaces/ # Layer 8 — CLI Click, Web FastAPI
42
+
43
+ # Arborescence legacy en cours de retrait (cf. docs/migration/) :
44
+ # core/, measurements/, engines/, llm/, pipelines/, report/, modules/
45
  ```
46
 
47
+ Règle d'import stricte : les flèches d'import vont uniquement
48
+ de l'extérieur vers l'intérieur (de bas en haut dans le diagramme).
49
+ Vérifié par `tests/architecture/test_layer_dependencies.py`.
50
+
51
  ## Guides d'extension
52
 
53
  - [Étendre le moteur narratif](narrative-engine.md) — ajouter un type
docs/developer/module-policy.md CHANGED
@@ -14,7 +14,7 @@ qu'un module soit acceptable.
14
 
15
  Pour qu'un module soit acceptable :
16
 
17
- 1. Il **hérite** de `picarones.core.modules.BaseModule` (Sprint 33).
18
  2. Il déclare ses `input_types` et `output_types` (parmi
19
  `ArtifactType.{IMAGE, TEXT, ALTO, PAGE, ENTITIES, READING_ORDER}`).
20
  3. Il fournit un `ModuleManifest` avec **5 champs obligatoires** :
@@ -80,11 +80,12 @@ manifest = ModuleManifest(
80
  ## Contrat `BaseModule`
81
 
82
  Tout module exécutable hérite de
83
- `picarones.core.modules.BaseModule` (Sprint 33). Le contrat minimal
84
  est :
85
 
86
  ```python
87
- from picarones.core.modules import ArtifactType, BaseModule
 
88
 
89
  class MyLlmCorrecteur(BaseModule):
90
  name = "my-llm-correcteur"
 
14
 
15
  Pour qu'un module soit acceptable :
16
 
17
+ 1. Il **hérite** de `picarones.domain.module_protocol.BaseModule` (Sprint 33).
18
  2. Il déclare ses `input_types` et `output_types` (parmi
19
  `ArtifactType.{IMAGE, TEXT, ALTO, PAGE, ENTITIES, READING_ORDER}`).
20
  3. Il fournit un `ModuleManifest` avec **5 champs obligatoires** :
 
80
  ## Contrat `BaseModule`
81
 
82
  Tout module exécutable hérite de
83
+ `picarones.domain.module_protocol.BaseModule` (Sprint 33). Le contrat minimal
84
  est :
85
 
86
  ```python
87
+ from picarones.domain.artifacts import ArtifactType
88
+ from picarones.domain.module_protocol import BaseModule
89
 
90
  class MyLlmCorrecteur(BaseModule):
91
  name = "my-llm-correcteur"
docs/explanation/architecture.md ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture Picarones — manifeste
2
+
3
+ > **Audience** : développeurs et mainteneurs. Ce document explique
4
+ > *pourquoi* le code est organisé comme il l'est, pas seulement *où
5
+ > sont les fichiers*. Pour la liste exhaustive des modules, lire
6
+ > directement le code — il est typé et documenté.
7
+
8
+ ## Deux arborescences cohabitent par design
9
+
10
+ Le projet est en transition entre une arborescence **legacy** (héritée
11
+ de la fondation 2025) et une arborescence **post-rewrite** (refondation
12
+ ciblée S27-S46, 2026). Cette cohabitation est explicite et finie dans
13
+ le temps :
14
+
15
+ | Arbo | Statut | Utilisation |
16
+ |------|--------|-------------|
17
+ | **Post-rewrite** | Canonique | **Tout nouveau code va ici.** |
18
+ | **Legacy** | Transitionnel | Reste exécutable le temps que les callers externes (HuggingFace Space, scripts BnF, notebooks de chercheurs) migrent. |
19
+
20
+ Le retrait du legacy est calendrier dans le CHANGELOG ; cf. aussi
21
+ `docs/migration/rewrite-status-s46.md`.
22
+
23
+ ## Arbo canonique — 8 cercles concentriques
24
+
25
+ ```
26
+ domain → formats → evaluation → pipeline → adapters → app → reports_v2 → interfaces
27
+ ```
28
+
29
+ **Règle de dépendance stricte** : les flèches d'import vont uniquement
30
+ de l'extérieur vers l'intérieur. Vérifié par
31
+ `tests/architecture/test_layer_dependencies.py`. Aucun shim — un
32
+ module a un seul emplacement canonique.
33
+
34
+ ### `picarones/domain/` — types purs
35
+
36
+ Couche 1 (la plus interne). Aucune dépendance d'exécution,
37
+ aucun I/O, aucun framework. Pydantic et stdlib uniquement.
38
+
39
+ | Module | Contenu |
40
+ |---|---|
41
+ | `artifacts.py` | `Artifact`, `ArtifactType` (10 types : IMAGE, RAW_TEXT, ALTO_XML, PAGE_XML, ENTITIES, READING_ORDER, ALIGNMENT, CORRECTED_TEXT, CANONICAL_DOCUMENT, CONFIDENCES) |
42
+ | `artifact_key.py` | `ArtifactKey` — clé canonique multi-paramètres pour la reprise par hash |
43
+ | `corpus.py` | `CorpusSpec`, métadonnées de corpus |
44
+ | `documents.py` | `DocumentRef`, `GroundTruthRef` |
45
+ | `evaluation_spec.py` | `MetricSpec`, `EvaluationView`, `EvaluationSpec` |
46
+ | `pipeline_spec.py` | `PipelineSpec`, `PipelineStep`, `INITIAL_STEP_ID` |
47
+ | `projection_spec.py` | `ProjectionSpec` (transformation candidate avant évaluation) |
48
+ | `provenance.py` | `ProvenanceRecord` |
49
+ | `run_manifest.py` | `RunManifest` — empreinte immuable d'un run, sérialisée en `run_manifest.json` |
50
+ | `errors.py` | Hiérarchie d'exceptions (`PicaronesError`, `AdapterStepError`, `ArtifactValidationError`, …) |
51
+
52
+ ### `picarones/formats/` — parsers et sérialiseurs
53
+
54
+ Lecture/écriture des formats externes : ALTO XML, PAGE XML, texte
55
+ normalisé. Dépend du domain ; aucune logique d'évaluation.
56
+
57
+ ### `picarones/evaluation/` — moteurs d'évaluation
58
+
59
+ | Sous-package | Rôle |
60
+ |---|---|
61
+ | `metrics/` | Métriques (CER/WER, philologiques, calibration, NER, layout…). Enregistrées via `@register_metric` au registre typé |
62
+ | `projectors/` | Projections inter-types (ALTO → texte, canonical → texte) avec `ProjectionReport` |
63
+ | `views/` | Vues d'évaluation : `TextView`, `AltoView`, `SearchView`. L'`EvaluationViewExecutor` aligne candidate + GT, applique normalisation + projection, calcule les métriques |
64
+ | `evaluation_engine.py` | Moteur central qui exécute une `EvaluationView` |
65
+ | `projection_engine.py` | Moteur de projection |
66
+ | `registry/` | `MetricRegistry` — découverte typée par signature `(input_type, output_type)` |
67
+
68
+ ### `picarones/pipeline/` — DAG d'étapes
69
+
70
+ Orchestration mono-document d'une pipeline composée :
71
+
72
+ | Module | Rôle |
73
+ |---|---|
74
+ | `executor.py` | `PipelineExecutor` — exécute un `PipelineSpec` step par step, capture `StepResult`, filtre outputs sur `step.output_types` |
75
+ | `planner.py` | `PipelinePlanner` — résout les `inputs_from`, valide la spec, calcule les métriques aux jonctions |
76
+ | `validation.py` | Validation statique d'une `PipelineSpec` (types s'enchaînent, pas de cycle) |
77
+ | `runner.py` | `CorpusRunner` — orchestration corpus-wide avec ProcessPool/ThreadPool, backpressure, timeout, cancellation |
78
+ | `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
79
+ | `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
80
+
81
+ ### `picarones/adapters/` — implémentations concrètes
82
+
83
+ C'est ici que vivent les **dépendances externes** (pytesseract, pero,
84
+ mistralai, openai, anthropic, google-cloud-vision, …).
85
+
86
+ | Sous-package | Adapters |
87
+ |---|---|
88
+ | `ocr/` | TesseractAdapter, PeroOCRAdapter, MistralOCRAdapter, GoogleVisionAdapter, AzureDocIntelAdapter, PrecomputedTextAdapter |
89
+ | `llm/` | AnthropicLLMAdapter, OpenAILLMAdapter, MistralLLMAdapter, OllamaLLMAdapter |
90
+ | `vlm/` | AnthropicVLMAdapter, OpenAIVLMAdapter, MistralVLMAdapter, OllamaVLMAdapter (héritage multiple `BaseVLMAdapter + BaseLLMAdapter`, MRO guard) |
91
+ | `corpus/` | local folder, IIIF, Gallica, HTR-United, HuggingFace Datasets, eScriptorium |
92
+ | `storage/` | `InMemoryArtifactStore`, `FilesystemArtifactStore`, `JobStore` (SQLite) |
93
+ | `output_paths.py` | Helper partagé `resolve_output_path` (workspace-aware, read-only-mount-safe) |
94
+ | `_retry.py` | Helper partagé `call_with_retry` (3 retries, backoff 2/4/8s, sur 429+5xx+timeout réseau) |
95
+
96
+ **Règle** : un adapter peut importer le domain et ses libs externes.
97
+ Il ne doit **jamais** importer `app/` ou `interfaces/`. Il n'a aucune
98
+ logique d'évaluation (un OCR adapter ne calcule pas le CER — il
99
+ produit un artefact texte que `evaluation/` consommera).
100
+
101
+ ### `picarones/app/` — services applicatifs
102
+
103
+ Orchestration entre adapters et evaluation.
104
+
105
+ | Module | Rôle |
106
+ |---|---|
107
+ | `services/run_orchestrator.py` | `RunOrchestrator.execute(RunSpec)` — point d'entrée d'un run complet |
108
+ | `services/benchmark_service.py` | `BenchmarkService.run` — exécute pipelines × vues × corpus, produit `RunResult` |
109
+ | `services/job_runner.py` | `JobRunner` — soumission asynchrone (thread daemon) avec persistance `JobStore` |
110
+ | `services/corpus_service.py` | Loading + sandboxing + extraction ZIP avec zip-slip protection |
111
+ | `services/dependencies.py` | `capture_dependencies_lock()` via `importlib.metadata` pour le `RunManifest` |
112
+ | `services/path_security.py` | `WorkspaceManager` — sandboxe par session |
113
+ | `services/registry_service.py` | Découverte des adapters et vues canoniques |
114
+ | `schemas/run_spec.py` | `RunSpec`, `StepSpec` — modèles YAML user-facing |
115
+ | `results.py` | `RunResult`, `RunDocumentResult`, `ReportRenderer` (alias type unique) |
116
+
117
+ ### `picarones/reports_v2/` — rendu déterministe
118
+
119
+ | Sous-package | Rôle |
120
+ |---|---|
121
+ | `csv/render.py` | `CsvReportRenderer` — un CSV plat (`run_id, doc, pipeline, view, metric, value, status`) |
122
+ | `json/render.py` | `JsonReportRenderer` — manifest + documents en JSON déterministe |
123
+ | `html/render.py` | `HtmlReportRenderer` — rapport autonome (TextView, AltoView, SearchView) |
124
+
125
+ Le rendu est strict : pas de JS dynamique, pas d'I/O, déterministe
126
+ bit-for-bit à entrée constante. Permet à un relecteur 5 ans plus tard
127
+ de hasher un rapport et de le citer.
128
+
129
+ ### `picarones/interfaces/` — points d'entrée user-facing
130
+
131
+ | Sous-package | Rôle |
132
+ |---|---|
133
+ | `cli/` | Click — `picarones-rewrite run`, `import_corpus`, `report` |
134
+ | `web/` | FastAPI — skeleton, routers (corpus, benchmark, jobs), middlewares de sécurité |
135
+
136
+ ## Arbo legacy — `picarones/{cli,web,engines,llm,pipelines,report,measurements,extras,modules,core}/`
137
+
138
+ Reste exécutable. Ne pas y ajouter de nouveau code. Une partie est
139
+ re-exportée depuis l'arbo canonique via des shims dépréciés (cf.
140
+ `picarones/pipeline/spec.py`, alias `DEFAULT_*_PROMPT` singuliers
141
+ dans `BaseLLMAdapter`/`BaseVLMAdapter`) qui émettent
142
+ `DeprecationWarning` à l'usage. Suppression effective prévue en 2.0.
143
+
144
+ ## Principes architecturaux
145
+
146
+ ### Pas de shim hors deprecation period
147
+
148
+ Un module a un seul emplacement canonique. Quand un module migre,
149
+ on choisit explicitement entre :
150
+
151
+ - **Suppression dure** (pour la dette interne, pas de caller externe).
152
+ - **Shim avec `DeprecationWarning`** (pour la stabilité d'API publique).
153
+ Le shim a une date de retrait inscrite dans le CHANGELOG.
154
+
155
+ ### Pas d'`except Exception: pass`
156
+
157
+ Toute fonctionnalité optionnelle qui échoue émet un
158
+ `logger.warning("[module] feature dégradée : %s", exc)` avec contexte.
159
+ Vérifié par `tests/architecture/test_no_side_effect_imports.py`.
160
+
161
+ ### Tests architecturaux comme garde-fous
162
+
163
+ Plusieurs tests verrouillent des invariants structurels que la revue
164
+ de code humaine raterait :
165
+
166
+ - `test_layer_dependencies.py` — circles strictement orientés
167
+ - `test_file_budgets.py` — pas de god-modules
168
+ - `test_doc_paths.py` — chemins cités dans la doc existent
169
+ - `test_output_paths_uniformity.py` — tous les adapters passent par `resolve_output_path`
170
+ - `test_storage_keys_filesystem_safe.py` — clés du store filesystem-safe (Windows)
171
+ - `test_manifest_reproducibility.py` — `RunManifest` capture tout pour rejouer
172
+ - `test_module_coverage.py` — chaque module a un test associé
173
+
174
+ ### Reproductibilité bit-for-bit
175
+
176
+ Le `RunManifest` capture systématiquement : `code_version`,
177
+ `pipeline_specs` complets, `adapter_kwargs`, `dependencies_lock`
178
+ (via `importlib.metadata`), `view_specs`, timestamps. La
179
+ sérialisation est déterministe (Pydantic ordered fields, JSON
180
+ sorted keys). Le hash du manifest peut être cité dans une
181
+ publication scientifique.
182
+
183
+ ## Évolution
184
+
185
+ L'évolution de l'architecture est documentée :
186
+
187
+ - Plans : [`docs/roadmap/evolution-2026.md`](../roadmap/evolution-2026.md)
188
+ - État du rewrite : [`docs/migration/rewrite-status-s46.md`](../migration/rewrite-status-s46.md)
189
+ - Audits institutionnels : [`docs/audits/`](../audits/)
190
+ - Politique d'API publique : [`docs/reference/api-stable.md`](../reference/api-stable.md)
docs/{developer → explanation}/narrative-engine.en.md RENAMED
@@ -1,5 +1,5 @@
1
  <!-- translation: machine + human review pending -->
2
- <!-- canonical: docs/developer/narrative-engine.md (FR) -->
3
 
4
  # Extending the narrative engine
5
 
@@ -13,7 +13,7 @@ contradiction), and renders them through YAML templates with
13
 
14
  ## Add a new detector in 5 steps
15
 
16
- ### 1. Add a `FactType` in `picarones/core/facts.py`
17
 
18
  ```python
19
  class FactType(str, Enum):
 
1
  <!-- translation: machine + human review pending -->
2
+ <!-- canonical: docs/explanation/narrative-engine.md (FR) -->
3
 
4
  # Extending the narrative engine
5
 
 
13
 
14
  ## Add a new detector in 5 steps
15
 
16
+ ### 1. Add a `FactType` in `picarones/domain/facts.py`
17
 
18
  ```python
19
  class FactType(str, Enum):
docs/{developer → explanation}/narrative-engine.md RENAMED
File without changes
docs/{cli-workflows.md → how-to/cli-workflows.md} RENAMED
@@ -216,5 +216,5 @@ pour découvrir la sortie sans corpus réel.
216
  run, diagnose, economics, edition, compare + helper `_run_workflow`.
217
  - [`picarones/cli/_pipeline.py`](../picarones/cli/_pipeline.py) —
218
  pipeline group.
219
- - Voir aussi [`docs/profiles.md`](profiles.md) et
220
- [`docs/views.md`](views.md).
 
216
  run, diagnose, economics, edition, compare + helper `_run_workflow`.
217
  - [`picarones/cli/_pipeline.py`](../picarones/cli/_pipeline.py) —
218
  pipeline group.
219
+ - Voir aussi [`docs/reference/normalization-profiles.md`](profiles.md) et
220
+ [`docs/reference/views.md`](views.md).
INSTALL.md → docs/how-to/install.md RENAMED
@@ -1,7 +1,13 @@
1
  # Guide d'installation — Picarones
2
 
3
  > Guide détaillé pour Linux, macOS et Windows.
4
- > Pour une installation en 5 minutes : voir [README.md](README.md#installation-rapide).
 
 
 
 
 
 
5
 
6
  ---
7
 
@@ -236,19 +242,7 @@ config_path: /path/to/pero_model/config.yaml
236
  EOF
237
  ```
238
 
239
- ### 5.3 Kraken (optionnel)
240
-
241
- ```bash
242
- pip install kraken
243
-
244
- # Télécharger un modèle
245
- kraken get 10.5281/zenodo.XXXXXXX
246
-
247
- # Lister les modèles installés
248
- kraken list
249
- ```
250
-
251
- ### 5.4 Ollama (LLMs locaux)
252
 
253
  ```bash
254
  # Installer Ollama
@@ -290,11 +284,6 @@ MISTRAL_API_KEY=...
290
  # Google Vision
291
  GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
292
 
293
- # AWS Textract
294
- AWS_ACCESS_KEY_ID=...
295
- AWS_SECRET_ACCESS_KEY=...
296
- AWS_DEFAULT_REGION=eu-west-1
297
-
298
  # Azure Document Intelligence
299
  AZURE_DOC_INTEL_ENDPOINT=https://...cognitiveservices.azure.com/
300
  AZURE_DOC_INTEL_KEY=...
 
1
  # Guide d'installation — Picarones
2
 
3
  > Guide détaillé pour Linux, macOS et Windows.
4
+ > Pour une installation en 5 minutes, voir le bloc *Setup* du
5
+ > [README](../../README.md).
6
+ >
7
+ > Audience : opérateur ou développeur qui installe Picarones en
8
+ > local ou sur un serveur. Pour un déploiement institutionnel
9
+ > (BnF, LoC, BL), voir aussi
10
+ > [`../operations/deployment-institutional.md`](../operations/deployment-institutional.md).
11
 
12
  ---
13
 
 
242
  EOF
243
  ```
244
 
245
+ ### 5.3 Ollama (LLMs locaux)
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  ```bash
248
  # Installer Ollama
 
284
  # Google Vision
285
  GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
286
 
 
 
 
 
 
287
  # Azure Document Intelligence
288
  AZURE_DOC_INTEL_ENDPOINT=https://...cognitiveservices.azure.com/
289
  AZURE_DOC_INTEL_KEY=...
docs/index.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Documentation Picarones — index par rôle
2
+
3
+ > **Architecture documentaire** : ce projet adopte le modèle
4
+ > [Diataxis](https://diataxis.fr/) — quatre quadrants :
5
+ > *tutorials* (apprendre), *how-to* (résoudre), *reference*
6
+ > (consulter), *explanation* (comprendre). Plus deux dossiers
7
+ > institutionnels : *governance* et *operations*.
8
+ >
9
+ > **Bilingue** : la **langue canonique est le français**. Une
10
+ > surface publique réduite est traduite en anglais — README,
11
+ > CONTRIBUTING, SECURITY, ACCESSIBILITY, deux tutoriels clés.
12
+ > Le reste reste FR. Politique assumée plutôt que bilingue partiel
13
+ > brouillé.
14
+
15
+ ---
16
+
17
+ ## Je suis…
18
+
19
+ ### …un chercheur ou archiviste qui veut benchmarker un corpus
20
+
21
+ Vous voulez exécuter Picarones sur vos documents, lire un rapport,
22
+ comprendre les chiffres.
23
+
24
+ 1. Installer : [`how-to/install.md`](how-to/install.md)
25
+ 2. Premier benchmark : [`tutorials/first-benchmark.md`](tutorials/first-benchmark.md)
26
+ 3. Lire le rapport produit : [`tutorials/reading-a-report.md`](tutorials/reading-a-report.md)
27
+ ([EN](tutorials/reading-a-report.en.md))
28
+ 4. Cas d'école pédagogiques : [`case-studies/`](case-studies/)
29
+ 5. Glossaire des métriques : [`reference/normalization-profiles.md`](reference/normalization-profiles.md),
30
+ [`reference/views.md`](reference/views.md)
31
+
32
+ ### …un opérateur qui doit déployer en environnement institutionnel
33
+
34
+ Vous installez Picarones sur un NAS BnF, un cluster LoC, un serveur BL.
35
+
36
+ 1. Déploiement institutionnel : [`operations/deployment-institutional.md`](operations/deployment-institutional.md)
37
+ 2. Conformité RGPD : [`operations/data-retention-rgpd.md`](operations/data-retention-rgpd.md)
38
+ 3. Runbook incidents : [`operations/runbook.md`](operations/runbook.md)
39
+ 4. Observabilité (logs, métriques, alerting) : [`operations/observability.md`](operations/observability.md)
40
+ 5. Process de release : [`operations/release-process.md`](operations/release-process.md)
41
+
42
+ ### …un développeur qui veut contribuer du code
43
+
44
+ Vous ajoutez un adapter, une vue, une métrique, un détecteur narratif.
45
+
46
+ 1. Vue d'ensemble du projet : [`/CONTRIBUTING.md`](../CONTRIBUTING.md)
47
+ ([EN](../CONTRIBUTING.en.md))
48
+ 2. Architecture en cercles : [`explanation/architecture.md`](explanation/architecture.md)
49
+ 3. Politique modules contribués : [`developer/module-policy.md`](developer/module-policy.md)
50
+ 4. Étendre un sous-système :
51
+ [glossaire](developer/extending-glossary.md) ([EN](developer/extending-glossary.en.md)) ·
52
+ [i18n](developer/extending-i18n.md) ([EN](developer/extending-i18n.en.md)) ·
53
+ [moteur narratif](developer/narrative-engine.md) ([EN](developer/narrative-engine.en.md))
54
+ 5. Écrire un module pour le banc d'essai : [`user/writing-a-pipeline-module.md`](user/writing-a-pipeline-module.md)
55
+
56
+ ### …un mainteneur ou auditeur de sécurité
57
+
58
+ Vous évaluez Picarones avant un déploiement, un audit, une revue.
59
+
60
+ 1. Politique de gouvernance : [`/GOVERNANCE.md`](../GOVERNANCE.md)
61
+ 2. Politique de sécurité : [`/SECURITY.md`](../SECURITY.md)
62
+ ([EN](../SECURITY.en.md))
63
+ 3. Threat model STRIDE : [`security/threat-model.md`](security/threat-model.md)
64
+ 4. API publique stable et politique de versioning : [`reference/api-stable.md`](reference/api-stable.md)
65
+ 5. Audits historiques : [`audits/`](audits/)
66
+ 6. État du rewrite et migration : [`migration/rewrite-status-s46.md`](migration/rewrite-status-s46.md)
67
+ 7. Reproductibilité bit-for-bit : [`reference/reproducibility-snapshots.md`](reference/reproducibility-snapshots.md)
68
+
69
+ ### …un Délégué à la Protection des Données (DPO)
70
+
71
+ Vous évaluez les implications RGPD avant signature.
72
+
73
+ 1. Politique de rétention RGPD : [`operations/data-retention-rgpd.md`](operations/data-retention-rgpd.md)
74
+ 2. Modèle d'accord de sous-traitance (DPA) : [`legal/dpa-template.md`](legal/dpa-template.md)
75
+ 3. Threat model : [`security/threat-model.md`](security/threat-model.md)
76
+ 4. Liste des sous-traitants potentiels (services cloud) :
77
+ `pricing.yaml` + section *Adapters cloud* dans
78
+ [`reference/api-stable.md`](reference/api-stable.md)
79
+
80
+ ---
81
+
82
+ ## Index thématique
83
+
84
+ ### Tutorials — j'apprends
85
+
86
+ | Document | Public | Langue |
87
+ |----------|--------|--------|
88
+ | [`tutorials/first-benchmark.md`](tutorials/first-benchmark.md) | Chercheur découvrant l'outil | FR |
89
+ | [`tutorials/reading-a-report.md`](tutorials/reading-a-report.md) | Chercheur lisant un rapport | FR + EN |
90
+ | [`tutorials/writing-a-pipeline-module.md`](tutorials/writing-a-pipeline-module.md) | Développeur tiers | FR |
91
+
92
+ ### How-to — je résous un problème concret
93
+
94
+ | Document | Cible |
95
+ |----------|-------|
96
+ | [`how-to/install.md`](how-to/install.md) | Installer en local ou serveur |
97
+ | [`how-to/cli-workflows.md`](how-to/cli-workflows.md) | Utiliser la CLI au quotidien |
98
+
99
+ ### Reference — je consulte le contrat
100
+
101
+ | Document | Sujet |
102
+ |----------|-------|
103
+ | [`reference/api-stable.md`](reference/api-stable.md) | API Python publique + politique semver |
104
+ | [`reference/views.md`](reference/views.md) | Vues d'évaluation (text, alto, search) |
105
+ | [`reference/normalization-profiles.md`](reference/normalization-profiles.md) | Profils de normalisation textuelle |
106
+ | [`reference/reproducibility-snapshots.md`](reference/reproducibility-snapshots.md) | Reproductibilité bit-for-bit |
107
+
108
+ ### Explanation — je comprends pourquoi
109
+
110
+ | Document | Sujet |
111
+ |----------|-------|
112
+ | [`explanation/architecture.md`](explanation/architecture.md) | Architecture en cercles, principes |
113
+ | [`explanation/narrative-engine.md`](explanation/narrative-engine.md) | Comment le moteur narratif fonctionne |
114
+
115
+ ### Operations — je déploie et j'opère
116
+
117
+ | Document | Sujet |
118
+ |----------|-------|
119
+ | [`operations/deployment-institutional.md`](operations/deployment-institutional.md) | Déploiement institutionnel |
120
+ | [`operations/runbook.md`](operations/runbook.md) | Réponse aux incidents |
121
+ | [`operations/observability.md`](operations/observability.md) | Logs, métriques, alerting |
122
+ | [`operations/data-retention-rgpd.md`](operations/data-retention-rgpd.md) | Conformité RGPD |
123
+ | [`operations/release-process.md`](operations/release-process.md) | Cycle de release |
124
+
125
+ ### Governance / security / legal
126
+
127
+ | Document | Sujet |
128
+ |----------|-------|
129
+ | [`/GOVERNANCE.md`](../GOVERNANCE.md) | Gouvernance |
130
+ | [`/SECURITY.md`](../SECURITY.md) | Sécurité (FR + EN) |
131
+ | [`/CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md) | Code de conduite |
132
+ | [`/ACCESSIBILITY.md`](../ACCESSIBILITY.md) | Accessibilité |
133
+ | [`security/threat-model.md`](security/threat-model.md) | Threat model STRIDE |
134
+ | [`legal/dpa-template.md`](legal/dpa-template.md) | DPA RGPD §28 |
135
+
136
+ ### Archives et historique
137
+
138
+ | Document | Sujet |
139
+ |----------|-------|
140
+ | [`/CHANGELOG.md`](../CHANGELOG.md) | Journal des versions (Keep-a-Changelog) |
141
+ | [`audits/`](audits/) | Audits historiques figés |
142
+ | [`migration/`](migration/) | Notes de migration entre versions majeures |
143
+ | [`roadmap/`](roadmap/) | Plans stratégiques |
144
+
145
+ ---
146
+
147
+ ## Conventions
148
+
149
+ - **Une seule arborescence canonique post-rewrite** :
150
+ `domain → formats → evaluation → pipeline → adapters → app → reports_v2 → interfaces`.
151
+ L'arbo legacy `picarones/{cli,web,engines,llm,pipelines,report}/`
152
+ reste exécutable mais n'accepte plus de nouveau code.
153
+ - **Tout chemin `picarones/.../X.py` cité dans la doc doit exister**.
154
+ Vérifié par `tests/architecture/test_doc_paths.py` (baseline 73,
155
+ doit décroître).
156
+ - **Les chiffres en prose qui dépendent de l'état du code** (compte
157
+ de tests, nombre d'adapters) sont régénérés par
158
+ `scripts/gen_readme_tables.py` — modifier le code, pas la doc.
159
+ - **Cohérence FR/EN** : un fichier `xxx.md` en FR + un fichier
160
+ `xxx.en.md` en EN miroir. Pas de fragments mêlés.
docs/legal/THIRD_PARTY_LICENSES.md ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party licenses
2
+
3
+ > **Audience** : équipe juridique, DSI institutionnelle, mainteneur
4
+ > de release. Audit des licences des dépendances tierces utilisées
5
+ > par Picarones, requis par Apache 2.0 §4(d) et par les politiques
6
+ > d'achat institutionnelles (BnF, LoC, BL).
7
+ >
8
+ > **Régénération** : ce fichier est censé être régénéré à chaque
9
+ > release par `scripts/gen_third_party_licenses.py` (à venir, cf.
10
+ > [`docs/roadmap/backlog.md`](../roadmap/backlog.md)). Tant que le
11
+ > script n'existe pas, mise à jour manuelle au moment de la release.
12
+ >
13
+ > **Date du dernier rafraîchissement** : 2026-05.
14
+
15
+ ## Politique générale
16
+
17
+ Picarones est distribué sous **Apache License 2.0**. Cette licence
18
+ est compatible avec toutes les licences listées ci-dessous (MIT, BSD,
19
+ PSF, Apache 2.0 elles-mêmes ; pas de dépendance GPL/LGPL/AGPL en
20
+ runtime).
21
+
22
+ Les dépendances optionnelles (extras `[mistral]`, `[anthropic]`,
23
+ `[openai]`, `[ollama]`, `[google]`, `[azure]`, `[hf]`, `[escriptorium]`,
24
+ `[iiif]`, `[stats]`, `[ner]`) ne sont chargées qu'à la demande de
25
+ l'utilisateur ; elles n'affectent pas la licence du distribué de base.
26
+
27
+ ## Dépendances de runtime (cœur)
28
+
29
+ | Paquet | Licence | Copyright | Usage |
30
+ |--------|---------|-----------|-------|
31
+ | [click](https://palletsprojects.com/p/click/) | BSD-3-Clause | © Pallets | CLI |
32
+ | [jiwer](https://github.com/jitsi/jiwer) | Apache-2.0 | © 8x8, Inc. | CER / WER |
33
+ | [Pillow](https://python-pillow.org/) | HPND (MIT-style) | © Jeffrey A. Clark + Pillow contributors | Images |
34
+ | [PyYAML](https://pyyaml.org/) | MIT | © Kirill Simonov | YAML |
35
+ | [pytesseract](https://github.com/madmaze/pytesseract) | Apache-2.0 | © Matthias A. Lee | OCR Tesseract wrapper |
36
+ | [tqdm](https://tqdm.github.io/) | MIT + MPL-2.0 | © tqdm contributors | Barres de progression |
37
+ | [numpy](https://numpy.org/) | BSD-3-Clause | © NumPy developers | Calculs numériques |
38
+ | [jinja2](https://palletsprojects.com/p/jinja/) | BSD-3-Clause | © Pallets | Templating HTML |
39
+ | [defusedxml](https://github.com/tiran/defusedxml) | PSF-2.0 | © Christian Heimes | Parsing XML sécurisé |
40
+ | [pydantic](https://docs.pydantic.dev/) | MIT | © Samuel Colvin and contributors | Modèles immuables |
41
+
42
+ ## Dépendances de runtime — extras
43
+
44
+ ### `[web]`
45
+
46
+ | Paquet | Licence | Usage |
47
+ |--------|---------|-------|
48
+ | [fastapi](https://fastapi.tiangolo.com/) | MIT | API web |
49
+ | [uvicorn](https://www.uvicorn.org/) | BSD-3-Clause | Serveur ASGI |
50
+ | [python-multipart](https://github.com/Kludex/python-multipart) | Apache-2.0 | Upload form-data |
51
+ | [starlette](https://www.starlette.io/) | BSD-3-Clause | (transitif via FastAPI) |
52
+ | [httpx](https://www.python-httpx.org/) | BSD-3-Clause | Client HTTP (tests) |
53
+
54
+ ### `[mistral]`
55
+
56
+ | Paquet | Licence | Usage |
57
+ |--------|---------|-------|
58
+ | [mistralai](https://github.com/mistralai/client-python) | Apache-2.0 | SDK Mistral OCR + chat/vision |
59
+
60
+ ### `[anthropic]`
61
+
62
+ | Paquet | Licence | Usage |
63
+ |--------|---------|-------|
64
+ | [anthropic](https://github.com/anthropics/anthropic-sdk-python) | MIT | SDK Claude |
65
+
66
+ ### `[openai]`
67
+
68
+ | Paquet | Licence | Usage |
69
+ |--------|---------|-------|
70
+ | [openai](https://github.com/openai/openai-python) | Apache-2.0 | SDK OpenAI |
71
+
72
+ ### `[ollama]`
73
+
74
+ | Paquet | Licence | Usage |
75
+ |--------|---------|-------|
76
+ | [ollama](https://github.com/ollama/ollama-python) | MIT | Client Ollama local |
77
+
78
+ ### `[google]`
79
+
80
+ | Paquet | Licence | Usage |
81
+ |--------|---------|-------|
82
+ | [google-cloud-vision](https://github.com/googleapis/python-vision) | Apache-2.0 | OCR Google Vision |
83
+
84
+ ### `[azure]`
85
+
86
+ | Paquet | Licence | Usage |
87
+ |--------|---------|-------|
88
+ | [azure-ai-documentintelligence](https://github.com/Azure/azure-sdk-for-python) | MIT | OCR Azure DI |
89
+
90
+ ### `[hf]`
91
+
92
+ | Paquet | Licence | Usage |
93
+ |--------|---------|-------|
94
+ | [datasets](https://github.com/huggingface/datasets) | Apache-2.0 | Datasets HuggingFace |
95
+ | [huggingface-hub](https://github.com/huggingface/huggingface_hub) | Apache-2.0 | Hub HuggingFace |
96
+
97
+ ### `[ner]`
98
+
99
+ | Paquet | Licence | Usage |
100
+ |--------|---------|-------|
101
+ | [spacy](https://spacy.io/) | MIT | NER |
102
+
103
+ ### `[stats]`
104
+
105
+ | Paquet | Licence | Usage |
106
+ |--------|---------|-------|
107
+ | [scipy](https://scipy.org/) | BSD-3-Clause | Tests statistiques (Friedman, Nemenyi) |
108
+
109
+ ## Dépendances de développement
110
+
111
+ Les paquets utilisés uniquement en développement (tests, lint,
112
+ sécurité) ne sont pas redistribués avec Picarones et n'apparaissent
113
+ dans aucun wheel. Pour traçabilité supply-chain :
114
+
115
+ | Paquet | Licence | Usage |
116
+ |--------|---------|-------|
117
+ | pytest | MIT | Tests unitaires |
118
+ | pytest-cov | MIT | Couverture |
119
+ | pytest-timeout | MIT | Timeout par test |
120
+ | ruff | MIT | Lint |
121
+ | mypy | MIT | Type checking |
122
+ | bandit | Apache-2.0 | Audit sécurité statique |
123
+ | pip-audit | Apache-2.0 | Audit CVE des dépendances |
124
+
125
+ ## Modèles tiers
126
+
127
+ Picarones n'embarque **aucun modèle tiers** dans ses wheels. Les
128
+ modèles sont :
129
+
130
+ - soit **téléchargés à l'usage** par l'utilisateur (Tesseract `*.traineddata`,
131
+ Pero OCR via Zenodo, modèles spaCy via `python -m spacy download`) ;
132
+ - soit **invoqués via des APIs cloud** sous le contrat du fournisseur
133
+ (Mistral AI, Anthropic, OpenAI, Google, Azure).
134
+
135
+ Les conditions d'utilisation de chaque modèle / API sont à la charge
136
+ de l'utilisateur et de l'institution déployant Picarones.
137
+
138
+ ## Police d'écriture / fontes
139
+
140
+ Picarones n'embarque aucune fonte. Les rapports HTML utilisent les
141
+ fontes système du navigateur.
142
+
143
+ ## Données
144
+
145
+ Aucun corpus, aucune image, aucune vérité terrain n'est embarquée
146
+ dans les wheels. Les fixtures de test (`tests/fixtures/`) sont
147
+ synthétiques (générées) ou citées depuis leur source originale (cf.
148
+ `tests/fixtures/reference_corpus/README.md`).
149
+
150
+ ## Comment signaler une omission
151
+
152
+ Une dépendance manquante, une licence incorrecte, un copyright
153
+ mal attribué : ouvrir une issue avec le label `legal` ou écrire à
154
+ l'adresse de contact dans [`/SECURITY.md`](../../SECURITY.md). Une
155
+ correction sera publiée dans la prochaine release patch.
docs/legal/dpa-template.md ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modèle d'Accord de Sous-Traitance (DPA)
2
+
3
+ > **Audience** : Délégué à la Protection des Données (DPO) de
4
+ > l'institution déployant Picarones, équipe juridique de cette même
5
+ > institution, mainteneur du projet.
6
+ >
7
+ > **Statut** : modèle de référence — à adapter et à signer entre
8
+ > l'institution (responsable de traitement) et chaque sous-traitant
9
+ > activé via les adapters cloud. Ce document **n'est pas un contrat
10
+ > en lui-même** ; il définit les clauses minimales à inclure.
11
+ >
12
+ > **Référence légale** : Article 28 du Règlement (UE) 2016/679 (RGPD),
13
+ > [version consolidée](https://eur-lex.europa.eu/eli/reg/2016/679/oj).
14
+
15
+ ## Pourquoi un DPA ?
16
+
17
+ Lorsqu'une institution patrimoniale (BnF, LoC, BL) déploie Picarones
18
+ en activant des adapters cloud (Mistral OCR, OpenAI, Anthropic,
19
+ Google Vision, Azure Document Intelligence), elle envoie des
20
+ documents qui peuvent contenir des **données à caractère personnel**
21
+ (PII) — typiquement :
22
+
23
+ - Registres d'état civil (naissances, mariages, décès).
24
+ - Recensements (noms, adresses, professions).
25
+ - Correspondance personnelle (lettres privées, journaux).
26
+ - Notes manuscrites avec mentions nominatives.
27
+
28
+ L'envoi de ces données à un tiers (le fournisseur cloud) constitue
29
+ une **sous-traitance** au sens RGPD §28 ; un accord écrit (DPA) est
30
+ **obligatoire** entre l'institution (responsable de traitement) et
31
+ chaque sous-traitant.
32
+
33
+ ## Périmètre
34
+
35
+ Ce modèle couvre la sous-traitance des opérations de transcription
36
+ OCR/HTR effectuées par des services cloud activés par l'institution
37
+ via Picarones. **Il ne couvre pas** :
38
+
39
+ - Le déploiement Picarones lui-même (l'institution est seule
40
+ responsable de l'instance).
41
+ - Les adapters locaux (Tesseract, Pero OCR, Ollama) qui n'envoient
42
+ rien à l'extérieur.
43
+
44
+ ## Clauses minimales (RGPD §28.3)
45
+
46
+ ### 1. Objet et durée du traitement
47
+
48
+ Transcription automatique de documents numérisés via OCR, HTR ou VLM
49
+ cloud, pour la durée du marché entre l'institution et le fournisseur.
50
+
51
+ ### 2. Nature et finalité du traitement
52
+
53
+ - **Nature** : envoi d'images de documents et/ou de fragments de
54
+ texte ; réception de transcriptions textuelles ou de descriptions
55
+ structurées (ALTO, JSON canonique).
56
+ - **Finalité** : fournir à l'institution un benchmark comparatif de
57
+ pipelines OCR/HTR sur son corpus, dans le cadre d'une évaluation
58
+ technique préalable à un déploiement de production.
59
+
60
+ ### 3. Type de données à caractère personnel
61
+
62
+ Selon le corpus envoyé. L'institution **doit identifier en amont**
63
+ si le corpus contient :
64
+
65
+ - Données nominatives (noms, prénoms, dates de naissance/décès…).
66
+ - Données sensibles au sens RGPD §9 (origine raciale ou ethnique,
67
+ opinions politiques, convictions religieuses, données de santé,
68
+ orientation sexuelle…).
69
+
70
+ Pour les corpus sensibles, l'institution **doit privilégier les
71
+ adapters locaux** (Tesseract, Pero OCR, Ollama) ou anonymiser le
72
+ corpus avant envoi.
73
+
74
+ ### 4. Catégories de personnes concernées
75
+
76
+ - Personnes citées dans les documents historiques (typiquement
77
+ défuntes, sauf mention contraire).
78
+ - Auteurs ou correspondants des documents.
79
+
80
+ ### 5. Obligations du sous-traitant
81
+
82
+ Le sous-traitant cloud s'engage à :
83
+
84
+ a) ne traiter les données que sur **instruction documentée** du
85
+ responsable (l'institution). Pas de réutilisation pour
86
+ entraînement de modèles, sauf consentement explicite (cf. §10).
87
+
88
+ b) garantir que les **personnes autorisées** à traiter les données
89
+ sont soumises à une obligation de confidentialité.
90
+
91
+ c) mettre en œuvre les **mesures de sécurité** énumérées au RGPD
92
+ §32 (chiffrement en transit, contrôle d'accès, journalisation,
93
+ tests réguliers).
94
+
95
+ d) ne pas recourir à un **autre sous-traitant** sans autorisation
96
+ écrite préalable et spécifique du responsable.
97
+
98
+ e) **assister** le responsable dans la réponse aux demandes
99
+ d'exercice de droits (accès, rectification, effacement…) et dans
100
+ les obligations de notification de violations.
101
+
102
+ f) **supprimer ou retourner** les données à la fin de la prestation,
103
+ sauf obligation légale de conservation.
104
+
105
+ g) mettre à disposition du responsable toutes les **informations
106
+ nécessaires** pour démontrer la conformité au §28.
107
+
108
+ ### 6. Localisation des traitements
109
+
110
+ L'institution **doit privilégier** les fournisseurs offrant un
111
+ hébergement et un traitement strictement dans l'Espace économique
112
+ européen (EEE).
113
+
114
+ | Adapter | Localisation par défaut | Disponibilité EEE |
115
+ |---------|------------------------|-------------------|
116
+ | Mistral OCR / chat | France (cf. [Mistral Trust](https://mistral.ai/security/)) | Oui |
117
+ | OpenAI | États-Unis | EU residency dispo via Enterprise |
118
+ | Anthropic Claude | États-Unis | EU residency limitée |
119
+ | Google Vision | Multi-régions | EEE configurable |
120
+ | Azure Document Intelligence | Multi-régions | EEE configurable |
121
+
122
+ Pour un transfert hors EEE, **clauses contractuelles types** (CCT)
123
+ 2021/914/UE applicables OBLIGATOIRES.
124
+
125
+ ### 7. Sécurité
126
+
127
+ Mesures minimales :
128
+
129
+ - Chiffrement TLS 1.2+ en transit.
130
+ - Pas d'enregistrement des prompts/réponses pour entraînement
131
+ (option à activer côté fournisseur, cf. §10).
132
+ - Logs d'accès conservés < 30 jours sauf incident de sécurité.
133
+ - Tests de pénétration au moins annuels (à charge du sous-traitant).
134
+
135
+ ### 8. Sous-sous-traitance
136
+
137
+ Liste des sous-sous-traitants autorisés à fournir au démarrage et à
138
+ chaque modification. L'institution dispose d'un droit d'objection
139
+ à toute nouvelle sous-sous-traitance.
140
+
141
+ ### 9. Audit
142
+
143
+ L'institution se réserve le droit, à ses frais et avec préavis
144
+ raisonnable (30 jours), de conduire un audit du sous-traitant ou de
145
+ mandater un tiers indépendant pour vérifier la conformité des
146
+ mesures techniques et organisationnelles.
147
+
148
+ ### 10. Réutilisation pour entraînement de modèles
149
+
150
+ **Disposition critique** pour le patrimoine numérique : les
151
+ documents envoyés sont la propriété intellectuelle de l'institution
152
+ (et parfois du domaine public) ; les fournisseurs ne doivent **PAS**
153
+ les utiliser pour entraîner leurs modèles sans accord écrit.
154
+
155
+ Configuration recommandée par fournisseur :
156
+
157
+ | Fournisseur | Comment opt-out |
158
+ |-------------|------------------|
159
+ | OpenAI | Compte Enterprise ou via API avec `data_retention=zero` |
160
+ | Anthropic | Compte Enterprise ; pas d'option opt-out sur API standard |
161
+ | Mistral | API Enterprise tier ; opt-out par défaut sur certains plans |
162
+ | Google Vision | Activer Workspace Data Loss Prevention |
163
+ | Azure | Activer "Customer-Managed Keys" + opt-out training |
164
+
165
+ ### 11. Notification de violation
166
+
167
+ Le sous-traitant s'engage à notifier l'institution **dans les 24
168
+ heures** de la connaissance d'une violation de données à caractère
169
+ personnel les concernant, par e-mail ET courrier signé.
170
+
171
+ ### 12. Effacement à fin de prestation
172
+
173
+ À la fin du marché ou à la résiliation, le sous-traitant restitue
174
+ ou supprime toutes les données dans un délai de 30 jours, et
175
+ fournit une **attestation de destruction**.
176
+
177
+ ## Annexes
178
+
179
+ ### Annexe 1 — Description du traitement
180
+
181
+ À compléter par l'institution :
182
+
183
+ - [ ] Nom du corpus traité
184
+ - [ ] Volume estimé (nombre de documents, taille en GB)
185
+ - [ ] Période de traitement (du / au)
186
+ - [ ] Liste des adapters cloud activés
187
+ - [ ] Volume de PII estimé dans le corpus
188
+
189
+ ### Annexe 2 — Mesures de sécurité
190
+
191
+ À compléter par le sous-traitant — référence :
192
+ [ANSSI Référentiel Général de Sécurité](https://www.ssi.gouv.fr/).
193
+
194
+ ### Annexe 3 — Liste des sous-sous-traitants autorisés
195
+
196
+ À compléter par le sous-traitant.
197
+
198
+ ## Procédure de signature
199
+
200
+ 1. L'institution remplit les annexes en fonction du corpus prévu.
201
+ 2. Le DPO de l'institution valide la liste des adapters cloud
202
+ activés (`AdapterRegistry`).
203
+ 3. Le contrat est signé par les deux parties (institution +
204
+ fournisseur cloud) AVANT activation de l'adapter en production.
205
+ 4. Une copie est conservée dans le dossier de conformité du
206
+ traitement (durée minimale : 5 ans après la fin du traitement).
207
+
208
+ ## Référence légale
209
+
210
+ - [Règlement (UE) 2016/679 — RGPD](https://eur-lex.europa.eu/eli/reg/2016/679/oj)
211
+ - [Lignes directrices CEPD sur les sous-traitants](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-072020-concepts-controller-and-processor-gdpr_fr)
212
+ - [Décision d'adéquation EU-US Data Privacy Framework (2023)](https://commission.europa.eu/document/fa09cbad-dd7d-4684-ace5-c1e932f3eda7_en)
213
+
214
+ ## Révisions
215
+
216
+ | Version | Date | Changements |
217
+ |---------|------|-------------|
218
+ | 1.0 | 2026-05 | Création initiale (S60), modèle aligné RGPD §28 |
docs/migration/SESSION_HANDOVER.md ADDED
@@ -0,0 +1,508 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Handover entre sessions Claude Code
2
+
3
+ > Ce document est lu en premier par chaque nouvelle session pour
4
+ > reprendre le travail sans se tromper. Il pointe vers les
5
+ > sources de vérité, signale les pièges connus, et donne la
6
+ > prochaine action concrète.
7
+
8
+ ---
9
+
10
+ ## 0. Principe directeur (mis à jour 2026-05)
11
+
12
+ **Suppression agressive, pas de shim qui survit à son usage.**
13
+
14
+ - Le projet est en stand-by jusqu'à la fin de la migration
15
+ complète. Personne (ni externe ni HuggingFace Space) ne
16
+ consommera l'API legacy avant cette fin.
17
+ - Pas de préservation de l'API publique : breaking changes
18
+ acceptés.
19
+ - Dès qu'un caller migre vers le canonique, son shim est
20
+ **supprimé** (pas conservé pour un usage hypothétique).
21
+ - Tout symbole legacy public doit être tracé dans
22
+ ``tests/architecture/test_legacy_canonical_parity.py`` :
23
+ `canonical: ...` (équivalent canonique existe), `dropped: ...`
24
+ (volontairement abandonné, justifié), ou `unmigrated: ...`
25
+ (cible prévue, en cours).
26
+
27
+ Le test ``test_legacy_canonical_parity`` garantit qu'**aucune
28
+ fonctionnalité legacy n'est silencieusement perdue** au cours
29
+ de la migration. C'est le journal de bord vivant.
30
+
31
+ ---
32
+
33
+ ## 1. Sources de vérité (par ordre de priorité)
34
+
35
+ 1. **[`legacy-retirement-plan.md`](legacy-retirement-plan.md)** —
36
+ plan maître des Phases 0-11 du retrait du legacy. Chaque
37
+ phase a un statut explicite (✅ terminée / ⏳ en cours / 📋 à
38
+ venir).
39
+ 2. **[`pipeline-convergence-plan.md`](pipeline-convergence-plan.md)** —
40
+ sous-plan détaillé de la convergence ``BaseModule`` /
41
+ ``PipelineRunner`` → ``StepExecutor`` / ``PipelineExecutor``
42
+ (Sub-phases 7.A-7.D).
43
+ 3. **[`../../tests/architecture/test_legacy_canonical_parity.py`](../../tests/architecture/test_legacy_canonical_parity.py)** —
44
+ journal vivant de la migration : table 3-états des symboles
45
+ legacy avec leur équivalent canonique. À mettre à jour à
46
+ chaque migration.
47
+ 4. **[`../../CLAUDE.md`](../../CLAUDE.md)** — règles d'architecture
48
+ à respecter, statut de la migration, et liens vers le reste.
49
+ 5. **`git log --oneline -10`** — les 10 derniers commits
50
+ donnent l'état réel. Le dernier commit message décrit
51
+ souvent la prochaine sub-phase à exécuter.
52
+
53
+ ---
54
+
55
+ ## 2. Vérifications avant de toucher au code
56
+
57
+ ```bash
58
+ # 1. Bonne branche ?
59
+ git branch --show-current
60
+ # → doit retourner: claude/repo-analysis-cukvm
61
+
62
+ # 2. Working tree propre ?
63
+ git status
64
+ # → doit retourner: nothing to commit, working tree clean
65
+
66
+ # 3. Tests verts à l'état initial ?
67
+ python -m pytest tests/ -q --no-header --tb=line
68
+ # → doit retourner: 5085 passed (au moment de la pause de session)
69
+
70
+ # 4. Lint vert ?
71
+ ruff check picarones/ tests/
72
+ # → doit retourner: All checks passed!
73
+ ```
74
+
75
+ Si l'une de ces vérifications échoue : **NE PAS** continuer le
76
+ sprint. Investiguer d'abord pourquoi l'état initial diverge de
77
+ celui annoncé dans CLAUDE.md.
78
+
79
+ ---
80
+
81
+ ## 3. Pièges connus (apprentissages des phases précédentes)
82
+
83
+ ### 3.A Architecture des couches
84
+
85
+ Voir CLAUDE.md section « Règles d'architecture critiques ».
86
+ Résumé :
87
+
88
+ - ``evaluation/`` ne peut pas importer ``pipeline.types`` —
89
+ c'est l'autre sens.
90
+ - ``evaluation/`` whitelist limitée : pas de pytesseract /
91
+ mistralai / azure / google / pero_ocr. Ces libs externes
92
+ vont dans ``adapters/``.
93
+ - ``reports_v2/`` ne peut importer que les canoniques
94
+ (``evaluation/metrics/``), pas les shims legacy
95
+ (``measurements/X.py``).
96
+
97
+ ### 3.B Pattern shim — UNIQUEMENT TRANSITOIRE
98
+
99
+ ⚠️ **Principe** : un shim n'existe que pour la durée d'un
100
+ sprint. Dès que tous ses consommateurs ont migré, il est
101
+ **supprimé**.
102
+
103
+ Pour un shim minimal (transitoire) :
104
+
105
+ ```python
106
+ """``picarones.X.Y`` — shim re-export (déprécié, suppression imminente).
107
+
108
+ Canonique : :mod:`picarones.canonical.path`. Phase X.Y du
109
+ retrait du legacy. Ce shim disparaît dès que tous les callers
110
+ auront migré (généralement dans le commit suivant).
111
+ """
112
+
113
+ from __future__ import annotations
114
+
115
+ import warnings
116
+
117
+ from picarones.canonical.path import * # noqa: F401, F403
118
+ # Si des callers consomment des noms privés (_FOO, etc.),
119
+ # les ré-exporter explicitement :
120
+ from picarones.canonical.path import _FOO # noqa: F401
121
+
122
+ warnings.warn(
123
+ "picarones.X.Y is deprecated and will be removed in 2.0. "
124
+ "Import from picarones.canonical.path instead.",
125
+ DeprecationWarning,
126
+ stacklevel=2,
127
+ )
128
+ ```
129
+
130
+ **Avant de créer un shim**, demandez-vous : « est-ce que je peux
131
+ juste migrer tous les callers maintenant et supprimer le legacy
132
+ en bloc ? » Si oui, faites-le — pas de shim intermédiaire.
133
+
134
+ ### 3.C ``test_module_coverage::TEST_ONLY_BASELINE``
135
+
136
+ Quand un shim ``measurements/X.py`` n'a plus de consommateur
137
+ production (parce qu'un renderer a migré vers le canonique
138
+ direct), ajouter ``"X"`` à ``TEST_ONLY_BASELINE`` dans
139
+ ``tests/architecture/test_module_coverage.py``. Sinon le test
140
+ ``test_no_new_test_only_modules`` échoue.
141
+
142
+ ### 3.D ``test_file_budgets``
143
+
144
+ Tout fichier ≥ 400 LOC doit avoir une entrée dans
145
+ ``FILE_BUDGETS`` avec budget = LOC actuel + ~15 %. Quand on
146
+ relocalise un fichier, retirer l'entrée du chemin legacy et
147
+ en créer une au chemin canonique avec le même budget.
148
+
149
+ ### 3.E ``test_doc_paths::BROKEN_PATHS_BASELINE``
150
+
151
+ Si un sub-plan ou doc référence un futur chemin Python
152
+ (``picarones/X/Y.py``) qui n'existe pas encore, le test
153
+ ``test_broken_doc_paths_below_baseline`` détecte la
154
+ référence cassée. Soit :
155
+
156
+ - Bumper ``BROKEN_PATHS_BASELINE`` du même montant.
157
+ - Ou reformuler la référence en code/backticks pour échapper
158
+ au pattern (``picarones/X/Y.py``).
159
+
160
+ Quand le fichier sera créé en réalité, abaisser
161
+ ``BROKEN_PATHS_BASELINE``.
162
+
163
+ ### 3.F Test parité legacy ↔ canonique
164
+
165
+ ``tests/architecture/test_legacy_canonical_parity.py`` maintient
166
+ une table 3-états (``LEGACY_PARITY``) :
167
+
168
+ - ``canonical: <module.symbol>`` — équivalent canonique existe.
169
+ Le test vérifie présence + signatures compatibles.
170
+ - ``dropped: <raison>`` — feature volontairement abandonnée
171
+ avec justification écrite.
172
+ - ``unmigrated: <cible prévue>`` — migration prévue ; cible
173
+ peut ne pas encore exister.
174
+
175
+ À chaque migration d'un symbole, **mettre à jour la table**.
176
+ Les symboles non trackés sont comptés via
177
+ ``BOOTSTRAP_BASELINE`` (à diminuer à chaque session).
178
+
179
+ Limites du test : il ne vérifie que la **présence** et les
180
+ **signatures**, pas le comportement réel. Les différences
181
+ sémantiques sont signalées via le champ ``behavior_diff``
182
+ optionnel.
183
+
184
+ ### 3.G README généré
185
+
186
+ Le compteur de tests dans `README.md` et `CLAUDE.md` est
187
+ synchronisé par `scripts/gen_readme_tables.py`. À chaque
188
+ fois que le nombre de tests change (ajout/retrait), lancer :
189
+
190
+ ```bash
191
+ python scripts/gen_readme_tables.py
192
+ ```
193
+
194
+ Sinon le test ``test_readme_tables_consistent_with_code``
195
+ échoue.
196
+
197
+ ---
198
+
199
+ ## 4. Inventaire actuel — quel legacy reste à migrer ?
200
+
201
+ (Snapshot au moment de la pause de session, mesuré via AST,
202
+ fiable.)
203
+
204
+ ### 4.A Imports legacy dans les tests
205
+
206
+ **62 fichiers** avec **361 statements** d'import depuis les
207
+ paquets legacy (``measurements``, ``llm``, ``pipelines``) —
208
+ Lots A à G terminés (cf. 4.D ci-dessous). Les paquets
209
+ ``engines/``, ``modules/``, ``report/`` et ``core/`` ont été
210
+ **entièrement supprimés**. Restent uniquement
211
+ ``measurements/`` (~25 modules de catégorie B/C/D),
212
+ ``llm/``, ``pipelines/`` et les sous-paquets d'interfaces
213
+ (``cli/``, ``web/``, ``extras/``).
214
+
215
+ Top chemins consommés :
216
+
217
+ | Imports | Chemin legacy |
218
+ |---------|---------------------------------------------------------------|
219
+ | 29 | ``from picarones.measurements.runner import run_benchmark`` |
220
+ | 18 | ``from picarones.measurements.metrics import MetricsResult`` |
221
+ | 16 | ``from picarones.measurements.statistics import wilcoxon_test`` |
222
+ | 13 | ``from picarones.measurements.metrics import compute_metrics`` |
223
+ | 10 | ``from picarones.measurements.robustness import degrade_image_bytes`` |
224
+
225
+ **Pourquoi c'est important** : ces tests passent par les shims
226
+ au lieu de pointer vers le canonique. Tant que ces imports
227
+ existent, on **ne peut pas supprimer les shims** (le test casse).
228
+
229
+ **Stratégie** : sed batch par chemin, valider les tests,
230
+ commit, avancer. Shims supprimés dans les Lots A
231
+ (``core.modules`` + ``core.facts``), B
232
+ (``core.metric_registry`` + ``core.metric_hooks`` +
233
+ ``core.metrics``), C (``core.results`` + ``core.corpus`` +
234
+ ``core.pipeline``) et D (34 shims plats de ``measurements/``
235
+ vers ``evaluation.metrics/``) sur la branche
236
+ ``claude/migrate-core-to-domain-8ubIT``.
237
+
238
+ ### 4.B Imports legacy en production (hors shims eux-mêmes)
239
+
240
+ **12 fichiers** avec **41 statements** dans des paquets
241
+ non-legacy qui pointent encore vers le legacy. À résoudre
242
+ sprint par sprint en migrant chaque caller.
243
+
244
+ ### 4.C Symboles legacy non tracés dans la table de parité
245
+
246
+ **110 symboles** publics dans les paquets legacy ne sont pas
247
+ encore dans
248
+ ``tests/architecture/test_legacy_canonical_parity.py::LEGACY_PARITY``.
249
+ Répartition :
250
+
251
+ - ``measurements/`` : 104
252
+ - ``pipelines/`` : 6
253
+
254
+ Le test ``test_no_untracked_legacy_symbol_above_baseline``
255
+ autorise temporairement 110 (``BOOTSTRAP_BASELINE = 110``).
256
+ À diminuer à chaque session.
257
+
258
+ ### 4.D Plan de bataille pour les imports tests
259
+
260
+ L'ordre recommandé, par lots de symboles cohérents :
261
+
262
+ 1. ✅ **Lot A — domain** (~40 imports migrés, shims supprimés) :
263
+ - ``core.modules.{ArtifactType, BaseModule, ExecutionMode}``
264
+ → ``domain.{artifacts, module_protocol}``
265
+ - ``core.facts.*`` → ``domain.facts.*``
266
+ - Shims ``picarones.core.modules`` + ``picarones.core.facts``
267
+ supprimés ; doc utilisateur (tutorials/, developer/,
268
+ reference/api-stable.md, explanation/narrative-engine.en.md)
269
+ pointe maintenant vers les canoniques.
270
+ 2. ✅ **Lot B — evaluation/metric_*** (~45 imports migrés, shims
271
+ supprimés) :
272
+ - ``core.metric_registry.*`` → ``evaluation.metric_registry.*``
273
+ - ``core.metric_hooks.*`` → ``evaluation.metric_hooks.*``
274
+ - ``core.metrics.*`` → ``evaluation.metric_result.*``
275
+ - Shims ``picarones.core.metric_registry`` +
276
+ ``picarones.core.metric_hooks`` + ``picarones.core.metrics``
277
+ supprimés ; ``docs/reference/normalization-profiles.md`` et
278
+ ``docs/reference/api-stable.md`` migrés vers les chemins
279
+ canoniques.
280
+ 3. ✅ **Lot C — evaluation/{benchmark_result, corpus, pipeline}**
281
+ (~75 imports migrés, shims supprimés) :
282
+ - ``core.results.*`` → ``evaluation.benchmark_result.*``
283
+ - ``core.corpus.*`` → ``evaluation.corpus.*``
284
+ - ``core.pipeline.*`` → ``evaluation.pipeline.*``
285
+ - Shims ``picarones.core.{results, corpus, pipeline}``
286
+ supprimés ; sections de ``docs/reference/api-stable.md``
287
+ migrées vers les chemins canoniques ; logger filter dans
288
+ ``test_sprint32_multi_level_gt`` aligné sur
289
+ ``picarones.evaluation.corpus``.
290
+ 4. ✅ **Lot D — evaluation/metrics/*** (~100 imports + 44
291
+ prod migrés, 34 shims supprimés en bloc) :
292
+ - ``measurements.{baseline_comparison, calibration,
293
+ char_scores, confusion, cost_projection, difficulty,
294
+ error_absorption, hallucination, image_predictive,
295
+ image_quality, incremental_comparison, inter_engine,
296
+ layout, levers, lexical_modernization, line_metrics,
297
+ longitudinal, marginal_cost, module_policy, ner_backends,
298
+ normalization, numerical_sequences, pricing, rare_tokens,
299
+ robustness_projection, roman_numerals, specialization,
300
+ structure, taxonomy, taxonomy_comparison,
301
+ taxonomy_cooccurrence, taxonomy_intra_doc, throughput,
302
+ worst_lines}`` → ``evaluation.metrics.{...}``.
303
+ - ``picarones/measurements/__init__.py`` réécrit pour
304
+ refléter la nouvelle composition (modules legacy
305
+ restants + `import picarones.evaluation.metrics`
306
+ unique pour déclencher les décorateurs).
307
+ - ``test_no_flat_files_in_measurements::WHITELIST_FLAT_FILES_S3``
308
+ réduit de 60 → 25 entrées.
309
+ - ``test_module_coverage::TEST_ONLY_BASELINE`` réduit
310
+ de 16 → 4 entrées.
311
+ - ``test_file_budgets::FILE_BUDGETS`` débarrassé des
312
+ entrées orphelines (inter_engine, levers,
313
+ normalization).
314
+ 5. ✅ **Lot E — adapters/legacy_*** (8 shims supprimés en bloc,
315
+ 0 import à migrer) :
316
+ - ``engines.*`` → ``adapters.legacy_engines.*``
317
+ - ``modules.alto_text_to_mono_region`` →
318
+ ``adapters.legacy_modules.alto_text_to_mono_region``
319
+ - Tous les callers tests + production avaient déjà été
320
+ migrés en amont (Lots A-D), donc le Lot E n'a fait que
321
+ supprimer les 8 shims orphelins.
322
+ - ``LEGACY_PACKAGES`` réduit (retrait d'``engines`` et
323
+ ``modules``) dans
324
+ ``test_no_legacy_imports_in_rewrite.py`` et
325
+ ``test_legacy_canonical_parity.py``.
326
+ - ``ENGINES_DIR`` dans
327
+ ``tests/docs/test_readme_consistency.py`` redirigé vers
328
+ ``picarones/adapters/legacy_engines/``.
329
+ 6. ✅ **Lot F — reports_v2** (37 shims supprimés en bloc, 7
330
+ imports tests à migrer + ``scripts/gen_readme_tables.py``
331
+ redirigé) :
332
+ - ``report.*_render`` → ``reports_v2.html.renderers.*`` (29 shims)
333
+ - ``report.{generator, comparison, snapshot}`` →
334
+ ``reports_v2.html.*`` (3 shims)
335
+ - ``report.{assets, colors, render_helpers}`` →
336
+ ``reports_v2._helpers.*`` (3 shims)
337
+ - ``report.diff_utils`` → ``evaluation._diff_utils`` (1 shim)
338
+ - ``report.glossary`` → ``reports_v2.glossary`` (sous-package)
339
+ - ``scripts/gen_readme_tables.py`` redirigé vers
340
+ ``picarones/adapters/legacy_engines/`` ;
341
+ ``docs/reference/views.md`` migré en place vers
342
+ ``picarones/reports_v2/html/{views, generator, renderers,
343
+ templates}``.
344
+ 7. ⏳ **Lot G — measurements/runner et co.** (reporté car
345
+ canonique absent — phase 6 du plan maître).
346
+ Réalisé partiellement : suppression des 2 derniers shims
347
+ de ``picarones/core/`` (``diff_utils``, ``xml_utils``).
348
+ Le sous-paquet ``core/`` n'existe plus du tout.
349
+
350
+ La part majeure du Lot G originel (``measurements/runner``
351
+ + ``pipelines/``) reste à faire ; elle nécessite **d'abord
352
+ la création** des canoniques ``app/services/run_orchestrator``
353
+ et ``adapters/llm/pipeline`` (couvrant ``OCRLLMPipeline``,
354
+ ``PipelineMode``, ``over_normalization``, ``run_benchmark``,
355
+ ``_compute_document_result``). Sans ces canoniques, un
356
+ simple sed est impossible — il faudrait migrer les 76
357
+ imports vers des modules qui n'existent pas encore.
358
+
359
+ 8. ✅ **Lot H — measurements.statistics → evaluation.statistics**
360
+ (~70 imports migrés, 9 shims supprimés en bloc) :
361
+ - ``measurements.statistics.{bootstrap, cdd_render,
362
+ clustering, correlation, distributions, friedman_nemenyi,
363
+ pareto, wilcoxon}`` → ``evaluation.statistics.{...}``.
364
+ - ``measurements/statistics/`` (sous-paquet entier)
365
+ supprimé.
366
+
367
+ 9. ✅ **Lot I — extras.importers → adapters.corpus**
368
+ (3 shims supprimés, ~15 imports migrés) :
369
+ - ``extras.importers.htr_united`` →
370
+ ``adapters.corpus.htr_united``.
371
+ - ``extras.importers.huggingface`` →
372
+ ``adapters.corpus.huggingface``.
373
+ - ``extras.importers._fallback_log`` →
374
+ ``adapters.corpus._fallback_log``.
375
+
376
+ 10. ✅ **Lot J — measurements.metrics.{MetricsResult,
377
+ aggregate_metrics} → evaluation.metric_result** (~25
378
+ imports migrés, 0 shim supprimé) :
379
+ - Migration partielle uniquement des symboles canoniquement
380
+ migrés (``MetricsResult``, ``aggregate_metrics``).
381
+ - ``compute_metrics`` reste dans
382
+ ``picarones.measurements.metrics`` car aucun canonique
383
+ n'existe pour cette fonction (sera traité avec le Lot G
384
+ reporté).
385
+
386
+ À chaque lot : sed → tests → commit. Les shims devenus
387
+ orphelins après le lot peuvent être **supprimés** dans le même
388
+ commit (principe « no shim survives its caller »).
389
+
390
+ ---
391
+
392
+ ## 5. Prochaine sub-phase à exécuter
393
+
394
+ **Sub-phase 7.B.2** — refactoriser le corps de
395
+ ``PipelineRunner.run`` dans
396
+ ``picarones/evaluation/pipeline.py`` (lignes 384-590) pour
397
+ qu'il délègue au canonique ``PipelineExecutor`` via le
398
+ wrapper ``_BaseModuleAdapter`` créé en 7.B.1.
399
+
400
+ ### Plan d'exécution
401
+
402
+ 1. **Lire** ``picarones/evaluation/pipeline.py:PipelineRunner.run``
403
+ en entier pour comprendre la logique actuelle (résolution
404
+ d'inputs versionnés, exécution chronométrée, capture
405
+ d'erreur, évaluation auto vs GT, conversion outputs).
406
+
407
+ 2. **Lire** ``picarones/pipeline/_legacy_module_adapter.py``
408
+ en entier pour comprendre les outils disponibles
409
+ (``_BaseModuleAdapter``, ``_PayloadRegistry``,
410
+ ``wrap_initial_inputs``).
411
+
412
+ 3. **Écrire** un nouveau corps de ``PipelineRunner.run`` qui :
413
+ - Crée un ``_PayloadRegistry`` par appel.
414
+ - Wrappe les ``initial_inputs`` legacy via
415
+ ``wrap_initial_inputs(...)``.
416
+ - Convertit la ``PipelineSpec`` legacy en ``PipelineSpec``
417
+ canonique (``picarones.domain.pipeline_spec.PipelineSpec``).
418
+ Chaque ``PipelineStep.module: BaseModule`` devient un
419
+ ``adapter_name: str``, et l'adapter est
420
+ ``_BaseModuleAdapter(module, registry)``.
421
+ - Construit un ``adapter_resolver`` qui retourne le
422
+ wrapper de chaque module.
423
+ - Construit un ``RunContext``.
424
+ - Convertit le ``Document`` legacy en ``DocumentRef``.
425
+ - Invoque ``PipelineExecutor.run(canonical_spec,
426
+ document_ref, canonical_inputs, context)``.
427
+ - Reconvertit le ``PipelineResult`` canonique en
428
+ ``PipelineResult`` legacy.
429
+ - Calcule ``junction_metrics`` en post-étape (parcourt
430
+ les ``StepResult.produced_artifacts``, lit le payload
431
+ du registre, appelle ``compute_at_junction`` contre la
432
+ GT du document si ``GTLevel`` correspond).
433
+
434
+ 4. **Tester** : tous les tests existants doivent toujours
435
+ passer (les 7 fichiers axe B + ``test_sprint63_pipeline_runner``,
436
+ etc.). C'est l'invariant de la sub-phase 7.B.2.
437
+
438
+ 5. **Lint** : ``ruff check picarones/ tests/``.
439
+
440
+ 6. **Commit + push** avec message décrivant ce qui a été
441
+ fait + pointer vers la sub-phase 7.B.3 comme prochaine
442
+ étape.
443
+
444
+ ### Alternative pragmatique
445
+
446
+ Si le refactor 7.B.2 est trop gros pour une session,
447
+ **commencer par le Lot A de la section 4.D** (migrer les ~30
448
+ imports tests qui consomment ``core.modules`` et
449
+ ``core.facts`` vers leur canonique ``domain/``). Cela vide
450
+ une portion de la table de parité et permet de **supprimer les
451
+ shims** ``core.modules.py`` et ``core.facts.py`` en bloc —
452
+ résultat tangible et bien aligné avec le principe
453
+ « suppression agressive ».
454
+
455
+ Pareil pour Lots B-F : chaque lot est indépendant, fait
456
+ progresser la migration, et démontre concrètement la
457
+ suppression du legacy.
458
+
459
+ ### Pièges anticipés pour 7.B.2
460
+
461
+ - **Sémantique différente des inputs entre legacy et canonique** :
462
+ le legacy passe ``Document.image_path`` comme un string
463
+ pur dans ``initial_inputs[ArtifactType.IMAGE]`` ; le canonique
464
+ attend un ``Artifact(uri=...)``. ``wrap_initial_inputs``
465
+ fait la conversion mais il faut s'assurer que les modules
466
+ consomment bien le ``uri`` côté `_BaseModuleAdapter`.
467
+
468
+ - **``junction_metrics`` calcul** : le legacy
469
+ ``PipelineRunner.run`` calcule ``junction_metrics`` à
470
+ chaque step (cf. ligne 519-540 actuellement). Le canonique
471
+ ``PipelineExecutor`` ne le fait pas. Il faut donc faire
472
+ ce calcul **après** l'exécution canonique, en parcourant
473
+ les artefacts produits et en lisant les payloads via le
474
+ registre.
475
+
476
+ - **``output_types`` partial** : si un module produit un
477
+ output type non déclaré, le legacy le tolère (on remplit
478
+ ``StepResult.output_types`` avec ce qui est effectivement
479
+ produit, pas ce qui est déclaré). Le canonique
480
+ ``PipelineExecutor`` rejette en ``error="missing_output: ..."``.
481
+ Vérifier la sémantique attendue par les tests.
482
+
483
+ - **Spec conversion** : ``PipelineStep`` legacy a
484
+ ``inputs_from: dict[ArtifactType, str]`` (mapping
485
+ type→step_name). ``PipelineStep`` canonique a
486
+ ``inputs_from: tuple[InputBinding, ...]``. Conversion
487
+ attentive nécessaire.
488
+
489
+ ---
490
+
491
+ ## 6. Commande de démarrage de la nouvelle session
492
+
493
+ Le user envoie simplement :
494
+
495
+ ```
496
+ Reprends la migration. Lis docs/migration/SESSION_HANDOVER.md
497
+ en entier d'abord, puis commence par les vérifications de la
498
+ section 2.
499
+ ```
500
+
501
+ Ou pour aller direct à l'action :
502
+
503
+ ```
504
+ Continue la sub-phase 7.B.2.
505
+ ```
506
+
507
+ (Claude Code va automatiquement lire CLAUDE.md à l'init, qui
508
+ pointera vers ce SESSION_HANDOVER.md et les plans détaillés.)
docs/migration/legacy-retirement-plan.md ADDED
@@ -0,0 +1,1239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plan de retrait complet du legacy — vers la 2.0
2
+
3
+ > **Décision stratégique** : pas de cohabitation legacy + rewrite à
4
+ > long terme. La 2.0 est livrée **sans aucune ligne legacy**.
5
+ > L'arborescence cible (domain → formats → evaluation → pipeline →
6
+ > adapters → app → reports_v2 → interfaces) est unique.
7
+ >
8
+ > **Critère absolu** : zéro bricolage, zéro semi-rendu, zéro
9
+ > régression de comportement éditorial. Une institution comme la
10
+ > BnF ne tolère pas un *partial rewrite*.
11
+ >
12
+ > **Pas de contrainte de date** : on livre quand tout est propre.
13
+ >
14
+ > **Document vivant** : ce plan est mis à jour à chaque phase
15
+ > achevée. Toute exception ou découverte doit être inscrite ici.
16
+
17
+ ## Définition de « done » universelle
18
+
19
+ Chaque phase est terminée quand **tous** les critères suivants sont
20
+ remplis :
21
+
22
+ 1. **Code** : les modules legacy de la phase ont été soit migrés,
23
+ soit déclarés sans équivalent et supprimés (avec justification).
24
+ 2. **Tests** : tous les tests qui pointaient vers le legacy sont
25
+ migrés vers le rewrite ; les nouveaux tests couvrent le rewrite
26
+ à un niveau ≥ celui du legacy.
27
+ 3. **Régression** : le harness `tests/regression/legacy_vs_rewrite/`
28
+ prouve que le rewrite produit les mêmes résultats que le legacy
29
+ sur les corpus de référence (tolérance ε explicite par métrique).
30
+ 4. **Doc** : la doc utilisateur, opérationnelle et architecturale
31
+ ne mentionne plus le legacy de la phase ; les chemins cassés
32
+ `tests/architecture/test_doc_paths.py` baseline diminue.
33
+ 5. **Lint** : `ruff check picarones/ tests/` clean.
34
+ 6. **Suite complète** : `pytest tests/` 100 % vert sur 3 OS × 3
35
+ versions Python (3.11, 3.12, 3.13).
36
+ 7. **Coverage** : ≥ 85 %, pas de dégradation > 0,5 pt vs. la phase
37
+ précédente.
38
+
39
+ ## Phases
40
+
41
+ ### Phase 0 — Foundation ✅ terminée
42
+
43
+ **Objectif** : poser les garde-fous qui rendent les 11 phases
44
+ suivantes **vérifiables** sans introduire de régression invisible.
45
+
46
+ **Livrables** :
47
+
48
+ - [x] `docs/migration/legacy-retirement-plan.md` (ce document) —
49
+ inventaire complet, phases, acceptance criteria.
50
+ - [x] `docs/migration/regression-tolerances.md` — table des
51
+ tolérances acceptables par métrique et type d'output (CER ε=0,
52
+ Wilcoxon ε=1e-9, HTML diff sémantique, narrative facts égalité
53
+ ensembliste, etc.).
54
+ - [x] `tests/regression/legacy_vs_rewrite/` — harness scaffolding :
55
+ fixtures de corpus synthétique (small=3 docs, medium=30 docs,
56
+ large laissé pour ajout opportuniste) + gestion golden snapshot
57
+ avec flag `--regen-golden` + comparateurs sémantiques (floats,
58
+ sets, JSON). Marker `regression` enregistré et exclu de
59
+ ``addopts`` par défaut (opt-in via `pytest -m regression`).
60
+ Smoke test couvre les 16 invariants du harness lui-même.
61
+ - [x] `tests/architecture/test_no_legacy_imports_in_rewrite.py` —
62
+ garantit qu'aucun fichier des paquets `domain/`, `formats/`,
63
+ `evaluation/`, `pipeline/`, `adapters/`, `app/`, `reports_v2/`,
64
+ `interfaces/` n'importe depuis un paquet legacy. AST-based,
65
+ pas regex syntaxique. État initial : **vert** — le rewrite est
66
+ déjà clean.
67
+
68
+ **Acceptance** : ✅ remplie. Le harness est prêt à recevoir les
69
+ tests de régression de chaque phase suivante (`test_phase1_*.py`,
70
+ `test_phase2_*.py`, etc.). Toute fonctionnalité migrée DOIT
71
+ avoir son test de régression ajouté ici en même temps que le
72
+ code.
73
+
74
+ ### Phase 1 — Foundation conceptuelle (`core/`, `domain/`) — partielle ✅
75
+
76
+ **Audit de migrabilité réelle** : 5 modules `core/` sur 9 dépendent
77
+ de `core/modules.py` (legacy `BaseModule` + `ArtifactType` 6 valeurs,
78
+ incompatible avec le superset `domain/artifacts.ArtifactType` 10
79
+ valeurs). Les migrer ferait dériver le comportement des callers
80
+ legacy — à reporter en **Phase 4** quand le runner et les métriques
81
+ seront rewrités.
82
+
83
+ **Migrés en Phase 1 — 3 modules** (sans dépendance à `core/modules`) :
84
+
85
+ | Legacy | Canonique rewrite | Statut |
86
+ |--------|-------------------|--------|
87
+ | `core/xml_utils.py` (44 LOC) | `formats/_xml_utils.py` + re-export `picarones.formats.safe_parse_xml` | ✅ shim posé |
88
+ | `core/diff_utils.py` (89 LOC) | `evaluation/_diff_utils.py` + re-export `picarones.evaluation.{compute_word_diff,compute_char_diff,diff_stats}` | ✅ shim posé |
89
+ | `core/facts.py` (229 LOC) | `domain/facts.py` + re-export `picarones.domain.{Fact,FactType,FactImportance,DetectorRegistry,detect_all}` | ✅ shim posé |
90
+
91
+ **Reportés en Phase 4** (couplage à `core/modules.ArtifactType` legacy
92
+ ou au modèle du runner legacy) :
93
+
94
+ | Legacy | Bloqueur |
95
+ |--------|----------|
96
+ | `core/results.py` (677 LOC, `BenchmarkResult` + 30 champs agrégés) | Modèle central du runner legacy ; convergence avec `app.results.RunResult` en Phase 4 (rewrite de `measurements/runner/`) |
97
+ | `core/pipeline.py` (571 LOC, legacy `PipelineSpec` + `BaseModule`) | Concept différent du `domain.pipeline_spec.PipelineSpec` ; convergence en Phase 6 (`pipelines/` legacy) |
98
+ | `core/corpus.py` (511 LOC, `Document` avec payloads typés) | Modèle data legacy ≠ `DocumentRef` du rewrite ; convergence en Phase 4 |
99
+ | `core/modules.py` (173 LOC, `BaseModule` + `ArtifactType` 6 valeurs) | Type legacy partagé par 50+ modules ; déprécation en Phase 4 |
100
+ | `core/metric_registry.py` + `metric_hooks.py` (686 LOC) | Importe `core.modules.ArtifactType` ; convergence en Phase 4 |
101
+ | `core/metrics.py` (144 LOC, `MetricsResult`) | Schéma legacy ≠ `ViewResult.metric_values` du rewrite ; convergence en Phase 4 |
102
+
103
+ **Effort consommé Phase 1** : ~1 jour (3 modules + audit + tests).
104
+ **Effort restant — reporté en Phase 4** : ~5-7 jours.
105
+
106
+ **Acceptance Phase 1 partielle** : 3 modules `core/` sont des shims
107
+ re-export propres avec `DeprecationWarning`. Le test architectural
108
+ `test_no_legacy_imports_in_rewrite.py` reste vert. `picarones/__init__.py`
109
+ top-level pointe désormais vers le canonique pour les modules
110
+ migrés (pas de spam de warning à `import picarones`). Les 6 autres
111
+ modules `core/` fonctionnent inchangés ; ils seront migrés au
112
+ moment de la migration de leurs callers.
113
+
114
+ ### Phase 2 — Statistics (`measurements/statistics/`) — ✅ terminée
115
+
116
+ **Modules migrés** : 8 modules (`wilcoxon.py`, `friedman_nemenyi.py`,
117
+ `bootstrap.py`, `pareto.py`, `clustering.py`, `correlation.py`,
118
+ `distributions.py`, `cdd_render.py`).
119
+
120
+ **Canonique** : `picarones/evaluation/statistics/`.
121
+
122
+ **Travaux** :
123
+
124
+ - 8 modules copiés bit-for-bit dans `evaluation/statistics/`.
125
+ - 1 import legacy dans `clustering.py` migré
126
+ (`picarones.core.diff_utils.compute_word_diff`
127
+ → `picarones.evaluation.compute_word_diff`).
128
+ - 1 import auto-référencé dans `friedman_nemenyi.py` migré
129
+ (`picarones.measurements.statistics.wilcoxon._normal_sf`
130
+ → `picarones.evaluation.statistics.wilcoxon._normal_sf`).
131
+ - `evaluation/statistics/__init__.py` ré-exporte 14 symboles
132
+ publics (`bootstrap_ci`, `wilcoxon_test`, `compute_pairwise_stats`,
133
+ `friedman_test`, `nemenyi_posthoc`, `build_critical_difference_svg`,
134
+ `compute_pareto_front`, `ErrorCluster`, `cluster_errors`,
135
+ `compute_correlation_matrix`, `compute_reliability_curve`,
136
+ `compute_venn_data`, `_SCIPY_AVAILABLE`, `_chi_square_sf`,
137
+ `_nemenyi_critical_value`, `_normal_sf`, `_rank_row`).
138
+ - 8 shims `measurements/statistics/<X>.py` + 1 shim
139
+ `measurements/statistics/__init__.py` avec `DeprecationWarning`,
140
+ `__all__` complet pour rétrocompat des callers (5 fichiers
141
+ `report/`, 6 fichiers tests).
142
+
143
+ **Effort réel** : ~1 jour (vs estimation 5-7 j — code mathématique
144
+ pur, pas de couplage applicatif comme prévu, mais aussi script
145
+ de génération de shims qui a accéléré).
146
+
147
+ **Acceptance** : suite par défaut 5019+ passed (inchangée), tests
148
+ ciblés sur les statistiques 226 passed, test architectural
149
+ anti-imports legacy reste vert (3 passed). Pas de régression
150
+ détectée — les algorithmes scipy/numpy sont déterministes par
151
+ construction (seed=42 partout) ; le rendu SVG est strictement
152
+ identique parce que c'est le même fichier.
153
+
154
+ ### Phase 3 — Narrative engine (`measurements/narrative/`) — ✅ terminée
155
+
156
+ **Modules migrés** : 11 modules + 2 templates YAML.
157
+
158
+ | Legacy | Canonique |
159
+ |--------|-----------|
160
+ | `measurements/narrative/__init__.py` | `reports_v2/narrative/__init__.py` |
161
+ | `measurements/narrative/arbiter.py` | `reports_v2/narrative/arbiter.py` |
162
+ | `measurements/narrative/registry.py` | `reports_v2/narrative/registry.py` |
163
+ | `measurements/narrative/renderer.py` | `reports_v2/narrative/renderer.py` |
164
+ | `measurements/narrative/detectors/__init__.py` | `reports_v2/narrative/detectors/__init__.py` |
165
+ | `measurements/narrative/detectors/_helpers.py` | `reports_v2/narrative/detectors/_helpers.py` |
166
+ | `measurements/narrative/detectors/ensemble.py` | `reports_v2/narrative/detectors/ensemble.py` (1 détecteur) |
167
+ | `measurements/narrative/detectors/history.py` | `reports_v2/narrative/detectors/history.py` (3 détecteurs) |
168
+ | `measurements/narrative/detectors/pareto.py` | `reports_v2/narrative/detectors/pareto.py` (2 détecteurs) |
169
+ | `measurements/narrative/detectors/quality.py` | `reports_v2/narrative/detectors/quality.py` (4 détecteurs) |
170
+ | `measurements/narrative/detectors/ranking.py` | `reports_v2/narrative/detectors/ranking.py` (5 détecteurs) |
171
+ | `measurements/narrative/detectors/stratum.py` | `reports_v2/narrative/detectors/stratum.py` (3 détecteurs) |
172
+ | `measurements/narrative/templates/fr.yaml` | `reports_v2/narrative/templates/fr.yaml` |
173
+ | `measurements/narrative/templates/en.yaml` | `reports_v2/narrative/templates/en.yaml` |
174
+
175
+ Total : **18 détecteurs en 6 familles + arbitre + renderer + 36
176
+ templates YAML FR/EN** migrés.
177
+
178
+ **Cible architecturale** : `picarones/reports_v2/narrative/` (le
179
+ narratif est de la **présentation**, pas du domaine — il vit du
180
+ côté rapport, pas de l'évaluation).
181
+
182
+ **Travaux** :
183
+
184
+ - 14 fichiers (11 .py + 1 _helpers.py + 2 .yaml) copiés depuis le
185
+ legacy vers le canonique.
186
+ - Tous les imports `picarones.core.facts` (11 occurrences) migrés
187
+ vers `picarones.domain.facts` (Phase 1 a déjà migré ce module).
188
+ - Tous les imports auto-référencés `picarones.measurements.narrative`
189
+ réécrits en `picarones.reports_v2.narrative`.
190
+ - Path des templates YAML auto-ajusté (relatif à `__file__`).
191
+ - 12 shims `measurements/narrative/*.py` + `_helpers.py` shim
192
+ manuel (privé, pas d'`__all__`).
193
+ - `_DEFAULT_REGISTRY` (singleton du registre des détecteurs)
194
+ ré-exporté explicitement par le shim `__init__.py` pour la
195
+ rétrocompat des tests S19.
196
+
197
+ **Effort réel** : ~1 jour (vs estimation 8-12 j — script de
198
+ génération de shims a fortement accéléré ; pas d'aléatoire ni
199
+ d'I/O dans les détecteurs, donc régression triviale par
200
+ construction).
201
+
202
+ **Acceptance** : tous les tests narratifs passent — Sprints 16,
203
+ 19, 23, 29, 36, 44, 46, 73, 90, 92, baseline_comparison, chantier
204
+ 5, reproducibility_ops. 322 tests ciblés passed. Test architectural
205
+ anti-imports legacy : 3 passed (le rewrite reste autonome).
206
+ Garde-fou anti-hallucination préservé (les détecteurs lisent
207
+ toujours le dict JSON d'entrée, pas une source externe).
208
+
209
+ ### Phase 4 — 35 mesures legacy (`measurements/*.py`) — partielle ✅
210
+
211
+ **Audit de migrabilité** : sur 35 mesures legacy, **24 étaient
212
+ déjà des re-exports** (Phase 4 partielle pré-existante avec un
213
+ canonique `evaluation/metrics/X.py`). Sur les 11 modules réellement
214
+ "contenu" :
215
+
216
+ - **9 sont migrés en Phase 4 (cette session)** sans toucher à
217
+ `core.modules` : autonomes ou en cascade vers d'autres modules
218
+ migrables.
219
+ - **13 modules réels** restent bloqués par
220
+ `core.modules.ArtifactType` (enum legacy 6 valeurs incompatible
221
+ avec le superset `domain.artifacts.ArtifactType` 10 valeurs ;
222
+ `TEXT` ≠ `RAW_TEXT`, `ALTO` ≠ `ALTO_XML`, `PAGE` ≠ `PAGE_XML`).
223
+ Substitution non transparente — exigerait un travail de
224
+ remapping sémantique sur chaque caller.
225
+
226
+ **Migrés en Phase 4 — 9 modules** :
227
+
228
+ | Legacy | Canonique | Notes |
229
+ |--------|-----------|-------|
230
+ | `measurements/char_scores.py` (307) | `evaluation/metrics/char_scores.py` | Autonome |
231
+ | `measurements/difficulty.py` (161) | `evaluation/metrics/difficulty.py` | Autonome |
232
+ | `measurements/ner_backends.py` (186) | `evaluation/metrics/ner_backends.py` | Autonome |
233
+ | `measurements/normalization.py` (51) | `evaluation/metrics/normalization.py` | Autonome |
234
+ | `measurements/structure.py` (182) | `evaluation/metrics/structure.py` | Autonome |
235
+ | `measurements/cost_projection.py` (140) | `evaluation/metrics/cost_projection.py` | dep `pricing` (déjà migré) |
236
+ | `measurements/specialization.py` (158) | `evaluation/metrics/specialization.py` | dep `inter_engine` (re-export déjà) |
237
+ | `measurements/taxonomy.py` (294) | `evaluation/metrics/taxonomy.py` | dep `char_scores` (en cascade) |
238
+ | `measurements/taxonomy_intra_doc.py` (178) | `evaluation/metrics/taxonomy_intra_doc.py` | dep `taxonomy` (en cascade) |
239
+
240
+ Total : **1657 lignes de code migrées + 9 shims legacy**.
241
+
242
+ **Bloqués — 13 modules + 1 sous-package + 6 modules `core/`** :
243
+
244
+ Reportés à une phase dédiée **« Phase 4-bis : ArtifactType
245
+ migration »** dont le périmètre est :
246
+
247
+ 1. Décider le mapping sémantique TEXT → RAW_TEXT vs CORRECTED_TEXT
248
+ (par module, en lisant le contexte d'usage).
249
+ 2. Migrer `core/modules.py` (`BaseModule` + `ArtifactType` 6
250
+ valeurs) vers `domain/module_protocol.py`.
251
+ 3. Migrer `core/metric_registry.py` + `core/metric_hooks.py` vers
252
+ `evaluation/registry/`.
253
+ 4. Adapter chaque module bloqué : `mufi.py`, `abbreviations.py`,
254
+ `early_modern_typography.py`, `modern_archives.py`,
255
+ `roman_numerals.py`, `unicode_blocks.py`, `equivalence_profile.py`,
256
+ `philological_hooks.py`, `ner.py`, `readability.py`,
257
+ `readability_hooks.py`, `searchability.py`,
258
+ `searchability_hooks.py`, `reading_order.py`, `alto_metrics.py`,
259
+ `numerical_sequences.py`, `numerical_sequences_hooks.py`,
260
+ `builtin_hooks.py`, `builtin_metrics.py`, `metrics.py`,
261
+ `pipeline_benchmark.py`, `pipeline_comparison.py`,
262
+ `pipeline_spec_loader.py`, `robustness.py`, `reliability.py`,
263
+ `history.py`.
264
+ 5. Migrer le sous-package `measurements/runner/` (orchestrateur
265
+ legacy → fondre dans `pipeline/` + `app/services/`).
266
+ 6. Migrer `core/results.py` (`BenchmarkResult` + 30 champs agrégés
267
+ → typed Artifacts dans `domain/`).
268
+ 7. Migrer `core/corpus.py` (`Document`/`Corpus`/`GTLevel` → modèle
269
+ convergent avec `domain.corpus`).
270
+
271
+ **Effort estimé Phase 4-bis** : 18-22 jours (vs 23-28 j initialement
272
+ estimés pour Phase 4 complète — la moitié déjà faite par les
273
+ re-exports pré-existants et les 9 modules de cette session).
274
+
275
+ **Acceptance Phase 4 partielle** : 9 modules migrés, 1191 tests
276
+ mesures passent (inchangés), test architectural anti-imports
277
+ legacy reste vert. Les 13 modules réels + 6 modules `core/`
278
+ restants sont documentés comme dépendant d'une migration
279
+ ArtifactType.
280
+
281
+ #### Tentative Phase 4-bis (avortée — diagnostic posé)
282
+
283
+ Une tentative de migration coordonnée de l'``ArtifactType`` a été
284
+ explorée puis revertée :
285
+
286
+ **Stratégie testée** : exploiter le mécanisme natif d'aliases
287
+ d'``Enum`` Python (un membre avec la même valeur qu'un autre devient
288
+ un alias). Ajout de ``TEXT = "raw_text"``, ``ALTO = "alto_xml"``,
289
+ ``PAGE = "page_xml"`` à ``domain.artifacts.ArtifactType`` + hook
290
+ ``_missing_`` pour accepter les valeurs string legacy. Puis
291
+ transformation de ``core/modules.py`` en shim qui ré-exporte
292
+ ``ArtifactType`` et ``BaseModule`` depuis le canonique.
293
+
294
+ **Conservé en place** : les aliases + ``_missing_`` dans
295
+ ``domain.artifacts.ArtifactType``. Inoffensif — aucun code legacy
296
+ ne les voit puisqu'aucun module legacy n'importe encore depuis le
297
+ canonique.
298
+
299
+ **Reverté** : le shim ``core/modules.py``. Cause : passer le
300
+ ``core.modules.ArtifactType`` du legacy enum 6 valeurs au superset
301
+ canonique change silencieusement ``ArtifactType.TEXT.value`` de
302
+ ``"text"`` à ``"raw_text"``. Or 27 tests legacy
303
+ (``test_sprint63_pipeline_runner``, ``test_sprint65_pipeline_comparison``,
304
+ ``test_sprint68_pipeline_comparison_html`` etc.) reposent sur le
305
+ fait que les clés des dicts ``junction_metrics`` produites par le
306
+ runner legacy sont les valeurs string legacy. Quand le runner
307
+ utilise ``at.value`` pour stocker, il stocke maintenant ``"raw_text"``,
308
+ et les tests qui cherchaient ``junction_metrics["text"]`` cassent.
309
+
310
+ Le diagnostic est plus profond qu'un simple rename : le legacy
311
+ ``BenchmarkResult.junction_metrics`` est un ``dict[str, dict]``
312
+ indexé par valeur string ; sa stabilité de format est implicitement
313
+ testée. Migrer ``core.modules.ArtifactType`` exige un travail
314
+ **par module** d'identification des dicts indexés par valeur
315
+ string, et soit (a) double-clé pour rétrocompat, (b) migration
316
+ ordonnée tests-en-même-temps.
317
+
318
+ **Plan rectifié pour Phase 4-bis** :
319
+
320
+ 1. Lister exhaustivement les dicts indexés par ``ArtifactType.value``
321
+ dans le legacy (``core/results.py``, ``core/pipeline.py``,
322
+ ``measurements/runner/``, ``measurements/pipeline_*``).
323
+ 2. Décider la stratégie par module : double-clé pendant la
324
+ migration vs migration coordonnée tests + code.
325
+ 3. Migrer un cluster à la fois en validant la suite après chaque.
326
+
327
+ **Effort rectifié** : 25-30 jours (vs 18-22 estimés initialement —
328
+ le couplage implicite des dicts indexés par valeur string n'avait
329
+ pas été vu à l'audit).
330
+
331
+ **Statut Phase 4-bis** : analyse posée, exécution reportée à un
332
+ sprint dédié de plusieurs sessions.
333
+
334
+ #### Phase 4-bis — Reprise et complétion (2026-05)
335
+
336
+ La reprise s'appuie sur le **diagnostic de la tentative avortée**
337
+ en adoptant la stratégie « double-clé » : on garde les aliases
338
+ legacy ``TEXT``/``ALTO``/``PAGE`` dans
339
+ ``domain.artifacts.ArtifactType``, et on s'engage à ce que tout
340
+ dict indexé par ``ArtifactType.value`` présente en parallèle la
341
+ clé canonique (``"raw_text"``) **et** la clé legacy (``"text"``)
342
+ quand un alias existe.
343
+
344
+ **Ajouts dans le canonique** :
345
+
346
+ - ``LEGACY_VALUE_ALIASES = {"raw_text": "text", "alto_xml": "alto",
347
+ "page_xml": "page"}`` dans ``domain.artifacts``.
348
+ - ``expand_legacy_keys(d)`` qui mute un dict pour y copier les
349
+ valeurs canoniques sous les alias legacy.
350
+ - ``BaseModule`` canonique dans ``domain/module_protocol.py``
351
+ (10 valeurs vs 6 legacy).
352
+
353
+ **Sites mis à jour** :
354
+
355
+ - ``core/pipeline.py`` : ``StepResult.junction_metrics`` enrichi
356
+ via ``expand_legacy_keys`` à la production.
357
+ ``PipelineResult.junction_metrics_for`` essaie la clé canonique
358
+ puis l'alias legacy.
359
+ ``_artifact_type_to_gt_level`` utilise une map explicite
360
+ ``ArtifactType → GTLevel`` (les valeurs canoniques
361
+ ``"raw_text"``/``"alto_xml"``/``"page_xml"`` ne matchent plus
362
+ ``GTLevel`` qui garde ``"text"``/``"alto"``/``"page"``).
363
+ - ``measurements/pipeline_benchmark.py`` :
364
+ ``StepAggregate.junction_metrics`` enrichi via
365
+ ``expand_legacy_keys`` après agrégation.
366
+ - ``measurements/pipeline_comparison.py`` :
367
+ ``_final_metric_value`` essaie canonique puis legacy.
368
+ - ``evaluation/metrics/module_policy.py`` : la comparaison
369
+ manifest vs déclaration normalise via les aliases (``"text"``
370
+ match ``"raw_text"``).
371
+
372
+ **Migration des callers** : 16 modules ``measurements/`` + 6
373
+ modules `core/`/`engines/`/`modules/`/`cli/`/`report/` migrés
374
+ de ``from picarones.core.modules import ArtifactType`` vers
375
+ ``from picarones.domain.artifacts import ArtifactType`` (et
376
+ ``from picarones.domain.module_protocol import BaseModule``
377
+ quand applicable).
378
+
379
+ **``core/modules.py``** : transformé en shim avec
380
+ ``DeprecationWarning`` à l'import.
381
+
382
+ **Tests adaptés** :
383
+
384
+ - ``test_public_api.py::test_artifact_type_values`` —
385
+ asserte le set canonique 10 valeurs.
386
+ - ``test_sprint33_module_interface.py::test_repr_shows_io_types``
387
+ — asserte ``"raw_text→raw_text"``.
388
+ - ``test_sprint68_pipeline_comparison_html.py::test_display_label_default``
389
+ — asserte ``"raw_text.cer"``.
390
+
391
+ **Acceptance Phase 4-bis** : 5019 tests passent (vs 5008 avant la
392
+ reprise — 11 tests étaient cassés par la première tentative et
393
+ sont maintenant verts). Les 24 fichiers de tests qui importent
394
+ encore ``from picarones.core.modules`` continuent à fonctionner via
395
+ le shim — ils ne deviendront erronés que quand le shim sera retiré
396
+ en 2.0.
397
+
398
+ **Reportés à Phase 4-ter** :
399
+
400
+ - ``core/metric_registry.py`` (263 l) et ``core/metric_hooks.py``
401
+ (423 l) restent en place : ils sont consommés par 30+ modules
402
+ ``measurements/`` via le décorateur ``@register_metric`` et les
403
+ hooks ``register_document_metric``/``register_corpus_aggregator``.
404
+ Le canonique existant ``evaluation/registry/registry.py`` utilise
405
+ un design **instance-based** (``MetricRegistry()`` explicite,
406
+ pas de décorateur module-level) qui est incompatible avec le
407
+ pattern legacy. La migration exige un choix de design (garder
408
+ les deux, fondre dans une API unique, etc.) qui dépasse Phase
409
+ 4-bis.
410
+ - ``core/metrics.py`` (144 l, ``MetricsResult`` +
411
+ ``aggregate_metrics``) reste en place : pas d'équivalent
412
+ canonique dans ``domain/`` ou ``evaluation/`` à ce jour. La
413
+ conversion nécessitera d'abord de créer le destinataire dans
414
+ ``domain.measurements`` (typé Pydantic au lieu de dataclass) ou
415
+ ``evaluation.aggregation``.
416
+ - ``core/results.py`` (``BenchmarkResult`` + champs agrégés) :
417
+ même statut.
418
+ - ``core/corpus.py`` (``Document``/``Corpus``/``GTLevel``) :
419
+ même statut. Note : ``GTLevel`` étant intentionnellement
420
+ conservé en parallèle d'``ArtifactType``, son retrait dépend
421
+ de la fin de migration des callers qui parsent les types de GT
422
+ par leur valeur string.
423
+
424
+ #### Phase 4-ter — Relocalisation Cercle 1 → Cercle 2 (2026-05)
425
+
426
+ Stratégie « relocaliser sans redessiner » : on déplace
427
+ verbatim les modules legacy de ``core/`` (Cercle 1) vers
428
+ ``evaluation/`` (Cercle 2) où ils appartiennent sémantiquement,
429
+ sans toucher à leur API publique. Le pattern module-level
430
+ décorateur (``@register_metric``, ``@register_document_metric``,
431
+ ``@register_corpus_aggregator``) est **conservé** — sa
432
+ convergence avec l'instance-based ``evaluation.registry.MetricRegistry``
433
+ (Sprint A14-S5) est laissée à un futur sprint dédié quand un
434
+ caller institutionnel le demandera.
435
+
436
+ **Migrations effectuées (A-D)** :
437
+
438
+ | Source legacy | Destination canonique | Lignes |
439
+ |---|---|---|
440
+ | ``core/metric_registry.py`` | ``evaluation/metric_registry.py`` | 264 |
441
+ | ``core/metric_hooks.py`` | ``evaluation/metric_hooks.py`` | 427 |
442
+ | ``core/metrics.py`` | ``evaluation/metric_result.py`` | 145 |
443
+ | ``core/results.py`` | ``evaluation/benchmark_result.py``| 702 |
444
+
445
+ Total : **1538 lignes** déplacées du Cercle 1 vers le Cercle 2.
446
+ Les chemins ``core/X.py`` deviennent des shims minimaux
447
+ (< 30 lignes chacun) avec ``DeprecationWarning`` à l'import.
448
+
449
+ **Adaptations transverses** :
450
+
451
+ - ``evaluation/benchmark_result.py`` ne peut pas importer
452
+ ``picarones.__version__`` (cycle d'import via
453
+ ``measurements/``). La résolution de version utilise
454
+ désormais ``importlib.metadata`` directement avec fallback
455
+ ``"1.0.0"``.
456
+ - ``tests/architecture/test_file_budgets.py`` mis à jour
457
+ pour pointer vers les nouveaux chemins canoniques.
458
+ - ``tests/core/test_public_api.py::TestCercle1IsLean.EXPECTED_CERCLE1``
459
+ ne contient plus que ``corpus.py`` et ``pipeline.py``
460
+ (les seuls modules ``core/`` réels qui restent).
461
+
462
+ **Reporté à Phase 4-quater** :
463
+
464
+ ``core/corpus.py`` (511 l, ``Document``/``Corpus``/``GTLevel`` +
465
+ payloads + ``load_corpus_from_directory``) reste en place.
466
+ Raison : il y a déjà ``domain.corpus.CorpusSpec`` (Pydantic,
467
+ immutable, structural) et ``domain.documents.DocumentRef``
468
+ en parallèle. La convergence des deux modèles
469
+ (``Document``/``Corpus`` historiques riches en behavior vs
470
+ ``CorpusSpec``/``DocumentRef`` purement déclaratifs) est un
471
+ choix de design (fondre, garder les deux, marquer l'un comme
472
+ view-de-l'autre…) qui dépasse Phase 4-ter. L'objectif Phase
473
+ 4-quater est de produire un RFC qui tranche cette question
474
+ puis migre les 14 callers en une fois.
475
+
476
+ **Acceptance Phase 4-ter (A-D)** : 5019 tests passent, lint
477
+ vert, architecture vérifiée (anti-cycles, file budgets,
478
+ EXPECTED_CERCLE1 mis à jour). Les 24+ fichiers de tests qui
479
+ importent encore via les chemins ``core/`` continuent à
480
+ fonctionner via les shims — déprécation visible mais
481
+ non-bloquante.
482
+
483
+ #### Phase 4-quater — Relocalisation de ``core/corpus.py`` (2026-05)
484
+
485
+ Décision RFC : **garder les deux modèles en parallèle**, sans
486
+ fusion. ``evaluation.corpus`` (riche en behavior, dataclass,
487
+ chargé en mémoire, runner-friendly) et
488
+ ``domain.corpus.CorpusSpec`` (Pydantic, immutable, déclaratif,
489
+ pipeline-executor-friendly) sont des projections différentes
490
+ d'un même domaine ; un convertisseur explicite
491
+ ``CorpusSpec ↔ Corpus`` viendra quand un caller institutionnel
492
+ l'exigera concrètement. Tenter une convergence prématurée
493
+ casserait soit le runner historique (qui consomme
494
+ ``Document.get_gt(level)`` + ``Corpus.has_ocr_text``), soit le
495
+ pipeline executor canonique (qui consomme l'immutabilité de
496
+ ``CorpusSpec`` pour la sérialisation YAML).
497
+
498
+ Migration effectuée
499
+ -------------------
500
+
501
+ | Source legacy | Destination canonique | Lignes |
502
+ |----------------------|-------------------------------|--------|
503
+ | ``core/corpus.py`` | ``evaluation/corpus.py`` | 533 |
504
+
505
+ Le chemin ``core/corpus.py`` devient un shim minimal
506
+ (< 30 lignes) avec ``DeprecationWarning`` à l'import. Les 14
507
+ callers de production (``cli/_pipeline``, ``cli/_robustness``,
508
+ ``cli/_workflows``, ``web/benchmark_utils``,
509
+ ``measurements/pipeline_benchmark``,
510
+ ``measurements/pipeline_comparison``,
511
+ ``measurements/robustness``, ``measurements/runner/orchestration``,
512
+ ``measurements/runner/ner_attach``,
513
+ ``extras/importers/{iiif,gallica,escriptorium}``,
514
+ ``core/pipeline``, et ``picarones/__init__.py``) sont migrés
515
+ vers ``picarones.evaluation.corpus``.
516
+
517
+ Note : ``GTLevel`` reste consommé en parallèle d'``ArtifactType``
518
+ par le runner — la convergence de ces deux énumérations est
519
+ liée au retrait du runner legacy lui-même (Phase 6+ du plan).
520
+
521
+ Adaptations transverses
522
+ -----------------------
523
+
524
+ - ``test_file_budgets.py`` : entrée ``core/corpus.py`` retirée,
525
+ remplacée par ``evaluation/corpus.py`` (budget identique 600).
526
+ - ``test_public_api.py::EXPECTED_CERCLE1`` : ``corpus.py``
527
+ retiré de la liste — il ne reste plus que ``pipeline.py``
528
+ comme module Cercle 1 réel.
529
+
530
+ État final de ``core/`` après Phase 4-quater
531
+ --------------------------------------------
532
+
533
+ Le répertoire ``picarones/core/`` ne contient désormais qu'**un
534
+ seul module réel** :
535
+
536
+ - ``pipeline.py`` (~570 l) — ``PipelineRunner`` + ``PipelineSpec``
537
+ + ``StepResult`` + ``PipelineResult``.
538
+
539
+ Tous les autres fichiers (``corpus.py``, ``modules.py``,
540
+ ``metric_registry.py``, ``metric_hooks.py``, ``metrics.py``,
541
+ ``results.py``, ``facts.py``, ``diff_utils.py``,
542
+ ``xml_utils.py``) sont des shims < 30 lignes avec
543
+ ``DeprecationWarning``.
544
+
545
+ **Acceptance Phase 4-quater** : 5019 tests passent (inchangé
546
+ depuis Phase 4-ter), lint vert, architecture vérifiée. Le
547
+ ``__init__.py`` racine (``picarones/__init__.py``) importe
548
+ maintenant directement depuis les chemins canoniques (Cercle
549
+ 1 ``domain/`` + Cercle 2 ``evaluation/``), seul ``core.pipeline``
550
+ reste référencé pour ses types.
551
+
552
+ **Reporté à Phase 5** :
553
+
554
+ - ``core/pipeline.py`` (``PipelineRunner``) — convergence avec
555
+ le pipeline executor canonique
556
+ (``picarones/pipeline/executor.py``, ``planner.py``,
557
+ ``runner.py``). C'est le dernier module ``core/`` réel ;
558
+ son retrait suppose que tous les callers passent par le
559
+ pipeline executor, ce qui implique l'écriture du sucre
560
+ syntaxique pour les benchmarks OCR mono-étape (typique
561
+ ``run_benchmark(corpus, [engine_a, engine_b])``).
562
+ - Convergence ``GTLevel`` ↔ ``ArtifactType`` (en attente du
563
+ retrait du runner legacy).
564
+
565
+ ### Phase 5 — Reports HTML (`report/`)
566
+
567
+ **Modules** :
568
+
569
+ - 22 renderers thématiques : `baseline_render.py`, `calibration_render.py`,
570
+ `difficulty_render.py`, `error_absorption_render.py`,
571
+ `image_predictive_render.py`, `incremental_comparison_render.py`,
572
+ `inter_engine_render.py`, `levers_render.py`,
573
+ `lexical_modernization_render.py`, `longitudinal_render.py`,
574
+ `marginal_cost_render.py`, `module_audit_render.py`,
575
+ `multirun_stability_render.py`, `ner_render.py`,
576
+ `numerical_sequences_render.py`, `philological_render.py`,
577
+ `pipeline_dag_render.py`, `pipeline_render.py`,
578
+ `rare_token_recall_render.py`, `readability_render.py`,
579
+ `robustness_projection_render.py`, `searchability_render.py`,
580
+ `specialization_render.py`, `stratification_render.py`,
581
+ `taxonomy_comparison_render.py`, `taxonomy_cooccurrence_render.py`,
582
+ `taxonomy_intra_doc_render.py`, `throughput_render.py`,
583
+ `worst_lines_render.py`.
584
+ - 5 vues : `views/{advanced_taxonomy,diagnostics,economics,
585
+ pipeline,robustness}.py`.
586
+ - `generator.py` (orchestrateur), `comparison.py`, `snapshot.py`,
587
+ `assets.py`, `colors.py`, `render_helpers.py`, `report_data/`.
588
+ - `templates/` (~10 fichiers Jinja2), `glossary/` (2 YAML, 25
589
+ entrées), `i18n/`, `vendor/`.
590
+
591
+ **Cible** : `picarones/reports_v2/html/views/<theme>.py` + helpers
592
+ partagés dans `reports_v2/html/_helpers/`. Glossaire dans
593
+ `reports_v2/html/glossary/`. Templates Jinja2 dans
594
+ `reports_v2/html/templates/`.
595
+
596
+ **Effort** : 12-18 jours.
597
+
598
+ **Acceptance** : régression bit-for-bit sur le HTML produit pour
599
+ les 3 corpus de référence. Aucun renderer legacy laissé.
600
+
601
+ #### Phase 5.A+B — Helpers + glossary + i18n (2026-05)
602
+
603
+ Première tranche du retrait du legacy ``report/`` : les utilitaires
604
+ purs et les ressources statiques, sans toucher aux 22 renderers
605
+ thématiques (qui consomment ``BenchmarkResult`` legacy et seront
606
+ migrés au fil des phases ultérieures, par lots).
607
+
608
+ **Migrations effectuées** :
609
+
610
+ | Source legacy | Destination canonique |
611
+ |--------------------------------------|------------------------------------------------|
612
+ | ``report/colors.py`` | ``reports_v2/_helpers/colors.py`` |
613
+ | ``report/render_helpers.py`` | ``reports_v2/_helpers/render_helpers.py`` |
614
+ | ``report/assets.py`` + ``vendor/`` | ``reports_v2/_helpers/assets.py`` + ``vendor/``|
615
+ | ``report/glossary/{fr,en}.yaml`` | ``reports_v2/glossary/{fr,en}.yaml`` |
616
+ | ``report/i18n/{fr,en}.json`` | ``reports_v2/i18n/{fr,en}.json`` |
617
+
618
+ ``report/diff_utils.py`` redirige désormais directement vers
619
+ ``picarones.evaluation`` (au lieu du double-shim via
620
+ ``core.diff_utils``).
621
+
622
+ **Shims** : tous les chemins legacy ``report/X`` restent disponibles
623
+ via des shims minimaux (< 25 lignes) avec ``DeprecationWarning``
624
+ à l'import.
625
+
626
+ **Adaptations transverses** :
627
+
628
+ - ``picarones/i18n.py`` : ``_I18N_DIR`` pointe désormais vers
629
+ ``reports_v2/i18n/``.
630
+ - 22 renderers ``report/*_render.py`` migrés sur leurs imports
631
+ internes vers ``picarones.reports_v2._helpers.*``.
632
+ - 28 fichiers de tests mis à jour (chemins ``picarones/report/i18n/*``
633
+ remplacés par ``picarones/reports_v2/i18n/*``).
634
+ - ``test_layer_dependencies.py::EXTERNAL_ALLOWED["reports_v2"]``
635
+ étendu à ``PIL`` (Pillow utilisé par ``_helpers/assets.py``
636
+ pour le redimensionnement d'images).
637
+ - ``test_file_budgets.py`` : entrée ``report/render_helpers.py``
638
+ remplacée par ``reports_v2/_helpers/render_helpers.py``
639
+ (budget 480 inchangé).
640
+
641
+ **Acceptance Phase 5.A+B** : 5019 tests passent, lint vert,
642
+ architecture vérifiée (anti-cycles, file budgets). Aucune
643
+ régression sur les renderers thématiques (toujours legacy).
644
+
645
+ #### Phase 5.C.batch1 — Lot 1 : 5 renderers les plus petits (2026-05)
646
+
647
+ Première vague de migration des 22 renderers thématiques. On
648
+ relocalise verbatim, sans toucher au contrat avec
649
+ ``BenchmarkResult`` legacy — la convergence avec ``RunResult``
650
+ canonique reste un sprint à part entière (5.D ou 5.E selon
651
+ priorité).
652
+
653
+ Convention de nommage : ``picarones.report.<theme>_render`` →
654
+ ``picarones.reports_v2.html.renderers.<theme>``. Le suffixe
655
+ ``_render`` est retiré (déjà implicite dans la position).
656
+
657
+ **Migrations effectuées** :
658
+
659
+ | Source legacy | Destination canonique |
660
+ |------------------------------------------|------------------------------------------------------|
661
+ | ``report/searchability_render.py`` (103) | ``reports_v2/html/renderers/searchability.py`` |
662
+ | ``report/specialization_render.py`` (113)| ``reports_v2/html/renderers/specialization.py`` |
663
+ | ``report/marginal_cost_render.py`` (111) | ``reports_v2/html/renderers/marginal_cost.py`` |
664
+ | ``report/rare_token_recall_render.py`` (116)| ``reports_v2/html/renderers/rare_token_recall.py``|
665
+ | ``report/readability_render.py`` (126) | ``reports_v2/html/renderers/readability.py`` |
666
+
667
+ Total : ~569 lignes relocalisées. Les chemins ``report/X_render.py``
668
+ deviennent des shims minimaux (< 20 lignes) avec
669
+ ``DeprecationWarning``.
670
+
671
+ **Adaptations transverses** :
672
+
673
+ - ``reports_v2/html/renderers/specialization.py`` import canonique
674
+ ``picarones.evaluation.metrics.specialization`` (au lieu du shim
675
+ legacy ``picarones.measurements.specialization``) pour respecter
676
+ la règle layer-dependencies (interdiction d'importer du legacy
677
+ depuis ``reports_v2/``).
678
+ - ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à
679
+ ``"specialization"`` : son shim legacy n'a plus de consommateur
680
+ production (le renderer est désormais dans ``reports_v2/``).
681
+ - 3 tests (``test_extra_metrics.py``,
682
+ ``test_sprint86_aii5_html.py``,
683
+ ``test_sprint87_readability_html.py``,
684
+ ``test_sprint89_specialization.py``) mis à jour pour pointer
685
+ vers les nouveaux chemins canoniques.
686
+ - ``picarones/report/generator.py`` mis à jour pour importer les
687
+ 5 renderers depuis ``reports_v2/html/renderers/``.
688
+
689
+ **Acceptance batch 1** : 5019 tests passent, lint vert,
690
+ architecture vérifiée.
691
+
692
+ **Reporté aux batches suivants** :
693
+
694
+ - Batch 2 ✅ (cf. ci-dessous) — 5 renderers (45-165 LOC).
695
+ - Batch 3 ✅ (cf. ci-dessous) — 5 renderers (173-222 LOC).
696
+ - Batch 4 ✅ (cf. ci-dessous) — 5 renderers (188-321 LOC).
697
+ - Batch 5 ✅ (cf. ci-dessous) — 5 renderers (148-314 LOC).
698
+ - Batch 6 ✅ (cf. ci-dessous) — 2 renderers (``levers``, ``philological``).
699
+ - Batch 7 ✅ (cf. ci-dessous) — pré-requis migrés
700
+ (``roman_numerals``, ``numerical_sequences``,
701
+ ``pipeline_benchmark``, ``pipeline_comparison``,
702
+ ``core/pipeline``) puis 2 renderers
703
+ (``numerical_sequences``, ``pipeline``).
704
+ - Phase 5.D ✅ — 5 vues (``views/*.py``).
705
+ - Phase 5.E ✅ — ``generator.py``, ``comparison.py``,
706
+ ``snapshot.py``, ``report_data/`` (8 fichiers), templates
707
+ Jinja2 (13 fichiers), ``picarones/i18n.py``.
708
+
709
+ Phase 5 est **terminée** côté ``report/``.
710
+
711
+ **Note sur ``core/pipeline.py``** : la Phase 5.C.batch7 a
712
+ *relocalisé* le ``PipelineRunner`` legacy de ``core/pipeline.py``
713
+ vers ``evaluation/pipeline.py``, mais **n'a pas effectué la
714
+ convergence** avec le canonique ``picarones.pipeline.executor``
715
+ (designs incompatibles : ``BaseModule`` vs ``StepExecutor``,
716
+ payloads bruts vs ``Artifact`` typés, dataclass mutable vs
717
+ Pydantic immutable, ...). L'audit détaillé + sub-plan est dans
718
+ ``docs/migration/pipeline-convergence-plan.md``. La
719
+ recommandation est la stratégie « wrapper legacy → canonique »
720
+ (3-4 sessions) qui préserve l'API publique des callers tout en
721
+ unifiant le moteur. Décision sur quand exécuter la convergence
722
+ laissée au prochain sprint dédié.
723
+
724
+ #### Phase 5.C.batch2 — Lot 2 : 5 renderers moyens (2026-05)
725
+
726
+ Deuxième vague. Substitution dans la sélection initiale :
727
+ ``numerical_sequences_render`` reporté au batch 3 (sa dépendance
728
+ ``measurements/numerical_sequences.py`` dépend elle-même de
729
+ ``measurements/roman_numerals.py``, deux modules legacy non
730
+ migrés vers ``evaluation/metrics/`` ; le renderer ne peut donc pas
731
+ les importer depuis le canonique). Remplacé par
732
+ ``longitudinal_render`` qui n'a pas de dépendance legacy.
733
+
734
+ **Migrations effectuées** :
735
+
736
+ | Source legacy | Destination canonique |
737
+ |----------------------------------------------|------------------------------------------------------|
738
+ | ``report/difficulty_render.py`` (45) | ``reports_v2/html/renderers/difficulty.py`` |
739
+ | ``report/lexical_modernization_render.py`` (114) | ``reports_v2/html/renderers/lexical_modernization.py`` |
740
+ | ``report/multirun_stability_render.py`` (151)| ``reports_v2/html/renderers/multirun_stability.py`` |
741
+ | ``report/throughput_render.py`` (154) | ``reports_v2/html/renderers/throughput.py`` |
742
+ | ``report/longitudinal_render.py`` (165) | ``reports_v2/html/renderers/longitudinal.py`` |
743
+
744
+ Total : ~629 lignes relocalisées. 5 nouveaux shims minimaux
745
+ (< 20 lignes) avec ``DeprecationWarning``.
746
+
747
+ **Adaptations transverses** :
748
+
749
+ - ``reports_v2/html/renderers/lexical_modernization.py`` import
750
+ canonique ``picarones.evaluation.metrics.lexical_modernization``
751
+ (au lieu du shim legacy ``picarones.measurements.lexical_modernization``).
752
+ - ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à
753
+ ``"lexical_modernization"`` (même rationale que ``specialization``
754
+ au batch 1).
755
+ - Tests + ``picarones/report/generator.py`` mis à jour pour les
756
+ 5 chemins canoniques.
757
+
758
+ **Acceptance batch 2** : 5019 tests passent, lint vert,
759
+ architecture vérifiée.
760
+
761
+ **Cumul Phase 5.C** (batches 1+2) : 10 / 22 renderers migrés
762
+ (~1198 lignes). 12 renderers restants.
763
+
764
+ #### Phase 5.C.batch3 — Lot 3 : 5 renderers moyens (2026-05)
765
+
766
+ Troisième vague. Tous les renderers sélectionnés sont
767
+ **purs sur le contrat** : import depuis ``_helpers/`` uniquement,
768
+ pas de dépendance sur des modules legacy non-migrés.
769
+
770
+ **Migrations effectuées** :
771
+
772
+ | Source legacy | Destination canonique |
773
+ |------------------------------------------------|--------------------------------------------------------|
774
+ | ``report/module_audit_render.py`` (173) | ``reports_v2/html/renderers/module_audit.py`` |
775
+ | ``report/incremental_comparison_render.py`` (201)| ``reports_v2/html/renderers/incremental_comparison.py``|
776
+ | ``report/image_predictive_render.py`` (207) | ``reports_v2/html/renderers/image_predictive.py`` |
777
+ | ``report/error_absorption_render.py`` (210) | ``reports_v2/html/renderers/error_absorption.py`` |
778
+ | ``report/ner_render.py`` (222) | ``reports_v2/html/renderers/ner.py`` |
779
+
780
+ Total : ~1013 lignes relocalisées. 5 nouveaux shims minimaux
781
+ (< 20 lignes) avec ``DeprecationWarning``.
782
+
783
+ **Adaptations transverses** :
784
+
785
+ - Tests + ``picarones/report/generator.py`` mis à jour pour les
786
+ 5 chemins canoniques.
787
+
788
+ **Acceptance batch 3** : 5019 tests passent, lint vert,
789
+ architecture vérifiée.
790
+
791
+ **Cumul Phase 5.C** (batches 1+2+3) : 15 / 22 renderers migrés
792
+ (~2211 lignes). 7 renderers restants.
793
+
794
+ #### Phase 5.C.batch4 — Lot 4 : 5 renderers moyens-gros (2026-05)
795
+
796
+ Quatrième vague. Tous les renderers sélectionnés sont **purs sur
797
+ le contrat externe** (import depuis ``_helpers/`` uniquement).
798
+ ``robustness_projection`` avait un import lazy interne vers
799
+ ``picarones.measurements.robustness_projection`` qui a été redirigé
800
+ vers le canonique ``picarones.evaluation.metrics.robustness_projection``.
801
+
802
+ **Migrations effectuées** :
803
+
804
+ | Source legacy | Destination canonique |
805
+ |------------------------------------------------|--------------------------------------------------------|
806
+ | ``report/stratification_render.py`` (188) | ``reports_v2/html/renderers/stratification.py`` |
807
+ | ``report/baseline_render.py`` (238) | ``reports_v2/html/renderers/baseline.py`` |
808
+ | ``report/inter_engine_render.py`` (245) | ``reports_v2/html/renderers/inter_engine.py`` |
809
+ | ``report/robustness_projection_render.py`` (252) | ``reports_v2/html/renderers/robustness_projection.py``|
810
+ | ``report/calibration_render.py`` (321) | ``reports_v2/html/renderers/calibration.py`` |
811
+
812
+ Total : ~1244 lignes relocalisées. 5 nouveaux shims minimaux
813
+ (< 20 lignes) avec ``DeprecationWarning``.
814
+
815
+ **Adaptations transverses** :
816
+
817
+ - ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à
818
+ ``"robustness_projection"`` (même rationale que les batches
819
+ précédents).
820
+ - Tests + ``picarones/report/generator.py`` mis à jour pour les
821
+ 5 chemins canoniques.
822
+
823
+ **Acceptance batch 4** : 5019 tests passent, lint vert,
824
+ architecture vérifiée.
825
+
826
+ **Cumul Phase 5.C** (batches 1+2+3+4) : 20 / 22 renderers migrés
827
+ (~3455 lignes). 2 renderers restants : ``pipeline_render`` (707 l)
828
+ et ``philological_render`` (595 l) — les XXL — auront leur propre
829
+ batch dédié.
830
+
831
+ #### Phase 5.C.batch5 — Lot 5 : 5 renderers moyens-gros (2026-05)
832
+
833
+ Cinquième vague. Inclut les 3 renderers de la famille
834
+ ``taxonomy``, ``worst_lines`` et ``pipeline_dag``. Restera ensuite
835
+ batch 6 (XXL + ``levers``) et la migration des 5 vues
836
+ (``views/*.py``).
837
+
838
+ **Migrations effectuées** :
839
+
840
+ | Source legacy | Destination canonique |
841
+ |-------------------------------------------------|------------------------------------------------------|
842
+ | ``report/taxonomy_intra_doc_render.py`` (148) | ``reports_v2/html/renderers/taxonomy_intra_doc.py`` |
843
+ | ``report/taxonomy_cooccurrence_render.py`` (161)| ``reports_v2/html/renderers/taxonomy_cooccurrence.py``|
844
+ | ``report/worst_lines_render.py`` (164) | ``reports_v2/html/renderers/worst_lines.py`` |
845
+ | ``report/taxonomy_comparison_render.py`` (233) | ``reports_v2/html/renderers/taxonomy_comparison.py`` |
846
+ | ``report/pipeline_dag_render.py`` (314) | ``reports_v2/html/renderers/pipeline_dag.py`` |
847
+
848
+ Total : ~1020 lignes relocalisées.
849
+
850
+ **Adaptations transverses** :
851
+
852
+ - ``reports_v2/html/renderers/worst_lines.py`` :
853
+ - import ``WorstLineEntry`` redirigé vers
854
+ ``picarones.evaluation.metrics.worst_lines``
855
+ - import ``compute_char_diff`` redirigé vers
856
+ ``picarones.evaluation`` (au lieu de ``picarones.core.diff_utils``,
857
+ rejeté par la règle layer-dependencies sur ``reports_v2``).
858
+
859
+ **Cumul Phase 5.C** (batches 1+2+3+4+5) : 20 + 5 = **25 renderers
860
+ migrés**, soit l'intégralité moins ``pipeline_render`` et
861
+ ``philological_render`` (XXL) et ``levers`` (oublié dans le plan
862
+ initial). Reste batch 6 (3 renderers) puis Phase 5.D (5 vues).
863
+
864
+ #### Phase 5.C.batch6 — Lot 6 : levers + philological (2026-05)
865
+
866
+ Sixième vague. Inclut le plus gros renderer non-bloqué
867
+ (``philological``, 527 LOC) et ``levers`` (249 LOC).
868
+ ``pipeline_render`` (707 l) reporté à un batch 7 dédié car il
869
+ dépend de ``measurements/pipeline_benchmark`` et
870
+ ``measurements/pipeline_comparison`` non encore migrés vers
871
+ ``evaluation/`` (rejetés par layer-dependencies).
872
+ ``numerical_sequences_render`` (149 l) reporté pour la même
873
+ raison (dépendance vers ``measurements/numerical_sequences``
874
+ qui dépend de ``measurements/roman_numerals``).
875
+
876
+ **Migrations effectuées** :
877
+
878
+ | Source legacy | Destination canonique |
879
+ |---------------------------------------------|------------------------------------------------------|
880
+ | ``report/levers_render.py`` (249) | ``reports_v2/html/renderers/levers.py`` |
881
+ | ``report/philological_render.py`` (527) | ``reports_v2/html/renderers/philological.py`` |
882
+
883
+ Total : ~776 lignes relocalisées.
884
+
885
+ **Adaptations transverses** :
886
+
887
+ - ``test_sprint82_levers.py`` : monkeypatch sur `_FORMATTERS`
888
+ pointe désormais vers le module canonique
889
+ ``picarones.reports_v2.html.renderers.levers``.
890
+ - ``test_file_budgets.py`` : entrée
891
+ ``report/philological_render.py`` retirée, remplacée par
892
+ ``reports_v2/html/renderers/philological.py`` (budget
893
+ inchangé à 700).
894
+
895
+ **Cumul Phase 5.C** (batches 1-6) : 27 / 29 renderers migrés
896
+ (~5232 lignes). 2 renderers restants pour batch 7 :
897
+ ``pipeline_render`` (707) et ``numerical_sequences_render`` (149).
898
+
899
+ **Acceptance batch 6** : 5019 tests passent, lint vert,
900
+ architecture vérifiée.
901
+
902
+ #### Phase 5.C.batch7 — Lot 7 : pré-requis + 2 derniers renderers (2026-05)
903
+
904
+ Le batch 7 finalise Phase 5.C en migrant **d'abord** les
905
+ modules de mesure dont dépendent les renderers
906
+ ``numerical_sequences`` et ``pipeline`` :
907
+
908
+ | Source legacy | Destination canonique |
909
+ |------------------------------------------------|------------------------------------------------|
910
+ | ``measurements/roman_numerals.py`` (478) | ``evaluation/metrics/roman_numerals.py`` |
911
+ | ``measurements/numerical_sequences.py`` (422) | ``evaluation/metrics/numerical_sequences.py`` |
912
+ | ``measurements/pipeline_benchmark.py`` (367) | ``evaluation/pipeline_benchmark.py`` |
913
+ | ``measurements/pipeline_comparison.py`` (301) | ``evaluation/pipeline_comparison.py`` |
914
+ | ``core/pipeline.py`` (607) | ``evaluation/pipeline.py`` |
915
+
916
+ Puis les 2 derniers renderers :
917
+
918
+ | Source legacy | Destination canonique |
919
+ |------------------------------------------------|------------------------------------------------------|
920
+ | ``report/numerical_sequences_render.py`` (149) | ``reports_v2/html/renderers/numerical_sequences.py`` |
921
+ | ``report/pipeline_render.py`` (707) | ``reports_v2/html/renderers/pipeline.py`` |
922
+
923
+ Total : ~3031 lignes relocalisées dans ce batch. 7 nouveaux
924
+ shims minimaux (< 25 lignes) avec ``DeprecationWarning``.
925
+
926
+ État final de ``picarones/core/``
927
+ ---------------------------------
928
+
929
+ Le répertoire ``picarones/core/`` est désormais **entièrement
930
+ constitué de shims** (10 fichiers, tous < 30 lignes). Aucun
931
+ module Cercle 1 réel ne subsiste — les abstractions vivent dans
932
+ ``domain/`` (Pydantic immutable) et ``evaluation/`` (riche en
933
+ behavior). ``EXPECTED_CERCLE1`` du test
934
+ ``test_public_api.py::TestCercle1IsLean`` est désormais un set
935
+ vide, documentant explicitement que la Phase 1 du retrait du
936
+ legacy est complète au niveau ``core/``.
937
+
938
+ Adaptations transverses
939
+ -----------------------
940
+
941
+ - Imports internes mis à jour entre modules canoniques
942
+ (``evaluation/metrics/numerical_sequences.py`` → canonique
943
+ ``roman_numerals``, ``evaluation/pipeline_comparison.py`` →
944
+ canonique ``pipeline_benchmark``, etc.).
945
+ - ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à
946
+ ``"numerical_sequences"``, ``"numerical_sequences_hooks"``,
947
+ ``"pipeline_benchmark"``, ``"pipeline_comparison"``.
948
+ - ``test_file_budgets.py`` : 4 entrées legacy retirées,
949
+ remplacées par les chemins canoniques.
950
+ - ``test_public_api.py::EXPECTED_CERCLE1`` : ``pipeline.py``
951
+ retiré (set désormais vide).
952
+ - ``docs/tutorials/writing-a-pipeline-module.md`` : tous les
953
+ imports mis à jour vers les chemins canoniques.
954
+
955
+ **Cumul Phase 5.C** (batches 1-7) : **29 / 29 renderers migrés**
956
+ (~8263 lignes au total). Phase 5.C est terminée.
957
+
958
+ **Acceptance batch 7** : 5019 tests passent, lint vert,
959
+ architecture vérifiée (anti-cycles, file budgets,
960
+ EXPECTED_CERCLE1 vide).
961
+
962
+ Restantes pour Phase 5
963
+ ----------------------
964
+
965
+ - Phase 5.D ✅ — 5 vues (``views/*.py``) migrées vers
966
+ ``reports_v2/html/views/``.
967
+ - Phase 5.E : ``generator.py``, ``comparison.py``,
968
+ ``snapshot.py``, ``report_data/``, templates Jinja2.
969
+
970
+ #### Phase 5.D — Migration des 5 vues thématiques (2026-05)
971
+
972
+ Phase 5.D migre les 5 vues thématiques (orchestrateurs des
973
+ renderers) vers ``reports_v2/html/views/``. Ces vues prennent un
974
+ ``report_data`` dict et composent plusieurs renderers en blocs
975
+ ``<details>`` collapsibles, avec adaptive masking.
976
+
977
+ **Migrations effectuées** :
978
+
979
+ | Source legacy | Destination canonique |
980
+ |------------------------------------------------|----------------------------------------------------|
981
+ | ``report/views/__init__.py`` (65) | ``reports_v2/html/views/__init__.py`` |
982
+ | ``report/views/advanced_taxonomy.py`` (245) | ``reports_v2/html/views/advanced_taxonomy.py`` |
983
+ | ``report/views/diagnostics.py`` (247) | ``reports_v2/html/views/diagnostics.py`` |
984
+ | ``report/views/economics.py`` (219) | ``reports_v2/html/views/economics.py`` |
985
+ | ``report/views/pipeline.py`` (237) | ``reports_v2/html/views/pipeline.py`` |
986
+ | ``report/views/robustness.py`` (101) | ``reports_v2/html/views/robustness.py`` |
987
+
988
+ Total : ~1114 lignes relocalisées. 6 nouveaux shims minimaux
989
+ (< 25 lignes) avec ``DeprecationWarning``.
990
+
991
+ **Adaptations transverses** :
992
+
993
+ - 6 imports de modules de mesure dans les vues redirigés vers
994
+ leurs canoniques ``evaluation/metrics/`` :
995
+ ``taxonomy_comparison``, ``incremental_comparison``,
996
+ ``levers``, ``image_predictive``, ``worst_lines``,
997
+ ``throughput``.
998
+ - ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu de 6
999
+ modules supplémentaires (mêmes raisons que les renderers).
1000
+ - Renderers ``reports_v2/html/renderers/`` cross-référencés
1001
+ par les vues — toujours au canonique.
1002
+
1003
+ **Acceptance Phase 5.D** : 5019 tests passent, lint vert,
1004
+ architecture vérifiée.
1005
+
1006
+ #### Phase 5.E — Migration generator + comparison + snapshot + report_data + templates + i18n (2026-05)
1007
+
1008
+ Phase 5.E finalise Phase 5 en migrant les derniers composants
1009
+ ``report/`` :
1010
+
1011
+ **Migrations effectuées** :
1012
+
1013
+ | Source legacy | Destination canonique |
1014
+ |------------------------------------------------|----------------------------------------------------|
1015
+ | ``report/generator.py`` (466) | ``reports_v2/html/generator.py`` |
1016
+ | ``report/comparison.py`` (409) | ``reports_v2/html/comparison.py`` |
1017
+ | ``report/snapshot.py`` (266) | ``reports_v2/html/snapshot.py`` |
1018
+ | ``report/report_data/__init__.py`` (132) | ``reports_v2/html/data/__init__.py`` |
1019
+ | ``report/report_data/_helpers.py`` (30) | ``reports_v2/html/data/_helpers.py`` |
1020
+ | ``report/report_data/documents.py`` (167) | ``reports_v2/html/data/documents.py`` |
1021
+ | ``report/report_data/engines.py`` (103) | ``reports_v2/html/data/engines.py`` |
1022
+ | ``report/report_data/extra_metrics.py`` (272) | ``reports_v2/html/data/extra_metrics.py`` |
1023
+ | ``report/report_data/pareto.py`` (159) | ``reports_v2/html/data/pareto.py`` |
1024
+ | ``report/report_data/scatter.py`` (56) | ``reports_v2/html/data/scatter.py`` |
1025
+ | ``report/report_data/statistics.py`` (216) | ``reports_v2/html/data/statistics.py`` |
1026
+ | ``report/templates/`` (13 fichiers) | ``reports_v2/html/templates/`` (13 fichiers) |
1027
+ | ``picarones/i18n.py`` (124) | ``picarones/reports_v2/i18n/__init__.py`` |
1028
+ | ``report/__init__.py`` (3) | shim re-export |
1029
+
1030
+ Total : ~2400 lignes relocalisées + 13 templates Jinja2 + le
1031
+ loader i18n. Au total **12 nouveaux shims minimaux** (< 25
1032
+ lignes) avec ``DeprecationWarning``.
1033
+
1034
+ **Adaptations transverses** :
1035
+
1036
+ - ``reports_v2/html/snapshot.py`` ne peut pas importer
1037
+ ``picarones.__version__`` (interdit par layer-deps) : utilise
1038
+ ``importlib.metadata`` avec fallback (idem qu'au Phase 4-ter).
1039
+ - ``reports_v2/html/snapshot.py`` import ``pricing`` redirigé
1040
+ vers le canonique ``evaluation/metrics/pricing``.
1041
+ - ``reports_v2/html/generator.py`` toutes les ~30 imports
1042
+ internes redirigés vers ``reports_v2/html/{data,renderers,
1043
+ views,snapshot}`` et ``evaluation/{statistics,metric_result,
1044
+ benchmark_result}``.
1045
+ - ``reports_v2/html/data/`` : 7 imports vers
1046
+ ``measurements/{statistics,difficulty,pricing,marginal_cost,
1047
+ rare_tokens,taxonomy_cooccurrence,taxonomy_intra_doc}``
1048
+ redirigés vers ``evaluation/{statistics,metrics/...}``.
1049
+ - ``reports_v2/html/views/`` : 6 imports vers
1050
+ ``measurements/{taxonomy_comparison,incremental_comparison,
1051
+ levers,image_predictive,worst_lines,throughput}`` redirigés
1052
+ vers ``evaluation/metrics/...``.
1053
+ - ``picarones/reports_v2/__init__.py`` : nouveau loader
1054
+ ``from picarones.reports_v2.html.generator import ReportGenerator``.
1055
+ - ``test_module_coverage.py::TEST_ONLY_BASELINE`` étendu à 3
1056
+ modules : ``statistics``, ``pricing``, ``difficulty``.
1057
+ - ``test_file_budgets.py`` : 2 entrées legacy retirées,
1058
+ remplacées par les chemins canoniques ; templates dir
1059
+ référencé via ``reports_v2/html/templates/``.
1060
+ - 28+ chemins de templates dans les tests redirigés vers
1061
+ ``reports_v2/html/templates/``.
1062
+ - Tests qui faisaient ``from picarones import i18n`` redirigés
1063
+ vers ``from picarones.reports_v2 import i18n`` (le shim ne
1064
+ ré-exporte pas ``_get_labels_cached`` — privé).
1065
+
1066
+ État final de ``picarones/report/``
1067
+ -----------------------------------
1068
+
1069
+ Le répertoire ``picarones/report/`` ne contient désormais
1070
+ **que des shims** (~30 fichiers). Aucun module avec du
1071
+ contenu réel ne subsiste. Le canonique vit intégralement
1072
+ dans ``picarones/reports_v2/html/`` (générateur + renderers
1073
+ + vues + données + templates + comparaison + snapshot).
1074
+
1075
+ **Acceptance Phase 5.E + Phase 5 entière** : 5019 tests
1076
+ passent, lint vert, architecture vérifiée (anti-cycles,
1077
+ file budgets, module coverage).
1078
+
1079
+ ### Phase 6 — Pipelines OCR+LLM (`pipelines/`)
1080
+
1081
+ **Modules** : `pipelines/base.OCRLLMPipeline` (3 modes), `pipelines/
1082
+ over_normalization.detect_over_normalization`.
1083
+
1084
+ **Cible** :
1085
+
1086
+ - Les 3 modes deviennent des `PipelineSpec` YAML composés (OCR
1087
+ adapter → LLM adapter avec `inputs_from`).
1088
+ - `over_normalization` devient une métrique enregistrée dans
1089
+ `evaluation/metrics/over_normalization.py`.
1090
+
1091
+ **Effort** : 3-5 jours.
1092
+
1093
+ **Acceptance** : les 3 callers internes (`web/benchmark_utils.py`,
1094
+ `measurements/runner/document.py`, `fixtures.py`) consomment des
1095
+ `PipelineSpec` YAML rewrite.
1096
+
1097
+ ### Phase 7 — Modules officiels (`modules/`)
1098
+
1099
+ **Module** : `modules/alto_text_to_mono_region.TextToAltoMonoRegion`
1100
+ (310 LOC) — baseline TEXT → ALTO.
1101
+
1102
+ **Cible** : `picarones.formats.alto.baseline_reconstruction` ou
1103
+ `picarones.evaluation.projectors.text_to_alto` (selon où la
1104
+ sémantique colle le mieux).
1105
+
1106
+ **Effort** : 1 jour.
1107
+
1108
+ ### Phase 8 — Importers (`extras/importers/`)
1109
+
1110
+ **Modules** : `iiif.py`, `gallica.py`, `escriptorium.py`, `_http.py`,
1111
+ `_fallback_log.py`.
1112
+
1113
+ **Cible** : `picarones/adapters/corpus/{iiif,gallica,escriptorium}.py`
1114
+ + helpers partagés dans `adapters/corpus/_http.py`.
1115
+
1116
+ **Effort** : 3-5 jours.
1117
+
1118
+ ### Phase 9 — Web UI riche (`web/`)
1119
+
1120
+ **Modules** : 9 routers (`config`, `engines`, `history`, `home`,
1121
+ `importers`, `normalization`, `reports`, `synthesis`, `system`) +
1122
+ utilitaires (`benchmark_utils.py`, `engine_utils.py`,
1123
+ `corpus_utils.py`, `config_utils.py`, `state.py`, `security.py`,
1124
+ `models.py`, `jobs.py`, `maintenance.py`, `app.py`) + templates
1125
+ Jinja2.
1126
+
1127
+ **Cible** : `picarones/interfaces/web/routers/<router>.py` + utils
1128
+ partagés dans `interfaces/web/_utils/` + templates dans
1129
+ `interfaces/web/templates/`.
1130
+
1131
+ **Effort** : 8-12 jours.
1132
+
1133
+ **Acceptance** : régression sur tous les `tests/web/test_sprint*.py`
1134
+ existants. L'UI riche (sélecteur moteurs dynamique, gallery,
1135
+ stratification, narrative inline, browse corpus) doit produire les
1136
+ mêmes pages HTML.
1137
+
1138
+ ### Phase 10 — CLI complète (`cli/`)
1139
+
1140
+ **Commandes** : 13 commandes legacy non couvertes (`metrics`,
1141
+ `engines`, `info`, `demo`, `diagnose`, `economics`, `edition`,
1142
+ `compare`, `import` group, `serve`, `history`, `robustness`,
1143
+ `pipeline` group avec sous-commandes `run` et `compare`).
1144
+
1145
+ **Cible** : `picarones/interfaces/cli/<command>.py`. L'entry point
1146
+ `console_scripts` du `pyproject.toml` doit pointer sur
1147
+ `picarones.interfaces.cli:cli` (à la place de `picarones.cli:cli`).
1148
+
1149
+ **Effort** : 4-6 jours.
1150
+
1151
+ ### Phase 11 — Retrait final + release 2.0
1152
+
1153
+ - Suppression des 10 packages legacy.
1154
+ - Suppression des shims `DeprecationWarning` introduits aux phases
1155
+ précédentes.
1156
+ - Mise à jour du `pyproject.toml` (`console_scripts`,
1157
+ `[project.urls]`).
1158
+ - Rédaction du CHANGELOG 2.0 final avec liste exhaustive des
1159
+ breaking changes (les utilisateurs externes ont eu
1160
+ `DeprecationWarning` à chaque phase).
1161
+ - Génération SBOM + signature SLSA Level 3 (cf.
1162
+ `docs/operations/supply-chain.md`).
1163
+ - Bump `_version.py` et tag `v2.0.0`.
1164
+
1165
+ **Effort** : 3-5 jours.
1166
+
1167
+ ## Estimation totale
1168
+
1169
+ | Phase | Effort min | Effort max |
1170
+ |-------|------------|------------|
1171
+ | 0 | 2 j | 3 j |
1172
+ | 1 | 5 j | 8 j |
1173
+ | 2 | 5 j | 7 j |
1174
+ | 3 | 8 j | 12 j |
1175
+ | 4 | 23 j | 28 j |
1176
+ | 5 | 12 j | 18 j |
1177
+ | 6 | 3 j | 5 j |
1178
+ | 7 | 1 j | 1 j |
1179
+ | 8 | 3 j | 5 j |
1180
+ | 9 | 8 j | 12 j |
1181
+ | 10 | 4 j | 6 j |
1182
+ | 11 | 3 j | 5 j |
1183
+ | **Total** | **77 j** | **110 j** |
1184
+
1185
+ Soit **3,5 à 5 mois** d'effort focalisé en mode développeur unique.
1186
+ Aucune contrainte de date — on livre quand c'est propre.
1187
+
1188
+ ## Stratégie de régression — invariant non négociable
1189
+
1190
+ À chaque phase :
1191
+
1192
+ 1. **Avant** : exécuter le harness legacy sur 3 corpus de référence
1193
+ (small / medium / large) → capture des outputs en JSON / HTML
1194
+ bit-for-bit.
1195
+ 2. **Pendant** : réécrire la fonctionnalité dans le rewrite.
1196
+ 3. **Après** : exécuter le harness rewrite et **diff** vs. snapshot
1197
+ legacy.
1198
+ 4. **Tolérance** : explicite par métrique dans
1199
+ `docs/migration/regression-tolerances.md`. Tout écart non
1200
+ tolerance = régression à corriger avant merge.
1201
+
1202
+ Cela évite le piège classique du rewrite : *« ça compile, ça tourne,
1203
+ mais le CER a glissé de 0,002 par doc »*.
1204
+
1205
+ ## Anti-bricolage — règles
1206
+
1207
+ 1. **Pas de double API** : pendant la migration d'un module, on ne
1208
+ garde **pas** le legacy en parallèle dans le code de production.
1209
+ Soit on importe l'ancien, soit le nouveau. Le harness de
1210
+ régression suffit pour valider.
1211
+ 2. **Pas de shim sans date de retrait** : tout `DeprecationWarning`
1212
+ introduit doit être inscrit dans le CHANGELOG avec date de
1213
+ retrait (la 2.0).
1214
+ 3. **Pas de TODO dans le code mergé** : un TODO = une issue ouverte
1215
+ référencée par numéro.
1216
+ 4. **Pas de copié-collé** : si une logique apparaît dans deux
1217
+ modules, extraire en helper partagé dès la deuxième occurrence.
1218
+ 5. **Pas de god-module** : `tests/architecture/test_file_budgets.py`
1219
+ reste l'autorité.
1220
+
1221
+ ## Statut
1222
+
1223
+ | Phase | Statut |
1224
+ |-------|--------|
1225
+ | 0 | ✅ Terminée |
1226
+ | 1 | ✅ Partielle (3/9 modules ; les 6 autres → Phase 4-bis) |
1227
+ | 2 | ✅ Terminée (8/8 modules statistics migrés) |
1228
+ | 3 | ✅ Terminée (11 modules narrative + 2 templates + 18 détecteurs migrés) |
1229
+ | 4 | ✅ Partielle (9 modules autonomes/cascade ; 13 modules + 6 modules `core/` + 1 sous-package → Phase 4-bis) |
1230
+ | 4-bis | 🟡 Diagnostic posé, exécution reportée (couplage dicts-string plus complexe que prévu — voir détail Phase 4) |
1231
+ | 5-11 | ⚪ À démarrer |
1232
+
1233
+ **Dernière mise à jour** : 2026-05 (Phase 4-bis tentative + revert + diagnostic).
1234
+
1235
+ **Reste en place suite à la tentative Phase 4-bis** : aliases
1236
+ ``TEXT``/``ALTO``/``PAGE`` dans ``domain.artifacts.ArtifactType``
1237
+ (inoffensif) + hook ``_missing_`` pour accepter les valeurs string
1238
+ legacy. Préparation pour la session future qui complétera Phase
1239
+ 4-bis.
docs/migration/pipeline-convergence-plan.md ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Audit & sub-plan — Convergence ``PipelineRunner`` legacy ↔ ``PipelineExecutor`` canonique
2
+
3
+ > **Note** : ce document est l'audit demandé en conclusion de Phase 5
4
+ > du plan de retrait du legacy
5
+ > (cf. ``docs/migration/legacy-retirement-plan.md``). Il identifie
6
+ > les différences entre les deux designs de pipeline, inventaire les
7
+ > callers, propose 3 stratégies de convergence et recommande un
8
+ > sub-plan d'exécution.
9
+
10
+ ---
11
+
12
+ ## 1. État des lieux
13
+
14
+ Deux designs cohabitent :
15
+
16
+ ### 1.A Legacy — ``picarones.evaluation.pipeline`` (ex-``core/pipeline.py``)
17
+
18
+ Sprint 63 (axe B), 607 lignes. Relocalisé en Phase 5.C.batch7
19
+ mais **non refactoré**.
20
+
21
+ **Caractéristiques** :
22
+
23
+ - ``PipelineRunner`` : classe statique avec ``.run(spec, document, initial_inputs) -> PipelineResult``.
24
+ - ``PipelineSpec`` : dataclass mutable.
25
+ - ``PipelineStep`` : dataclass avec ``module: BaseModule`` (instance Python).
26
+ - ``StepResult`` : dataclass avec ``junction_metrics: dict[str, dict[str, Any]]``.
27
+ - ``PipelineResult`` : dataclass avec ``steps: list[StepResult]``.
28
+ - Modules : ``BaseModule`` ABC consommant des **payloads bruts**
29
+ (``{ArtifactType: str | dict | list | ...}``).
30
+ - Évaluation : ``compute_at_junction`` automatique à chaque étape
31
+ contre la GT du document si ``GTLevel`` correspond.
32
+ - Pas de cache d'artefacts.
33
+ - Pas de ``ExecutionPlan`` séparé — résolution implicite des
34
+ inputs au runtime via un bag versionné.
35
+
36
+ ### 1.B Canonique — ``picarones.pipeline.executor`` + ``planner`` + ``protocols``
37
+
38
+ Sprints S6-S7-S28, design rewrite ciblé.
39
+
40
+ **Caractéristiques** :
41
+
42
+ - ``PipelineExecutor`` : classe instanciable avec
43
+ ``adapter_resolver`` injecté + ``planner`` optionnel +
44
+ ``artifact_store`` optionnel.
45
+ - Méthode ``run(spec, document, initial_inputs, context) ->
46
+ PipelineResult`` (compat S7) qui plan-then-execute.
47
+ - Méthode canonique ``run_plan(plan, document, initial_inputs,
48
+ context)`` qui consomme un ``ExecutionPlan`` pré-calculé.
49
+ - ``PipelineSpec`` : Pydantic immutable
50
+ (``picarones.domain.pipeline_spec``), sérialisable YAML.
51
+ - ``PipelineStep`` : Pydantic immutable avec ``adapter_name: str``
52
+ (pas d'instance — résolution applicative).
53
+ - ``ExecutionPlan`` : produit du ``PipelinePlanner`` — porte
54
+ ``StepInputBinding`` explicites + ``MetricJunction`` détectées.
55
+ - ``StepResult`` : Pydantic immutable avec
56
+ ``produced_artifacts: dict[str, str]`` (map ArtifactType.value →
57
+ Artifact.id).
58
+ - ``PipelineResult`` : Pydantic immutable avec
59
+ ``artifacts: tuple[Artifact, ...]``.
60
+ - Adapters : ``StepExecutor`` Protocol (runtime-checkable)
61
+ consommant des **``Artifact`` typés**
62
+ (``{ArtifactType: Artifact(uri, content_hash, provenance)}``).
63
+ - Cache d'artefacts via ``ArtifactCachePort`` (Sprint S29 + S47).
64
+ - ``RunContext`` Pydantic injecté à chaque ``execute()`` —
65
+ document_id, code_version, pipeline_name, workspace_uri.
66
+
67
+ ---
68
+
69
+ ## 2. Différences API détaillées
70
+
71
+ | Dimension | Legacy (``evaluation.pipeline``) | Canonique (``pipeline.executor``) |
72
+ |----------------------------|------------------------------------------|-----------------------------------------------|
73
+ | Construction | classe statique | instance avec deps injectées |
74
+ | Spec | dataclass mutable | Pydantic immutable, YAML-sérialisable |
75
+ | Step | porte ``module: BaseModule`` | porte ``adapter_name: str`` |
76
+ | Résolution adapters | implicite (instance dans spec) | explicite (``adapter_resolver`` callable) |
77
+ | Résolution inputs | implicite (last-producer-wins) | explicite (``StepInputBinding``) |
78
+ | Validation spec | au runtime | au planning (``PipelinePlanner``) |
79
+ | Type passé aux modules | payload brut (str, dict, list…) | ``Artifact`` typé |
80
+ | Provenance | absente | ``ProvenanceRecord`` automatique |
81
+ | Hash de contenu | absent | ``Artifact.content_hash`` SHA-256 |
82
+ | Cache inter-runs | absent | ``ArtifactCachePort`` |
83
+ | ``RunContext`` | absent | injecté à chaque step |
84
+ | Évaluation auto vs GT | oui, à chaque step | non (sortie : artefacts seulement) |
85
+ | ``junction_metrics`` | dans ``StepResult`` | absent du runtime, calculé à part |
86
+ | Représentation des étapes | objets Python uniquement | YAML + Python |
87
+
88
+ ---
89
+
90
+ ## 3. Inventaire des callers
91
+
92
+ ### 3.A Legacy (``evaluation.pipeline``)
93
+
94
+ **Production** (4 fichiers) :
95
+
96
+ - ``picarones/__init__.py`` — re-export de ``PipelineRunner``,
97
+ ``PipelineSpec``, ``PipelineStep``, ``StepResult``,
98
+ ``PipelineResult`` dans l'API publique.
99
+ - ``picarones/evaluation/pipeline_benchmark.py`` — orchestre
100
+ l'exécution corpus-wide via ``PipelineRunner.run()``.
101
+ - ``picarones/evaluation/pipeline_comparison.py`` — compare N
102
+ ``PipelineSpec`` via ``run_pipeline_benchmark``.
103
+ - ``picarones/measurements/pipeline_spec_loader.py`` — charge des
104
+ YAML legacy en ``PipelineSpec`` + ``PipelineStep`` legacy
105
+ (avec instanciation des modules par ``adapter_name``).
106
+
107
+ **Tests** : 7 fichiers de tests directs (``test_sprint63_*``,
108
+ ``test_sprint64_*``, ``test_sprint65_*``, ``test_sprint66_*``,
109
+ ``test_sprint67_*``, ``test_sprint68_*``, etc.).
110
+
111
+ ### 3.B Canonique (``pipeline.executor``)
112
+
113
+ **Production** : 0 caller production (le rewrite n'a pas encore
114
+ de service applicatif qui consomme l'executor canonique en
115
+ mode mono-document).
116
+
117
+ **Tests** : 9 fichiers de tests directs. Tests-only à ce jour.
118
+
119
+ **Conclusion sur les callers** : le legacy est en production,
120
+ le canonique est test-only. La convergence doit migrer le
121
+ legacy **sans casser les 7+4 = 11 fichiers tests/prod
122
+ existants**.
123
+
124
+ ---
125
+
126
+ ## 4. Stratégies de convergence
127
+
128
+ ### 4.A Wrapper legacy → canonique
129
+
130
+ Le legacy ``PipelineRunner.run(spec, document, initial_inputs)``
131
+ devient un **adaptateur** qui :
132
+
133
+ 1. Convertit la ``PipelineSpec`` legacy (dataclass + module
134
+ instance) en ``PipelineSpec`` canonique (Pydantic +
135
+ adapter_name).
136
+ 2. Wrappe chaque ``BaseModule`` en ``StepExecutor`` Protocol.
137
+ 3. Convertit les payloads bruts en ``Artifact`` (uri inline,
138
+ content_hash calculé).
139
+ 4. Injecte un ``adapter_resolver`` ad hoc qui retourne les
140
+ wrappers.
141
+ 5. Invoque ``PipelineExecutor.run(spec, document, initial_inputs,
142
+ context)``.
143
+ 6. Reconvertit le ``PipelineResult`` canonique en ``PipelineResult``
144
+ legacy avec ``junction_metrics`` calculées à partir des
145
+ artefacts produits.
146
+
147
+ **Avantages** :
148
+ - Préserve l'API legacy → 0 caller cassé en production.
149
+ - Unifie le moteur d'exécution → 1 seul code path à maintenir.
150
+ - Cohérent avec la philosophie "no breaking change for callers".
151
+
152
+ **Inconvénients** :
153
+ - 200-400 LOC de glue (conversion bidirectionnelle de types).
154
+ - Coût de performance : double conversion à chaque step.
155
+ - Le double modèle ``Artifact``/payload reste visible côté
156
+ modules (le wrapper masque mais le concept demeure).
157
+
158
+ **Effort** : 2-3 sessions.
159
+
160
+ ### 4.B Migration complète
161
+
162
+ Migrer chaque caller legacy vers l'API canonique :
163
+
164
+ 1. ``pipeline_benchmark`` : passe de ``PipelineRunner.run`` à
165
+ ``PipelineExecutor.run_plan``. Les ``StepAggregate`` doivent
166
+ accepter la nouvelle structure ``StepResult`` (Pydantic).
167
+ 2. ``pipeline_comparison`` : idem.
168
+ 3. ``pipeline_spec_loader`` : produit des ``PipelineSpec`` Pydantic
169
+ au lieu de dataclass. Plus de ``module`` instance — juste
170
+ ``adapter_name``.
171
+ 4. ``__init__.py`` : ré-exporte le canonique.
172
+ 5. Tests : 7 fichiers à refactorer (mock adapters → ``StepExecutor``
173
+ Protocol, payloads → ``Artifact``).
174
+
175
+ **Avantages** :
176
+ - 1 seul design. Le legacy disparaît complètement.
177
+ - Pas de glue ni de double conversion.
178
+ - Conforme à la cible architecturale du rewrite.
179
+
180
+ **Inconvénients** :
181
+ - Massive : ~2500 LOC à toucher entre prod + tests.
182
+ - Le contrat des modules tiers (``BaseModule`` → ``StepExecutor``)
183
+ change. Un caller externe (BnF, HF Space) qui utilise
184
+ ``PipelineRunner.run`` casse silencieusement.
185
+ - Risque de régression non détectée sur les ~7 tests sprints
186
+ axe B (les fixtures sont volumineuses).
187
+ - Évaluation auto vs GT (legacy : à chaque step) doit être
188
+ ré-implémentée comme une post-étape canonique.
189
+
190
+ **Effort** : 5-7 sessions.
191
+
192
+ ### 4.C Cohabitation documentée
193
+
194
+ État actuel. Document explicitement que les deux designs sont
195
+ volontaires. Convergence reportée à un sprint dédié quand un
196
+ caller institutionnel l'exigera (BnF demande un YAML déclaratif
197
+ non-instanciable, ou HF Space veut le cache d'artefacts).
198
+
199
+ **Avantages** :
200
+ - 0 risque de régression maintenant.
201
+ - Permet de continuer le retrait du legacy sur les autres
202
+ paquets (Phases 6-11) sans buter sur ce sujet complexe.
203
+ - Le canonique reste prêt pour le jour où il sera vraiment
204
+ nécessaire.
205
+
206
+ **Inconvénients** :
207
+ - 2 designs à maintenir.
208
+ - L'objectif "core/ vide" du retrait du legacy n'est pas
209
+ totalement atteint : ``evaluation/pipeline.py`` reste un module
210
+ "legacy-style" en cercle 2.
211
+ - Risque que le canonique reste mort-né si personne ne le
212
+ réclame.
213
+
214
+ **Effort** : 0 (juste documentation).
215
+
216
+ ---
217
+
218
+ ## 5. Recommandation
219
+
220
+ > **Mise à jour 2026-05** : l'utilisateur a précisé que le projet
221
+ > est en stand-by jusqu'à la fin de la migration complète et que
222
+ > la rétrocompat de l'API publique n'est pas une contrainte. Cela
223
+ > élimine l'avantage principal de la stratégie 4.A (wrapper) et
224
+ > rend la stratégie 4.B (migration complète) recommandée :
225
+
226
+ **Stratégie 4.B — Migration complète** est la voie cible.
227
+
228
+ Bénéfices avec contrainte API levée :
229
+
230
+ - 1 seul design final, plus de wrapper interne à maintenir.
231
+ - Le contrat des modules tiers (``BaseModule`` → ``StepExecutor``)
232
+ peut changer sans gérer la rétrocompat.
233
+ - Les ``Artifact`` typés (provenance, content_hash, uri) deviennent
234
+ natifs partout — pas de double conversion.
235
+
236
+ Risques résiduels :
237
+
238
+ - ~2500 LOC à toucher entre prod + tests.
239
+ - L'évaluation auto vs GT (legacy : à chaque step) doit être
240
+ ré-implémentée comme une post-étape canonique.
241
+ - Risque de régression sur les ~7 tests sprints axe B
242
+ (fixtures volumineuses).
243
+ - Plusieurs sessions de travail nécessaires (5-7 sessions).
244
+
245
+ ---
246
+
247
+ ## 6. Découvertes additionnelles (audit complémentaire)
248
+
249
+ L'audit initial parlait de 4 callers de production de
250
+ ``PipelineRunner``. Une investigation plus poussée révèle un
251
+ écosystème legacy plus large, qui doit être inclus dans le plan :
252
+
253
+ ### 6.A Legacy engines (`picarones/engines/`, ~1500 LOC)
254
+
255
+ 5 modules OCR legacy qui héritent de ``BaseOCREngine`` (lui-même
256
+ extension de ``BaseModule``) :
257
+
258
+ - ``engines/base.py:BaseOCREngine``
259
+ - ``engines/tesseract.py:TesseractEngine`` (177 l)
260
+ - ``engines/pero_ocr.py:PeroOCREngine`` (182 l)
261
+ - ``engines/mistral_ocr.py:MistralOCREngine`` (231 l)
262
+ - ``engines/google_vision.py:GoogleVisionEngine`` (256 l)
263
+ - ``engines/azure_doc_intel.py:AzureDocIntelEngine``
264
+
265
+ **Équivalents canoniques existent** dans
266
+ ``picarones/adapters/ocr/`` (TesseractAdapter, PeroOCRAdapter,
267
+ etc.) et implémentent déjà ``StepExecutor``. Mais les noms de
268
+ classes et les APIs publiques **diffèrent** — pas un simple shim.
269
+
270
+ Callers production des engines legacy :
271
+ - ``picarones/web/benchmark_utils.py``
272
+ - ``picarones/pipelines/base.py`` (lui-même legacy, Phase 6)
273
+
274
+ ### 6.B Legacy LLM (``picarones/llm/``, ~67 LOC)
275
+
276
+ **Déjà migré** : tous les fichiers sont des shims qui
277
+ ré-exportent depuis ``picarones/adapters/llm/``. Rien à faire.
278
+
279
+ ### 6.C Legacy modules officiels (``picarones/modules/``)
280
+
281
+ - ``modules/alto_text_to_mono_region.py:TextToAltoMonoRegion``
282
+ (310 LOC) — extension de ``BaseModule``.
283
+
284
+ **Pas d'équivalent canonique** à ce jour. Cible documentée :
285
+ ``picarones/formats/alto/baseline_reconstruction.py`` ou
286
+ ``picarones/evaluation/projectors/text_to_alto.py``
287
+ (cf. Phase 7 du plan de retrait).
288
+
289
+ ### 6.D Sémantique des payloads vs Artifacts
290
+
291
+ La conversion ``BaseModule.process`` ↔ ``StepExecutor.execute``
292
+ n'est pas triviale parce que :
293
+
294
+ - Le legacy passe des **payloads bruts** :
295
+ - ``ArtifactType.IMAGE`` → ``str`` (chemin du fichier image)
296
+ - ``ArtifactType.RAW_TEXT`` → ``str`` (contenu textuel inline)
297
+ - ``ArtifactType.ALTO_XML`` → ``str`` (contenu XML inline)
298
+ - ``ArtifactType.ENTITIES`` → ``list[dict]``
299
+ - Le canonique passe des ``Artifact`` Pydantic immutables :
300
+ - ``uri`` (filesystem ou URI distant)
301
+ - ``content_hash`` (SHA-256)
302
+ - ``provenance`` (``ProvenanceRecord``)
303
+ - **pas de champ ``content`` direct** — le contenu se lit via
304
+ ``uri``.
305
+
306
+ Pour les tests legacy qui injectent du contenu inline (mock
307
+ modules retournant ``"hello"``), il faut **soit** :
308
+
309
+ 1. Persister le contenu dans un fichier temporaire et pointer
310
+ ``artifact.uri`` dessus.
311
+ 2. Ajouter une convention ``data:`` URI pour le contenu inline.
312
+ 3. Étendre ``Artifact`` avec un champ ``inline_payload: bytes |
313
+ None`` optionnel.
314
+
315
+ Décision recommandée : **option 1** (fichier temporaire), parce
316
+ qu'elle préserve la sémantique « un artefact a toujours un
317
+ identifiant filesystem » et permet le cache/provenance proprement.
318
+
319
+ ---
320
+
321
+ ## 7. Sub-plan d'exécution révisé (stratégie 4.B)
322
+
323
+ ### Sub-phase 7.A — Migration des adapters concrets
324
+
325
+ Bouclage de la migration des adapters legacy (engines/llm/modules)
326
+ vers les canoniques avant de toucher aux pipeline runners.
327
+
328
+ **Étapes** :
329
+
330
+ 1. ``engines/`` → shims pointant vers ``adapters/ocr/`` (avec
331
+ alias de classes : ``TesseractEngine = TesseractAdapter``,
332
+ etc.).
333
+ 2. Mise à jour des callers de ``engines/`` à utiliser
334
+ ``adapters/ocr/`` directement.
335
+ 3. ``modules/alto_text_to_mono_region.py`` → migré vers
336
+ ``picarones/evaluation/projectors/text_to_alto.py`` (canonique
337
+ en ``StepExecutor``).
338
+ 4. Suppression du shim ``engines/``.
339
+
340
+ **Effort** : 2-3 sessions.
341
+
342
+ ### Sub-phase 7.B — Migration des callers ``PipelineRunner``
343
+
344
+ Une fois les adapters unifiés sur ``StepExecutor`` :
345
+
346
+ 1. ``pipeline_spec_loader`` : produit des ``picarones.domain.pipeline_spec.PipelineSpec``
347
+ (Pydantic) avec ``adapter_name: str`` au lieu d'instances.
348
+ 2. ``pipeline_benchmark`` : consomme ``PipelineExecutor.run_plan``.
349
+ ``StepAggregate`` accepte ``StepResult`` Pydantic canonique.
350
+ 3. ``pipeline_comparison`` : idem.
351
+ 4. ``__init__.py`` : ré-exporte les canoniques.
352
+
353
+ **Effort** : 2 sessions.
354
+
355
+ ### Sub-phase 7.C — Refactor des tests
356
+
357
+ Les 7 fichiers de tests legacy axe B (sprints 63-68 + 95) :
358
+
359
+ - Mocks ``BaseModule`` → mocks ``StepExecutor`` Protocol.
360
+ - Payloads bruts → ``Artifact`` (avec helper
361
+ ``make_inline_artifact(content, type_)`` pour réduire le
362
+ boilerplate).
363
+ - ``Document`` legacy → ``DocumentRef`` canonique.
364
+ - Fixtures ``junction_metrics`` → ré-implémentation via
365
+ post-étape canonique.
366
+
367
+ **Effort** : 1-2 sessions.
368
+
369
+ ### Sub-phase 7.D — Suppression du legacy
370
+
371
+ 1. Suppression de ``evaluation/pipeline.PipelineRunner``,
372
+ ``PipelineSpec``, ``PipelineStep``, ``StepResult``,
373
+ ``PipelineResult`` (le legacy).
374
+ 2. Suppression de ``domain/module_protocol.BaseModule``.
375
+ 3. Le module ``evaluation/pipeline.py`` réduit à
376
+ ``_artifact_type_to_gt_level`` ou supprimé totalement.
377
+ 4. ``core/pipeline.py`` (shim) supprimé.
378
+ 5. ``core/modules.py`` (shim) supprimé.
379
+
380
+ **Effort** : 0.5 session (suppression mécanique).
381
+
382
+ ---
383
+
384
+ ## 8. Total effort révisé (stratégie 4.B)
385
+
386
+ | Sub-phase | Description | Effort |
387
+ |-----------|--------------------------------------------|------------------|
388
+ | 7.A | Migration adapters concrets | 2-3 sessions |
389
+ | 7.B | Migration callers PipelineRunner | 2 sessions |
390
+ | 7.C | Refactor des tests | 1-2 sessions |
391
+ | 7.D | Suppression du legacy | 0.5 session |
392
+ | **Total** | **Migration complète** | **5-8 sessions** |
393
+
394
+ ---
395
+
396
+ ## 9. Ordre d'exécution recommandé
397
+
398
+ L'ordre **bottom-up** est plus sûr : à chaque étape, les tests
399
+ restent verts.
400
+
401
+ ```
402
+ Sub-phase 7.A (adapters) → Sub-phase 7.B (orchestration) →
403
+ Sub-phase 7.C (tests) → Sub-phase 7.D (suppression)
404
+ ```
405
+
406
+ L'ordre **top-down** (start by removing PipelineRunner, then
407
+ fix everything that breaks) est plus risqué mais plus rapide
408
+ si on accepte une période de tests rouges.
409
+
410
+ Recommandation : **bottom-up**, par étapes verticales testables.
docs/migration/regression-tolerances.md ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tolérances de régression — legacy ↔ rewrite
2
+
3
+ > **Audience** : développeur qui migre une fonctionnalité legacy
4
+ > vers le rewrite, reviewer qui relit la PR.
5
+ >
6
+ > **Référence** : [`legacy-retirement-plan.md`](legacy-retirement-plan.md).
7
+ >
8
+ > **Contrat** : le harness `tests/regression/legacy_vs_rewrite/`
9
+ > exécute legacy + rewrite sur les mêmes corpus de référence et
10
+ > compare leurs sorties. Toute divergence au-delà de la tolérance
11
+ > ε définie ici est une **régression à corriger avant merge**.
12
+ >
13
+ > Une régression peut être :
14
+ >
15
+ > - **Intentionnelle** : la phase de migration corrige un bug
16
+ > historique → la tolérance est temporairement relâchée AVEC
17
+ > commentaire pointant vers l'issue.
18
+ > - **Inattendue** : c'est ce que ce document est censé empêcher.
19
+
20
+ ## Principe général
21
+
22
+ Pour une fonctionnalité donnée, la sortie du rewrite **doit être
23
+ égale** à celle du legacy à la tolérance ε près. L'égalité est :
24
+
25
+ - **Bit-for-bit** quand l'output est déterministe (texte, hash, JSON).
26
+ - **Sémantique** quand l'output structurel a des libertés (ordre des
27
+ éléments d'un set, indentation HTML, ordre des facts narratifs
28
+ équivalents).
29
+
30
+ ## Table des tolérances par type d'output
31
+
32
+ ### Métriques numériques
33
+
34
+ | Métrique | ε | Justification |
35
+ |----------|---|---------------|
36
+ | `cer_raw`, `cer_nfc`, `cer_caseless`, `cer_diplomatic` | **0** (bit-for-bit) | jiwer est déterministe ; toute différence = changement de pré/post-processing |
37
+ | `wer`, `mer`, `wil` | **0** | idem |
38
+ | `bleu`, `chrf` | **1e-9** | flottants — réordonnancements internes acceptables |
39
+ | `precision`, `recall`, `f1` (NER) | **1e-9** | flottants |
40
+ | `mufi_coverage`, `abbreviation_expansion_score` | **0** | comptage entier sur ensembles fermés |
41
+ | `roman_numerals_accuracy` | **0** | parsing déterministe |
42
+ | `unicode_blocks_accuracy` | **0** | tables Unicode déterministes |
43
+ | `reading_order_f1` (ICDAR 2015) | **1e-9** | algorithme déterministe, flottants |
44
+ | `layout_f1` | **1e-9** | flottants |
45
+ | `confusion_matrix.entries` | **0** | comptage entier |
46
+ | `taxonomy.error_class_*` | **0** | classification déterministe sur règles |
47
+
48
+ ### Tests statistiques
49
+
50
+ | Test | ε | Justification |
51
+ |------|---|---------------|
52
+ | Wilcoxon `p_value` | **1e-9** | scipy `wilcoxon` est déterministe à entrée constante |
53
+ | Friedman `chi2`, `p_value` | **1e-9** | idem |
54
+ | Nemenyi (matrice p-values) | **1e-9** | dérivé de Friedman |
55
+ | Bootstrap CI 95 % | **1e-3** | random seed FIXÉ explicitement (cf. `bootstrap.py` du legacy : `seed=42`) ; la tolérance laisse une marge minuscule pour les ré-implémentations qui itéreraient dans un ordre différent à seed identique |
56
+ | Pareto front (set d'engines dominants) | **0** (bit-for-bit en tant qu'ensemble) | dominance Pareto stable sur entrées identiques |
57
+ | CDD (Critical Difference Diagram) coordonnées SVG | **1e-3** sur les positions (px) | rendu Matplotlib peut varier sur des sub-pixels selon backend |
58
+ | Clustering (labels) | **0** sur l'**ensemble** des classes (l'étiquetage interne 0/1/2 peut différer mais la partition doit être identique) | un test custom compare les partitions, pas les labels |
59
+ | Corrélation Spearman / Pearson | **1e-9** | flottants |
60
+
61
+ ### Calibration
62
+
63
+ | Output | ε | Justification |
64
+ |--------|---|---------------|
65
+ | ECE, MCE | **1e-9** | flottants, pas d'aléatoire |
66
+ | Reliability diagram (bins, freq, conf) | **0** sur les bins, **1e-9** sur les valeurs | binning déterministe |
67
+
68
+ ### Confidences sidecar (S50 sur Tesseract)
69
+
70
+ | Output | ε |
71
+ |--------|---|
72
+ | `tokens[].text` | **0** (string identique) |
73
+ | `tokens[].confidence` | **0** | Tesseract retourne un entier 0-100 ; division exacte par 100 → flottant binairement identique en IEEE-754 |
74
+ | `extractor`, `model_version` | **0** |
75
+
76
+ ### HTML (rapport `reports_v2/html/render.py`)
77
+
78
+ Le diff HTML est **structurel**, pas lexical :
79
+
80
+ - Mêmes éléments DOM avec mêmes attributs sémantiques (`data-*`,
81
+ `aria-*`, `id`, `class`).
82
+ - Mêmes valeurs textuelles dans les nœuds de texte.
83
+ - L'**ordre** des sections doit être identique.
84
+ - L'indentation et le whitespace inter-éléments sont **ignorés**.
85
+ - Le contenu d'un `<script>` est comparé après normalisation
86
+ d'espace blanc.
87
+
88
+ Implémenté via une fonction `assert_html_semantically_equal(a, b)`
89
+ qui parse les deux HTML avec `lxml` (ou `html.parser` fallback) et
90
+ compare l'arbre.
91
+
92
+ ### CSV (`reports_v2/csv/render.py`)
93
+
94
+ | Output | ε |
95
+ |--------|---|
96
+ | Header row | **0** (identique exact) |
97
+ | Data rows (set non ordonné) | **0** sur l'ensemble |
98
+ | Ordre des lignes | autorisé à différer | les renderers triaient parfois différemment ; seule l'égalité ensembliste est exigée |
99
+ | Format des nombres | **0** (le rewrite formate à 6 décimales `f"{v:.6f}"`) | déterministe |
100
+
101
+ ### JSON (`reports_v2/json/render.py`)
102
+
103
+ | Output | ε |
104
+ |--------|---|
105
+ | Bit-for-bit identique | **0** | le rewrite utilise `model_dump(mode="json")` Pydantic + `json.dumps(sort_keys=True, indent=2, ensure_ascii=False)` ; le legacy doit être amené au même contrat dans la phase concernée |
106
+
107
+ ### Narrative facts (Phase 3)
108
+
109
+ | Aspect | ε |
110
+ |--------|---|
111
+ | Ensemble des `Fact` produits (par `FactType`) | **0** sur l'ensemble | l'arbitre peut réordonner mais pas inventer ni rater un fact |
112
+ | Payload de chaque fact (les valeurs numériques citées) | **0** (bit-for-bit) | garde-fou anti-hallucination |
113
+ | Templates rendus FR + EN | **0** sur le texte | déterministe par `str.format_map` |
114
+ | Ordre final des facts dans la synthèse | **autorisé à différer** | l'arbitre du rewrite peut choisir un ordre différent si la priorité est respectée — un test custom valide « les facts HIGH apparaissent avant les MEDIUM » plutôt que l'ordre exact |
115
+
116
+ ### Rapport HTML — sections legacy spécifiques (Phase 5)
117
+
118
+ Pour chaque renderer migré (calibration, NER, Pareto, narrative,
119
+ philological, etc.), un cas-test de régression dédié vit dans
120
+ `tests/regression/legacy_vs_rewrite/test_phase5_<renderer>.py`.
121
+ Le snapshot legacy est figé en début de phase.
122
+
123
+ ## Aléatoire — politique
124
+
125
+ Tout module qui utilise `random` doit :
126
+
127
+ 1. Accepter un argument `seed: int` ou utiliser une seed fixée
128
+ explicitement.
129
+ 2. Documenter la seed dans son docstring.
130
+ 3. Le harness de régression utilise toujours **seed=42**.
131
+
132
+ Modules concernés au legacy :
133
+
134
+ - `measurements/statistics/bootstrap.py` (seed=42)
135
+ - `measurements/runner/workers.py` (pas d'aléatoire — confirmé)
136
+ - `core/results.py` (pas d'aléatoire — confirmé)
137
+
138
+ ## Adaptateurs cloud (Mistral, OpenAI, Anthropic, Google, Azure)
139
+
140
+ Les appels réseau ne sont **pas** rejoués pendant la régression —
141
+ le test serait non-déterministe et coûteux. Stratégie :
142
+
143
+ 1. Le harness utilise des **fixtures de réponses figées** (JSON
144
+ capturé en local lors de la création du corpus de référence).
145
+ 2. Le legacy et le rewrite reçoivent **la même fixture** ; le test
146
+ vérifie que tous deux produisent le même output structurel.
147
+ 3. Si une dépendance SDK change la sérialisation (rare), le test
148
+ pète bruyamment et la PR doit re-frigorifier la fixture.
149
+
150
+ Aucune tolérance non triviale n'est nécessaire — l'égalité
151
+ bit-for-bit est tenable parce que l'aléatoire vient du cloud, pas
152
+ du parser.
153
+
154
+ ## Procédure d'exception (régression intentionnelle)
155
+
156
+ Quand une migration corrige un bug historique légitime :
157
+
158
+ 1. Ouvrir une issue GitHub avec le label `regression-intentional`.
159
+ 2. Référencer le numéro d'issue dans le commit qui modifie la
160
+ tolérance.
161
+ 3. Ajouter une entrée dans la section *« Régressions intentionnelles
162
+ acceptées »* ci-dessous, **avant** le merge.
163
+ 4. La tolérance peut être relâchée temporairement ; au merge, soit
164
+ le snapshot legacy est mis à jour pour refléter le nouveau
165
+ comportement (correct), soit la tolérance reste serrée pour les
166
+ prochaines migrations.
167
+
168
+ ## Régressions intentionnelles acceptées
169
+
170
+ | Date | Issue | Phase | Module | Description |
171
+ |------|-------|-------|--------|-------------|
172
+ | (aucune à ce jour) | | | | |
173
+
174
+ ## Révisions
175
+
176
+ | Version | Date | Changements |
177
+ |---------|------|-------------|
178
+ | 1.0 | 2026-05 | Création initiale (Phase 0 du plan de retrait legacy) |
docs/operations/deployment-institutional.md CHANGED
@@ -8,7 +8,7 @@
8
  > plutôt que sur HuggingFace Space public.
9
  >
10
  > Pour le déploiement HuggingFace Space ou un usage local rapide,
11
- > voir [`INSTALL.md`](../../INSTALL.md).
12
 
13
  ## Pré-requis
14
 
 
8
  > plutôt que sur HuggingFace Space public.
9
  >
10
  > Pour le déploiement HuggingFace Space ou un usage local rapide,
11
+ > voir [`how-to/install.md`](../how-to/install.md).
12
 
13
  ## Pré-requis
14
 
docs/operations/observability.md ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Observabilité — Picarones
2
+
3
+ > **Audience** : opérateur (DSI institutionnelle, SRE). Décrit
4
+ > comment instrumenter Picarones pour qu'il soit observable depuis
5
+ > Prometheus, Grafana, Loki, Datadog, etc.
6
+ >
7
+ > Pour la réponse aux incidents, voir [`runbook.md`](runbook.md).
8
+ > Pour le déploiement, voir [`deployment-institutional.md`](deployment-institutional.md).
9
+
10
+ ## Principes
11
+
12
+ Picarones expose trois types de signaux :
13
+
14
+ 1. **Logs structurés** (stdlib `logging`). Tous les modules
15
+ utilisent `logger = logging.getLogger(__name__)`. Niveaux
16
+ conventionnels : DEBUG, INFO, WARNING, ERROR. Aucun `print` en
17
+ production.
18
+ 2. **Audit trail** spécifique : `[audit] <event> <key=value>`
19
+ (par convention). Émis par les endpoints sensibles
20
+ (`POST/DELETE /api/jobs`).
21
+ 3. **Endpoints de santé** : `GET /health`, `GET /version`.
22
+
23
+ L'export vers une plateforme observabilité (Prometheus, Datadog, ELK)
24
+ est laissé au déploiement institutionnel — Picarones ne pousse rien
25
+ de lui-même.
26
+
27
+ ## Logs structurés
28
+
29
+ ### Format recommandé
30
+
31
+ Configurer le root logger en JSON pour l'ingestion automatique :
32
+
33
+ ```python
34
+ # /etc/picarones/logging.yaml
35
+ version: 1
36
+ disable_existing_loggers: false
37
+ formatters:
38
+ json:
39
+ format: '{"ts":"%(asctime)s","lvl":"%(levelname)s","logger":"%(name)s","msg":"%(message)s"}'
40
+ handlers:
41
+ stdout:
42
+ class: logging.StreamHandler
43
+ stream: ext://sys.stdout
44
+ formatter: json
45
+ loggers:
46
+ picarones:
47
+ level: INFO
48
+ handlers: [stdout]
49
+ propagate: false
50
+ root:
51
+ level: WARNING
52
+ handlers: [stdout]
53
+ ```
54
+
55
+ Activer au démarrage :
56
+
57
+ ```bash
58
+ PICARONES_LOG_CONFIG=/etc/picarones/logging.yaml \
59
+ uvicorn picarones.interfaces.web:create_app --factory ...
60
+ ```
61
+
62
+ ### Niveaux par module
63
+
64
+ | Module | Niveau prod recommandé |
65
+ |--------|------------------------|
66
+ | `picarones.adapters.*` | INFO |
67
+ | `picarones.app.services.*` | INFO |
68
+ | `picarones.interfaces.web.*` | INFO |
69
+ | `picarones.pipeline.*` | INFO (DEBUG si chasse à un bug d'orchestration) |
70
+ | `picarones.evaluation.*` | WARNING (très verbeux en INFO) |
71
+ | `picarones.adapters._retry` | WARNING (déjà bavard sur les retries) |
72
+
73
+ ### Exemples de lignes utiles à monitorer
74
+
75
+ | Pattern | Signification | Alerte |
76
+ |---------|---------------|--------|
77
+ | `[adapter] erreur retryable.*` | Cloud API instable | > 10/min sur 5 min → page |
78
+ | `OCRAdapterError` | Échec définitif d'OCR | > 5/min → warning |
79
+ | `[job_runner] job .* en échec` | Job s'est terminé en error | track per-IP |
80
+ | `[audit] job_submitted` | Soumission de job | tracker pour audit RGPD |
81
+ | `[audit] job_cancelled` | Annulation de job | tracker pour audit RGPD |
82
+ | `WinError 87` | Filename Windows invalide | DEVRAIT être 0 (corrigé S59) — sinon régression |
83
+ | `database is locked` | SQLite contention | > 1/min → page |
84
+
85
+ ## Audit trail
86
+
87
+ Les opérations sensibles produisent un log INFO normalisé :
88
+
89
+ ```
90
+ INFO [audit] job_submitted job_id=abc123 corpus=bnf_xviii from=10.0.0.42
91
+ INFO [audit] job_cancelled job_id=abc123 from=10.0.0.42
92
+ ```
93
+
94
+ Ces lignes sont **destinées à être conservées** selon la politique
95
+ RGPD de l'institution (cf. [`data-retention-rgpd.md`](data-retention-rgpd.md)).
96
+ Stockage minimum recommandé : 90 jours (audit interne) ; 5 ans si
97
+ soumis aux Archives nationales.
98
+
99
+ Pour ingestion SIEM :
100
+
101
+ ```
102
+ filter '[audit] '
103
+ extract job_id, corpus, from
104
+ forward to siem.bnf.fr:514 (syslog)
105
+ ```
106
+
107
+ ## Endpoints de santé
108
+
109
+ ### `GET /health`
110
+
111
+ Réponse `200 OK` si le process est en mesure de servir. Vérifie :
112
+
113
+ - `JobStore` accessible (lecture)
114
+ - `WorkspaceManager` accessible (écriture sandbox)
115
+ - Pas de check sur les API cloud (un cloud down ne doit pas planter
116
+ les health probes locales)
117
+
118
+ ```json
119
+ {
120
+ "status": "ok",
121
+ "version": "1.3.0-dev",
122
+ "job_store": "ok",
123
+ "workspace": "ok"
124
+ }
125
+ ```
126
+
127
+ À utiliser comme **liveness probe** (Kubernetes) ou **healthcheck**
128
+ (Docker). Recommandation : every 30s, fail after 3 consecutive.
129
+
130
+ ### `GET /version`
131
+
132
+ Réponse :
133
+
134
+ ```json
135
+ {
136
+ "version": "1.3.0-dev",
137
+ "code_version": "git-sha-abc1234",
138
+ "python": "3.11.15"
139
+ }
140
+ ```
141
+
142
+ Utile pour déterminer la version déployée sans accès au filesystem.
143
+
144
+ ## Métriques (à venir)
145
+
146
+ Picarones n'expose pas encore d'endpoint Prometheus `/metrics`.
147
+ Recommandation immédiate : monitorer les logs.
148
+
149
+ **Backlog** (cf. [`/docs/roadmap/backlog.md`](../roadmap/backlog.md)) :
150
+
151
+ - Compteur `picarones_jobs_total{status="complete|error|cancelled"}`
152
+ - Histogramme `picarones_job_duration_seconds`
153
+ - Compteur `picarones_adapter_calls_total{adapter, status}`
154
+ - Histogramme `picarones_adapter_latency_seconds{adapter}`
155
+ - Gauge `picarones_jobs_running` (instantané)
156
+
157
+ Implémentation visée : `prometheus_client` middleware FastAPI optionnel.
158
+
159
+ ## Tracing distribué
160
+
161
+ Pour les institutions qui orchestrent Picarones avec d'autres services
162
+ (ETL, cataloguing), le tracing OpenTelemetry est recommandé.
163
+
164
+ État actuel : pas d'instrumentation native. Une instrumentation
165
+ opportuniste via `opentelemetry-instrumentation-fastapi` peut être
166
+ activée par le déploiement sans modifier Picarones :
167
+
168
+ ```python
169
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
170
+ from picarones.interfaces.web import create_app
171
+
172
+ app = create_app(state=...)
173
+ FastAPIInstrumentor.instrument_app(app)
174
+ ```
175
+
176
+ ## Dashboards Grafana — squelette
177
+
178
+ Les panels recommandés pour un dashboard Picarones :
179
+
180
+ 1. **Jobs throughput** — courbes par status (complete/error/cancelled),
181
+ stack area, 24 h.
182
+ 2. **Adapter latency p50/p95/p99** par adapter (Tesseract, Pero,
183
+ Mistral OCR, Google Vision, Azure DI, OpenAI, Anthropic, Mistral
184
+ chat, Ollama).
185
+ 3. **Error rate par adapter** — % d'erreurs sur la dernière heure.
186
+ 4. **Concurrence** — `picarones_jobs_running` actuel, comparé à
187
+ `PICARONES_MAX_CONCURRENT_JOBS`.
188
+ 5. **Workspace size** — `du -sh /var/lib/picarones/workspaces` via
189
+ exporter node.
190
+ 6. **Heap RSS** du process Picarones (via node_exporter ou
191
+ process_exporter).
192
+
193
+ ## SLOs suggérés
194
+
195
+ Pour un déploiement institutionnel ouvert aux chercheurs :
196
+
197
+ | Métrique | SLO 30j | Action si dépassé |
198
+ |----------|---------|-------------------|
199
+ | Disponibilité `/health` | 99.5 % | Investiguer infra |
200
+ | Job completion rate | > 95 % | Examiner taux d'erreurs adapter |
201
+ | API p95 latency (CRUD jobs) | < 500 ms | Profiler le `JobStore` |
202
+ | Cloud adapter retry rate | < 5 % | Demander quota plus haut |
203
+
204
+ ## Révisions
205
+
206
+ | Version | Date | Changements |
207
+ |---------|------|-------------|
208
+ | 1.0 | 2026-05 | Création initiale (S60) |
docs/operations/runbook.md ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Runbook — réponse aux incidents Picarones
2
+
3
+ > **Audience** : opérateur (DSI institutionnelle, SRE) en garde
4
+ > active. Ce document liste les incidents prévisibles et les
5
+ > procédures de mitigation. Pour le déploiement initial, voir
6
+ > [`deployment-institutional.md`](deployment-institutional.md) ;
7
+ > pour l'observabilité, voir [`observability.md`](observability.md).
8
+ >
9
+ > **Convention** : chaque scénario suit le format
10
+ > `Symptôme → Diagnostic → Mitigation → Suivi`.
11
+
12
+ ## Index des scénarios
13
+
14
+ | ID | Scénario | Sévérité | Page |
15
+ |----|----------|----------|------|
16
+ | INC-01 | Job stuck en `running` | MAJOR | [§INC-01](#inc-01--job-stuck-en-running) |
17
+ | INC-02 | Disk full sur le workspace | BLOCKER | [§INC-02](#inc-02--disk-full-sur-le-workspace) |
18
+ | INC-03 | Cloud API rate limit / quota dépassé | MAJOR | [§INC-03](#inc-03--cloud-api-rate-limit) |
19
+ | INC-04 | SQLite `database is locked` | MAJOR | [§INC-04](#inc-04--sqlite-database-is-locked) |
20
+ | INC-05 | Memory leak (RSS qui croît continûment) | MAJOR | [§INC-05](#inc-05--memory-leak) |
21
+ | INC-06 | Compromission d'une clé API cloud | BLOCKER | [§INC-06](#inc-06--compromission-de-cl%C3%A9-api) |
22
+ | INC-07 | Rapport HTML corrompu / non-déterministe | MEDIUM | [§INC-07](#inc-07--rapport-html-corrompu) |
23
+ | INC-08 | CI bloquée > 30 min (déjà vu) | MEDIUM | [§INC-08](#inc-08--ci-bloqu%C3%A9e) |
24
+ | INC-09 | Upgrade qui casse les jobs en cours | MAJOR | [§INC-09](#inc-09--upgrade-casse-jobs) |
25
+ | INC-10 | Restauration depuis backup | MEDIUM | [§INC-10](#inc-10--restauration-backup) |
26
+
27
+ ---
28
+
29
+ ## INC-01 — Job stuck en `running`
30
+
31
+ **Symptôme**. `GET /api/jobs/{job_id}` retourne `status=running`
32
+ depuis > 1 heure alors que le corpus tient en quelques minutes.
33
+
34
+ **Diagnostic**.
35
+
36
+ ```bash
37
+ # 1. Le thread daemon existe-t-il encore ?
38
+ curl -s http://localhost:7860/api/jobs/{job_id} | jq '.status, .progress'
39
+
40
+ # 2. Les logs montrent-ils une activité récente ?
41
+ journalctl -u picarones -n 200 | grep "{job_id}"
42
+
43
+ # 3. Y a-t-il un appel cloud bloqué ?
44
+ ss -tnp | grep :443 # connexions TLS sortantes
45
+ ```
46
+
47
+ Causes typiques :
48
+
49
+ - Appel cloud qui hang sans timeout (anciens adapters).
50
+ - Workspace en read-only (impossible d'écrire le résultat).
51
+ - Process daemon mort sans avoir mis à jour le statut.
52
+
53
+ **Mitigation**.
54
+
55
+ ```bash
56
+ # Forcer l'annulation (dégrade en cancelled, pas en error).
57
+ curl -X DELETE http://localhost:7860/api/jobs/{job_id}
58
+
59
+ # Si le service ne répond plus :
60
+ systemctl restart picarones
61
+ # Au boot, le lifespan hook ``mark_orphaned_jobs_interrupted`` bascule
62
+ # automatiquement les jobs ``running`` en ``interrupted``.
63
+ ```
64
+
65
+ **Suivi**. Vérifier que le `JobRunner` n'a pas d'autres threads
66
+ zombies via `len(runner._threads)` (devrait redescendre). Si
67
+ récurrent, instrumenter avec un timeout de soft-cap par job.
68
+
69
+ ---
70
+
71
+ ## INC-02 — Disk full sur le workspace
72
+
73
+ **Symptôme**. Les jobs échouent en `error` avec
74
+ `OSError: [Errno 28] No space left on device`. L'API web peut
75
+ elle-même planter au boot (`JobStore` ne peut plus persister).
76
+
77
+ **Diagnostic**.
78
+
79
+ ```bash
80
+ df -h /var/lib/picarones/workspaces # ou le path configuré
81
+ du -sh /var/lib/picarones/workspaces/*
82
+ ```
83
+
84
+ Coupable typique : caches d'artefacts non purgés (`InMemoryArtifactStore`
85
+ n'a pas de TTL ; `FilesystemArtifactStore` non plus).
86
+
87
+ **Mitigation**.
88
+
89
+ ```bash
90
+ # 1. Identifier les workspaces les plus gros.
91
+ du -sh /var/lib/picarones/workspaces/* | sort -rh | head -10
92
+
93
+ # 2. Purger les workspaces dont aucun job actif ne dépend (lookup
94
+ # via JobStore).
95
+ sqlite3 /var/lib/picarones/jobs.db \
96
+ "SELECT job_id, status, payload FROM jobs WHERE status NOT IN ('pending', 'running');" \
97
+ | jq -r '.payload | fromjson | .output_dir'
98
+
99
+ # 3. Pour chaque output_dir terminé, archiver puis supprimer.
100
+ tar czf /backup/picarones-archive-$(date +%F).tar.gz <output_dirs>
101
+ rm -rf <output_dirs>
102
+ ```
103
+
104
+ **Suivi**. Établir une politique de rétention dans
105
+ [`data-retention-rgpd.md`](data-retention-rgpd.md). Recommandation :
106
+ purger les workspaces > 30 jours sans accès.
107
+
108
+ ---
109
+
110
+ ## INC-03 — Cloud API rate limit
111
+
112
+ **Symptôme**. Logs WARN : `[adapter] erreur retryable (tentative 3/4,
113
+ attente 8s) : 429 Too Many Requests`. Job se termine en error après
114
+ épuisement des retries.
115
+
116
+ **Diagnostic**.
117
+
118
+ ```bash
119
+ # Compter les 429 dans la dernière heure.
120
+ journalctl -u picarones --since "1 hour ago" \
121
+ | grep "429" | wc -l
122
+
123
+ # Identifier les jobs concernés.
124
+ journalctl -u picarones --since "1 hour ago" \
125
+ | grep -B2 "429" | grep "job_runner"
126
+ ```
127
+
128
+ Causes typiques : un benchmark de 5000 documents lance 5000 appels
129
+ en parallèle, dépasse la quota de l'organisation cloud.
130
+
131
+ **Mitigation immédiate**.
132
+
133
+ ```bash
134
+ # 1. Réduire le parallélisme du runner (env var).
135
+ sed -i 's/PICARONES_RUNNER_MAX_WORKERS=8/PICARONES_RUNNER_MAX_WORKERS=2/' /etc/picarones/.env
136
+ systemctl restart picarones
137
+
138
+ # 2. Re-soumettre les jobs en error qui se sont arrêtés au milieu.
139
+ # (Picarones ne fait pas de resume automatique sur erreur cloud — le
140
+ # cache d'artefacts du PipelineExecutor évite de re-exécuter les
141
+ # steps déjà terminés au prochain run.)
142
+ ```
143
+
144
+ **Mitigation long terme**. Demander une quota plus haute au
145
+ fournisseur cloud, ou ajouter un throttle au niveau adapter (token
146
+ bucket par adapter).
147
+
148
+ ---
149
+
150
+ ## INC-04 — SQLite `database is locked`
151
+
152
+ **Symptôme**. Logs ERROR : `sqlite3.OperationalError: database is
153
+ locked`. Touche typiquement le `JobStore`.
154
+
155
+ **Diagnostic**.
156
+
157
+ ```bash
158
+ # 1. Compter les processes qui ont la DB ouverte.
159
+ lsof /var/lib/picarones/jobs.db
160
+
161
+ # 2. Vérifier le mode WAL.
162
+ sqlite3 /var/lib/picarones/jobs.db "PRAGMA journal_mode;"
163
+ # Devrait répondre "wal". Si "delete" ou "rollback", le WAL n'a pas
164
+ # pris.
165
+ ```
166
+
167
+ Causes : un process autre que Picarones a ouvert la DB (backup
168
+ maladroit), ou le filesystem ne supporte pas WAL (FAT32, NFS sans
169
+ verrous).
170
+
171
+ **Mitigation**.
172
+
173
+ ```bash
174
+ # 1. Stopper l'autre process si identifié.
175
+ # 2. Si NFS : remonter avec ``-o nolock`` côté serveur ne marche PAS
176
+ # (WAL exige des verrous). Solution : déplacer ``jobs.db`` sur un
177
+ # filesystem local et exporter le résultat via NFS read-only.
178
+ # 3. Si filesystem ne supporte vraiment pas WAL, le code retombe sur
179
+ # ``rollback journal`` (cf. job_store.py:185-189) — fonctionnel
180
+ # mais bloquant en lecture pendant les écritures.
181
+
182
+ # Test de santé.
183
+ sqlite3 /var/lib/picarones/jobs.db "PRAGMA integrity_check;"
184
+ ```
185
+
186
+ **Suivi**. Configurer un monitoring du `journal_mode` au boot.
187
+
188
+ ---
189
+
190
+ ## INC-05 — Memory leak
191
+
192
+ **Symptôme**. RSS du process Picarones croît continûment au-delà
193
+ de 2 GB après plusieurs heures.
194
+
195
+ **Diagnostic**.
196
+
197
+ ```bash
198
+ # Profiling minimal sans installer d'outil.
199
+ ps -o pid,rss,cmd -p $(pgrep picarones) | tail -1
200
+
201
+ # Si py-spy disponible :
202
+ py-spy dump --pid $(pgrep picarones)
203
+ ```
204
+
205
+ Causes connues :
206
+
207
+ - `JobRunner._threads` non nettoyé (FIXÉ en S58).
208
+ - `RateLimitMiddleware._buckets` non borné (FIXÉ en S58 — eviction LRU).
209
+ - Caches d'artefacts in-memory accumulés (cf. INC-02).
210
+
211
+ **Mitigation**.
212
+
213
+ ```bash
214
+ systemctl restart picarones
215
+ # Le lifespan hook nettoie les jobs orphelins ; les caches in-memory
216
+ # sont vidés par redémarrage.
217
+ ```
218
+
219
+ **Suivi**. Si récurrent, exporter `picarones._mem_audit` (à
220
+ implémenter — backlog) et corréler avec les jobs actifs.
221
+
222
+ ---
223
+
224
+ ## INC-06 — Compromission de clé API
225
+
226
+ **Symptôme**. Facturation cloud anormale, ou notification du
227
+ fournisseur (« nous avons détecté une utilisation suspecte de votre
228
+ clé »).
229
+
230
+ **Mitigation immédiate** (dans l'ordre).
231
+
232
+ ```bash
233
+ # 1. Révoquer la clé chez le fournisseur (console cloud).
234
+ # 2. Stopper Picarones pour éviter qu'il ne tente de relancer avec
235
+ # la clé invalidée.
236
+ systemctl stop picarones
237
+ # 3. Rotater la clé dans le secret store.
238
+ vault kv put secret/picarones OPENAI_API_KEY=sk-NEW...
239
+ # 4. Reload + redémarrage.
240
+ systemctl start picarones
241
+ # 5. Audit des jobs récents pour identifier les exfiltrations.
242
+ sqlite3 /var/lib/picarones/jobs.db \
243
+ "SELECT job_id, payload, created_at FROM jobs ORDER BY created_at DESC LIMIT 100;"
244
+ ```
245
+
246
+ **Suivi**. Notifier le DPO institutionnel sous 24 h si des
247
+ documents avec PII (registres, état civil) ont été envoyés à l'API
248
+ compromise. Voir [`data-retention-rgpd.md`](data-retention-rgpd.md).
249
+
250
+ ---
251
+
252
+ ## INC-07 — Rapport HTML corrompu
253
+
254
+ **Symptôme**. Deux runs identiques produisent des rapports HTML
255
+ différents byte-for-byte.
256
+
257
+ **Diagnostic**.
258
+
259
+ ```bash
260
+ # Comparer les hashes de manifests.
261
+ sha256sum run-A/run_manifest.json run-B/run_manifest.json
262
+
263
+ # Si différents : un des paramètres canoniques a divergé.
264
+ diff <(jq -S . run-A/run_manifest.json) <(jq -S . run-B/run_manifest.json)
265
+ ```
266
+
267
+ Causes typiques : un adapter cloud (gpt-4o, claude) qui a une
268
+ température > 0 → non-déterminisme natif. Vérifier les
269
+ `adapter_kwargs` dans le manifest.
270
+
271
+ **Mitigation**. Forcer `temperature: 0.0` dans la `RunSpec` YAML.
272
+ Pour les benchmarks de reproductibilité, exclure les adapters
273
+ non-déterministes.
274
+
275
+ ---
276
+
277
+ ## INC-08 — CI bloquée
278
+
279
+ **Symptôme**. Un job GitHub Actions reste en `queued` ou
280
+ `in_progress` > 30 minutes pour ce qui devrait être un test < 5 min.
281
+
282
+ **Diagnostic**. Vérifier dans cet ordre :
283
+
284
+ 1. **Codecov upload hang** (déjà vu — 50+ min) → couvert par
285
+ `timeout-minutes: 5` sur l'étape Codecov + `fail_ci_if_error: false`
286
+ depuis le S59.
287
+ 2. **Live tests qui s'exécutent** au lieu d'être deselected → le
288
+ marker `live` doit être dans `addopts` de `pyproject.toml`
289
+ (vérifié par les tests dual-lang).
290
+ 3. **Codespaces / runner épuisé** → annuler manuellement le job,
291
+ relancer.
292
+
293
+ **Mitigation**. Annuler le workflow run (UI GitHub Actions),
294
+ relancer. Si récurrent, élever un incident infra GitHub.
295
+
296
+ ---
297
+
298
+ ## INC-09 — Upgrade casse jobs
299
+
300
+ **Symptôme**. Après `git pull && pip install -e .`, les jobs
301
+ soumis avant l'upgrade échouent en `error`.
302
+
303
+ **Diagnostic**. Le `JobStore` utilise une table `schema_version` ;
304
+ une bump de SCHEMA_VERSION sans migration livre `JobStoreError` au
305
+ boot.
306
+
307
+ **Mitigation**.
308
+
309
+ ```bash
310
+ # 1. Stopper le service AVANT l'upgrade.
311
+ systemctl stop picarones
312
+
313
+ # 2. Backup du JobStore.
314
+ cp /var/lib/picarones/jobs.db /var/lib/picarones/jobs.db.bak
315
+
316
+ # 3. Upgrade.
317
+ git pull && pip install -e ".[dev,web]"
318
+
319
+ # 4. Vérifier le schéma.
320
+ sqlite3 /var/lib/picarones/jobs.db "SELECT version FROM schema_version;"
321
+
322
+ # 5. Démarrer. Le dispatcher applique automatiquement les
323
+ # migrations enregistrées dans ``_MIGRATIONS``.
324
+ systemctl start picarones
325
+ ```
326
+
327
+ **Suivi**. Tester chaque upgrade en staging avant prod.
328
+
329
+ ---
330
+
331
+ ## INC-10 — Restauration depuis backup
332
+
333
+ **Symptôme**. Corruption ou perte du workspace ou de la DB jobs.
334
+
335
+ **Pré-requis**. Backup récent (recommandé : snapshot quotidien du
336
+ volume `/var/lib/picarones/`).
337
+
338
+ **Mitigation**.
339
+
340
+ ```bash
341
+ # 1. Stopper le service.
342
+ systemctl stop picarones
343
+
344
+ # 2. Restaurer.
345
+ rsync -av /backup/picarones-2026-05-XX/ /var/lib/picarones/
346
+
347
+ # 3. Vérifier l'intégrité SQLite.
348
+ sqlite3 /var/lib/picarones/jobs.db "PRAGMA integrity_check;"
349
+
350
+ # 4. Démarrer. Les jobs ``running`` au moment du backup seront
351
+ # automatiquement marqués ``interrupted`` par le lifespan hook.
352
+ systemctl start picarones
353
+ ```
354
+
355
+ **Suivi**. Communiquer aux utilisateurs que les jobs en cours au
356
+ moment du backup sont à relancer.
357
+
358
+ ---
359
+
360
+ ## Escalade
361
+
362
+ Si un incident dépasse les procédures ci-dessus :
363
+
364
+ 1. Documenter l'observation dans un fichier `incidents/<date>.md`
365
+ (snapshot du symptôme + commandes lancées + résultat).
366
+ 2. Ouvrir une issue GitHub avec le label `incident`.
367
+ 3. Pour une vulnérabilité de sécurité, suivre la procédure de
368
+ [`/SECURITY.md`](../../SECURITY.md) (canal privé).
369
+
370
+ ## Révisions
371
+
372
+ | Version | Date | Changements |
373
+ |---------|------|-------------|
374
+ | 1.0 | 2026-05 | Création initiale (S60), 10 scénarios |
docs/operations/supply-chain.md ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supply chain — SBOM, SLSA, signatures
2
+
3
+ > **Audience** : DSI institutionnelle et conformité réglementaire
4
+ > (EU CRA — Cyber Resilience Act, exigible à partir de 2027 pour les
5
+ > livraisons à des organismes publics européens).
6
+ >
7
+ > Décrit comment Picarones documente sa chaîne d'approvisionnement
8
+ > logicielle et permet à une institution de vérifier l'intégrité
9
+ > d'un wheel ou d'une image Docker avant déploiement.
10
+
11
+ ## SBOM (Software Bill of Materials)
12
+
13
+ ### Format CycloneDX
14
+
15
+ Picarones produit un SBOM au format **CycloneDX 1.5 JSON** à chaque
16
+ release. Le SBOM liste l'intégralité des paquets Python installés
17
+ dans l'environnement de build avec :
18
+
19
+ - `name`, `version`, `purl` (package URL canonique).
20
+ - `licenses` (SPDX expression).
21
+ - `hashes` (SHA-256 du wheel).
22
+ - `dependencies` (graphe de dépendance complet).
23
+
24
+ Génération locale :
25
+
26
+ ```bash
27
+ pip install cyclonedx-bom
28
+ python scripts/gen_sbom.py --output sbom.json
29
+ ```
30
+
31
+ Génération automatique dans la CI : voir
32
+ [`.github/workflows/release.yml`](../../.github/workflows/release.yml)
33
+ qui attache `sbom.json` à chaque GitHub Release.
34
+
35
+ ### Image Docker
36
+
37
+ L'image Docker `ghcr.io/maribakulj/picarones:<version>` embarque son
38
+ propre SBOM (couche métadonnées BuildKit) :
39
+
40
+ ```bash
41
+ docker buildx imagetools inspect \
42
+ ghcr.io/maribakulj/picarones:<version> \
43
+ --format '{{ json .SBOM }}'
44
+ ```
45
+
46
+ ## SLSA Provenance
47
+
48
+ [SLSA](https://slsa.dev/) (Supply-chain Levels for Software Artifacts)
49
+ formalise le niveau de confiance qu'on peut accorder à un artefact
50
+ livré.
51
+
52
+ ### État actuel : SLSA Level 2
53
+
54
+ - **Build** isolé sur GitHub-hosted runners, traçable au commit SHA.
55
+ - **Provenance** générée automatiquement par
56
+ [`docker/build-push-action@v5`](https://github.com/docker/build-push-action)
57
+ avec `provenance: true`.
58
+
59
+ Inspection :
60
+
61
+ ```bash
62
+ docker buildx imagetools inspect \
63
+ ghcr.io/maribakulj/picarones:<version> \
64
+ --format '{{ json .Provenance }}'
65
+ ```
66
+
67
+ ### Trajectoire vers SLSA Level 3
68
+
69
+ Pour atteindre le niveau 3 (signature non-falsifiable), prochaines
70
+ étapes (cf. [`/docs/roadmap/backlog.md`](../roadmap/backlog.md)) :
71
+
72
+ 1. Signer chaque wheel PyPI avec [Sigstore](https://www.sigstore.dev/)
73
+ via `pypi-attestations` (PEP 740).
74
+ 2. Signer le SBOM avec `cosign sign-blob` lors de la release.
75
+ 3. Publier les attestations sur Rekor (transparency log).
76
+
77
+ ## Vérification côté institution
78
+
79
+ Avant déploiement, l'institution peut vérifier qu'un wheel n'a pas
80
+ été altéré entre le build CI et le download :
81
+
82
+ ```bash
83
+ # 1. Téléchargement.
84
+ pip download picarones==<version> --no-deps -d /tmp/audit/
85
+
86
+ # 2. Vérification du hash contre le SBOM.
87
+ sha256sum /tmp/audit/picarones-*.whl
88
+ jq -r '.components[] | select(.name == "picarones") | .hashes[0].content' sbom.json
89
+ # Les deux valeurs doivent matcher.
90
+
91
+ # 3. (Future, SLSA L3) Vérification de la signature Sigstore.
92
+ # cosign verify-blob --bundle picarones-<version>.whl.sigstore picarones-<version>.whl
93
+ ```
94
+
95
+ ## Politique de mise à jour des dépendances
96
+
97
+ - **CVE critique** (CVSS ≥ 9.0) : patch release sous 7 jours.
98
+ - **CVE élevée** (7.0 ≤ CVSS < 9.0) : minor release sous 30 jours.
99
+ - **CVE moyenne** : prise en compte au prochain cycle de release.
100
+
101
+ Surveillance :
102
+
103
+ - `pip-audit` exécuté en CI sur chaque push (cf.
104
+ [`/.github/workflows/precommit.yml`](../../.github/workflows/precommit.yml)).
105
+ - Dependabot / Renovate sur `pyproject.toml` pour les minor / patch.
106
+
107
+ ## Conformité EU CRA (anticipation)
108
+
109
+ L'EU Cyber Resilience Act, applicable à partir de 2027 pour les
110
+ produits livrés à des entités publiques de l'UE, exigera :
111
+
112
+ | Exigence CRA | Statut Picarones |
113
+ |--------------|------------------|
114
+ | SBOM machine-readable | ✅ CycloneDX 1.5 |
115
+ | Vulnerability disclosure policy | ✅ [`/SECURITY.md`](../../SECURITY.md) + RFC 9116 [`/.well-known/security.txt`](../../.well-known/security.txt) |
116
+ | Coordinated vulnerability disclosure | ✅ GitHub Security Advisories |
117
+ | Cryptographic signing of releases | 🔧 SLSA L2 actuel, L3 prévu |
118
+ | Vulnerability handling within reasonable timeframes | ✅ Politique documentée ci-dessus |
119
+ | Security updates for at least 5 years | 🔧 Politique LTS à définir avant 1.0 GA |
120
+
121
+ ## Révisions
122
+
123
+ | Version | Date | Changements |
124
+ |---------|------|-------------|
125
+ | 1.0 | 2026-05 | Création initiale (S60) |
docs/{views → reference}/alto-view.md RENAMED
File without changes
docs/{api-stable.md → reference/api-stable.md} RENAMED
@@ -1,26 +1,34 @@
1
- # API publique stable de Picarones (Cercle 1)
2
-
3
- Phase D du chantier de refonte en 3 cercles engagement contractuel
4
- de stabilité de l'API publique du Cercle 1.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  ## Définition
7
 
8
- L'API publique de Picarones est constituée des classes, fonctions,
9
- constantes et types listés ci-dessous, exportés depuis le sous-package
10
- `picarones.core/`. Ce qui est dans cette liste constitue **un contrat
11
- de stabilité** : nous nous engageons à ne pas le casser entre versions
12
- mineures (semver `1.x.0`).
13
 
14
- Ce qui n'est pas dans cette liste y compris les modules historiques
15
- qui ont été déplacés vers `picarones.measurements/`, `picarones.extras/`
16
- et accessibles via shims rétrocompat — peut évoluer à tout moment
17
  sans bump majeur.
18
 
19
- Les imports historiques (`from picarones.core.confusion import ...`,
20
- `from picarones.core.narrative.facts import ...`, etc.) restent
21
- fonctionnels mais ne font **pas** partie de l'API publique stable :
22
- ce sont des aliases rétrocompat. Pour de la nouveauté, préférer
23
- `from picarones.measurements.confusion import ...`.
24
 
25
  ## Test automatique
26
 
@@ -30,7 +38,7 @@ ou change de forme.
30
 
31
  ## Liste exhaustive
32
 
33
- ### `picarones.core.corpus`
34
 
35
  ```python
36
  class GTLevel(str, Enum):
@@ -51,12 +59,18 @@ GT_SUFFIXES: dict[GTLevel, str] # mapping niveau → suffixe fichier
51
  def load_corpus_from_directory(path) -> Corpus
52
  ```
53
 
54
- ### `picarones.core.modules`
55
 
56
  ```python
57
  class ArtifactType(str, Enum):
58
- IMAGE, TEXT, ALTO, PAGE, ENTITIES, READING_ORDER
 
 
 
59
 
 
 
 
60
  class BaseModule(ABC):
61
  input_types: tuple[ArtifactType, ...]
62
  output_types: tuple[ArtifactType, ...]
@@ -71,7 +85,7 @@ class BaseModule(ABC):
71
  ExecutionMode = Literal["io", "cpu"]
72
  ```
73
 
74
- ### `picarones.core.results`
75
 
76
  ```python
77
  class DocumentResult: # résultat moteur sur un doc (CER, métriques, taxonomy…)
@@ -105,7 +119,7 @@ def run_benchmark(
105
  ) -> BenchmarkResult
106
  ```
107
 
108
- ### `picarones.core.pipeline`
109
 
110
  ```python
111
  class PipelineStep:
@@ -144,7 +158,7 @@ def load_comparison_specs_from_yaml(path) -> tuple[list[PipelineSpec], dict]
144
  def load_comparison_specs_from_dict(data: dict) -> tuple[list[PipelineSpec], dict]
145
  ```
146
 
147
- ### `picarones.core.metric_registry`
148
 
149
  ```python
150
  class MetricSpec: # frozen dataclass : name, func, input_types, ...
@@ -156,7 +170,7 @@ def select_metrics(input_types) -> list[MetricSpec]
156
  def compute_at_junction(reference, hypothesis, input_types, *, skip_on_error=True) -> dict
157
  ```
158
 
159
- ### `picarones.core.metric_hooks`
160
 
161
  ```python
162
  # Profils — constantes
@@ -241,11 +255,11 @@ 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.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.
248
- - **Internes des modules Cercle 1** : les noms commençant par `_`
249
  ne font pas partie de l'API publique. Les tests Sprints
250
  historiques qui les importent (Sprint 13/42) sont préservés mais
251
  par effort, pas par contrat.
@@ -258,9 +272,9 @@ Un bump majeur sera nécessaire pour :
258
  - Changer la signature d'une fonction publique de manière non
259
  rétrocompatible.
260
  - Casser le format de sérialisation du `BenchmarkResult.to_json()`.
261
- - Renommer un module Cercle 1.
262
 
263
- ## Modules historiques rétrocompat (non Cercle 1)
264
 
265
  Les imports suivants continuent à fonctionner mais ne font pas partie
266
  de l'API publique stable. Ils peuvent évoluer ou être retirés en
@@ -275,7 +289,7 @@ from picarones.measurements.calibration import compute_calibration_metrics
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/)
@@ -296,8 +310,8 @@ Pour les **nouvelles** intégrations, préférer les chemins canoniques :
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
303
  automatique qui échoue si un nom listé ici disparaît.
 
1
+ # API publique stable de Picarones
2
+
3
+ > **Statut** : ce document décrivait l'API publique du Cercle 1
4
+ > historique (`picarones.core/`). Le projet est en cours de
5
+ > retrait du legacy vers une **architecture 8 couches**
6
+ > (`domain → formats → evaluation → pipeline → adapters → app
7
+ > → reports_v2 → interfaces`, cf.
8
+ > [`docs/explanation/architecture.md`](../explanation/architecture.md)).
9
+ >
10
+ > **Pendant la migration** (jusqu'à la version 2.0), l'API
11
+ > publique est en cours de refonte. Tous les chemins legacy
12
+ > (`picarones.core.X`, `picarones.measurements.X`, etc.) sont
13
+ > des shims `DeprecationWarning` qui ré-exportent depuis le
14
+ > canonique. Les nouveaux imports doivent utiliser les chemins
15
+ > canoniques (`picarones.domain.*`, `picarones.evaluation.*`).
16
+ >
17
+ > Le tableau de parité legacy ↔ canonique vit dans
18
+ > [`tests/architecture/test_legacy_canonical_parity.py`](../../tests/architecture/test_legacy_canonical_parity.py).
19
 
20
  ## Définition
21
 
22
+ L'API publique stable de Picarones est constituée des classes,
23
+ fonctions, constantes et types listés ci-dessous, désormais
24
+ exportés depuis l'arborescence canonique.
 
 
25
 
26
+ Ce qui n'est pas dans cette liste peut évoluer à tout moment
 
 
27
  sans bump majeur.
28
 
29
+ Les imports historiques restent fonctionnels via shims pendant
30
+ la migration ; ils ne font **pas** partie de l'API publique
31
+ stable et émettent un `DeprecationWarning`.
 
 
32
 
33
  ## Test automatique
34
 
 
38
 
39
  ## Liste exhaustive
40
 
41
+ ### `picarones.evaluation.corpus`
42
 
43
  ```python
44
  class GTLevel(str, Enum):
 
59
  def load_corpus_from_directory(path) -> Corpus
60
  ```
61
 
62
+ ### `picarones.domain.artifacts`
63
 
64
  ```python
65
  class ArtifactType(str, Enum):
66
+ IMAGE, RAW_TEXT, CORRECTED_TEXT, ALTO_XML, PAGE_XML,
67
+ CANONICAL_DOCUMENT, ENTITIES, READING_ORDER, ALIGNMENT, CONFIDENCES
68
+ # Aliases legacy pour rétrocompat : TEXT, ALTO, PAGE
69
+ ```
70
 
71
+ ### `picarones.domain.module_protocol`
72
+
73
+ ```python
74
  class BaseModule(ABC):
75
  input_types: tuple[ArtifactType, ...]
76
  output_types: tuple[ArtifactType, ...]
 
85
  ExecutionMode = Literal["io", "cpu"]
86
  ```
87
 
88
+ ### `picarones.evaluation.benchmark_result`
89
 
90
  ```python
91
  class DocumentResult: # résultat moteur sur un doc (CER, métriques, taxonomy…)
 
119
  ) -> BenchmarkResult
120
  ```
121
 
122
+ ### `picarones.evaluation.pipeline`
123
 
124
  ```python
125
  class PipelineStep:
 
158
  def load_comparison_specs_from_dict(data: dict) -> tuple[list[PipelineSpec], dict]
159
  ```
160
 
161
+ ### `picarones.evaluation.metric_registry`
162
 
163
  ```python
164
  class MetricSpec: # frozen dataclass : name, func, input_types, ...
 
170
  def compute_at_junction(reference, hypothesis, input_types, *, skip_on_error=True) -> dict
171
  ```
172
 
173
+ ### `picarones.evaluation.metric_hooks`
174
 
175
  ```python
176
  # Profils — constantes
 
255
  reflètent ces changements.
256
  - **Modules `picarones.extras/`** : statut variable selon le
257
  sous-package (academic / governance / historical / importers).
258
+ Voir `docs/explanation/architecture.md`.
259
  - **Comportement des renderers HTML** : la structure des fichiers HTML
260
  peut évoluer entre versions mineures. Nous gardons les noms des
261
  vues principales.
262
+ - **Internes des modules canoniques** : les noms commençant par `_`
263
  ne font pas partie de l'API publique. Les tests Sprints
264
  historiques qui les importent (Sprint 13/42) sont préservés mais
265
  par effort, pas par contrat.
 
272
  - Changer la signature d'une fonction publique de manière non
273
  rétrocompatible.
274
  - Casser le format de sérialisation du `BenchmarkResult.to_json()`.
275
+ - Renommer un module de l'arborescence canonique.
276
 
277
+ ## Modules historiques rétrocompat (non canoniques)
278
 
279
  Les imports suivants continuent à fonctionner mais ne font pas partie
280
  de l'API publique stable. Ils peuvent évoluer ou être retirés en
 
289
 
290
  # Moteur narratif (déplacé vers picarones.measurements.narrative/)
291
  from picarones.measurements.narrative import build_synthesis
292
+ from picarones.domain.facts import Fact, FactType, FactImportance
293
  from picarones.measurements.narrative.detectors import detect_global_leader_cer
294
 
295
  # Plugins (déplacés vers picarones.extras/)
 
310
 
311
  ## Voir aussi
312
 
313
+ - [`docs/explanation/architecture.md`](architecture.md) — cartographie
314
  des 3 cercles + critères d'assignation.
315
+ - [`docs/explanation/architecture.md`](architecture.md) — vue d'ensemble post-chantiers.
316
  - [`tests/test_public_api.py`](../tests/test_public_api.py) — test
317
  automatique qui échoue si un nom listé ici disparaît.
docs/{views → reference}/comparing-views.md RENAMED
File without changes
docs/{profiles.md → reference/normalization-profiles.md} RENAMED
@@ -4,7 +4,7 @@ Picarones expose **7 profils de calcul** qui modulent les métriques
4
  calculées par le runner selon le use case. Chaque profil active un
5
  sous-ensemble des **12 hooks document-level** et **12 agrégateurs
6
  corpus-level** du registre central
7
- ([`picarones/core/metric_hooks.py`](../picarones/core/metric_hooks.py)).
8
 
9
  ## Synoptique
10
 
@@ -21,7 +21,7 @@ corpus-level** du registre central
21
  > **Note rétrocompat** : aujourd'hui les profils `philological`, `diagnostics`,
22
  > `economics`, `pipeline` et `full` activent **le même ensemble** que `standard`
23
  > côté hooks calculés. Ce qui change, c'est la **vue HTML rendue** : chaque
24
- > profil active des sous-sections différentes du rapport (cf. `docs/views.md`).
25
  > Les profils sont volontairement génériques pour permettre aux contributeurs
26
  > futurs d'ajouter des hooks spécifiques sans casser l'API.
27
 
@@ -127,11 +127,11 @@ reproductibilité scientifique maximale.
127
 
128
  ## Comment ajouter un hook personnalisé
129
 
130
- Voir [`docs/developer/narrative-engine.md`](developer/narrative-engine.md)
131
  pour le détail. Pattern de base :
132
 
133
  ```python
134
- from picarones.core.metric_hooks import (
135
  register_document_metric, PROFILE_DIAGNOSTICS, PROFILE_FULL,
136
  )
137
 
@@ -148,7 +148,7 @@ def my_hook(*, ground_truth, hypothesis, image_path, corpus_lang, ocr_result):
148
 
149
  ## Code source
150
 
151
- - [`picarones/core/metric_hooks.py`](../picarones/core/metric_hooks.py)
152
  — registre, profils, `run_document_hooks()`, `run_corpus_aggregators()`.
153
  - [`picarones/measurements/builtin_hooks.py`](../picarones/measurements/builtin_hooks.py)
154
  — les 12 hooks doc + 12 agrégateurs natifs Picarones.
 
4
  calculées par le runner selon le use case. Chaque profil active un
5
  sous-ensemble des **12 hooks document-level** et **12 agrégateurs
6
  corpus-level** du registre central
7
+ ([`picarones/evaluation/metric_hooks.py`](../picarones/evaluation/metric_hooks.py)).
8
 
9
  ## Synoptique
10
 
 
21
  > **Note rétrocompat** : aujourd'hui les profils `philological`, `diagnostics`,
22
  > `economics`, `pipeline` et `full` activent **le même ensemble** que `standard`
23
  > côté hooks calculés. Ce qui change, c'est la **vue HTML rendue** : chaque
24
+ > profil active des sous-sections différentes du rapport (cf. `docs/reference/views.md`).
25
  > Les profils sont volontairement génériques pour permettre aux contributeurs
26
  > futurs d'ajouter des hooks spécifiques sans casser l'API.
27
 
 
127
 
128
  ## Comment ajouter un hook personnalisé
129
 
130
+ Voir [`docs/explanation/narrative-engine.md`](developer/narrative-engine.md)
131
  pour le détail. Pattern de base :
132
 
133
  ```python
134
+ from picarones.evaluation.metric_hooks import (
135
  register_document_metric, PROFILE_DIAGNOSTICS, PROFILE_FULL,
136
  )
137
 
 
148
 
149
  ## Code source
150
 
151
+ - [`picarones/evaluation/metric_hooks.py`](../picarones/evaluation/metric_hooks.py)
152
  — registre, profils, `run_document_hooks()`, `run_corpus_aggregators()`.
153
  - [`picarones/measurements/builtin_hooks.py`](../picarones/measurements/builtin_hooks.py)
154
  — les 12 hooks doc + 12 agrégateurs natifs Picarones.
docs/{reproducibility-snapshots.md → reference/reproducibility-snapshots.md} RENAMED
File without changes
docs/{views → reference}/text-view.md RENAMED
File without changes
docs/{views.md → reference/views.md} RENAMED
@@ -62,7 +62,7 @@ orphelins** identifiés dans l'audit initial :
62
 
63
  #### Vue « Coût et performance » (`build_economics_view_html`)
64
 
65
- Module : [`picarones/report/views/economics.py`](../picarones/report/views/economics.py).
66
  Activée si :
67
  - `engine_reports` fournis avec durations non nulles.
68
  - (Optionnel) `extra_html_blocks` pour cost projection / marginal cost.
@@ -73,7 +73,7 @@ Sous-sections :
73
 
74
  #### Vue « Taxonomie avancée » (`build_advanced_taxonomy_view_html`)
75
 
76
- Module : [`picarones/report/views/advanced_taxonomy.py`](../picarones/report/views/advanced_taxonomy.py).
77
  Activée si ≥ 2 moteurs ont une `aggregated_taxonomy`.
78
 
79
  Sous-sections :
@@ -85,7 +85,7 @@ Sous-sections :
85
 
86
  #### Vue « Diagnostic approfondi » (`build_diagnostics_view_html`)
87
 
88
- Module : [`picarones/report/views/diagnostics.py`](../picarones/report/views/diagnostics.py).
89
  Activée si `detect_levers()` produit au moins un levier (typique sur
90
  un bench standard) ou si données opt-in fournies.
91
 
@@ -106,7 +106,7 @@ servent à composer des **rapports autonomes** :
106
 
107
  ### Vue « Pipeline composée » (`build_pipeline_view_html`)
108
 
109
- Module : [`picarones/report/views/pipeline.py`](../picarones/report/views/pipeline.py).
110
 
111
  Utilisée par `picarones pipeline run` (ou par tout outil qui consomme un
112
  `PipelineBenchmarkResult`). Sous-sections :
@@ -122,7 +122,7 @@ Utilisée par `picarones pipeline run` (ou par tout outil qui consomme un
122
 
123
  ### Vue « Robustesse projetée » (`build_robustness_view_html`)
124
 
125
- Module : [`picarones/report/views/robustness.py`](../picarones/report/views/robustness.py).
126
 
127
  Utilisée par le workflow `picarones robustness`. Sous-sections :
128
 
@@ -141,14 +141,14 @@ défini dans `economics.py` :
141
 
142
  ## Code source
143
 
144
- - [`picarones/report/generator.py`](../picarones/report/generator.py)
145
  — orchestrateur Jinja2 qui appelle les renderers et passe leurs sorties
146
  au template.
147
- - [`picarones/report/views/`](../picarones/report/views/) — 5 modules de
148
  composition (chantier 3).
149
- - [`picarones/report/*_render.py`](../picarones/report/) — 26 renderers
150
  atomiques.
151
- - [`picarones/report/templates/view_analyses.html`](../picarones/report/templates/view_analyses.html)
152
  — template Jinja2 qui inclut les blocs.
153
  - [`tests/test_views.py`](../tests/test_views.py) — tests d'intégration
154
  des 5 vues du chantier 3.
 
62
 
63
  #### Vue « Coût et performance » (`build_economics_view_html`)
64
 
65
+ Module : [`picarones/reports_v2/html/views/economics.py`](../picarones/reports_v2/html/views/economics.py).
66
  Activée si :
67
  - `engine_reports` fournis avec durations non nulles.
68
  - (Optionnel) `extra_html_blocks` pour cost projection / marginal cost.
 
73
 
74
  #### Vue « Taxonomie avancée » (`build_advanced_taxonomy_view_html`)
75
 
76
+ Module : [`picarones/reports_v2/html/views/advanced_taxonomy.py`](../picarones/reports_v2/html/views/advanced_taxonomy.py).
77
  Activée si ≥ 2 moteurs ont une `aggregated_taxonomy`.
78
 
79
  Sous-sections :
 
85
 
86
  #### Vue « Diagnostic approfondi » (`build_diagnostics_view_html`)
87
 
88
+ Module : [`picarones/reports_v2/html/views/diagnostics.py`](../picarones/reports_v2/html/views/diagnostics.py).
89
  Activée si `detect_levers()` produit au moins un levier (typique sur
90
  un bench standard) ou si données opt-in fournies.
91
 
 
106
 
107
  ### Vue « Pipeline composée » (`build_pipeline_view_html`)
108
 
109
+ Module : [`picarones/reports_v2/html/views/pipeline.py`](../picarones/reports_v2/html/views/pipeline.py).
110
 
111
  Utilisée par `picarones pipeline run` (ou par tout outil qui consomme un
112
  `PipelineBenchmarkResult`). Sous-sections :
 
122
 
123
  ### Vue « Robustesse projetée » (`build_robustness_view_html`)
124
 
125
+ Module : [`picarones/reports_v2/html/views/robustness.py`](../picarones/reports_v2/html/views/robustness.py).
126
 
127
  Utilisée par le workflow `picarones robustness`. Sous-sections :
128
 
 
141
 
142
  ## Code source
143
 
144
+ - [`picarones/reports_v2/html/generator.py`](../picarones/reports_v2/html/generator.py)
145
  — orchestrateur Jinja2 qui appelle les renderers et passe leurs sorties
146
  au template.
147
+ - [`picarones/reports_v2/html/views/`](../picarones/reports_v2/html/views/) — 5 modules de
148
  composition (chantier 3).
149
+ - [`picarones/reports_v2/html/renderers/`](../picarones/reports_v2/html/renderers/) — 26 renderers
150
  atomiques.
151
+ - [`picarones/reports_v2/html/templates/view_analyses.html`](../picarones/reports_v2/html/templates/view_analyses.html)
152
  — template Jinja2 qui inclut les blocs.
153
  - [`tests/test_views.py`](../tests/test_views.py) — tests d'intégration
154
  des 5 vues du chantier 3.
BACKLOG_POST_LIVRAISON.md → docs/roadmap/backlog.md RENAMED
File without changes
docs/roadmap/rewrite-2026.md CHANGED
@@ -43,37 +43,38 @@ Le rewrite ciblé attaque ces trois problèmes ensemble.
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 2ALTO, 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 5CLI, 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`,
 
43
 
44
  ```
45
  picarones/
46
+ domain/ # Couche 1 — types purs (Artifact, PipelineSpec,
47
  # EvaluationSpec, DocumentRef, Provenance)
48
+ formats/ # Couche 2 — ALTO, PAGE, normalisation texte
49
+ alto/
50
+ pagexml/
51
+ text/
52
+ evaluation/ # Couche 3 — vues, projecteurs, métriques
53
  views/
54
  projectors/
55
  metrics/
56
  registry.py
57
+ pipeline/ # Couche 4 — exécution canonique
58
  executor.py
59
  cache.py
60
  spec.py
61
+ adapters/ # Couche 5moteurs OCR/LLM/VLM, importers, storage
 
 
 
 
62
  ocr/
63
  llm/
64
  vlm/
65
  corpus/
66
  storage/
67
+ app/ # Couche 6 — services applicatifs
68
  services/
69
  schemas/
70
+ reports_v2/ # Couche 7rendu HTML / JSON / CSV
 
 
 
71
  html/
72
  json/
73
  csv/
74
+ narrative/
75
+ interfaces/ # Couche 8 — CLI, web
76
+ json/
77
+ csv/
78
  ```
79
 
80
  Pivot mental : l'objet central n'est plus `Engine + BenchmarkResult`,
docs/security/threat-model.md ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Threat model — Picarones
2
+
3
+ > **Audience** : DSI institutionnelle (BnF, LoC, BL), auditeur
4
+ > sécurité, mainteneur. Ce document complète
5
+ > [`/SECURITY.md`](../../SECURITY.md) en formalisant le modèle de
6
+ > menace. Méthodologie : **STRIDE** (Microsoft) + adaptation
7
+ > patrimoine numérique.
8
+ >
9
+ > **Périmètre** : déploiement institutionnel — Picarones tourne sur
10
+ > une infrastructure interne (NAS, cluster Kubernetes), un workspace
11
+ > partagé entre chercheurs, des clés API cloud côté serveur.
12
+ >
13
+ > **Hors périmètre** : déploiement public HuggingFace Space (mode
14
+ > ouvert anonymisé, sans secrets), CLI mono-utilisateur en local
15
+ > (modèle de menace = celui de la machine de l'utilisateur).
16
+ >
17
+ > **Statut** : v1, 2026-05. À réviser à chaque release majeure ou
18
+ > incident sécurité.
19
+
20
+ ## Acteurs
21
+
22
+ | Acteur | Confiance | Capacités |
23
+ |--------|-----------|-----------|
24
+ | **Utilisateur authentifié** (chercheur, archiviste BnF) | Modéré | Upload corpus, lance benchmark, lit rapport, télécharge artefacts |
25
+ | **Utilisateur invité** (lecteur d'un rapport publié) | Bas | Lit un rapport HTML produit |
26
+ | **Opérateur** (DSI institutionnelle) | Élevé | Déploie, configure, accède aux logs, gère les clés API |
27
+ | **Mainteneur** (équipe Picarones) | Élevé sur le code | Push code, release, accès limité aux instances de production |
28
+ | **Attaquant externe** | Aucune | Internet public ou utilisateur malveillant |
29
+
30
+ ## Actifs à protéger
31
+
32
+ | Actif | Sensibilité | Pourquoi |
33
+ |-------|-------------|----------|
34
+ | **Corpus uploadés** | RGPD (peut contenir PII : registres d'état civil) | Article 4 RGPD — données personnelles si nominatives |
35
+ | **Vérités terrain (GT)** | Propriété intellectuelle de l'institution | Investissement humain coûteux ; secret de fait |
36
+ | **Clés API cloud** (`OPENAI_API_KEY`, etc.) | Secret crédential | Compromission = facturation arbitraire + exfiltration de données |
37
+ | **Résultats de benchmark** | Faible (résultats agrégés) | Sauf si attribués nominativement à un transcripteur |
38
+ | **Logs applicatifs** | Modéré (PII collatéral, métadonnées corpus) | Audit trail = preuve juridique mais aussi cible |
39
+ | **Code source** | Public (OSS) | Intégrité supply-chain (signed releases, SBOM, SLSA) |
40
+ | **Base SQLite des jobs** | Modéré (historique des runs, paramètres) | Permet de reconstituer l'activité d'un utilisateur |
41
+
42
+ ## Surfaces d'attaque
43
+
44
+ ```
45
+ ┌──────────────────────────────────────────────────────────┐
46
+ │ Internet / Intranet │
47
+ └─────────────────────┬────────────────────────────────────┘
48
+
49
+
50
+ ┌───────────────────────────────────────┐
51
+ │ FastAPI (interfaces/web) │ ← S1 (HTTP), S2 (auth)
52
+ │ - SecurityHeadersMiddleware │
53
+ │ - BodySizeLimitMiddleware │
54
+ │ - RateLimitMiddleware │
55
+ │ - AuthenticationMiddleware (opt-in) │
56
+ └────────────────────┬──────────────────┘
57
+
58
+
59
+ ┌───────────────────────────────────────┐
60
+ │ RunOrchestrator + JobRunner │ ← S3 (job exec)
61
+ │ - WorkspaceManager (sandbox) │
62
+ │ - ZIP extraction (zip-slip safe) │
63
+ └────────────────────┬──────────────────┘
64
+
65
+ ┌───────────────┼─────────────────┐
66
+ ▼ ▼ ▼
67
+ ┌──────────┐ ┌───────────┐ ┌─────────────┐
68
+ │ Adapters │ │ Adapters │ │ Storage │ ← S4 (cloud)
69
+ │ OCR cloud│ │ LLM cloud │ │ filesystem │ ← S5 (FS)
70
+ │ (HTTPS) │ │ (HTTPS) │ │ + SQLite │ ← S6 (DB)
71
+ └──────────┘ └───────────┘ └─────────────┘
72
+ ```
73
+
74
+ ## Menaces — analyse STRIDE
75
+
76
+ ### S — Spoofing (usurpation d'identité)
77
+
78
+ | ID | Menace | Mitigation |
79
+ |----|--------|------------|
80
+ | S1 | Un attaquant se fait passer pour un utilisateur authentifié | `AuthenticationMiddleware` opt-in avec `AuthenticationBackend` Protocol — l'institution branche son SSO/LDAP/JWT. Les endpoints `/health` et `/version` restent publics pour les sondes. |
81
+ | S2 | Un client forge `X-Forwarded-For` pour spoofer son IP dans le rate limit | `RateLimitMiddleware.trust_proxy_count: int` (défaut 0 = XFF ignoré). Lecture du Nème IP en partant de la fin de la chaîne XFF. Test `tests/interfaces/web/test_rate_limit_xff.py` (7 cas). |
82
+ | S3 | Un attaquant publie un faux package `picarones` sur PyPI | Le projet n'est pas encore sur PyPI public. À la publication : signer les wheels avec Sigstore et publier le SLSA provenance level 3 (cf. backlog). |
83
+
84
+ ### T — Tampering (altération)
85
+
86
+ | ID | Menace | Mitigation |
87
+ |----|--------|------------|
88
+ | T1 | Un utilisateur uploade un ZIP avec des chemins zip-slip pour écrire hors workspace | `WorkspaceManager` sandboxe par session, extraction ZIP filtre les chemins absolus et `..`. |
89
+ | T2 | Un caller construit `DocumentRef(id="../../etc/passwd")` programmatiquement | `_DOC_ID_RE` regex `^[A-Za-z0-9_.\-/]+$` + validateur Pydantic explicite qui rejette tout segment `..` (S59 #M3). |
90
+ | T3 | Un attaquant altère le schéma SQLite `jobs.db` entre deux démarrages | `JobStore.SCHEMA_VERSION` + dispatcher `_MIGRATIONS` qui rejette dur les schémas downgrade. Pas de mitigation contre une altération en place — c'est au filesystem. |
91
+ | T4 | Un cache d'artefact corrompu ferait diverger un run | `ArtifactKey.hash_hex()` multi-paramètres (inputs hash + step + code_version + params + projection_spec) — un cache pollué est rejeté à la lecture parce que la clé ne match plus. |
92
+ | T5 | Une fonte / modèle local est remplacé par un fichier malveillant | Picarones ne charge aucun modèle automatiquement. Les modèles Tesseract et Pero sont pointés explicitement par l'utilisateur ; à charge à lui de vérifier les hashes. |
93
+
94
+ ### R — Repudiation (non-répudiation)
95
+
96
+ | ID | Menace | Mitigation |
97
+ |----|--------|------------|
98
+ | R1 | Un utilisateur lance un job coûteux puis nie l'avoir fait | `[audit]` log INFO sur `POST /api/jobs` et `DELETE /api/jobs/{id}` avec IP source (S59 #M2). Logs structurés à conserver côté ops selon la politique RGPD. |
99
+ | R2 | Un attaquant modifie un rapport persisté pour falsifier les chiffres | Le `RunManifest` est byte-déterministe (`model_dump_json` Pydantic ordered). Le hash SHA-256 du manifest peut être cité dans une publication pour ancrer la version. Signature cryptographique : non implémentée, à arbitrer (cf. backlog). |
100
+ | R3 | Un mainteneur publie une release sans laisser de trace | GitHub Actions `release.yml` enregistre l'identité GitHub du déclencheur ; SLSA provenance (à venir) attestera la chaîne build → wheel. |
101
+
102
+ ### I — Information disclosure
103
+
104
+ | ID | Menace | Mitigation |
105
+ |----|--------|------------|
106
+ | I1 | Une clé API cloud (`OPENAI_API_KEY`, etc.) fuit dans un log applicatif | Les adapters ne logent jamais la clé — vérifié par revue de code. Les exceptions cloud sont catchées et le message reformulé sans inclure de header. À durcir : un test `bandit` dans la CI sur les patterns `api_key` en variable de log. |
107
+ | I2 | Un rapport HTML embarque un CSP permissif et leak via XSS | `CSP: default-src 'self'`, pas de `unsafe-inline`, vérifié par `tests/interfaces/web/test_sprint_a14_s49_security.py`. Le moteur narratif rend les chiffres via templates YAML (pas de injection HTML). |
108
+ | I3 | Le workspace partagé fait fuiter le corpus d'un chercheur à un autre | `WorkspaceManager` sandboxe par `session_id` ; aucun caller ne peut sortir de son workspace via `resolve_output_path`. |
109
+ | I4 | Un endpoint `GET /api/jobs/{job_id}` divulgue les paramètres d'un autre utilisateur | Pas d'isolation multi-tenants à ce jour — défaut documenté. Le déploiement institutionnel doit ajouter une couche d'autorisation par utilisateur (cf. `AuthenticationMiddleware`). |
110
+ | I5 | Un attaquant lit `dependencies_lock` du `RunManifest` pour cibler une CVE | Acceptable — `dependencies_lock` est public par design (reproductibilité). La défense est de patcher rapidement les CVE via `pip-audit` en CI. |
111
+
112
+ ### D — Denial of Service
113
+
114
+ | ID | Menace | Mitigation |
115
+ |----|--------|------------|
116
+ | D1 | Upload ZIP géant qui sature le disque | `BodySizeLimitMiddleware` (défaut 100 MiB). **Limite connue** : ne couvre pas `Transfer-Encoding: chunked` — recommandation = nginx `client_max_body_size` en amont (cf. [`operations/runbook.md`](../operations/runbook.md)). |
117
+ | D2 | Flood de requêtes saturant le rate limit en mémoire | `RateLimitMiddleware` avec eviction LRU `max_clients=10000` (S58). Pas atomique sous très haute concurrence — best-effort assumé. |
118
+ | D3 | Job qui hang sur appel cloud (timeout réseau) | `pytest-timeout 5 min` par test ; `urllib.request.urlopen(timeout=)` configurable par adapter ; `call_with_retry` partagé (3 retries 2/4/8s) qui FAIL fast si non-retryable. |
119
+ | D4 | DAG cyclique ou infini dans une `PipelineSpec` | Validation statique avec détection de cycle dans `pipeline/validation.py` ; rejet `PipelineSpecError` au load. |
120
+ | D5 | XML billion-laughs / XXE sur upload ALTO/PAGE | `defusedxml` exclusif dans `formats/alto/parser.py` et `formats/pagexml/parser.py`. |
121
+
122
+ ### E — Elevation of privilege
123
+
124
+ | ID | Menace | Mitigation |
125
+ |----|--------|------------|
126
+ | E1 | Un module contribué tiers s'exécute avec des privilèges qu'il ne devrait pas | `BaseModule` interface stricte ; `module_policy.audit_module` valide qu'un module externe ne dérive que de `BaseModule` et déclare ses `input_types`/`output_types` proprement. Pas de sandboxing process — un module malicieux peut faire `os.system`. |
127
+ | E2 | Un utilisateur web arrive à exécuter du code arbitraire via l'API | `RunSpec` est validé par Pydantic ; `adapter_class` est un dotted-path résolu via `importlib.import_module` mais filtré contre une liste explicite via `RegistryService.bootstrap_defaults()`. Une release institutionnelle doit verrouiller cette liste. |
128
+
129
+ ## Risques résiduels acceptés
130
+
131
+ | ID | Risque | Pourquoi accepté |
132
+ |----|--------|------------------|
133
+ | RR1 | Le rate limit n'est pas atomique sous très haute concurrence | Best-effort suffit pour usage institutionnel ; un Redis-backed rate limiter est l'évolution si besoin |
134
+ | RR2 | Un module Python contribué peut faire des `os.system` arbitraires | Le modèle de confiance est *« le mainteneur a revu le code »* — pas de sandbox process. Pour un usage institutionnel multi-tenant, déployer dans un conteneur isolé par tenant. |
135
+ | RR3 | Les clés API cloud sont en variables d'environnement, pas en HSM | Standard de l'industrie ; un Vault-backed secret store est l'évolution si la DSI l'exige. |
136
+ | RR4 | Pas d'isolation multi-tenants par user dans le workspace web | Documentée explicitement ; déploiement multi-tenants doit ajouter sa propre couche d'autorisation. |
137
+
138
+ ## Procédure de signalement
139
+
140
+ Voir [`/SECURITY.md`](../../SECURITY.md) pour le canal de
141
+ divulgation responsable. La version anglaise est dans
142
+ [`/SECURITY.en.md`](../../SECURITY.en.md).
143
+
144
+ ## Révisions
145
+
146
+ | Version | Date | Changements |
147
+ |---------|------|-------------|
148
+ | 1.0 | 2026-05 | Création initiale (S60), méthodologie STRIDE |
docs/{user → tutorials}/reading-a-report.en.md RENAMED
@@ -1,5 +1,5 @@
1
  <!-- translation: machine + human review pending -->
2
- <!-- canonical: docs/user/reading-a-report.md (FR) -->
3
 
4
  # Reading a Picarones report
5
 
@@ -98,6 +98,6 @@ relative path and loaded by the browser on-demand
98
  ## Further reading
99
 
100
  - [Glossary] (embedded in report, accessible via `?` icons)
101
- - [docs/developer/narrative-engine.en.md](../developer/narrative-engine.en.md) — adding a detector
102
  - [docs/developer/extending-glossary.en.md](../developer/extending-glossary.en.md) — enriching the glossary
103
  - [SPECS.md](../../SPECS.md) — full project specifications
 
1
  <!-- translation: machine + human review pending -->
2
+ <!-- canonical: docs/tutorials/reading-a-report.md (FR) -->
3
 
4
  # Reading a Picarones report
5
 
 
98
  ## Further reading
99
 
100
  - [Glossary] (embedded in report, accessible via `?` icons)
101
+ - [docs/explanation/narrative-engine.en.md](../developer/narrative-engine.en.md) — adding a detector
102
  - [docs/developer/extending-glossary.en.md](../developer/extending-glossary.en.md) — enriching the glossary
103
  - [SPECS.md](../../SPECS.md) — full project specifications
docs/{user → tutorials}/reading-a-report.md RENAMED
@@ -24,7 +24,7 @@ Visible dès l'ouverture, sans navigation. Contient :
24
  1. **Synthèse factuelle** — 3 à 5 phrases générées mécaniquement à
25
  partir des résultats. Aucun LLM dans la chaîne, donc le texte est
26
  reproductible bit-à-bit. Chaque nombre cité est traçable au JSON
27
- de résultats. Voir [docs/developer/narrative-engine.md] pour la liste
28
  complète des faits que le moteur peut détecter.
29
  2. **Critical Difference Diagram** (Friedman-Nemenyi) — un graphique
30
  horizontal qui place chaque moteur sur un axe de rang moyen. Les
@@ -133,7 +133,7 @@ LibreOffice.
133
  ## Pour aller plus loin
134
 
135
  - [Glossaire complet] (intégré dans le rapport, accessible via les `?`)
136
- - [docs/developer/narrative-engine.md] — comment ajouter un détecteur
137
  - [docs/developer/extending-glossary.md] — comment enrichir le glossaire
138
  - [SPECS.md] — spécifications complètes du projet
139
 
 
24
  1. **Synthèse factuelle** — 3 à 5 phrases générées mécaniquement à
25
  partir des résultats. Aucun LLM dans la chaîne, donc le texte est
26
  reproductible bit-à-bit. Chaque nombre cité est traçable au JSON
27
+ de résultats. Voir [docs/explanation/narrative-engine.md] pour la liste
28
  complète des faits que le moteur peut détecter.
29
  2. **Critical Difference Diagram** (Friedman-Nemenyi) — un graphique
30
  horizontal qui place chaque moteur sur un axe de rang moyen. Les
 
133
  ## Pour aller plus loin
134
 
135
  - [Glossaire complet] (intégré dans le rapport, accessible via les `?`)
136
+ - [docs/explanation/narrative-engine.md] — comment ajouter un détecteur
137
  - [docs/developer/extending-glossary.md] — comment enrichir le glossaire
138
  - [SPECS.md] — spécifications complètes du projet
139
 
docs/{user → tutorials}/writing-a-pipeline-module.md RENAMED
@@ -17,8 +17,9 @@
17
  ## TL;DR
18
 
19
  ```python
20
- from picarones.core.modules import BaseModule, ArtifactType
21
- from picarones.core.pipeline import (
 
22
  PipelineRunner, PipelineSpec, PipelineStep,
23
  )
24
 
@@ -150,7 +151,7 @@ class NERExtractor(BaseModule):
150
  ### 3.a Mono-document (Sprint 63)
151
 
152
  ```python
153
- from picarones.core.pipeline import (
154
  PipelineRunner, PipelineSpec, PipelineStep,
155
  )
156
 
@@ -178,7 +179,7 @@ que `Document.ground_truths` porte une `TextGT` (ou `AltoGT`,
178
  ### 3.b Corpus complet (Sprint 64)
179
 
180
  ```python
181
- from picarones.measurements.pipeline_benchmark import run_pipeline_benchmark
182
 
183
  bench = run_pipeline_benchmark(spec, my_corpus)
184
  print(bench.n_pipelines_succeeded, "/", bench.n_docs)
@@ -203,7 +204,7 @@ bench = run_pipeline_benchmark(spec, corpus, initial_inputs_factory=my_factory)
203
  ### 3.c Comparer N pipelines (Sprint 65)
204
 
205
  ```python
206
- from picarones.measurements.pipeline_comparison import compare_pipelines
207
 
208
  comparison = compare_pipelines(
209
  [spec_baseline, spec_with_correcteur_a, spec_with_correcteur_b],
@@ -259,7 +260,7 @@ Sans `inputs_from`, `correct_b` aurait reçu la sortie de
259
 
260
  ```python
261
  from pathlib import Path
262
- from picarones.report.pipeline_render import build_pipeline_report_html
263
 
264
  bench = run_pipeline_benchmark(spec, corpus)
265
  Path("rapport_pipeline.html").write_text(
@@ -270,8 +271,8 @@ Path("rapport_pipeline.html").write_text(
270
  ### 4.b Comparaison de N pipelines (Sprint 68)
271
 
272
  ```python
273
- from picarones.core.modules import ArtifactType
274
- from picarones.report.pipeline_render import (
275
  RankingSpec, build_pipeline_comparison_report_html,
276
  )
277
 
 
17
  ## TL;DR
18
 
19
  ```python
20
+ from picarones.domain.artifacts import ArtifactType
21
+ from picarones.domain.module_protocol import BaseModule
22
+ from picarones.evaluation.pipeline import (
23
  PipelineRunner, PipelineSpec, PipelineStep,
24
  )
25
 
 
151
  ### 3.a Mono-document (Sprint 63)
152
 
153
  ```python
154
+ from picarones.evaluation.pipeline import (
155
  PipelineRunner, PipelineSpec, PipelineStep,
156
  )
157
 
 
179
  ### 3.b Corpus complet (Sprint 64)
180
 
181
  ```python
182
+ from picarones.evaluation.pipeline_benchmark import run_pipeline_benchmark
183
 
184
  bench = run_pipeline_benchmark(spec, my_corpus)
185
  print(bench.n_pipelines_succeeded, "/", bench.n_docs)
 
204
  ### 3.c Comparer N pipelines (Sprint 65)
205
 
206
  ```python
207
+ from picarones.evaluation.pipeline_comparison import compare_pipelines
208
 
209
  comparison = compare_pipelines(
210
  [spec_baseline, spec_with_correcteur_a, spec_with_correcteur_b],
 
260
 
261
  ```python
262
  from pathlib import Path
263
+ from picarones.reports_v2.html.renderers.pipeline import build_pipeline_report_html
264
 
265
  bench = run_pipeline_benchmark(spec, corpus)
266
  Path("rapport_pipeline.html").write_text(
 
271
  ### 4.b Comparaison de N pipelines (Sprint 68)
272
 
273
  ```python
274
+ from picarones.domain.artifacts import ArtifactType
275
+ from picarones.reports_v2.html.renderers.pipeline import (
276
  RankingSpec, build_pipeline_comparison_report_html,
277
  )
278