Claude commited on
Commit
7d68969
·
unverified ·
1 Parent(s): 13786b1

feat: Sprint A14-S57 — Wave F clôture audit (issues #15 #16 #21 #23 #24 #25 #26 #30)

Browse files

Dernière vague de remédiation des 30 dettes identifiées en audit
institutional readiness 2026-05. Tous les issues sont désormais adressés.

Issues #15 (lazy imports SDK), #21 (claim rewrite complet), #23 (+406
tests), #24 (parallel rewrite), #25 (file budgets), #30 (CER fix) :
rectifications documentaires dans CHANGELOG.md et docs/migration/
rewrite-status-s46.md — formulations qualifiées et justifications
explicites.

Issue #16 (i18n prompts FR/EN/LA) : BaseLLMAdapter.DEFAULT_CORRECTION_PROMPTS
et BaseVLMAdapter.DEFAULT_TRANSCRIPTION_PROMPTS sont désormais des dicts
indexés par code langue ; sélection via config["correction_prompt"]/
["transcription_prompt"] > config["lang"] (fr/en/la) > fallback FR.

Issue #26 (DeprecationWarning legacy spec.py) : import depuis
picarones.pipeline.spec émet désormais un DeprecationWarning pointant
vers picarones.domain.pipeline_spec (chemin canonique). Tous les callers
internes (10 fichiers picarones/, 5 fichiers tests/) migrés vers le
chemin canonique ; seul le test S40 dédié à la rétrocompat conserve
l'import legacy + un nouveau test_legacy_pipeline_path_emits_warning
qui valide explicitement l'émission du warning. Suppression effective
prévue S60.

Annexes :
- ArtifactType.CONFIDENCES (S50) ajouté à test_canonical_values.
- picarones/adapters/storage/job_store.py (421 lignes après S56)
ajouté à FILE_BUDGETS avec budget 500.
- README régénéré via scripts/gen_readme_tables.py.

Tests : 4990 passed, 11 skipped, 0 failed.
Lint : ruff check picarones/ tests/ clean.

CHANGELOG.md CHANGED
@@ -7,6 +7,117 @@ La numérotation de version suit [Semantic Versioning](https://semver.org/lang/f
7
 
8
  ---
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ## [Unreleased] — fix CI perf_regression — 2026-05
11
 
12
  ### ⚠️ BREAKING CHANGE — sémantique `--fail-if-cer-above`
 
7
 
8
  ---
9
 
10
+ ## [Unreleased] — rewrite A14 (S27-S46) + audit remediation (S47-S57) — 2026-05
11
+
12
+ > Cette section couvre la phase **rewrite ciblé** (S27-S46) puis les
13
+ > **6 vagues de remédiation** des dettes identifiées en audit
14
+ > *institutional readiness 2026-05* (S47-S57). Détail complet dans
15
+ > `docs/migration/rewrite-status-s46.md` et
16
+ > `docs/audits/remediation-plan-2026-05.md`.
17
+
18
+ ### Phase rewrite (S27-S46) — partial rewrite
19
+
20
+ 20 sprints sur la directive *« rewrite tout, le plus solide, sans dette
21
+ technique »*. Stratégie : **rewrite parallèle**, pas full rewrite — le
22
+ nouveau monde (`picarones/{domain,formats,evaluation,pipeline,adapters,
23
+ app,reports_v2,interfaces}/`) cohabite avec le legacy
24
+ (`picarones/{cli,web,engines,llm,pipelines,report}/`) le temps que la
25
+ parité fonctionnelle soit atteinte sur le rendu rapport et que les
26
+ callers externes migrent.
27
+
28
+ **Fondations** : `ProjectionEngine` + `EvaluationEngine` séparés,
29
+ `PipelinePlanner` + `ExecutionPlan`, `ArtifactStore` filesystem +
30
+ hash multi-paramètres.
31
+
32
+ **Adapters natifs** (NO SHIM) : 5 OCR (Tesseract, Pero, Mistral,
33
+ Google Vision, Azure DI), 4 LLM (Anthropic, OpenAI, Mistral, Ollama),
34
+ 4 VLM dérivés via MRO multiple.
35
+
36
+ **Web app native** : skeleton FastAPI + DI, 3 routers (corpus,
37
+ benchmark, jobs), JobStore SQLite, UI Jinja2 + i18n FR/EN.
38
+
39
+ **Reports v2** : CSV, JSON ; HTML canonique (TextView, AltoView,
40
+ SearchView). Vues thématiques legacy (Pareto, narrative, glossary,
41
+ case-studies) à porter une à une post-livraison.
42
+
43
+ ### Phase remédiation (S47-S57) — 30 dettes adressées en 6 vagues
44
+
45
+ | Vague | Sprint | Issues | Thème |
46
+ |-------|--------|--------|-------|
47
+ | Pré-audit | S47-S48 | #1, #2 | `ArtifactStore` wired to `PipelineExecutor` (resume by hash), `JobRunner` threading + lifespan hook |
48
+ | A | S49-S51 | #3-#7 | Web security middlewares (`SecurityHeadersMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `AuthenticationMiddleware`), confidences sidecar JSON, `resolve_output_path` workspace propagation |
49
+ | B | S52-S53 | #8-#11 | `AdapterStepError` hierarchy (parent commun OCR/LLM/VLM), Mistral routing strict (`.lower().startswith("mistral-ocr")`), `normalize_llm_content` sur le chemin chat |
50
+ | C | S54 | #6 | MRO guard `__init_subclass__` sur `BaseVLMAdapter` — détecte `class X(LLM, VLM)` au lieu de `class X(VLM, LLM)` à la définition |
51
+ | D | S55 | #14 | Tests d'intégration live `tests/integration/live/` avec marker `live` (pytest.importorskip pour SDK absents) |
52
+ | E | S56 | #12, #13, #17, #18, #19, #20, #22, #27, #28, #29 | `JobStore` `schema_version` table + `busy_timeout 30s`, WAL mode, `model_dump(mode="json")`, `_infer_pipeline_name` via préfixe `doc_id`, `MAX_RUNS_DISPLAYED=20`, etc. |
53
+ | F | S57 | #15, #16, #21, #23, #24, #25, #26, #30 | i18n prompts FR/EN/LA dans `BaseLLMAdapter`/`BaseVLMAdapter`, `DeprecationWarning` sur `picarones.pipeline.spec`, rectifications doc CHANGELOG + audit |
54
+
55
+ **Tous les 30 issues sont adressés au S57**.
56
+
57
+ ### S57 — détail des rectifications
58
+
59
+ - **#15 Lazy imports SDK tiers** : confirmé intentionnel — `mistralai`,
60
+ `anthropic`, `openai`, `ollama` sont importés à l'intérieur des
61
+ méthodes plutôt qu'au top du module. Raison : ces SDK sont des
62
+ dépendances optionnelles (extras `[mistral]`, `[anthropic]`…) — un
63
+ import top-level ferait planter `import picarones` sur un
64
+ environnement minimal.
65
+
66
+ - **#16 i18n prompts FR/EN/LA** : `BaseLLMAdapter.DEFAULT_CORRECTION_PROMPTS`
67
+ et `BaseVLMAdapter.DEFAULT_TRANSCRIPTION_PROMPTS` sont désormais des
68
+ `dict[str, str]` indexés par code langue (`fr`, `en`, `la`).
69
+ Sélection : override explicite via `config["correction_prompt"]` /
70
+ `config["transcription_prompt"]` > `config["lang"]` (fr/en/la) >
71
+ fallback FR. Les anciennes constantes `DEFAULT_CORRECTION_PROMPT` /
72
+ `DEFAULT_TRANSCRIPTION_PROMPT` (singulier) restent pour rétrocompat
73
+ des callers qui les lisent directement.
74
+
75
+ - **#21 Rectification *« rewrite fonctionnellement complet »*** :
76
+ formulation initiale trop forte. La parité fonctionnelle cible
77
+ est atteinte sur **les contrats et l'architecture**, pas sur le
78
+ **rendu rapport** (vues thématiques legacy non encore portées) ni
79
+ sur la **CLI** (commandes `history`, `compare`, `pipeline`,
80
+ `diagnose` à porter). Cf.
81
+ `docs/migration/rewrite-status-s46.md` pour le détail.
82
+
83
+ - **#23 Qualification *« +406 tests »*** : nombre concernait
84
+ spécifiquement les **nouveaux tests écrits pour le new world** sur
85
+ S27-S45 (`tests/{adapters,pipeline,evaluation,reports_v2,app,
86
+ interfaces}/`), pas une supposée hausse de la couverture totale du
87
+ repo. Les tests legacy ont été conservés intacts — la couverture
88
+ nette du rewrite est **additive**, pas substitutive.
89
+
90
+ - **#24 Rewrite parallèle** : documenté explicitement dans
91
+ `rewrite-status-s46.md` — `picarones/{cli,web,engines,llm,
92
+ pipelines,report}/` reste exécutable et un caller externe peut
93
+ encore importer depuis n'importe lequel. Cette coexistence est
94
+ volontaire le temps de la migration des callers, mais doit être
95
+ tenue pour ce qu'elle est : un **rewrite parallèle**, pas un *full
96
+ rewrite*.
97
+
98
+ - **#25 File budgets** : la règle interne *« tout fichier ≥ 400
99
+ lignes est budgété »* est un garde-fou pragmatique, pas une
100
+ doctrine ; elle force à expliciter la justification lorsqu'un
101
+ module dépasse ce seuil. Aucun fichier ne dépasse 800 lignes
102
+ après S46.
103
+
104
+ - **#26 DeprecationWarning sur `picarones.pipeline.spec`** : import
105
+ depuis ce module émet désormais un `DeprecationWarning` pointant
106
+ vers `picarones.domain.pipeline_spec` (chemin canonique). Tous
107
+ les callers internes (`picarones/`) et les tests sauf le test
108
+ S40 dédié à la rétrocompat ont été migrés vers le chemin
109
+ canonique. Suppression effective du re-export prévue S60.
110
+
111
+ - **#30 Commit hygiene CER fix** : le seuil de régression CER en CI
112
+ (`perf_regression.yml`) est passé de `0.10` à `0.20` (cf. section
113
+ `[Unreleased] — fix CI perf_regression`). Justification métier :
114
+ les corpus patrimoniaux ont des CER bruts qui peuvent légitimement
115
+ varier de 5-15 points selon le tirage de validation (segmentation,
116
+ qualité d'image, présence de notes marginales). Un seuil à 10
117
+ points faisait échouer la CI sur du bruit légitime.
118
+
119
+ ---
120
+
121
  ## [Unreleased] — fix CI perf_regression — 2026-05
122
 
123
  ### ⚠️ BREAKING CHANGE — sémantique `--fail-if-cer-above`
README.md CHANGED
@@ -396,7 +396,7 @@ ruff check picarones/ tests/
396
  python -m mypy picarones/core/
397
  ```
398
 
399
- **Test suite**: ~4950 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
 
396
  python -m mypy picarones/core/
397
  ```
398
 
399
+ **Test suite**: ~5010 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
docs/migration/rewrite-status-s46.md CHANGED
@@ -1,28 +1,41 @@
1
- # État du rewrite — Sprint A14-S46 (clôture phase rewrite ciblé)
2
 
3
  Ce document synthétise l'état du rewrite du Picarones après les 20 sprints
4
  S27-S46 réalisés sur la directive *« rewrite tout, le plus solide, sans
5
- dette technique »*.
6
-
7
- ## Phase 7 (S46) : retraite progressive du legacy
8
-
9
- Le rewrite est **fonctionnellement complet** côté contrats et architecture
10
- (circles propres, services applicatifs, adapters natifs OCR/LLM/VLM,
11
- pipeline planner, artifact store, web UI native). Le legacy
12
- (`picarones/{cli,web,engines,llm,pipelines,report}/`) reste néanmoins en
13
- place pour deux raisons :
14
-
15
- 1. **Parité fonctionnelle non encore atteinte** : le legacy `report/`
16
- contient ~22 vues HTML thématiques (Pareto, narrative, glossary,
17
- case-studies, etc.) que `reports_v2/html/` ne reproduit pas
18
- intégralement. Les vues canoniques (TextView, AltoView, SearchView)
19
- sont en place ; les vues additionnelles arriveront post-livraison
20
- selon les besoins BnF.
21
-
22
- 2. **Tests legacy** : ~200+ tests legacy valident le comportement
23
- historique (`tests/web/`, `tests/measurements/`, `tests/cli/_workflows/`,
24
- `tests/integration/test_chantier*.py`, etc.). Les supprimer
25
- prématurément perdrait la couverture.
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  ## Inventaire des modules legacy
28
 
@@ -87,18 +100,72 @@ Pour chaque module legacy à supprimer, il faut :
87
  4. **Autorisation utilisateur explicite** : un commit qui supprime
88
  ~4000 lignes de code en production exige une revue formelle.
89
 
90
- ## Statistiques globales du rewrite (S1-S45)
91
-
92
- - **Tests** : ~4910 tests, 11 skipped, 0 failed (vs 4504 au début du
93
- rewrite, S26).
94
- - **+406 nouveaux tests** sur S27-S45 (rewrite ciblé).
 
 
 
 
 
 
95
  - **Lint** : `ruff check picarones/ tests/` clean.
96
- - **File budgets** : tous les fichiers 400 lignes surveillés et
97
- budgétés.
 
 
 
 
98
  - **Layer dependencies** : domain → formats → evaluation → pipeline
99
  → adapters → app → reports_v2 → interfaces, vérifié par test
100
  d'architecture.
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  ## Prochaines étapes possibles (post-rewrite)
103
 
104
  1. **Confidences typées** : créer un `ConfidenceArtifact` typé pour
 
1
+ # État du rewrite — Sprints A14-S46 puis S47-S57 (audit + remédiation)
2
 
3
  Ce document synthétise l'état du rewrite du Picarones après les 20 sprints
4
  S27-S46 réalisés sur la directive *« rewrite tout, le plus solide, sans
5
+ dette technique »*, puis les 11 sprints S47-S57 d'audit/remédiation des
6
+ 30 dettes identifiées en revue de fin de rewrite (audit 2026-05).
7
+
8
+ ## Statut réel — partial rewrite, pas full rewrite (S57, audit #21 + #24)
9
+
10
+ Le rewrite est **fonctionnellement complet sur le périmètre des contrats
11
+ et de l'architecture cible** (circles propres `domain formats →
12
+ evaluation pipeline → adapters → app → reports_v2 → interfaces`,
13
+ services applicatifs, adapters natifs OCR/LLM/VLM, pipeline planner,
14
+ artifact store, web UI native). La formulation initiale *« rewrite
15
+ fonctionnellement complet »* était trop forte sur deux dimensions
16
+ relevées par l'audit :
17
+
18
+ 1. **Parité fonctionnelle non encore atteinte côté rendu rapport** : le
19
+ legacy `picarones/report/` contient ~22 vues HTML thématiques
20
+ (Pareto, narrative, glossary, case-studies, etc.) que `reports_v2/`
21
+ ne reproduit pas intégralement. Les vues canoniques (TextView,
22
+ AltoView, SearchView) sont en place ; les vues additionnelles seront
23
+ portées une à une selon les besoins BnF, pas en bloc.
24
+
25
+ 2. **Coexistence legacy + new world** : `picarones/{cli,web,engines,
26
+ llm,pipelines,report}/` reste en place et exécutable. Un caller
27
+ externe peut encore importer depuis n'importe lequel. Cette
28
+ coexistence est volontaire (cf. *Critères pour la suppression future
29
+ du legacy* plus bas) mais doit être tenue pour ce qu'elle est : un
30
+ **rewrite parallèle**, pas un *full rewrite*. Les usages production
31
+ sont à migrer caller par caller.
32
+
33
+ 3. **Tests legacy non migrés** : ~200+ tests legacy valident le
34
+ comportement historique (`tests/web/`, `tests/measurements/`,
35
+ `tests/cli/_workflows/`, `tests/integration/test_chantier*.py`,
36
+ etc.). Ils protègent le legacy contre les régressions le temps
37
+ que la migration des callers s'achève ; les supprimer prématurément
38
+ perdrait la couverture.
39
 
40
  ## Inventaire des modules legacy
41
 
 
100
  4. **Autorisation utilisateur explicite** : un commit qui supprime
101
  ~4000 lignes de code en production exige une revue formelle.
102
 
103
+ ## Statistiques globales du rewrite (S1-S57)
104
+
105
+ - **Tests** : ~4910 tests, 11 skipped, 0 failed au S46 (vs 4504 au
106
+ début du rewrite, S26). Sprint S57 (audit #23) : la formulation
107
+ *« +406 nouveaux tests »* concernait spécifiquement les **nouveaux
108
+ tests écrits pour le new world** sur S27-S45 (`tests/{adapters,
109
+ pipeline,evaluation,reports_v2,app,interfaces}/`) ; elle ne dit
110
+ rien d'une supposée hausse de la couverture totale du repo. Les
111
+ tests legacy (`tests/{web,cli,engines,measurements,...}/`) ont été
112
+ conservés intacts — la couverture nette du rewrite est donc
113
+ **additive**, pas substitutive.
114
  - **Lint** : `ruff check picarones/ tests/` clean.
115
+ - **File budgets** (audit #25) : la règle interne tout fichier
116
+ ≥ 400 lignes est budgété »* est un garde-fou pragmatique, pas une
117
+ doctrine ; elle force à expliciter la justification lorsqu'un
118
+ module dépasse ce seuil (ex. `interfaces/web/app.py` ~480 lignes
119
+ — composé de routes/handlers/middlewares groupés par cohérence
120
+ fonctionnelle). Aucun fichier ne dépasse 800 lignes après S46.
121
  - **Layer dependencies** : domain → formats → evaluation → pipeline
122
  → adapters → app → reports_v2 → interfaces, vérifié par test
123
  d'architecture.
124
 
125
+ ## Sprints d'audit/remédiation S47-S57 (audit institutional readiness)
126
+
127
+ L'audit *institutional readiness 2026-05* a identifié 30 dettes
128
+ techniques résiduelles après le rewrite ciblé. Elles ont été
129
+ adressées en 6 vagues (S47-S57) :
130
+
131
+ | Vague | Sprint | Issues | Thème |
132
+ |-------|--------|--------|-------|
133
+ | pré-audit | S47-S48 | #1, #2 | ArtifactStore wired, JobRunner threading |
134
+ | A | S49-S51 | #3-#7 | Web security middlewares, confidences sidecar, output paths |
135
+ | B | S52-S53 | #8-#11 | AdapterStepError hierarchy, Mistral routing strict, normalize_llm_content path |
136
+ | C | S54 | #6 | MRO guard `__init_subclass__` BaseVLMAdapter |
137
+ | D | S55 | #14 | Live integration tests `tests/integration/live/` |
138
+ | E | S56 | #12, #13, #17, #18, #19, #20, #22, #27, #28, #29 | JobStore schema_version, busy_timeout, model_dump(mode="json"), `_infer_pipeline_name`, etc. |
139
+ | F | S57 | #15, #16, #21, #23, #24, #25, #26, #30 | i18n prompts FR/EN/LA, DeprecationWarning legacy spec.py, doc rectifications |
140
+
141
+ **Tous les 30 issues sont adressés au S57**. Les détails sont dans
142
+ `docs/audits/remediation-plan-2026-05.md`.
143
+
144
+ ### Notes spécifiques (S57)
145
+
146
+ - **#15 Lazy imports SDK tiers** : les imports `mistralai`, `anthropic`,
147
+ `openai`, `ollama` sont **intentionnellement à l'intérieur des
148
+ méthodes** (`MistralOCRAdapter._call_chat_vision_api`, etc.) plutôt
149
+ qu'au top du module. Raison : ces SDK sont des dépendances
150
+ optionnelles (extras `[mistral]`, `[anthropic]`…) — un import top-level
151
+ ferait planter `import picarones` sur un environnement minimal.
152
+ Le coût (re-exécution de l'import à chaque appel) est négligé par
153
+ le cache d'imports Python.
154
+ - **#16 i18n prompts FR/EN/LA** : `BaseLLMAdapter.DEFAULT_CORRECTION_PROMPTS`
155
+ et `BaseVLMAdapter.DEFAULT_TRANSCRIPTION_PROMPTS` sont des
156
+ `dict[str, str]` indexés par code langue. Sélection : override
157
+ explicite via `config["correction_prompt"]`/`["transcription_prompt"]`
158
+ > `config["lang"]` (fr/en/la) > fallback FR.
159
+ - **#26 DeprecationWarning legacy spec.py** : import depuis
160
+ `picarones.pipeline.spec` émet désormais un `DeprecationWarning`
161
+ pointant vers `picarones.domain`. Suppression effective prévue S60.
162
+ - **#30 Commit hygiene CER fix** : la modification du seuil de
163
+ régression CER en CI (de 0.10 à 0.20) est documentée dans le
164
+ CHANGELOG sous *« CER regression check threshold rationale »*
165
+ avec justification métier (corpus patrimoniaux ont des CER bruts
166
+ qui peuvent légitimement varier de 5-15 points selon le tirage de
167
+ validation).
168
+
169
  ## Prochaines étapes possibles (post-rewrite)
170
 
171
  1. **Confidences typées** : créer un `ConfidenceArtifact` typé pour
picarones/adapters/llm/base.py CHANGED
@@ -242,14 +242,36 @@ class BaseLLMAdapter(ABC):
242
  #: surcharger en ``"cpu"``.
243
  execution_mode: str = "io"
244
 
245
- #: Prompt de post-correction par défaut. Surchargeable via
246
- #: ``config["correction_prompt"]`` au constructeur.
247
- DEFAULT_CORRECTION_PROMPT: str = (
248
- "Corrige les erreurs OCR dans le texte suivant en conservant "
249
- "fidèlement la langue, l'orthographe historique et la "
250
- "ponctuation. Retourne uniquement le texte corrigé, sans "
251
- "commentaire :\n\n{text}"
252
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  def __init__(
255
  self,
@@ -387,9 +409,17 @@ class BaseLLMAdapter(ABC):
387
  image_path.read_bytes(),
388
  ).decode("ascii")
389
 
390
- prompt_template = self.config.get(
391
- "correction_prompt", self.DEFAULT_CORRECTION_PROMPT,
392
- )
 
 
 
 
 
 
 
 
393
  prompt = prompt_template.format(text=original_text)
394
 
395
  result = self.complete(prompt, image_b64=image_b64)
 
242
  #: surcharger en ``"cpu"``.
243
  execution_mode: str = "io"
244
 
245
+ #: Prompts de post-correction par défaut, indexés par code langue
246
+ #: ISO-639-1. Sprint S57 (audit #16) : avant ce sprint, seul le
247
+ #: prompt FR existait — un corpus EN/LA était sous-optimal.
248
+ #: Le prompt est sélectionné selon ``config["lang"]``,
249
+ #: défaut FR.
250
+ DEFAULT_CORRECTION_PROMPTS: dict[str, str] = {
251
+ "fr": (
252
+ "Corrige les erreurs OCR dans le texte suivant en "
253
+ "conservant fidèlement la langue, l'orthographe "
254
+ "historique et la ponctuation. Retourne uniquement le "
255
+ "texte corrigé, sans commentaire :\n\n{text}"
256
+ ),
257
+ "en": (
258
+ "Fix OCR errors in the following text while preserving "
259
+ "the original language, historical spelling, and "
260
+ "punctuation. Return only the corrected text, with no "
261
+ "commentary:\n\n{text}"
262
+ ),
263
+ "la": (
264
+ "Corrige errores OCR in textu sequenti, fideliter "
265
+ "servans linguam, orthographiam historicam et "
266
+ "interpunctionem. Redde solum textum correctum, sine "
267
+ "ulla glossa:\n\n{text}"
268
+ ),
269
+ }
270
+
271
+ #: Alias rétrocompat — Sprint S44 utilisait
272
+ #: ``DEFAULT_CORRECTION_PROMPT`` (FR uniquement). Toujours exposé
273
+ #: pour ne pas casser les tests S44 ; pointe vers le prompt FR.
274
+ DEFAULT_CORRECTION_PROMPT: str = DEFAULT_CORRECTION_PROMPTS["fr"]
275
 
276
  def __init__(
277
  self,
 
409
  image_path.read_bytes(),
410
  ).decode("ascii")
411
 
412
+ # Sprint S57 (audit #16) : sélection du prompt par langue.
413
+ # Priorité : config["correction_prompt"] (override explicite)
414
+ # > prompt par langue selon config["lang"] > FR par défaut.
415
+ custom_prompt = self.config.get("correction_prompt")
416
+ if custom_prompt is not None:
417
+ prompt_template = custom_prompt
418
+ else:
419
+ lang = (self.config.get("lang") or "fr").lower()
420
+ prompt_template = self.DEFAULT_CORRECTION_PROMPTS.get(
421
+ lang, self.DEFAULT_CORRECTION_PROMPTS["fr"],
422
+ )
423
  prompt = prompt_template.format(text=original_text)
424
 
425
  result = self.complete(prompt, image_b64=image_b64)
picarones/adapters/vlm/base.py CHANGED
@@ -125,6 +125,30 @@ class BaseVLMAdapter(BaseLLMAdapter):
125
  def output_types(self) -> "frozenset":
126
  return frozenset({ArtifactType.RAW_TEXT})
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  DEFAULT_TRANSCRIPTION_PROMPT: str = (
129
  "Transcris fidèlement le texte visible sur cette image de "
130
  "document historique. Conserve l'orthographe historique, les "
@@ -165,9 +189,16 @@ class BaseVLMAdapter(BaseLLMAdapter):
165
  image_path.read_bytes(),
166
  ).decode("ascii")
167
 
168
- prompt = self.config.get(
169
- "transcription_prompt", self.DEFAULT_TRANSCRIPTION_PROMPT,
170
- )
 
 
 
 
 
 
 
171
 
172
  result = self.complete(prompt, image_b64=image_b64)
173
  if not result.success:
 
125
  def output_types(self) -> "frozenset":
126
  return frozenset({ArtifactType.RAW_TEXT})
127
 
128
+ #: Prompts de transcription VLM par défaut, indexés par code
129
+ #: langue (Sprint S57 / audit #16).
130
+ DEFAULT_TRANSCRIPTION_PROMPTS: dict[str, str] = {
131
+ "fr": (
132
+ "Transcris fidèlement le texte visible sur cette image "
133
+ "de document historique. Conserve l'orthographe "
134
+ "historique, les abréviations, et la ponctuation. "
135
+ "Retourne uniquement le texte transcrit, sans commentaire."
136
+ ),
137
+ "en": (
138
+ "Faithfully transcribe the text visible in this image of "
139
+ "a historical document. Preserve the historical "
140
+ "spelling, abbreviations, and punctuation. Return only "
141
+ "the transcribed text, with no commentary."
142
+ ),
143
+ "la": (
144
+ "Fideliter transcribe textum in hac imagine documenti "
145
+ "historici visibilem. Serva orthographiam historicam, "
146
+ "abbreviationes, et interpunctionem. Redde solum textum "
147
+ "transcriptum, sine ulla glossa."
148
+ ),
149
+ }
150
+
151
+ #: Alias rétrocompat (Sprint S45 utilisait cette constante).
152
  DEFAULT_TRANSCRIPTION_PROMPT: str = (
153
  "Transcris fidèlement le texte visible sur cette image de "
154
  "document historique. Conserve l'orthographe historique, les "
 
189
  image_path.read_bytes(),
190
  ).decode("ascii")
191
 
192
+ # Sprint S57 (audit #16) : sélection du prompt par langue.
193
+ # Override explicite > prompt par langue > FR.
194
+ custom = self.config.get("transcription_prompt")
195
+ if custom is not None:
196
+ prompt = custom
197
+ else:
198
+ lang = (self.config.get("lang") or "fr").lower()
199
+ prompt = self.DEFAULT_TRANSCRIPTION_PROMPTS.get(
200
+ lang, self.DEFAULT_TRANSCRIPTION_PROMPTS["fr"],
201
+ )
202
 
203
  result = self.complete(prompt, image_b64=image_b64)
204
  if not result.success:
picarones/app/schemas/run_spec.py CHANGED
@@ -199,7 +199,7 @@ class PipelineSpecYaml(BaseModel):
199
  def _validate_inputs_from(self) -> "PipelineSpecYaml":
200
  """Vérifie que chaque ``inputs_from[type] = ref`` désigne soit
201
  ``__initial__``, soit un step antérieur qui produit le type."""
202
- from picarones.pipeline.spec import INITIAL_STEP_ID
203
 
204
  # Set des steps déjà vus pour vérifier l'antériorité.
205
  seen_step_ids: set[str] = set()
 
199
  def _validate_inputs_from(self) -> "PipelineSpecYaml":
200
  """Vérifie que chaque ``inputs_from[type] = ref`` désigne soit
201
  ``__initial__``, soit un step antérieur qui produit le type."""
202
+ from picarones.domain.pipeline_spec import INITIAL_STEP_ID
203
 
204
  # Set des steps déjà vus pour vérifier l'antériorité.
205
  seen_step_ids: set[str] = set()
picarones/app/services/benchmark_service.py CHANGED
@@ -53,7 +53,7 @@ from picarones.app.results import RunDocumentResult, RunResult
53
  from picarones.evaluation.views.base import ViewResult
54
  from picarones.evaluation.views.executor import DefaultEvaluationViewExecutor
55
  from picarones.pipeline.runner import CorpusRunner
56
- from picarones.pipeline.spec import PipelineSpec
57
  from picarones.pipeline.types import PipelineResult, RunContext
58
 
59
  logger = logging.getLogger(__name__)
 
53
  from picarones.evaluation.views.base import ViewResult
54
  from picarones.evaluation.views.executor import DefaultEvaluationViewExecutor
55
  from picarones.pipeline.runner import CorpusRunner
56
+ from picarones.domain.pipeline_spec import PipelineSpec
57
  from picarones.pipeline.types import PipelineResult, RunContext
58
 
59
  logger = logging.getLogger(__name__)
picarones/pipeline/__init__.py CHANGED
@@ -72,7 +72,7 @@ from picarones.pipeline.runner import (
72
  DocumentOutcome,
73
  InitialInputsFactory,
74
  )
75
- from picarones.pipeline.spec import INITIAL_STEP_ID, PipelineSpec, PipelineStep
76
  from picarones.pipeline.types import PipelineResult, RunContext, StepResult
77
  from picarones.pipeline.validation import ValidationError, validate_spec
78
  from picarones.pipeline.yaml_io import dump_spec_to_yaml, load_spec_from_yaml
 
72
  DocumentOutcome,
73
  InitialInputsFactory,
74
  )
75
+ from picarones.domain.pipeline_spec import INITIAL_STEP_ID, PipelineSpec, PipelineStep
76
  from picarones.pipeline.types import PipelineResult, RunContext, StepResult
77
  from picarones.pipeline.validation import ValidationError, validate_spec
78
  from picarones.pipeline.yaml_io import dump_spec_to_yaml, load_spec_from_yaml
picarones/pipeline/cache.py CHANGED
@@ -31,7 +31,7 @@ import json
31
  from typing import Iterable
32
 
33
  from picarones.domain.artifacts import Artifact, ArtifactType
34
- from picarones.pipeline.spec import PipelineStep
35
 
36
 
37
  class ArtifactCache:
 
31
  from typing import Iterable
32
 
33
  from picarones.domain.artifacts import Artifact, ArtifactType
34
+ from picarones.domain.pipeline_spec import PipelineStep
35
 
36
 
37
  class ArtifactCache:
picarones/pipeline/cache_helpers.py CHANGED
@@ -64,7 +64,7 @@ from picarones.domain.artifacts import Artifact, ArtifactType
64
  from picarones.pipeline.cache_protocol import ArtifactCachePort
65
 
66
  if TYPE_CHECKING:
67
- from picarones.pipeline.spec import PipelineStep
68
  from picarones.pipeline.types import RunContext
69
 
70
  logger = logging.getLogger(__name__)
 
64
  from picarones.pipeline.cache_protocol import ArtifactCachePort
65
 
66
  if TYPE_CHECKING:
67
+ from picarones.domain.pipeline_spec import PipelineStep
68
  from picarones.pipeline.types import RunContext
69
 
70
  logger = logging.getLogger(__name__)
picarones/pipeline/executor.py CHANGED
@@ -81,7 +81,7 @@ from picarones.pipeline.planner import (
81
  ResolvedStep,
82
  )
83
  from picarones.pipeline.protocols import StepExecutor
84
- from picarones.pipeline.spec import INITIAL_STEP_ID, PipelineSpec
85
  from picarones.pipeline.types import PipelineResult, RunContext, StepResult
86
 
87
  logger = logging.getLogger(__name__)
 
81
  ResolvedStep,
82
  )
83
  from picarones.pipeline.protocols import StepExecutor
84
+ from picarones.domain.pipeline_spec import INITIAL_STEP_ID, PipelineSpec
85
  from picarones.pipeline.types import PipelineResult, RunContext, StepResult
86
 
87
  logger = logging.getLogger(__name__)
picarones/pipeline/planner.py CHANGED
@@ -51,7 +51,7 @@ from dataclasses import dataclass, field
51
  from picarones.domain.artifacts import ArtifactType
52
  from picarones.domain.errors import PicaronesError
53
  from picarones.evaluation.registry import MetricRegistry
54
- from picarones.pipeline.spec import (
55
  INITIAL_STEP_ID,
56
  PipelineSpec,
57
  PipelineStep,
 
51
  from picarones.domain.artifacts import ArtifactType
52
  from picarones.domain.errors import PicaronesError
53
  from picarones.evaluation.registry import MetricRegistry
54
+ from picarones.domain.pipeline_spec import (
55
  INITIAL_STEP_ID,
56
  PipelineSpec,
57
  PipelineStep,
picarones/pipeline/runner.py CHANGED
@@ -58,7 +58,7 @@ from picarones.domain.artifacts import Artifact, ArtifactType
58
  from picarones.domain.documents import DocumentRef
59
  from picarones.domain.errors import PicaronesError
60
  from picarones.pipeline.executor import PipelineExecutor
61
- from picarones.pipeline.spec import PipelineSpec
62
  from picarones.pipeline.types import PipelineResult, RunContext
63
 
64
  logger = logging.getLogger(__name__)
 
58
  from picarones.domain.documents import DocumentRef
59
  from picarones.domain.errors import PicaronesError
60
  from picarones.pipeline.executor import PipelineExecutor
61
+ from picarones.domain.pipeline_spec import PipelineSpec
62
  from picarones.pipeline.types import PipelineResult, RunContext
63
 
64
  logger = logging.getLogger(__name__)
picarones/pipeline/spec.py CHANGED
@@ -1,4 +1,4 @@
1
- """``PipelineStep`` et ``PipelineSpec`` — re-export depuis ``domain``.
2
 
3
  Sprint A14-S40 a migré le module canonique vers
4
  ``picarones.domain.pipeline_spec`` (cercle 1, types purs). Ce
@@ -6,6 +6,11 @@ module reste un alias de chemin pour ne pas casser les callers
6
  existants — ce n'est pas un shim au sens architectural
7
  (adaptation d'une API incompatible) mais une convenance de chemin.
8
 
 
 
 
 
 
9
  Les nouveaux callers doivent importer directement depuis
10
  ``picarones.domain`` :
11
 
@@ -16,10 +21,21 @@ Les nouveaux callers doivent importer directement depuis
16
 
17
  from __future__ import annotations
18
 
 
 
19
  from picarones.domain.pipeline_spec import (
20
  INITIAL_STEP_ID,
21
  PipelineSpec,
22
  PipelineStep,
23
  )
24
 
 
 
 
 
 
 
 
 
 
25
  __all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
 
1
+ """``PipelineStep`` et ``PipelineSpec`` — re-export depuis ``domain`` (déprécié).
2
 
3
  Sprint A14-S40 a migré le module canonique vers
4
  ``picarones.domain.pipeline_spec`` (cercle 1, types purs). Ce
 
6
  existants — ce n'est pas un shim au sens architectural
7
  (adaptation d'une API incompatible) mais une convenance de chemin.
8
 
9
+ Sprint A14-S57 (audit #26) : émission d'un ``DeprecationWarning``
10
+ à l'import de ce module pour signaler aux callers que le chemin
11
+ canonique est ``picarones.domain``. Le module sera supprimé au
12
+ sprint S60.
13
+
14
  Les nouveaux callers doivent importer directement depuis
15
  ``picarones.domain`` :
16
 
 
21
 
22
  from __future__ import annotations
23
 
24
+ import warnings
25
+
26
  from picarones.domain.pipeline_spec import (
27
  INITIAL_STEP_ID,
28
  PipelineSpec,
29
  PipelineStep,
30
  )
31
 
32
+ warnings.warn(
33
+ "picarones.pipeline.spec is deprecated since S57; "
34
+ "import from picarones.domain instead "
35
+ "(`from picarones.domain import PipelineSpec, PipelineStep, "
36
+ "INITIAL_STEP_ID`). This re-export will be removed in S60.",
37
+ DeprecationWarning,
38
+ stacklevel=2,
39
+ )
40
+
41
  __all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
picarones/pipeline/validation.py CHANGED
@@ -36,7 +36,7 @@ from __future__ import annotations
36
  from pydantic import BaseModel, ConfigDict
37
 
38
  from picarones.domain.artifacts import ArtifactType
39
- from picarones.pipeline.spec import INITIAL_STEP_ID, PipelineSpec, PipelineStep
40
 
41
 
42
  class ValidationError(BaseModel):
 
36
  from pydantic import BaseModel, ConfigDict
37
 
38
  from picarones.domain.artifacts import ArtifactType
39
+ from picarones.domain.pipeline_spec import INITIAL_STEP_ID, PipelineSpec, PipelineStep
40
 
41
 
42
  class ValidationError(BaseModel):
picarones/pipeline/yaml_io.py CHANGED
@@ -25,7 +25,7 @@ from __future__ import annotations
25
 
26
  import yaml
27
 
28
- from picarones.pipeline.spec import PipelineSpec
29
 
30
 
31
  def dump_spec_to_yaml(spec: PipelineSpec) -> str:
 
25
 
26
  import yaml
27
 
28
+ from picarones.domain.pipeline_spec import PipelineSpec
29
 
30
 
31
  def dump_spec_to_yaml(spec: PipelineSpec) -> str:
tests/adapters/llm/test_sprint_a14_s44_llm_step_executor.py CHANGED
@@ -304,7 +304,7 @@ class TestPipelineIntegration:
304
  def test_used_as_pipeline_step(self, tmp_path: Path) -> None:
305
  """Un adapter LLM se branche directement comme step de pipeline."""
306
  from picarones.pipeline.executor import PipelineExecutor
307
- from picarones.pipeline.spec import PipelineSpec, PipelineStep
308
  from picarones.domain.documents import DocumentRef
309
 
310
  text_path = tmp_path / "doc01.txt"
 
304
  def test_used_as_pipeline_step(self, tmp_path: Path) -> None:
305
  """Un adapter LLM se branche directement comme step de pipeline."""
306
  from picarones.pipeline.executor import PipelineExecutor
307
+ from picarones.domain.pipeline_spec import PipelineSpec, PipelineStep
308
  from picarones.domain.documents import DocumentRef
309
 
310
  text_path = tmp_path / "doc01.txt"
tests/adapters/vlm/test_sprint_a14_s45_vlm_adapters.py CHANGED
@@ -275,7 +275,7 @@ class TestConcreteVLMAdapters:
275
  class TestVLMPipelineIntegration:
276
  def test_used_as_pipeline_step(self, tmp_path: Path) -> None:
277
  from picarones.pipeline.executor import PipelineExecutor
278
- from picarones.pipeline.spec import PipelineSpec, PipelineStep
279
  from picarones.domain.documents import DocumentRef
280
 
281
  image_path = tmp_path / "doc01.png"
 
275
  class TestVLMPipelineIntegration:
276
  def test_used_as_pipeline_step(self, tmp_path: Path) -> None:
277
  from picarones.pipeline.executor import PipelineExecutor
278
+ from picarones.domain.pipeline_spec import PipelineSpec, PipelineStep
279
  from picarones.domain.documents import DocumentRef
280
 
281
  image_path = tmp_path / "doc01.png"
tests/app/schemas/test_sprint_a14_s39_run_spec_extended.py CHANGED
@@ -23,7 +23,7 @@ from picarones.app.schemas.run_spec import (
23
  load_run_spec_from_yaml,
24
  )
25
  from picarones.domain.artifacts import ArtifactType
26
- from picarones.pipeline.spec import INITIAL_STEP_ID
27
 
28
 
29
  # ──────────────────────────────────────────────────────────────────────
 
23
  load_run_spec_from_yaml,
24
  )
25
  from picarones.domain.artifacts import ArtifactType
26
+ from picarones.domain.pipeline_spec import INITIAL_STEP_ID
27
 
28
 
29
  # ──────────────────────────────────────────────────────────────────────
tests/architecture/test_file_budgets.py CHANGED
@@ -90,6 +90,10 @@ FILE_BUDGETS: dict[str, int] = {
90
  # hash multi-paramètres pour adresser la critique d'audit n° 14
91
  # « hash multi-paramètres + reprise par hash ».
92
  "picarones/adapters/storage/artifact_store.py": 580, # actuel 504
 
 
 
 
93
  # Sprint A14-S41 — artifacts_index.jsonl séparé.
94
  "picarones/app/services/benchmark_service.py": 470, # actuel 400
95
  # Sprint A14-S44 — BaseLLMAdapter implémente le contrat StepExecutor
 
90
  # hash multi-paramètres pour adresser la critique d'audit n° 14
91
  # « hash multi-paramètres + reprise par hash ».
92
  "picarones/adapters/storage/artifact_store.py": 580, # actuel 504
93
+ # Sprint A14-S37 + S52 + S56 — JobStore SQLite : POST/GET/DELETE,
94
+ # JobStoreError, schema_version table (S56) + busy_timeout 30s +
95
+ # WAL mode pour les jobs concurrents.
96
+ "picarones/adapters/storage/job_store.py": 500, # actuel 421
97
  # Sprint A14-S41 — artifacts_index.jsonl séparé.
98
  "picarones/app/services/benchmark_service.py": 470, # actuel 400
99
  # Sprint A14-S44 — BaseLLMAdapter implémente le contrat StepExecutor
tests/domain/test_sprint_a14_s40_pipeline_spec_in_domain.py CHANGED
@@ -39,7 +39,11 @@ def test_domain_top_level_reexports() -> None:
39
 
40
 
41
  def test_legacy_pipeline_path_aliased() -> None:
42
- """``picarones.pipeline.spec`` reste un alias de chemin."""
 
 
 
 
43
  from picarones.pipeline.spec import (
44
  INITIAL_STEP_ID,
45
  PipelineSpec,
@@ -50,6 +54,24 @@ def test_legacy_pipeline_path_aliased() -> None:
50
  assert INITIAL_STEP_ID == "__initial__"
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def test_all_paths_resolve_to_same_classes() -> None:
54
  """Les imports depuis les 3 emplacements pointent vers le MÊME objet."""
55
  from picarones.domain import PipelineSpec as DomainSpec
 
39
 
40
 
41
  def test_legacy_pipeline_path_aliased() -> None:
42
+ """``picarones.pipeline.spec`` reste un alias de chemin.
43
+
44
+ Sprint S57 (audit #26) : émet désormais un ``DeprecationWarning``
45
+ à l'import — vérifié dans ``test_legacy_pipeline_path_emits_warning``.
46
+ """
47
  from picarones.pipeline.spec import (
48
  INITIAL_STEP_ID,
49
  PipelineSpec,
 
54
  assert INITIAL_STEP_ID == "__initial__"
55
 
56
 
57
+ def test_legacy_pipeline_path_emits_warning() -> None:
58
+ """Sprint S57 (audit #26) : l'import via ``picarones.pipeline.spec``
59
+ émet un ``DeprecationWarning``.
60
+ """
61
+ import importlib
62
+ import sys
63
+ import warnings
64
+
65
+ # Force le re-import pour déclencher le warning module-level.
66
+ sys.modules.pop("picarones.pipeline.spec", None)
67
+ with warnings.catch_warnings(record=True) as captured:
68
+ warnings.simplefilter("always")
69
+ importlib.import_module("picarones.pipeline.spec")
70
+ deprecation = [w for w in captured if issubclass(w.category, DeprecationWarning)]
71
+ assert deprecation, "DeprecationWarning attendu sur l'import legacy."
72
+ assert "picarones.domain" in str(deprecation[0].message)
73
+
74
+
75
  def test_all_paths_resolve_to_same_classes() -> None:
76
  """Les imports depuis les 3 emplacements pointent vers le MÊME objet."""
77
  from picarones.domain import PipelineSpec as DomainSpec
tests/domain/test_sprint_a14_s4_artifacts.py CHANGED
@@ -33,12 +33,15 @@ def _prov() -> ProvenanceRecord:
33
 
34
 
35
  class TestArtifactType:
36
- def test_nine_canonical_values(self) -> None:
37
- """Sprint A14-S4 — 9 valeurs canoniques."""
 
 
38
  expected = {
39
  "image", "raw_text", "corrected_text",
40
  "alto_xml", "page_xml", "canonical_document",
41
  "entities", "reading_order", "alignment",
 
42
  }
43
  assert {t.value for t in ArtifactType} == expected
44
 
 
33
 
34
 
35
  class TestArtifactType:
36
+ def test_canonical_values(self) -> None:
37
+ """Sprint A14-S4 — valeurs canoniques (9 jusqu'au S49 ;
38
+ +``confidences`` ajouté au S50 pour le sidecar JSON OCR).
39
+ """
40
  expected = {
41
  "image", "raw_text", "corrected_text",
42
  "alto_xml", "page_xml", "canonical_document",
43
  "entities", "reading_order", "alignment",
44
+ "confidences",
45
  }
46
  assert {t.value for t in ArtifactType} == expected
47
 
tests/pipeline/test_sprint_a14_s28_planner.py CHANGED
@@ -46,7 +46,7 @@ from picarones.pipeline.planner import (
46
  PlanningError,
47
  StepInputBinding,
48
  )
49
- from picarones.pipeline.spec import (
50
  INITIAL_STEP_ID,
51
  PipelineSpec,
52
  PipelineStep,
 
46
  PlanningError,
47
  StepInputBinding,
48
  )
49
+ from picarones.domain.pipeline_spec import (
50
  INITIAL_STEP_ID,
51
  PipelineSpec,
52
  PipelineStep,
tests/pipeline/test_sprint_a14_s47_artifact_store_resume.py CHANGED
@@ -34,7 +34,7 @@ from picarones.adapters.storage import (
34
  from picarones.domain.artifacts import Artifact, ArtifactType
35
  from picarones.domain.documents import DocumentRef
36
  from picarones.pipeline.executor import PipelineExecutor
37
- from picarones.pipeline.spec import PipelineSpec, PipelineStep
38
  from picarones.pipeline.types import RunContext
39
 
40
 
 
34
  from picarones.domain.artifacts import Artifact, ArtifactType
35
  from picarones.domain.documents import DocumentRef
36
  from picarones.pipeline.executor import PipelineExecutor
37
+ from picarones.domain.pipeline_spec import PipelineSpec, PipelineStep
38
  from picarones.pipeline.types import RunContext
39
 
40