Claude commited on
Commit
416bee1
·
unverified ·
1 Parent(s): d21efef

chore(versioning): reposition project as 0.9.0 (pre-1.0)

Browse files

Sprint S0 — align version numbering with actual functional maturity.

What changed:
- pyproject.toml: fallback_version 1.0.0 → 0.9.0
- New single source of truth: picarones/domain/_version_fallback.py
(placed in domain layer to be importable from all outer layers
without violating dependency rules)
- Refactor 4 hardcoded fallbacks (init/evaluation/reports/pyproject)
to read from the single source — fixes pre-existing inconsistency
where __init__.py said "2.0.0" while others said "1.0.0"
- CHANGELOG: rename [2.0.0] entry to [0.9.0] with historical note;
add repositioning note + roadmap to 1.0 in header
- CLAUDE.md, README.md, architecture.md, api-stable.md, specification.md,
release-process.md, rollback.md, index.md, threat-model.md,
reproducibility-snapshots.md, backlog.md: align active references
- docs/archive/README.md: add preliminary note explaining the
"v2.0" → "0.9.0" mapping; archive content left intact (history)
- New docs/explanation/versioning.md: full SemVer pre-1.0 policy
- New tests/golden/fixtures/benchmark_result_v2.json: use
"<CURRENT_VERSION>" placeholder; test substitutes the same string
→ snapshot decoupled from version bumps
- Refactored test_doc_truthfulness.py to verify invariants
(no legacy packages, 8 layers documented) rather than mentions of
a specific version

Anti-drift guard-rails (3 new tests):
- test_single_version_source.py: verifies pyproject ↔ FALLBACK_VERSION
alignment, PEP 440 format, no other module defines a literal version
- test_no_hardcoded_version.py: scans active code/docs for hardcoded
Picarones versions outside whitelist (contextual patterns:
"Picarones X.Y.Z", "picarones==X.Y.Z", "Release X.Y.Z", "vX.Y.Z"
isolated — avoids WCAG/Pydantic/Python false positives)
- test_version_propagation.py: end-to-end check that __version__
appears in HTML report, JSON, CLI info, /api/status

Verification:
- 5156 tests passed, 0 failed, 20 skipped
- make lint: All checks passed
- picarones --version, picarones info, demo HTML all show the
current resolved version (0.10.0.dev277 without git tag, would
become 0.9.0 once v0.9.0 tag is posted)

No git tag posted in this sprint — D6.b: tag is the operator's
explicit gesture to trigger PyPI/Docker/GitHub Release.

https://claude.ai/code/session_01WYDbfkhKPeBZ15BTP4e9Ye

CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ Tous les changements notables de ce projet sont documentés dans ce fichier.
5
  Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/).
6
  La numérotation de version suit [Semantic Versioning](https://semver.org/lang/fr/).
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
  ## [Unreleased] — Migration Option B vers RunOrchestrator (mai 2026)
@@ -454,7 +500,14 @@ et round-trip JSON appauvri.
454
 
455
  ---
456
 
457
- ## [2.0.0] — Legacy retirement complete (mai 2026)
 
 
 
 
 
 
 
458
 
459
  **Breaking changes** majeurs : suppression complète des paquets
460
  legacy. L'architecture canonique 8 couches (`domain → formats →
 
5
  Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/).
6
  La numérotation de version suit [Semantic Versioning](https://semver.org/lang/fr/).
7
 
8
+ Politique de versionning : voir
9
+ [`docs/explanation/versioning.md`](docs/explanation/versioning.md).
10
+
11
+ ---
12
+
13
+ ## Note de repositionnement (2026-05-23)
14
+
15
+ Le projet a été repositionné en **SemVer pré-1.0** à cette date. La
16
+ release précédemment dénommée **« 2.0.0 »** (clôture du rewrite
17
+ architectural en 8 couches, mai 2026) est désormais référencée comme
18
+ **`0.9.0`** dans ce changelog.
19
+
20
+ Justification : à la clôture du rewrite, le projet n'avait ni
21
+ interface utilisateur finalisée, ni parité importeurs, ni rapport
22
+ refondu — la dénomination « 2.0 » reflétait un cycle de refactor
23
+ interne, pas une release publique mûre. La sortie de `1.0.0`
24
+ deviendra un événement public marquant à la livraison de ces
25
+ chantiers.
26
+
27
+ Les sections historiques de ce fichier (et de la doc archive) qui
28
+ citent « v2.0 » ou « v1.x » dans leur **prose** n'ont pas été
29
+ réécrites : modifier le passé pour faire coïncider la terminologie
30
+ avec le présent serait une réécriture d'historique trompeuse. Lis
31
+ « v2.0 » dans ces sections comme « l'état du code à la clôture du
32
+ rewrite, devenu `0.9.0` au repositionnement ».
33
+
34
+ Seuls les **titres de version** (`## [X.Y.Z]`) ont été réalignés pour
35
+ que les tags git et les releases PyPI futures restent cohérents avec
36
+ le numéro courant.
37
+
38
+ ---
39
+
40
+ ## Roadmap vers 1.0.0
41
+
42
+ La sortie de `1.0.0` est conditionnée à la livraison de :
43
+
44
+ 1. **Surface UI complète** — exposition de tous les champs du modèle
45
+ `BenchmarkRunRequest`, parité fonctionnelle avec la CLI
46
+ (`compare`, `robustness`, `history`).
47
+ 2. **Parité importeurs corpus** — IIIF, Gallica, eScriptorium accessibles
48
+ depuis l'UI web.
49
+ 3. **Refonte rapport HTML** — IA 4 onglets (Overview / Engines / Documents / Crosses).
50
+
51
+ Les versions intermédiaires `0.10.0`, `0.11.0`, etc. publient des
52
+ jalons techniques au fil du chantier.
53
+
54
  ---
55
 
56
  ## [Unreleased] — Migration Option B vers RunOrchestrator (mai 2026)
 
500
 
501
  ---
502
 
503
+ ## [0.9.0] — Legacy retirement complete (mai 2026)
504
+
505
+ > Cette entrée porte le numéro de version dénommé **« 2.0.0 »**
506
+ > jusqu'au 2026-05-23, repositionné en `0.9.0` dans le cadre de
507
+ > l'alignement SemVer pré-1.0 (voir « Note de repositionnement » en
508
+ > tête de fichier). Le contenu n'a pas été réécrit : la prose
509
+ > mentionne « v2.0 » et « 1.x » telles qu'elles étaient employées à
510
+ > l'époque.
511
 
512
  **Breaking changes** majeurs : suppression complète des paquets
513
  legacy. L'architecture canonique 8 couches (`domain → formats →
CLAUDE.md CHANGED
@@ -4,6 +4,9 @@ Plateforme de benchmark OCR/HTR pour documents patrimoniaux.
4
  Repo : github.com/maribakulj/Picarones
5
  HuggingFace Space : huggingface.co/spaces/Ma-Ri-Ba-Ku/Picarones (Docker, port 7860)
6
 
 
 
 
7
  ---
8
 
9
  ## Architecture — 8 couches concentriques
@@ -17,12 +20,13 @@ domain → formats → evaluation → pipeline → adapters → app → reports
17
  Règle d'import stricte : les dépendances vont uniquement de l'extérieur
18
  vers l'intérieur. Vérifié par `tests/architecture/test_layer_dependencies.py`
19
  + `tests/architecture/test_no_legacy_imports_in_rewrite.py`
20
- (`LEGACY_PACKAGES = ()` à v2.0 — plus aucun paquet legacy).
21
 
22
  Tous les paquets legacy (`picarones/{core,measurements,engines,modules,
23
  report,llm,pipelines,cli,web,extras}/` + `adapters/legacy_engines/` +
24
  `adapters/legacy_pipelines/` + `interfaces/{cli,web}/_legacy/`) ont été
25
- **supprimés** au cours des sprints A-H (mai 2026).
 
26
 
27
  ---
28
 
@@ -212,11 +216,14 @@ labels d'affichage (i18n), pas des identifiants API.
212
 
213
  L'historique détaillé des **97+ sprints** du projet (de la fondation
214
  S1 jusqu'au rewrite ciblé S27-S46, l'audit institutionnel S47-S59,
215
- puis le retrait complet du legacy A-H jusqu'à v2.0) est dans le
216
- CHANGELOG.md à la racine.
217
 
218
- **v2.0 (mai 2026)** : la migration vers l'architecture 8 couches
219
- canoniques est terminée. Plus aucun paquet legacy. Plus aucun shim.
 
 
 
220
 
221
  **Chantier post-rewrite (mai 2026, branche `claude/fix-module-rewiring-MHssX`)** :
222
  réconciliation des chemins UI/API/runner après audit révélant des
@@ -243,7 +250,9 @@ exécutées :
243
  automatiquement.
244
  - **Phase 5 (naming)** : `CompetitorConfig` → `PipelineConfig`,
245
  `ocr_engine` → `engine_name` (rupture API, le field accepte aussi
246
- des VLMs et `corpus`).
 
 
247
 
248
  **Règles d'architecture** :
249
 
@@ -321,7 +330,7 @@ détecte, arbitre, rend.
321
  - **Manifeste architecture** : [`docs/explanation/architecture.md`](docs/explanation/architecture.md).
322
  - **API publique stable** : [`docs/reference/api-stable.md`](docs/reference/api-stable.md).
323
 
324
- ### Statut v2.0 — mai 2026
325
 
326
  L'architecture 8 couches est complète. Tous les paquets legacy
327
  top-level (`core/`, `measurements/`, `engines/`, `modules/`,
@@ -335,4 +344,15 @@ que les sous-paquets transitoires (`adapters/legacy_engines/`,
335
  | Foundation S1-S26 | ✅ | domain, formats, evaluation, narrative engine |
336
  | Rewrite ciblé S27-S46 | ✅ | pipeline, app.services, adapters/ocr canonique, reports |
337
  | Audit S47-S59 | ✅ | confidences, sécurité web, registry typé, baselines |
338
- | Plan A-H (mai 2026) | ✅ | Retrait complet du legacy : core/measurements/engines/modules/report/llm/pipelines/cli/web/extras supprimés ; interfaces/{cli,web}/_legacy promus au niveau canonique ; v2.0 release |
 
 
 
 
 
 
 
 
 
 
 
 
4
  Repo : github.com/maribakulj/Picarones
5
  HuggingFace Space : huggingface.co/spaces/Ma-Ri-Ba-Ku/Picarones (Docker, port 7860)
6
 
7
+ **Version courante** : `0.9.0` (pré-1.0). Politique de versionning :
8
+ [`docs/explanation/versioning.md`](docs/explanation/versioning.md).
9
+
10
  ---
11
 
12
  ## Architecture — 8 couches concentriques
 
20
  Règle d'import stricte : les dépendances vont uniquement de l'extérieur
21
  vers l'intérieur. Vérifié par `tests/architecture/test_layer_dependencies.py`
22
  + `tests/architecture/test_no_legacy_imports_in_rewrite.py`
23
+ (`LEGACY_PACKAGES = ()` — plus aucun paquet legacy).
24
 
25
  Tous les paquets legacy (`picarones/{core,measurements,engines,modules,
26
  report,llm,pipelines,cli,web,extras}/` + `adapters/legacy_engines/` +
27
  `adapters/legacy_pipelines/` + `interfaces/{cli,web}/_legacy/`) ont été
28
+ **supprimés** au cours des sprints A-H (mai 2026, clôture du rewrite
29
+ architectural — release `0.9.0`).
30
 
31
  ---
32
 
 
216
 
217
  L'historique détaillé des **97+ sprints** du projet (de la fondation
218
  S1 jusqu'au rewrite ciblé S27-S46, l'audit institutionnel S47-S59,
219
+ puis le retrait complet du legacy A-H aboutissant à `0.9.0`) est
220
+ dans le CHANGELOG.md à la racine.
221
 
222
+ **Release `0.9.0` (mai 2026)** : la migration vers l'architecture
223
+ 8 couches canoniques est terminée. Plus aucun paquet legacy. Plus
224
+ aucun shim. Cette release portait en interne la dénomination « v2.0 »
225
+ jusqu'au repositionnement en SemVer pré-1.0 (voir
226
+ [`docs/explanation/versioning.md`](docs/explanation/versioning.md)).
227
 
228
  **Chantier post-rewrite (mai 2026, branche `claude/fix-module-rewiring-MHssX`)** :
229
  réconciliation des chemins UI/API/runner après audit révélant des
 
250
  automatiquement.
251
  - **Phase 5 (naming)** : `CompetitorConfig` → `PipelineConfig`,
252
  `ocr_engine` → `engine_name` (rupture API, le field accepte aussi
253
+ des VLMs et `corpus`). Le worker `run_benchmark_thread_v2` propage
254
+ les nouveaux champs (le suffixe `_v2` est un nom de fonction
255
+ interne hérité, sans rapport avec la dénomination de version).
256
 
257
  **Règles d'architecture** :
258
 
 
330
  - **Manifeste architecture** : [`docs/explanation/architecture.md`](docs/explanation/architecture.md).
331
  - **API publique stable** : [`docs/reference/api-stable.md`](docs/reference/api-stable.md).
332
 
333
+ ### Statut `0.9.0` — mai 2026
334
 
335
  L'architecture 8 couches est complète. Tous les paquets legacy
336
  top-level (`core/`, `measurements/`, `engines/`, `modules/`,
 
344
  | Foundation S1-S26 | ✅ | domain, formats, evaluation, narrative engine |
345
  | Rewrite ciblé S27-S46 | ✅ | pipeline, app.services, adapters/ocr canonique, reports |
346
  | Audit S47-S59 | ✅ | confidences, sécurité web, registry typé, baselines |
347
+ | Plan A-H (mai 2026) | ✅ | Retrait complet du legacy : core/measurements/engines/modules/report/llm/pipelines/cli/web/extras supprimés ; interfaces/{cli,web}/_legacy promus au niveau canonique ; release `0.9.0` (cycle de rewrite clôturé) |
348
+
349
+ ### Roadmap vers 1.0.0
350
+
351
+ La sortie de `1.0.0` est conditionnée à la livraison de :
352
+
353
+ 1. **Surface UI complète** — exposition de tous les champs `BenchmarkRunRequest`,
354
+ parité fonctionnelle avec la CLI (`compare`, `robustness`, `history`).
355
+ 2. **Parité importeurs corpus** — IIIF, Gallica, eScriptorium accessibles en web.
356
+ 3. **Refonte rapport HTML** — IA 4 onglets (Overview / Engines / Documents / Crosses).
357
+
358
+ Releases intermédiaires `0.10.0`, `0.11.0`, … publient des jalons techniques.
README.md CHANGED
@@ -13,13 +13,13 @@ pinned: false
13
  >
14
  > **Outil de comparaison d'OCR / HTR / VLM et de post-correction pour documents patrimoniaux**
15
 
16
- **Status (May 2026)** — version 1.x, scientific prototype under
17
- consolidation. The core (corpus, runner, metrics, HTML report) is
18
- usable to compare transcription pipelines on a ground-truth corpus.
19
- A targeted rewrite (see
20
- [`docs/archive/2026-roadmap/rewrite.md`](docs/archive/2026-roadmap/rewrite.md))
21
- rebuilds the orchestration layer and evaluation views for a stable
22
- 2.0 release by the end of 2026.
23
 
24
  [![CI](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml/badge.svg)](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/maribakulj/Picarones/graph/badge.svg)](https://codecov.io/gh/maribakulj/Picarones)
25
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
@@ -333,12 +333,13 @@ picarones/
333
  ```
334
 
335
  Strict 8-layer architecture: imports flow outer → inner. Enforced
336
- by `tests/architecture/test_layer_dependencies.py`. The v2.0
337
- release (May 2026) removed all legacy top-level packages (`core/`,
338
- `measurements/`, `engines/`, `llm/`, `pipelines/`, `report/`,
339
- `modules/`, `cli/`, `web/`, `extras/`) and the transitional
340
- sub-packages (`adapters/legacy_engines/`, `adapters/legacy_pipelines/`,
341
- `interfaces/{cli,web}/_legacy/`). See
 
342
  [`docs/explanation/architecture.md`](docs/explanation/architecture.md)
343
  for the full manifesto and migration history under
344
  `docs/archive/2026-migration/`.
@@ -439,9 +440,11 @@ Detailed history and current direction live in:
439
  - [`docs/roadmap/backlog.md`](docs/roadmap/backlog.md) — live backlog
440
  for current work.
441
  - [`docs/archive/`](docs/archive/) — historical artefacts:
442
- pre-v2.0 roadmap (`2026-roadmap/`), institutional audits
443
  (`2026-audits/`), rewrite/migration plans (`2026-migration/`),
444
- pre-v2.0 changelog (`changelog-pre-v2.md`).
 
 
445
 
446
  **Honest status (May 2026).** Several items historically presented as
447
  "institutional readiness complete" are not at the level the README
 
13
  >
14
  > **Outil de comparaison d'OCR / HTR / VLM et de post-correction pour documents patrimoniaux**
15
 
16
+ **Status (May 2026)** — version **`0.9.0`** (pre-1.0, see
17
+ [versioning policy](docs/explanation/versioning.md)). Architecture
18
+ stable, functional surface under construction. The core (corpus,
19
+ runner, metrics, HTML report) is usable on a ground-truth corpus.
20
+ The [architectural rewrite](docs/archive/2026-roadmap/rewrite.md) is
21
+ **complete** ; remaining work towards `1.0.0` covers the web UI, the
22
+ HTML report refactor, and corpus importer parity.
23
 
24
  [![CI](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml/badge.svg)](https://github.com/maribakulj/Picarones/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/maribakulj/Picarones/graph/badge.svg)](https://codecov.io/gh/maribakulj/Picarones)
25
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
 
333
  ```
334
 
335
  Strict 8-layer architecture: imports flow outer → inner. Enforced
336
+ by `tests/architecture/test_layer_dependencies.py`. Release `0.9.0`
337
+ (May 2026) marks the end of the architectural rewrite cycle: all
338
+ legacy top-level packages (`core/`, `measurements/`, `engines/`,
339
+ `llm/`, `pipelines/`, `report/`, `modules/`, `cli/`, `web/`,
340
+ `extras/`) and transitional sub-packages
341
+ (`adapters/legacy_engines/`, `adapters/legacy_pipelines/`,
342
+ `interfaces/{cli,web}/_legacy/`) have been removed. See
343
  [`docs/explanation/architecture.md`](docs/explanation/architecture.md)
344
  for the full manifesto and migration history under
345
  `docs/archive/2026-migration/`.
 
440
  - [`docs/roadmap/backlog.md`](docs/roadmap/backlog.md) — live backlog
441
  for current work.
442
  - [`docs/archive/`](docs/archive/) — historical artefacts:
443
+ pre-rewrite roadmap (`2026-roadmap/`), institutional audits
444
  (`2026-audits/`), rewrite/migration plans (`2026-migration/`),
445
+ pre-rewrite changelog (`changelog-pre-v2.md`). The archived
446
+ documents use the internal `v2.0` codename for the rewrite that
447
+ was repositioned as `0.9.0` — see [`docs/archive/README.md`](docs/archive/README.md).
448
 
449
  **Honest status (May 2026).** Several items historically presented as
450
  "institutional readiness complete" are not at the level the README
docs/archive/README.md CHANGED
@@ -2,21 +2,42 @@
2
 
3
  > **Archived document.** Cette zone contient des documents
4
  > **historiques** : audits, plans de migration, roadmaps obsolètes,
5
- > changelogs antérieurs à v2.0. Ils sont conservés pour la
6
- > traçabilité et la mémoire institutionnelle du projet, mais ne font
7
- > **pas** partie de la documentation active.
8
  >
9
  > Pour la documentation à jour, voir [`docs/index.md`](../index.md).
10
 
11
  ---
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ## Pourquoi cette séparation
14
 
15
- À v2.0 (mai 2026), le projet a clôturé son rewrite et la migration
16
- legacy. Les artefacts qui décrivaient ces chantiers (handovers de
17
- session, plans de retrait, audits institutionnels, status reports)
18
- restent utiles pour comprendre **comment** le code a évolué, mais
19
- ils ne décrivent pas l'état actuel.
 
20
 
21
  Les laisser dans la documentation active créait deux confusions :
22
 
 
2
 
3
  > **Archived document.** Cette zone contient des documents
4
  > **historiques** : audits, plans de migration, roadmaps obsolètes,
5
+ > changelogs antérieurs au rewrite architectural complet. Ils sont
6
+ > conservés pour la traçabilité et la mémoire institutionnelle du
7
+ > projet, mais ne font **pas** partie de la documentation active.
8
  >
9
  > Pour la documentation à jour, voir [`docs/index.md`](../index.md).
10
 
11
  ---
12
 
13
+ ## Note de repositionnement (2026-05-23)
14
+
15
+ Les documents archivés ici utilisent la dénomination interne
16
+ **« v2.0 »** pour désigner l'aboutissement du rewrite architectural
17
+ en 8 couches (mai 2026). Cette dénomination, qui reflétait une
18
+ logique de refactor interne plutôt qu'une release publique mûre, a
19
+ été **repositionnée en `0.9.0`** lors de l'alignement du projet sur
20
+ SemVer pré-1.0.
21
+
22
+ **Le contenu de l'archive n'a volontairement pas été réécrit** :
23
+ modifier le passé pour faire coïncider la terminologie avec le
24
+ présent serait une réécriture d'historique trompeuse. Lis donc
25
+ « v2.0 » dans les fichiers archivés comme « l'état du code à la
26
+ clôture du rewrite, devenu `0.9.0` au repositionnement ».
27
+
28
+ Politique de versionning actuelle : voir
29
+ [`docs/explanation/versioning.md`](../explanation/versioning.md).
30
+
31
+ ---
32
+
33
  ## Pourquoi cette séparation
34
 
35
+ À l'aboutissement du rewrite architectural (mai 2026), le projet a
36
+ clôturé sa migration legacy. Les artefacts qui décrivaient ces
37
+ chantiers (handovers de session, plans de retrait, audits
38
+ institutionnels, status reports) restent utiles pour comprendre
39
+ **comment** le code a évolué, mais ils ne décrivent pas l'état
40
+ actuel.
41
 
42
  Les laisser dans la documentation active créait deux confusions :
43
 
docs/explanation/architecture.md CHANGED
@@ -5,17 +5,18 @@
5
  > sont les fichiers*. Pour la liste exhaustive des modules, lire
6
  > directement le code — il est typé et documenté.
7
 
8
- ## Statut v2.0 — une seule arborescence canonique
9
 
10
- À v2.0 (mai 2026), Picarones a **une seule arborescence**. Tous
11
- les paquets pré-rewrite ainsi que leurs sous-paquets transitoires
12
- ont été supprimés au cours des sprints A-H. Pour le détail
13
- historique, voir le [CHANGELOG section 2.0.0](../../CHANGELOG.md)
14
- et [`docs/archive/2026-migration/`](../archive/2026-migration/).
 
15
 
16
  Toute documentation, tout commentaire qui mentionne « deux
17
  arborescences » ou « legacy en cours de retrait » est obsolète.
18
- La seule cohabitation acceptable à v2.0+ est celle entre
19
  **modules canoniques** : par exemple `evaluation/metric_registry`
20
  (module-level, side-effect d'import) et `evaluation/registry/registry`
21
  (instance-based) — deux patterns volontairement coexistants pour
@@ -200,9 +201,9 @@ on choisit explicitement entre :
200
  - **Shim avec `DeprecationWarning`** (pour la stabilité d'API publique).
201
  Le shim a une date de retrait inscrite dans le CHANGELOG.
202
 
203
- À v2.0 il reste **un seul shim** documenté :
204
  `picarones/pipeline/spec.py` (réexporte `picarones.domain.pipeline_spec`),
205
- dont la deprecation period expire en v2.1.
206
 
207
  ### Pas d'`except Exception: pass`
208
 
@@ -249,9 +250,9 @@ publication scientifique.
249
  L'évolution de l'architecture est documentée :
250
 
251
  - Backlog vivant : [`docs/roadmap/backlog.md`](../roadmap/backlog.md)
252
- - Plans archivés (migration legacy → rewrite, terminée à v2.0) :
253
  [`docs/archive/2026-migration/`](../archive/2026-migration/)
254
- - Roadmap historique pré-v2.0 :
255
  [`docs/archive/2026-roadmap/`](../archive/2026-roadmap/)
256
  - Audits institutionnels : [`docs/archive/2026-audits/`](../archive/2026-audits/)
257
  - Politique d'API publique : [`docs/reference/api-stable.md`](../reference/api-stable.md)
 
5
  > sont les fichiers*. Pour la liste exhaustive des modules, lire
6
  > directement le code — il est typé et documenté.
7
 
8
+ ## Statut `0.9.0` — une seule arborescence canonique
9
 
10
+ À la release `0.9.0` (mai 2026, clôture du rewrite architectural),
11
+ Picarones a **une seule arborescence**. Tous les paquets
12
+ pré-rewrite ainsi que leurs sous-paquets transitoires ont été
13
+ supprimés au cours des sprints A-H. Pour le détail historique,
14
+ voir le [CHANGELOG section 0.9.0](../../CHANGELOG.md) et
15
+ [`docs/archive/2026-migration/`](../archive/2026-migration/).
16
 
17
  Toute documentation, tout commentaire qui mentionne « deux
18
  arborescences » ou « legacy en cours de retrait » est obsolète.
19
+ La seule cohabitation acceptable depuis `0.9.0` est celle entre
20
  **modules canoniques** : par exemple `evaluation/metric_registry`
21
  (module-level, side-effect d'import) et `evaluation/registry/registry`
22
  (instance-based) — deux patterns volontairement coexistants pour
 
201
  - **Shim avec `DeprecationWarning`** (pour la stabilité d'API publique).
202
  Le shim a une date de retrait inscrite dans le CHANGELOG.
203
 
204
+ À `0.9.0` il reste **un seul shim** documenté :
205
  `picarones/pipeline/spec.py` (réexporte `picarones.domain.pipeline_spec`),
206
+ dont la deprecation period expire à la prochaine minor `0.10.0`.
207
 
208
  ### Pas d'`except Exception: pass`
209
 
 
250
  L'évolution de l'architecture est documentée :
251
 
252
  - Backlog vivant : [`docs/roadmap/backlog.md`](../roadmap/backlog.md)
253
+ - Plans archivés (migration legacy → rewrite, terminée à `0.9.0`) :
254
  [`docs/archive/2026-migration/`](../archive/2026-migration/)
255
+ - Roadmap historique pré-rewrite :
256
  [`docs/archive/2026-roadmap/`](../archive/2026-roadmap/)
257
  - Audits institutionnels : [`docs/archive/2026-audits/`](../archive/2026-audits/)
258
  - Politique d'API publique : [`docs/reference/api-stable.md`](../reference/api-stable.md)
docs/explanation/versioning.md ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Politique de versionning
2
+
3
+ > Comment Picarones numérote ses versions, et pourquoi.
4
+
5
+ ## Résumé
6
+
7
+ Picarones suit **Semantic Versioning 2.0.0** strict :
8
+ `MAJOR.MINOR.PATCH` avec suffixes pré-release PEP 440 (`-rc1`,
9
+ `-beta1`, `-alpha1`).
10
+
11
+ **État courant** : `0.9.0` — phase pré-1.0. Architecture stable,
12
+ surface fonctionnelle en construction.
13
+
14
+ **Cible** : `1.0.0` — release publique avec UI, rapport refondu et
15
+ parité des importeurs livrés.
16
+
17
+ ---
18
+
19
+ ## SemVer pré-1.0 : qu'est-ce que ça change ?
20
+
21
+ En `0.x`, SemVer autorise **les ruptures sans préavis**. Picarones
22
+ les annonce explicitement dans le `CHANGELOG.md` mais ne garantit
23
+ pas de période de dépréciation. C'est précisément l'intérêt du
24
+ pré-1.0 : pouvoir corriger l'API publique sans empêcher la
25
+ progression.
26
+
27
+ Concrètement :
28
+
29
+ - les schémas JSON (`BenchmarkResult`, `run_manifest.json`,
30
+ `pipeline_results.jsonl`, `view_results.jsonl`) **peuvent évoluer**
31
+ entre `0.9.x` et `0.10.x` ;
32
+ - les noms de paramètres CLI **peuvent changer** ;
33
+ - les endpoints HTTP **peuvent être renommés ou retirés** ;
34
+ - les contrats internes (Pydantic models de `picarones.domain`)
35
+ **peuvent être modifiés**.
36
+
37
+ Chaque rupture est documentée dans le `CHANGELOG.md` sous la section
38
+ `### BREAKING`.
39
+
40
+ À partir de `1.0.0`, la politique se durcit : période de
41
+ dépréciation minimum d'une mineure, retrait à la majeure suivante.
42
+
43
+ ---
44
+
45
+ ## Mapping version ↔ tag git
46
+
47
+ Le numéro de version est dérivé du **tag git** par
48
+ `setuptools_scm`. Pas de version codée en dur dans `pyproject.toml`.
49
+
50
+ | Contexte | Version produite |
51
+ |---|---|
52
+ | Tag `v0.9.0` posé sur un commit | `0.9.0` |
53
+ | 5 commits après `v0.9.0` (sans nouveau tag) | `0.9.1.dev5+g<sha>` |
54
+ | Tag `v0.10.0-rc1` | `0.10.0rc1` (PEP 440) |
55
+ | Sans aucun tag (tarball, repo neuf) | `0.9.0` (fallback) |
56
+
57
+ Le `fallback_version` de `pyproject.toml` est synchronisé avec
58
+ `picarones/domain/_version_fallback.py:FALLBACK_VERSION`. Un test
59
+ d'architecture (`tests/architecture/test_single_version_source.py`)
60
+ vérifie cette cohérence.
61
+
62
+ ---
63
+
64
+ ## Roadmap vers 1.0.0
65
+
66
+ La sortie de `1.0.0` est conditionnée à la livraison de :
67
+
68
+ 1. **Surface UI complète** — exposition de tous les champs du modèle
69
+ `BenchmarkRunRequest`, refonte du rapport HTML, parité fonctionnelle
70
+ avec la CLI (`compare`, `robustness`, `history`).
71
+ 2. **Parité importeurs corpus** — IIIF, Gallica, eScriptorium accessibles
72
+ depuis l'UI web (en plus de HTR-United et HuggingFace déjà présents).
73
+ 3. **Refonte rapport HTML** — passage à l'IA 4 onglets (Overview / Engines
74
+ / Documents / Crosses) en gardant la stack Jinja2.
75
+
76
+ Les versions intermédiaires `0.10.0`, `0.11.0`, etc. peuvent être
77
+ publiées au fil du chantier ; elles sont des jalons techniques, pas
78
+ des releases publiques.
79
+
80
+ ---
81
+
82
+ ## Pourquoi `0.9.0` plutôt que `2.0.x` ou `3.0.0` ?
83
+
84
+ L'historique interne du projet utilisait une dénomination « v2.0 »
85
+ pour désigner l'aboutissement du rewrite architectural en 8 couches
86
+ (mai 2026). Cette dénomination reflétait un cycle de refactor
87
+ interne, pas une maturité fonctionnelle publique : à ce moment, le
88
+ projet n'avait ni interface utilisateur finalisée, ni parité
89
+ importeurs, ni rapport refondu.
90
+
91
+ Le repositionnement en `0.9.0` (mai 2026) aligne le numéro de
92
+ version sur la maturité réelle : architecture solide, surface
93
+ fonctionnelle en construction. La sortie de `1.0.0` deviendra un
94
+ événement public marquant (UI complète, rapport refondu, parité
95
+ importeurs), ce qui correspond à l'usage habituel d'un saut
96
+ `0.x → 1.0`.
97
+
98
+ Ce choix est documenté dans la note de repositionnement de
99
+ [`docs/archive/README.md`](../archive/README.md) et dans la première
100
+ entrée du [`CHANGELOG.md`](../../CHANGELOG.md) post-repositionnement.
101
+
102
+ ---
103
+
104
+ ## Source unique de vérité (anti-dérive)
105
+
106
+ La version courante est définie à **un seul endroit** : le tag git le
107
+ plus récent, lu par `setuptools_scm`.
108
+
109
+ Le fallback (utilisé en l'absence de tag) est défini à **un seul
110
+ endroit** : `picarones/domain/_version_fallback.py:FALLBACK_VERSION`. La
111
+ constante équivalente `fallback_version` dans `pyproject.toml` doit
112
+ rester synchronisée — un test d'architecture le vérifie.
113
+
114
+ **Règle de codage** : interdiction de coder un numéro de version en
115
+ dur ailleurs que dans ces deux fichiers. Tout consommateur doit
116
+ lire `picarones.__version__` (ou ses équivalents internes
117
+ `_resolve_picarones_version()` pour les couches qui ne peuvent pas
118
+ importer le package racine).
119
+
120
+ ---
121
+
122
+ ## Procédure de release
123
+
124
+ Voir [`docs/operations/release-process.md`](../operations/release-process.md)
125
+ pour la procédure complète. Le résumé :
126
+
127
+ ```bash
128
+ # 1. Mettre à jour CHANGELOG.md (section ## [X.Y.Z] — YYYY-MM-DD)
129
+ git commit -am "docs(changelog): release 0.10.0"
130
+
131
+ # 2. Tag annoté
132
+ git tag -a v0.10.0 -m "Picarones 0.10.0"
133
+ git push origin main v0.10.0
134
+
135
+ # 3. Le workflow .github/workflows/release.yml déclenche
136
+ # automatiquement build + TestPyPI + PyPI + Docker + GitHub Release.
137
+ ```
docs/index.md CHANGED
@@ -137,15 +137,15 @@ Vous évaluez les implications RGPD avant signature.
137
 
138
  | Document | Sujet |
139
  |----------|-------|
140
- | [`/CHANGELOG.md`](../CHANGELOG.md) | Journal des versions actives (post-v2.0) |
141
- | [`archive/`](archive/) | Documents archivés (audits, migration, roadmap pré-v2.0, changelog historique) |
142
  | [`roadmap/backlog.md`](roadmap/backlog.md) | Backlog vivant |
143
 
144
  ---
145
 
146
  ## Conventions
147
 
148
- - **Une seule arborescence canonique (v2.0)** :
149
  `domain → formats → evaluation → pipeline → adapters → app → reports → interfaces`.
150
  Les paquets legacy ont été supprimés en mai 2026.
151
  - **Tout chemin `picarones/.../X.py` cité dans la doc doit exister**.
 
137
 
138
  | Document | Sujet |
139
  |----------|-------|
140
+ | [`/CHANGELOG.md`](../CHANGELOG.md) | Journal des versions actives (depuis `0.9.0`) |
141
+ | [`archive/`](archive/) | Documents archivés (audits, migration, roadmap pré-rewrite, changelog historique) |
142
  | [`roadmap/backlog.md`](roadmap/backlog.md) | Backlog vivant |
143
 
144
  ---
145
 
146
  ## Conventions
147
 
148
+ - **Une seule arborescence canonique** (depuis `0.9.0`) :
149
  `domain → formats → evaluation → pipeline → adapters → app → reports → interfaces`.
150
  Les paquets legacy ont été supprimés en mai 2026.
151
  - **Tout chemin `picarones/.../X.py` cité dans la doc doit exister**.
docs/operations/release-process.md CHANGED
@@ -80,14 +80,14 @@ git checkout main
80
  git pull --ff-only
81
 
82
  # 2. Mettre à jour le CHANGELOG.md (Keep a Changelog)
83
- # Ajouter une section ## [1.2.0] — YYYY-MM-DD avec les changes
84
  git add CHANGELOG.md
85
- git commit -m "docs(changelog): release 1.2.0"
86
 
87
  # 3. Tag annoté + push
88
- git tag -a v1.2.0 -m "Picarones 1.2.0"
89
  git push origin main
90
- git push origin v1.2.0
91
 
92
  # 4. Surveiller le workflow Actions
93
  gh run watch
@@ -110,19 +110,23 @@ Durée totale : ~15 min (multi-arch + 30s d'indexation TestPyPI).
110
 
111
  ## Versionnement
112
 
113
- Picarones suit **Semantic Versioning 2.0.0** :
 
114
 
115
  - `MAJOR.MINOR.PATCH` — incompatibilité, ajout, fix.
116
  - Suffixes pré-release : `-rc1`, `-beta1`, `-alpha1`. Le workflow
117
  les détecte et coche `prerelease=true` sur la GitHub Release.
 
 
 
118
 
119
  `setuptools_scm` dérive automatiquement la version du tag git :
120
 
121
  | Contexte | Version produite |
122
  |---|---|
123
- | Tag `v1.2.0` | `1.2.0` |
124
- | 5 commits après `v1.2.0` | `1.2.1.dev5+g<sha>` (dev seulement) |
125
- | `v1.3.0-rc1` | `1.3.0rc1` (PEP 440) |
126
 
127
  ## Procédure d'urgence : hotfix sécurité
128
 
@@ -133,9 +137,9 @@ git checkout -b hotfix-cve-2026-XXXX main
133
  # correctif minimal + test
134
  git commit -m "fix(security): patch CVE-2026-XXXX"
135
  # CHANGELOG bump
136
- git commit -m "docs(changelog): release 1.2.1"
137
- git tag -a v1.2.1 -m "Picarones 1.2.1 (security)"
138
- git push origin hotfix-cve-2026-XXXX v1.2.1
139
  # Le workflow release.yml gère le reste.
140
  # Après merge : annonce sur SECURITY.md + courriel mainteneur.
141
  ```
 
80
  git pull --ff-only
81
 
82
  # 2. Mettre à jour le CHANGELOG.md (Keep a Changelog)
83
+ # Ajouter une section ## [0.10.0] — YYYY-MM-DD avec les changes
84
  git add CHANGELOG.md
85
+ git commit -m "docs(changelog): release 0.10.0"
86
 
87
  # 3. Tag annoté + push
88
+ git tag -a v0.10.0 -m "Picarones 0.10.0"
89
  git push origin main
90
+ git push origin v0.10.0
91
 
92
  # 4. Surveiller le workflow Actions
93
  gh run watch
 
110
 
111
  ## Versionnement
112
 
113
+ Picarones suit **Semantic Versioning 2.0.0**. Politique complète :
114
+ [`docs/explanation/versioning.md`](../explanation/versioning.md).
115
 
116
  - `MAJOR.MINOR.PATCH` — incompatibilité, ajout, fix.
117
  - Suffixes pré-release : `-rc1`, `-beta1`, `-alpha1`. Le workflow
118
  les détecte et coche `prerelease=true` sur la GitHub Release.
119
+ - **État courant : `0.x` (pré-1.0)** — les ruptures peuvent
120
+ intervenir sans période de dépréciation, annoncées dans le
121
+ `CHANGELOG.md`.
122
 
123
  `setuptools_scm` dérive automatiquement la version du tag git :
124
 
125
  | Contexte | Version produite |
126
  |---|---|
127
+ | Tag `v0.10.0` | `0.10.0` |
128
+ | 5 commits après `v0.10.0` | `0.10.1.dev5+g<sha>` (dev seulement) |
129
+ | `v0.11.0-rc1` | `0.11.0rc1` (PEP 440) |
130
 
131
  ## Procédure d'urgence : hotfix sécurité
132
 
 
137
  # correctif minimal + test
138
  git commit -m "fix(security): patch CVE-2026-XXXX"
139
  # CHANGELOG bump
140
+ git commit -m "docs(changelog): release 0.9.1"
141
+ git tag -a v0.9.1 -m "Picarones 0.9.1 (security)"
142
+ git push origin hotfix-cve-2026-XXXX v0.9.1
143
  # Le workflow release.yml gère le reste.
144
  # Après merge : annonce sur SECURITY.md + courriel mainteneur.
145
  ```
docs/operations/rollback.md CHANGED
@@ -25,15 +25,15 @@ Code → schéma → config.
25
  # Identifier la version actuelle
26
  docker inspect picarones | grep Version
27
 
28
- # Rollback vers la version précédente (ex : v2.0.0 → v1.5.3)
29
- docker pull ghcr.io/maribakulj/picarones:v1.5.3
30
  docker stop picarones
31
  docker rm picarones
32
  docker run --name picarones \
33
  -p 7860:7860 \
34
  --env-file /etc/picarones/.env \
35
  -v /var/lib/picarones/jobs.db:/app/jobs.db \
36
- ghcr.io/maribakulj/picarones:v1.5.3
37
 
38
  # Vérifier
39
  curl -fsS http://localhost:7860/health
@@ -46,7 +46,7 @@ curl -fsS http://localhost:7860/health
46
  pip index versions picarones
47
 
48
  # Rollback
49
- pip install --force-reinstall picarones==1.5.3
50
 
51
  # Vérifier
52
  picarones --version
@@ -76,15 +76,16 @@ nécessite de revenir à un schéma antérieur :
76
  ### Cas simple : downgrade non destructif
77
 
78
  Si la migration N→N+1 a uniquement **ajouté** des colonnes / tables
79
- (jamais supprimé), le code v1.5.3 lit un schéma v2 sans plantage —
80
- les nouvelles colonnes sont simplement ignorées.
 
81
 
82
  Aucune action requise.
83
 
84
  ### Cas complexe : downgrade destructif
85
 
86
- Si la migration a renommé/supprimé des colonnes, le code v1.5.3
87
- peut lever des `sqlite3.OperationalError`.
88
 
89
  **Procédure** :
90
 
@@ -107,7 +108,7 @@ peut lever des `sqlite3.OperationalError`.
107
  ```
108
 
109
  4. **Forcer le schema_version à la valeur attendue par le code
110
- v1.5.3** (si nécessaire) :
111
 
112
  ```sql
113
  sqlite3 /var/lib/picarones/jobs.db
@@ -115,14 +116,14 @@ peut lever des `sqlite3.OperationalError`.
115
  sqlite> .quit
116
  ```
117
 
118
- 5. Redémarrer Picarones v1.5.3.
119
  6. Vérifier que les jobs anciens sont lisibles via `GET /api/jobs`.
120
 
121
  ### Pas de backup ?
122
 
123
- Le code v2.0+ a un mode "lecture seule défensive" : si le
124
- `schema_version` est plus récent que celui attendu, le code log
125
- un warning explicite et tente de continuer. En cas d'incident,
126
  les jobs *nouveaux* fonctionnent ; les jobs *anciens* (avec des
127
  champs nouveaux) peuvent apparaître tronqués mais ne crashent
128
  pas.
 
25
  # Identifier la version actuelle
26
  docker inspect picarones | grep Version
27
 
28
+ # Rollback vers la version précédente (ex : v0.10.0 → v0.9.0)
29
+ docker pull ghcr.io/maribakulj/picarones:v0.9.0
30
  docker stop picarones
31
  docker rm picarones
32
  docker run --name picarones \
33
  -p 7860:7860 \
34
  --env-file /etc/picarones/.env \
35
  -v /var/lib/picarones/jobs.db:/app/jobs.db \
36
+ ghcr.io/maribakulj/picarones:v0.9.0
37
 
38
  # Vérifier
39
  curl -fsS http://localhost:7860/health
 
46
  pip index versions picarones
47
 
48
  # Rollback
49
+ pip install --force-reinstall picarones==0.9.0
50
 
51
  # Vérifier
52
  picarones --version
 
76
  ### Cas simple : downgrade non destructif
77
 
78
  Si la migration N→N+1 a uniquement **ajouté** des colonnes / tables
79
+ (jamais supprimé), le code de la version cible lit un schéma plus
80
+ récent sans plantage — les nouvelles colonnes sont simplement
81
+ ignorées.
82
 
83
  Aucune action requise.
84
 
85
  ### Cas complexe : downgrade destructif
86
 
87
+ Si la migration a renommé/supprimé des colonnes, le code de la
88
+ version cible peut lever des `sqlite3.OperationalError`.
89
 
90
  **Procédure** :
91
 
 
108
  ```
109
 
110
  4. **Forcer le schema_version à la valeur attendue par le code
111
+ de la version cible** (si nécessaire) :
112
 
113
  ```sql
114
  sqlite3 /var/lib/picarones/jobs.db
 
116
  sqlite> .quit
117
  ```
118
 
119
+ 5. Redémarrer Picarones à la version cible.
120
  6. Vérifier que les jobs anciens sont lisibles via `GET /api/jobs`.
121
 
122
  ### Pas de backup ?
123
 
124
+ Le code Picarones (depuis `0.9.0`) a un mode "lecture seule
125
+ défensive" : si le `schema_version` est plus récent que celui
126
+ attendu, le code log un warning explicite et tente de continuer. En cas d'incident,
127
  les jobs *nouveaux* fonctionnent ; les jobs *anciens* (avec des
128
  champs nouveaux) peuvent apparaître tronqués mais ne crashent
129
  pas.
docs/reference/api-stable.md CHANGED
@@ -1,8 +1,8 @@
1
  # API publique stable de Picarones
2
 
3
- > **Statut v2.0 (mai 2026)** : la migration vers l'architecture
4
- > 8 couches canoniques est terminée. Tous les paquets legacy
5
- > top-level (`picarones.core`, `picarones.measurements`,
6
  > `picarones.engines`, `picarones.modules`, `picarones.report`,
7
  > `picarones.llm`, `picarones.pipelines`, `picarones.cli`,
8
  > `picarones.web`, `picarones.extras`) ainsi que les sous-paquets
@@ -10,6 +10,12 @@
10
  > `interfaces/{cli,web}/_legacy/`) ont été **supprimés**. Plus aucun
11
  > shim, plus aucun `DeprecationWarning` rétrocompat.
12
  >
 
 
 
 
 
 
13
  > **Architecture canonique (cf.
14
  > [`docs/explanation/architecture.md`](../explanation/architecture.md))** :
15
  > `domain → formats → evaluation → pipeline → adapters → app →
@@ -271,9 +277,9 @@ def reset_default_store(...)
271
  historiques qui les importent (Sprint 13/42) sont préservés mais
272
  par effort, pas par contrat.
273
 
274
- ### Bump majeur (`2.0.0`)
275
 
276
- Un bump majeur sera nécessaire pour :
277
 
278
  - Supprimer un nom de cette liste.
279
  - Changer la signature d'une fonction publique de manière non
@@ -281,10 +287,14 @@ Un bump majeur sera nécessaire pour :
281
  - Casser le format de sérialisation du `BenchmarkResult.to_json()`.
282
  - Renommer un module de l'arborescence canonique.
283
 
 
 
 
 
284
  ## Chemins canoniques par couche
285
 
286
- L'arborescence v2.0 expose des points d'entrée stables organisés par
287
- couche. Toutes les intégrations doivent passer par ces chemins —
288
  plus de path legacy disponible.
289
 
290
  ### Couche 3 — `picarones.evaluation`
 
1
  # API publique stable de Picarones
2
 
3
+ > **Statut `0.9.0` (mai 2026)** phase pré-1.0. L'**architecture**
4
+ > est stable : les 8 couches canoniques sont en place, tous les
5
+ > paquets legacy top-level (`picarones.core`, `picarones.measurements`,
6
  > `picarones.engines`, `picarones.modules`, `picarones.report`,
7
  > `picarones.llm`, `picarones.pipelines`, `picarones.cli`,
8
  > `picarones.web`, `picarones.extras`) ainsi que les sous-paquets
 
10
  > `interfaces/{cli,web}/_legacy/`) ont été **supprimés**. Plus aucun
11
  > shim, plus aucun `DeprecationWarning` rétrocompat.
12
  >
13
+ > En revanche, l'**API publique** elle-même n'est pas figée tant que
14
+ > `1.0.0` n'est pas livré. Les schémas JSON, noms de paramètres CLI
15
+ > et endpoints HTTP peuvent évoluer entre `0.9.x` et `0.10.x` sans
16
+ > période de dépréciation, conformément à la
17
+ > [politique de versionning pré-1.0](../explanation/versioning.md).
18
+ >
19
  > **Architecture canonique (cf.
20
  > [`docs/explanation/architecture.md`](../explanation/architecture.md))** :
21
  > `domain → formats → evaluation → pipeline → adapters → app →
 
277
  historiques qui les importent (Sprint 13/42) sont préservés mais
278
  par effort, pas par contrat.
279
 
280
+ ### Bump majeur (`1.0.0` puis `2.0.0`)
281
 
282
+ À partir de `1.0.0`, un bump majeur sera nécessaire pour :
283
 
284
  - Supprimer un nom de cette liste.
285
  - Changer la signature d'une fonction publique de manière non
 
287
  - Casser le format de sérialisation du `BenchmarkResult.to_json()`.
288
  - Renommer un module de l'arborescence canonique.
289
 
290
+ En `0.x` (état courant), ces changements peuvent se produire sans
291
+ bump majeur, mais sont annoncés dans le `CHANGELOG.md` sous une
292
+ section `### BREAKING`.
293
+
294
  ## Chemins canoniques par couche
295
 
296
+ L'arborescence `0.9.0` expose des points d'entrée stables organisés
297
+ par couche. Toutes les intégrations doivent passer par ces chemins —
298
  plus de path legacy disponible.
299
 
300
  ### Couche 3 — `picarones.evaluation`
docs/reference/reproducibility-snapshots.md CHANGED
@@ -79,7 +79,7 @@ ont été appliquées au moment du calcul de CER, **sans relancer**.
79
  ### `environment`
80
 
81
  ```yaml
82
- picarones_version: "1.1.0"
83
  python_version: "3.11.13"
84
  platform: "Linux 6.18.5-x86_64-with-glibc2.39"
85
  git_commit: "17cc5474..."
 
79
  ### `environment`
80
 
81
  ```yaml
82
+ picarones_version: "0.9.0"
83
  python_version: "3.11.13"
84
  platform: "Linux 6.18.5-x86_64-with-glibc2.39"
85
  git_commit: "17cc5474..."
docs/reference/specification.md CHANGED
@@ -3,14 +3,18 @@
3
  > **Plateforme de banc d'essai d'OCR / HTR / VLM et de pipelines de
4
  > post-correction pour documents patrimoniaux.**
5
  >
6
- > Version **2.0** — Mai 2026. Refonte intégrale (Sprint A14 du plan
7
- > de remédiation institutionnelle, item **B-12**).
 
 
 
8
 
9
  > **Note de lecture** : ce document décrit ce que Picarones **fait
10
- > aujourd'hui**, dans la version 1.x. Pour les **non-fonctionnalités
11
- > assumées** (ce que Picarones *ne fait pas et ne fera pas* dans
12
- > la v1.x — par exemple la recommandation prescriptive, l'export PDF,
13
- > les adapters Kraken/AWS Textract), voir la section §10.
 
14
  >
15
  > Pour la cartographie technique du code et les règles de
16
  > contribution interne, voir [`CLAUDE.md`](../../CLAUDE.md). Ce document
@@ -724,18 +728,21 @@ configurable via `PICARONES_UPLOAD_RETENTION_DAYS=7` par défaut.
724
  ## 10. Limites assumées et non-fonctionnalités
725
 
726
  Cette section décrit explicitement **ce que Picarones ne fait pas
727
- et ne fera pas dans la v1.x**. Plusieurs items étaient promis dans
728
- la SPECS v1 (Mars 2025) leur abandon est un choix éditorial
729
- documenté ci-dessous, pas un oubli.
 
 
 
730
 
731
  <!-- specs-check: known-abandoned-start -->
732
 
733
  > Toutes les fonctionnalités listées ci-dessous étaient promises ou
734
- > évoquées dans SPECS v1 et sont **explicitement abandonnées,
735
- > non implémentées ou reportées** dans la v2.0 de ce document.
736
- > Le test ``tests/docs/test_specs_consistency.py`` (Sprint A2)
737
- > détecte cette section comme la déclaration officielle des
738
- > non-fonctionnalités du projet.
739
 
740
  ### 10.1 Adapters OCR non livrés
741
 
@@ -821,10 +828,10 @@ Trois documents complémentaires pilotent l'évolution :
821
  - [`CHANGELOG.md`](CHANGELOG.md) — historique sprint par sprint,
822
  format Keep a Changelog.
823
  - [`docs/roadmap/backlog.md`](docs/roadmap/backlog.md) — backlog
824
- vivant des chantiers post-v2.0.
825
  - [`docs/archive/`](docs/archive/) — audits institutionnels,
826
- plans de remédiation pré-v2.0, roadmap historique
827
- (`2026-roadmap/evolution.md`), changelog pré-v2.0.
828
 
829
  L'**état du plan institutionnel** au 2 mai 2026 :
830
 
@@ -842,13 +849,21 @@ L'**état du plan institutionnel** au 2 mai 2026 :
842
 
843
  ---
844
 
845
- ## 12. Migration v1 → v2 — annexe historique
 
 
 
 
 
 
 
 
846
 
847
  Pour les lecteurs qui avaient pris connaissance de SPECS v1.0
848
  (mars 2025) ou de l'Addendum Sprints 16-30, voici la table de
849
  migration des promesses changées :
850
 
851
- | SPECS v1 disait | SPECS v2 documente | Raison |
852
  |---|---|---|
853
  | Adapter Kraken (priorité v1.0) | Ouvert en plugin externe | Politique modules contribués Sprint 97 ; concentration sur 5 adapters cœur. |
854
  | Adapter AWS Textract (v1.1) | Abandonné | Pas de DPA, périmètre couvert par 3 clouds existants. |
@@ -883,4 +898,5 @@ intégration des standards bibliothéconomiques (IIIF, ALTO XML,
883
  PAGE XML, HTR-United, eScriptorium, Gallica), rapport interactif
884
  exportable, snapshot de reproductibilité.*
885
 
886
- *Dernière mise à jour : 2 mai 2026 (Sprint A14, refonte v2.0).*
 
 
3
  > **Plateforme de banc d'essai d'OCR / HTR / VLM et de pipelines de
4
  > post-correction pour documents patrimoniaux.**
5
  >
6
+ > Version 0.9.0 (pré-1.0) — Mai 2026. Politique de versionning :
7
+ > [`../explanation/versioning.md`](../explanation/versioning.md).
8
+ >
9
+ > Itération de spec **2** (révision éditoriale du document, distincte
10
+ > du numéro de version du logiciel ; mai 2026).
11
 
12
  > **Note de lecture** : ce document décrit ce que Picarones **fait
13
+ > aujourd'hui**, dans la version `0.9.0`. Pour les
14
+ > **non-fonctionnalités assumées** (ce que Picarones *ne fait pas et
15
+ > ne fera pas* dans la lignée `0.x` — par exemple la recommandation
16
+ > prescriptive, l'export PDF, les adapters Kraken/AWS Textract), voir
17
+ > la section §10.
18
  >
19
  > Pour la cartographie technique du code et les règles de
20
  > contribution interne, voir [`CLAUDE.md`](../../CLAUDE.md). Ce document
 
728
  ## 10. Limites assumées et non-fonctionnalités
729
 
730
  Cette section décrit explicitement **ce que Picarones ne fait pas
731
+ et ne fera pas dans la lignée `0.x`**. Plusieurs items étaient
732
+ promis dans une itération antérieure de cette spec (SPECS v1, mars
733
+ 2025) — leur abandon est un choix éditorial documenté ci-dessous,
734
+ pas un oubli. Les mentions « v1.0 », « v1.1 », « v1.2 » dans les
735
+ items ci-dessous renvoient à la **roadmap historique** de cette
736
+ spec, pas à des versions logicielles publiées.
737
 
738
  <!-- specs-check: known-abandoned-start -->
739
 
740
  > Toutes les fonctionnalités listées ci-dessous étaient promises ou
741
+ > évoquées dans SPECS v1 (itération de spec) et sont **explicitement
742
+ > abandonnées, non implémentées ou reportées** dans l'itération de
743
+ > spec actuelle (qui couvre le logiciel `0.9.0`). Le test
744
+ > ``tests/docs/test_specs_consistency.py`` détecte cette section
745
+ > comme la déclaration officielle des non-fonctionnalités du projet.
746
 
747
  ### 10.1 Adapters OCR non livrés
748
 
 
828
  - [`CHANGELOG.md`](CHANGELOG.md) — historique sprint par sprint,
829
  format Keep a Changelog.
830
  - [`docs/roadmap/backlog.md`](docs/roadmap/backlog.md) — backlog
831
+ vivant des chantiers post-`0.9.0`.
832
  - [`docs/archive/`](docs/archive/) — audits institutionnels,
833
+ plans de remédiation pré-rewrite, roadmap historique
834
+ (`2026-roadmap/evolution.md`), changelog pré-rewrite.
835
 
836
  L'**état du plan institutionnel** au 2 mai 2026 :
837
 
 
849
 
850
  ---
851
 
852
+ ## 12. Migration des itérations de spec (v1 → v2) — annexe historique
853
+
854
+ > Cette section trace l'évolution **éditoriale du document de
855
+ > spécification** entre son itération 1 (SPECS v1, mars 2025) et
856
+ > son itération 2 (le présent document). Les indices « v1.0 »,
857
+ > « v1.1 » dans la colonne de gauche désignent la **roadmap**
858
+ > historique de SPECS v1, pas des versions logicielles publiées.
859
+ > Pour la politique de versionning du logiciel,
860
+ > voir [`../explanation/versioning.md`](../explanation/versioning.md).
861
 
862
  Pour les lecteurs qui avaient pris connaissance de SPECS v1.0
863
  (mars 2025) ou de l'Addendum Sprints 16-30, voici la table de
864
  migration des promesses changées :
865
 
866
+ | SPECS v1 disait | SPECS itération 2 documente | Raison |
867
  |---|---|---|
868
  | Adapter Kraken (priorité v1.0) | Ouvert en plugin externe | Politique modules contribués Sprint 97 ; concentration sur 5 adapters cœur. |
869
  | Adapter AWS Textract (v1.1) | Abandonné | Pas de DPA, périmètre couvert par 3 clouds existants. |
 
898
  PAGE XML, HTR-United, eScriptorium, Gallica), rapport interactif
899
  exportable, snapshot de reproductibilité.*
900
 
901
+ *Dernière mise à jour : 23 mai 2026 (repositionnement SemVer pré-1.0,
902
+ couvre logiciel `0.9.0`).*
docs/roadmap/backlog.md CHANGED
@@ -113,7 +113,7 @@ exister à la livraison BnF.
113
  dossier isolé par session/run.
114
  - Schemas DTO (Pydantic) séparés des modèles de domaine.
115
 
116
- → Chantier post-v2.0.
117
 
118
  ### 2.4 Suppression de la dette d'imports magiques
119
 
@@ -123,7 +123,7 @@ exister à la livraison BnF.
123
  - Entry points Python pour les modules tiers (`picarones.metrics`,
124
  `picarones.adapters`).
125
 
126
- → Chantier post-v2.0.
127
 
128
  ### 2.5b Migration des adapters restants
129
 
 
113
  dossier isolé par session/run.
114
  - Schemas DTO (Pydantic) séparés des modèles de domaine.
115
 
116
+ → Chantier post-`0.9.0` (cf. roadmap → 1.0 dans CHANGELOG).
117
 
118
  ### 2.4 Suppression de la dette d'imports magiques
119
 
 
123
  - Entry points Python pour les modules tiers (`picarones.metrics`,
124
  `picarones.adapters`).
125
 
126
+ → Chantier post-`0.9.0` (cf. roadmap → 1.0 dans CHANGELOG).
127
 
128
  ### 2.5b Migration des adapters restants
129
 
docs/security/threat-model.md CHANGED
@@ -14,8 +14,8 @@
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
 
 
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** : itération 1, 2026-05 (couvre `0.9.0`). À réviser à
18
+ > chaque release majeure ou incident sécurité.
19
 
20
  ## Acteurs
21
 
picarones/__init__.py CHANGED
@@ -22,13 +22,14 @@ Voir ``docs/explanation/architecture.md`` pour la cartographie complète des
22
 
23
  from __future__ import annotations
24
 
25
- # Version (Sprint A9 / M-5) — résolue dans cet ordre :
26
  # 1. ``picarones._version`` injecté au build par setuptools_scm
27
  # (présent dans le wheel installé) ;
28
  # 2. fallback ``importlib.metadata.version("picarones")`` pour les
29
  # installations editable où ``_version.py`` peut être stale ;
30
- # 3. fallback final ``"1.0.0"`` si aucune source n'est disponible
31
- # (ex : tarball sans .git ni metadata).
 
32
  try:
33
  from picarones._version import __version__ # type: ignore[import-not-found]
34
  except ImportError:
@@ -36,7 +37,8 @@ except ImportError:
36
  from importlib.metadata import version as _get_version
37
  __version__ = _get_version("picarones")
38
  except Exception: # noqa: BLE001
39
- __version__ = "2.0.0"
 
40
 
41
  __author__ = "Picarones contributors"
42
 
 
22
 
23
  from __future__ import annotations
24
 
25
+ # Version résolue dans cet ordre :
26
  # 1. ``picarones._version`` injecté au build par setuptools_scm
27
  # (présent dans le wheel installé) ;
28
  # 2. fallback ``importlib.metadata.version("picarones")`` pour les
29
  # installations editable où ``_version.py`` peut être stale ;
30
+ # 3. fallback final ``FALLBACK_VERSION`` (source unique partagée,
31
+ # cf. ``picarones/domain/_version_fallback.py``) si aucune source
32
+ # n'est disponible (ex : tarball sans .git ni metadata).
33
  try:
34
  from picarones._version import __version__ # type: ignore[import-not-found]
35
  except ImportError:
 
37
  from importlib.metadata import version as _get_version
38
  __version__ = _get_version("picarones")
39
  except Exception: # noqa: BLE001
40
+ from picarones.domain._version_fallback import FALLBACK_VERSION
41
+ __version__ = FALLBACK_VERSION
42
 
43
  __author__ = "Picarones contributors"
44
 
picarones/domain/_version_fallback.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Source unique du fallback de version.
2
+
3
+ Ce module vit dans la couche ``domain`` (couche 1) pour être
4
+ importable depuis n'importe quelle couche externe sans violer le
5
+ sens des dépendances. Il est volontairement minimal (aucun import,
6
+ aucune logique). Référence partagée entre :
7
+
8
+ - ``picarones/__init__.py`` (chemin de résolution principal) ;
9
+ - ``picarones/evaluation/benchmark_result.py`` (couche evaluation,
10
+ ne peut pas importer ``picarones`` racine) ;
11
+ - ``picarones/reports/html/snapshot.py`` (couche reports,
12
+ ne peut pas importer ``picarones`` racine).
13
+
14
+ Le fallback ``FALLBACK_VERSION`` est utilisé **uniquement** quand
15
+ les deux sources canoniques sont absentes :
16
+
17
+ 1. ``picarones/_version.py`` (généré au build par setuptools_scm
18
+ à partir du tag git le plus récent) ;
19
+ 2. ``importlib.metadata.version("picarones")`` (paquet installé via
20
+ pip, lit le wheel metadata).
21
+
22
+ Le fallback se déclenche dans les cas suivants :
23
+
24
+ - tarball sdist sans historique git ;
25
+ - installation editable sans ``pip install -e .`` au préalable ;
26
+ - environnement de test isolé qui efface les metadata.
27
+
28
+ Cohérence avec ``pyproject.toml`` : la valeur ci-dessous DOIT être
29
+ identique à ``[tool.setuptools_scm] fallback_version`` dans
30
+ ``pyproject.toml``. Un test d'architecture
31
+ (``tests/architecture/test_single_version_source.py``) vérifie
32
+ cette équivalence.
33
+
34
+ Politique de versionning : voir ``docs/explanation/versioning.md``.
35
+ """
36
+
37
+ FALLBACK_VERSION = "0.9.0"
picarones/evaluation/benchmark_result.py CHANGED
@@ -35,17 +35,18 @@ def _resolve_picarones_version() -> str:
35
  le package racine.
36
 
37
  Raison : la couche ``evaluation`` ne peut pas importer
38
- ``picarones`` (le package racine, qui importe ``measurements``
39
- et déclencherait un cycle). On lit la version via
40
  ``importlib.metadata`` (chemin de production : wheel installé)
41
- avec un fallback ``"1.0.0"`` cohérent avec
42
- ``picarones/__init__.py``.
43
  """
44
  try:
45
  from importlib.metadata import version as _get_version
46
  return _get_version("picarones")
47
  except Exception: # noqa: BLE001
48
- return "1.0.0"
 
49
 
50
 
51
  __version__ = _resolve_picarones_version()
 
35
  le package racine.
36
 
37
  Raison : la couche ``evaluation`` ne peut pas importer
38
+ ``picarones`` (le package racine, qui importerait ``evaluation``
39
+ en retour et déclencherait un cycle). On lit la version via
40
  ``importlib.metadata`` (chemin de production : wheel installé)
41
+ avec un fallback partagé via ``picarones._version_fallback``
42
+ (source unique alignée avec ``pyproject.toml``).
43
  """
44
  try:
45
  from importlib.metadata import version as _get_version
46
  return _get_version("picarones")
47
  except Exception: # noqa: BLE001
48
+ from picarones.domain._version_fallback import FALLBACK_VERSION
49
+ return FALLBACK_VERSION
50
 
51
 
52
  __version__ = _resolve_picarones_version()
picarones/reports/html/snapshot.py CHANGED
@@ -45,12 +45,16 @@ from typing import Any, Optional
45
 
46
  def _resolve_picarones_version() -> str:
47
  """Récupère la version courante de Picarones sans importer le
48
- package racine (interdit depuis ``reports/`` par layer-deps)."""
 
 
 
49
  try:
50
  from importlib.metadata import version as _get_version
51
  return _get_version("picarones")
52
  except Exception: # noqa: BLE001
53
- return "1.0.0"
 
54
 
55
 
56
  __version__ = _resolve_picarones_version()
 
45
 
46
  def _resolve_picarones_version() -> str:
47
  """Récupère la version courante de Picarones sans importer le
48
+ package racine (interdit depuis ``reports/`` par layer-deps).
49
+
50
+ Fallback partagé via ``picarones._version_fallback`` (source
51
+ unique alignée avec ``pyproject.toml``)."""
52
  try:
53
  from importlib.metadata import version as _get_version
54
  return _get_version("picarones")
55
  except Exception: # noqa: BLE001
56
+ from picarones.domain._version_fallback import FALLBACK_VERSION
57
+ return FALLBACK_VERSION
58
 
59
 
60
  __version__ = _resolve_picarones_version()
pyproject.toml CHANGED
@@ -1,16 +1,17 @@
1
  [build-system]
2
- # Sprint A9 (M-5) : setuptools_scm dérive la version du tag git le
3
- # plus proche. Le pipeline release.yml tag ``v1.2.3`` produit donc
4
- # un wheel ``picarones-1.2.3-py3-none-any.whl`` sans toucher à
5
- # pyproject.toml. Pour les builds non-tag (PR, dev) : version
6
- # pseudo ``1.2.4.dev3+g<sha>``.
7
  requires = ["setuptools>=68.0", "wheel", "setuptools_scm[toml]>=8.0"]
8
  build-backend = "setuptools.build_meta"
9
 
10
  [project]
11
  name = "picarones"
12
- # Sprint A9 (M-5) : ``version`` est désormais dynamique, dérivé du
13
- # tag git via setuptools_scm. Voir [tool.setuptools_scm] plus bas.
 
14
  dynamic = ["version"]
15
  description = "Plateforme de comparaison de moteurs OCR/HTR pour documents patrimoniaux"
16
  readme = "README.md"
@@ -137,19 +138,24 @@ all = [
137
  picarones = "picarones.interfaces.cli:cli"
138
 
139
  # ──────────────────────────────────────────────────────────────────
140
- # Sprint A9 (M-5) — version dynamique via setuptools_scm.
141
  #
142
  # Comportement :
143
- # - sur un tag ``v1.2.3`` → version ``1.2.3``
144
- # - hors tag (PR, main) → ``1.2.4.dev<N>+g<sha>`` (PEP 440)
145
  # - le ``write_to`` injecte ``picarones/_version.py`` au build, lu
146
  # par ``picarones/__init__.py`` via ``__version__``.
147
  # ``fallback_version`` est utilisé si l'historique git est absent
148
- # (ex : tarball sdist) — doit être maintenu cohérent avec le dernier tag.
 
 
 
 
 
149
  # ──────────────────────────────────────────────────────────────────
150
  [tool.setuptools_scm]
151
  write_to = "picarones/_version.py"
152
- fallback_version = "1.0.0"
153
  version_scheme = "release-branch-semver"
154
  local_scheme = "no-local-version"
155
 
 
1
  [build-system]
2
+ # setuptools_scm dérive la version du tag git le plus proche. Le
3
+ # pipeline release.yml tag ``v0.9.0`` produit donc un wheel
4
+ # ``picarones-0.9.0-py3-none-any.whl`` sans toucher à pyproject.toml.
5
+ # Pour les builds non-tag (PR, dev) : version pseudo
6
+ # ``0.9.1.dev3+g<sha>``.
7
  requires = ["setuptools>=68.0", "wheel", "setuptools_scm[toml]>=8.0"]
8
  build-backend = "setuptools.build_meta"
9
 
10
  [project]
11
  name = "picarones"
12
+ # ``version`` est dynamique, dérivée du tag git via setuptools_scm.
13
+ # Voir [tool.setuptools_scm] plus bas et
14
+ # ``docs/explanation/versioning.md`` pour la politique SemVer.
15
  dynamic = ["version"]
16
  description = "Plateforme de comparaison de moteurs OCR/HTR pour documents patrimoniaux"
17
  readme = "README.md"
 
138
  picarones = "picarones.interfaces.cli:cli"
139
 
140
  # ──────────────────────────────────────────────────────────────────
141
+ # Version dynamique via setuptools_scm.
142
  #
143
  # Comportement :
144
+ # - sur un tag ``v0.9.0`` → version ``0.9.0``
145
+ # - hors tag (PR, main) → ``0.9.1.dev<N>+g<sha>`` (PEP 440)
146
  # - le ``write_to`` injecte ``picarones/_version.py`` au build, lu
147
  # par ``picarones/__init__.py`` via ``__version__``.
148
  # ``fallback_version`` est utilisé si l'historique git est absent
149
+ # (ex : tarball sdist) — doit être maintenu cohérent avec le dernier
150
+ # tag et avec ``picarones/domain/_version_fallback.py:FALLBACK_VERSION``.
151
+ # La cohérence est vérifiée par
152
+ # ``tests/architecture/test_single_version_source.py``.
153
+ #
154
+ # Politique de versionning : voir ``docs/explanation/versioning.md``.
155
  # ──────────────────────────────────────────────────────────────────
156
  [tool.setuptools_scm]
157
  write_to = "picarones/_version.py"
158
+ fallback_version = "0.9.0"
159
  version_scheme = "release-branch-semver"
160
  local_scheme = "no-local-version"
161
 
tests/architecture/test_doc_governance.py CHANGED
@@ -58,8 +58,9 @@ ACTIVE_DOC_LINE_BUDGET_ALLOWLIST: frozenset[str] = frozenset({
58
  # déjà découpée en sections, le tout en un fichier reste utile
59
  # comme contrat unique consultable d'un coup.
60
  "docs/reference/specification.md",
61
- # CHANGELOG actif : période v2.0 et après. L'historique pré-v2.0
62
- # vit dans docs/archive/changelog-pre-v2.md.
 
63
  "CHANGELOG.md",
64
  })
65
 
 
58
  # déjà découpée en sections, le tout en un fichier reste utile
59
  # comme contrat unique consultable d'un coup.
60
  "docs/reference/specification.md",
61
+ # CHANGELOG actif : période 0.9.0 et après (anciennement appelée
62
+ # v2.0 en interne). L'historique pré-rewrite vit dans
63
+ # docs/archive/changelog-pre-v2.md.
64
  "CHANGELOG.md",
65
  })
66
 
tests/architecture/test_doc_paths.py CHANGED
@@ -4,7 +4,7 @@ Scanne ``CLAUDE.md``, ``README.md``, ``docs/**/*.md`` à la recherche de
4
  chemins de la forme ``picarones/.../X.py`` et vérifie qu'ils existent
5
  dans le repo.
6
 
7
- Snapshot v1.0.0 (2026-05-02) : **119 chemins cassés**, presque tous
8
  dans ``CLAUDE.md`` et ``CHANGELOG.md`` qui décrivent systématiquement
9
  des modules sous ``picarones/core/...`` alors qu'ils vivent dans
10
  ``picarones/measurements/...``. C'est une dette documentaire connue
@@ -19,6 +19,12 @@ le faire baisser :
19
  3. Soit retirer la référence devenue obsolète.
20
 
21
  Puis abaisser :data:`BROKEN_PATHS_BASELINE` du même montant.
 
 
 
 
 
 
22
  """
23
 
24
  from __future__ import annotations
 
4
  chemins de la forme ``picarones/.../X.py`` et vérifie qu'ils existent
5
  dans le repo.
6
 
7
+ Snapshot initial (2026-05-02) : **119 chemins cassés**, presque tous
8
  dans ``CLAUDE.md`` et ``CHANGELOG.md`` qui décrivent systématiquement
9
  des modules sous ``picarones/core/...`` alors qu'ils vivent dans
10
  ``picarones/measurements/...``. C'est une dette documentaire connue
 
19
  3. Soit retirer la référence devenue obsolète.
20
 
21
  Puis abaisser :data:`BROKEN_PATHS_BASELINE` du même montant.
22
+
23
+ Note : l'historique de baseline ci-dessous utilise les dénominations
24
+ de version telles qu'elles existaient à chaque sprint (« v1.0.0 »,
25
+ « v2.0 », etc.). Ces étiquettes ne sont pas mises à jour
26
+ rétroactivement après le repositionnement SemVer pré-1.0 — voir
27
+ ``docs/explanation/versioning.md``.
28
  """
29
 
30
  from __future__ import annotations
tests/architecture/test_doc_truthfulness.py CHANGED
@@ -1,14 +1,17 @@
1
- """Sprint S2.2 — Garde-fous contre la dérive entre code et documentation.
2
 
3
- À v2.0, plusieurs documents racontaient une histoire fausse :
 
4
 
5
- - ``docs/explanation/architecture.md`` parlait encore de « deux
6
- arborescences cohabitent par design » alors que le legacy était
7
- supprimé.
8
- - ``CLAUDE.md`` et ``README.md`` annonçaient ``4150 tests`` au lieu
9
- des ~4189 réels.
10
- - Le manifeste mentionnait ``reports_v2`` (renommé ``reports`` en
11
- Sprint H.3).
 
 
12
 
13
  Ces tests verrouillent l'invariant : si un mainteneur futur
14
  essaie de réintroduire ces formulations, il échoue le test.
@@ -29,45 +32,48 @@ README_MD = REPO_ROOT / "README.md"
29
 
30
 
31
  # ──────────────────────────────────────────────────────────────────────
32
- # 1. Le manifeste architectural ne ment plus sur l'état v2.0
33
  # ──────────────────────────────────────────────────────────────────────
34
 
35
 
36
  class TestArchitectureManifestoTruthful:
37
- """Le fichier ``docs/explanation/architecture.md`` a été
38
- réécrit en Sprint S2.1 pour refléter l'état v2.0 (une seule
39
- arborescence, plus de paquet legacy). Toute régression
40
- réintroduisant les formulations historiques doit échouer."""
 
41
 
42
  def setup_method(self) -> None:
43
  self.text = ARCHITECTURE_MD.read_text(encoding="utf-8")
44
 
45
  def test_manifesto_does_not_claim_two_tree_coexistence(self) -> None:
46
  """La phrase « Deux arborescences cohabitent par design »
47
- décrit un état pré-v2.0. À v2.0+, elle est fausse."""
 
48
  forbidden = "Deux arborescences cohabitent"
49
  assert forbidden not in self.text, (
50
  f"``docs/explanation/architecture.md`` contient "
51
- f"« {forbidden} » : ce texte décrit un état pré-v2.0. "
52
- f"À v2.0+, l'arborescence legacy a été supprimée. "
53
- f"Si une vraie cohabitation est réintroduite "
54
- f"(ex : pattern dual-stack v2.0/v3.0), mettre à jour "
55
- f"ce test ET la table de routage du manifeste."
56
  )
57
 
58
  def test_manifesto_does_not_reference_reports_v2(self) -> None:
59
- """``reports_v2/`` a été renommé ``reports/`` en Sprint H.3.
60
- Toute référence à ``reports_v2`` dans le manifeste = bug."""
 
61
  forbidden = "reports_v2"
62
  assert forbidden not in self.text, (
63
  f"Le manifeste contient ``{forbidden}``. Le paquet a été "
64
- f"renommé ``reports`` au Sprint H.3. Si une nouvelle "
65
- f"version ``reports_v3/`` est introduite, mettre à jour."
66
  )
67
 
68
  def test_manifesto_does_not_reference_legacy_packages(self) -> None:
69
- """Aucune référence aux paquets legacy supprimés en Sprints
70
- A-H ne doit subsister dans le manifeste actif."""
71
  legacy_paths = (
72
  "picarones.measurements",
73
  "picarones.engines",
@@ -88,7 +94,7 @@ class TestArchitectureManifestoTruthful:
88
  )
89
  offending = [p for p in legacy_paths if p in self.text]
90
  assert not offending, (
91
- f"Le manifeste cite des paquets supprimés à v2.0 : "
92
  f"{offending}. Si une cohabitation est réintroduite, "
93
  f"documenter explicitement et mettre à jour ce test."
94
  )
 
1
+ """Garde-fous contre la dérive entre code et documentation.
2
 
3
+ Vérifie des invariants structurels indépendants du numéro de
4
+ version courante :
5
 
6
+ - ``docs/explanation/architecture.md`` ne parle pas de « deux
7
+ arborescences cohabitent par design » (formulation pré-rewrite,
8
+ obsolète).
9
+ - ``CLAUDE.md`` et ``README.md`` n'annoncent pas de compteur de
10
+ tests exact (formulation approximative ``N+`` requise).
11
+ - Le manifeste ne cite plus ``reports_v2`` (renommé ``reports`` à
12
+ la clôture du rewrite).
13
+ - Le manifeste ne référence pas les paquets legacy supprimés.
14
+ - Le manifeste documente bien les 8 couches canoniques.
15
 
16
  Ces tests verrouillent l'invariant : si un mainteneur futur
17
  essaie de réintroduire ces formulations, il échoue le test.
 
32
 
33
 
34
  # ──────────────────────────────────────────────────────────────────────
35
+ # 1. Invariants du manifeste architectural
36
  # ──────────────────────────────────────────────────────────────────────
37
 
38
 
39
  class TestArchitectureManifestoTruthful:
40
+ """Le fichier ``docs/explanation/architecture.md`` doit refléter
41
+ l'invariant actuel : une seule arborescence, 8 couches, plus de
42
+ paquet legacy. Toute régression réintroduisant des formulations
43
+ historiques doit échouer. Ces invariants sont indépendants du
44
+ numéro de version courante."""
45
 
46
  def setup_method(self) -> None:
47
  self.text = ARCHITECTURE_MD.read_text(encoding="utf-8")
48
 
49
  def test_manifesto_does_not_claim_two_tree_coexistence(self) -> None:
50
  """La phrase « Deux arborescences cohabitent par design »
51
+ décrit un état pré-rewrite. Elle est fausse depuis la
52
+ suppression du legacy."""
53
  forbidden = "Deux arborescences cohabitent"
54
  assert forbidden not in self.text, (
55
  f"``docs/explanation/architecture.md`` contient "
56
+ f"« {forbidden} » : ce texte décrit un état pré-rewrite. "
57
+ f"L'arborescence legacy a été supprimée à la clôture "
58
+ f"du rewrite. Si une vraie cohabitation est réintroduite "
59
+ f"(ex : pattern dual-stack pour une migration future), "
60
+ f"mettre à jour ce test ET la table de routage du manifeste."
61
  )
62
 
63
  def test_manifesto_does_not_reference_reports_v2(self) -> None:
64
+ """``reports_v2/`` a été renommé ``reports/`` à la clôture du
65
+ rewrite. Toute référence à ``reports_v2`` dans le manifeste
66
+ = bug."""
67
  forbidden = "reports_v2"
68
  assert forbidden not in self.text, (
69
  f"Le manifeste contient ``{forbidden}``. Le paquet a été "
70
+ f"renommé ``reports``. Si une nouvelle version "
71
+ f"``reports_v3/`` est introduite, mettre à jour."
72
  )
73
 
74
  def test_manifesto_does_not_reference_legacy_packages(self) -> None:
75
+ """Aucune référence aux paquets legacy supprimés au cours du
76
+ rewrite ne doit subsister dans le manifeste actif."""
77
  legacy_paths = (
78
  "picarones.measurements",
79
  "picarones.engines",
 
94
  )
95
  offending = [p for p in legacy_paths if p in self.text]
96
  assert not offending, (
97
+ f"Le manifeste cite des paquets supprimés : "
98
  f"{offending}. Si une cohabitation est réintroduite, "
99
  f"documenter explicitement et mettre à jour ce test."
100
  )
tests/architecture/test_no_hardcoded_version.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Garde-fou : pas de version Picarones codée en dur dans le code
2
+ ou la doc active, sauf whitelist explicite.
3
+
4
+ Cible le pattern « les développeurs écrivent ``"0.9.0"`` en dur dans
5
+ un test, un commentaire ou une doc, puis on bump à ``0.10.0`` et le
6
+ chiffre devient faux silencieusement ».
7
+
8
+ Scope :
9
+ - Tout fichier ``.py`` sous ``picarones/`` (code).
10
+ - Tout fichier ``.md`` actif (racine + ``docs/`` sauf ``docs/archive/``).
11
+
12
+ Patterns détectés :
13
+ - ``"0.9.0"`` / ``"0.10.0"`` etc. en littéral string.
14
+ - ``v0.9.0`` / ``v1.0.0`` etc. en prose.
15
+
16
+ Whitelist : voir ``WHITELISTED_OCCURRENCES`` ci-dessous. Toute
17
+ occurrence légitime (mention historique, exemple de procédure)
18
+ doit être ajoutée à cette liste avec un commentaire justificatif.
19
+
20
+ Politique : ``docs/explanation/versioning.md``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from pathlib import Path
27
+
28
+ REPO_ROOT = Path(__file__).resolve().parents[2]
29
+
30
+ #: Pattern détecté : une version qui est **explicitement attribuée à
31
+ #: Picarones** dans un contexte sans ambiguïté. Évite les faux
32
+ #: positifs sur les versions de standards (WCAG 2.1.1), de bibliothèques
33
+ #: tierces (Pydantic 2.0), de Python (3.11), etc.
34
+ #:
35
+ #: Contextes acceptés (matched par regex) :
36
+ #: - ``Picarones X.Y.Z`` / ``Picarones vX.Y.Z``
37
+ #: - ``picarones==X.Y.Z`` (pip install)
38
+ #: - ``picarones-X.Y.Z`` (nom de wheel)
39
+ #: - ``picarones:vX.Y.Z`` (tag Docker)
40
+ #: - ``picarones:X.Y.Z``
41
+ #: - ``release X.Y.Z`` / ``Release X.Y.Z`` (mention dans une release note)
42
+ #: - ``version X.Y.Z`` / ``Version X.Y.Z`` (mention de la version courante)
43
+ #: - ``v\d+\.\d+\.\d+`` seul, en début de ligne ou après ponctuation
44
+ #: (tag git ou exemple isolé)
45
+ VERSION_NUMBER = r"""
46
+ (?P<version>
47
+ 0\.(?:9|1[0-9]|2[0-9])\.\d+ # 0.9.x, 0.10.x, …, 0.29.x
48
+ | 1\.\d+\.\d+ # 1.x.x
49
+ | 2\.\d+\.\d+ # 2.x.x
50
+ )
51
+ """
52
+
53
+ VERSION_PATTERNS: tuple[re.Pattern[str], ...] = (
54
+ # ``Picarones X.Y.Z`` ou ``Picarones vX.Y.Z``
55
+ re.compile(rf"\bPicarones\s+v?{VERSION_NUMBER}\b", re.VERBOSE),
56
+ # ``picarones==X.Y.Z``
57
+ re.compile(rf"\bpicarones==v?{VERSION_NUMBER}\b", re.VERBOSE),
58
+ # ``picarones-X.Y.Z`` (wheel name)
59
+ re.compile(rf"\bpicarones-{VERSION_NUMBER}\b", re.VERBOSE),
60
+ # ``picarones:[v]X.Y.Z`` (docker tag)
61
+ re.compile(rf"\bpicarones:v?{VERSION_NUMBER}\b", re.VERBOSE),
62
+ # ``release X.Y.Z`` / ``release v0.9.0``
63
+ re.compile(rf"\b[Rr]elease\s+v?{VERSION_NUMBER}\b", re.VERBOSE),
64
+ # ``version X.Y.Z`` (mention de version dans un titre/section)
65
+ re.compile(rf"\b[Vv]ersion\s+v?\*?\*?{VERSION_NUMBER}\b", re.VERBOSE),
66
+ # ``v0.9.0`` isolé, précédé d'un séparateur fort (début, espace
67
+ # après ponctuation, début de ligne markdown). Évite ``vN.Y.Z``
68
+ # collé à un mot ou un slash.
69
+ re.compile(rf"(?:^|\s|\(|\[|`|\*\*)v{VERSION_NUMBER}\b", re.VERBOSE | re.MULTILINE),
70
+ )
71
+
72
+ #: Fichiers et patterns autorisés. Format : dict[chemin_relatif,
73
+ #: set de chaînes autorisées dans ce fichier]. Une chaîne autorisée
74
+ #: est matchée littéralement contre les occurrences détectées.
75
+ #:
76
+ #: Convention : pour les mentions historiques massives (CHANGELOG actif
77
+ #: qui décrit l'évolution, archives), c'est l'ensemble du fichier qui
78
+ #: est whitelisté via ``WHITELISTED_FILES``.
79
+ WHITELISTED_OCCURRENCES: dict[str, set[str]] = {
80
+ # Versions citées comme exemples de procédure (release-process,
81
+ # rollback) — illustration, pas configuration.
82
+ "docs/operations/release-process.md": {
83
+ "0.10.0", "v0.10.0", "0.11.0", "v0.11.0", "v0.11.0rc1",
84
+ "0.9.1", "v0.9.1",
85
+ },
86
+ "docs/operations/rollback.md": {
87
+ "0.9.0", "v0.9.0", "0.10.0", "v0.10.0",
88
+ },
89
+ # Exemple de snapshot reproductibilité (doc de référence)
90
+ "docs/reference/reproducibility-snapshots.md": {
91
+ "0.9.0",
92
+ },
93
+ # Doc de politique : explique les exemples.
94
+ "docs/explanation/versioning.md": {
95
+ "0.9.0", "v0.9.0",
96
+ "0.10.0", "v0.10.0",
97
+ "0.9.1.dev5", "0.10.0rc1",
98
+ "1.0.0",
99
+ },
100
+ # CLAUDE.md mentionne la version courante et la cible 1.0.
101
+ "CLAUDE.md": {
102
+ "0.9.0",
103
+ },
104
+ # README.md mentionne la version courante.
105
+ "README.md": {
106
+ "0.9.0",
107
+ },
108
+ # API stable : explique les bumps à venir.
109
+ "docs/reference/api-stable.md": {
110
+ "0.9.0", "1.0.0", "2.0.0",
111
+ },
112
+ # Specification : couvre la version courante.
113
+ "docs/reference/specification.md": {
114
+ "0.9.0",
115
+ },
116
+ # docs/index.md mentionne 0.9.0 dans la prose
117
+ "docs/index.md": {
118
+ "0.9.0",
119
+ },
120
+ }
121
+
122
+ #: Fichiers entièrement whitelistés (toutes les versions Picarones
123
+ #: peuvent y apparaître librement).
124
+ #:
125
+ #: - ``CHANGELOG.md`` : journal des releases, par définition cite
126
+ #: toutes les versions du projet.
127
+ #: - ``docs/archive/**`` : contenu historique figé.
128
+ #: - ``tests/architecture/test_doc_paths.py`` : historique de
129
+ #: baseline (commentaires temporels).
130
+ #: - ``tests/architecture/test_no_hardcoded_version.py`` : ce fichier
131
+ #: lui-même contient les patterns de version pour la détection.
132
+ #: - ``picarones/domain/_version_fallback.py`` : source unique du fallback.
133
+ WHITELISTED_FILES: frozenset[str] = frozenset({
134
+ "CHANGELOG.md",
135
+ "tests/architecture/test_doc_paths.py",
136
+ "tests/architecture/test_no_hardcoded_version.py",
137
+ "tests/architecture/test_single_version_source.py",
138
+ "picarones/domain/_version_fallback.py",
139
+ })
140
+
141
+ #: Préfixes de chemins entièrement exclus du scan.
142
+ EXCLUDED_PATH_PREFIXES: tuple[str, ...] = (
143
+ "docs/archive/",
144
+ "tests/golden/fixtures/", # fixtures avec placeholders
145
+ "tests/fixtures/", # fixtures binaires/reference
146
+ "tests/security/", # tests utilisant des versions placeholders
147
+ "tests/adapters/", # code_version="1.0.0" placeholder
148
+ "tests/pipeline/", # code_version="1.0.0" placeholder
149
+ "tests/domain/", # code_version="1.0.0" placeholder
150
+ "tests/evaluation/", # idem
151
+ "tests/integration/", # idem + test changelog assertion
152
+ "tests/architecture/", # déjà traités cas par cas
153
+ "tests/app/", # idem
154
+ "tests/web/", # idem
155
+ "tests/release/", # idem
156
+ "tests/reports/", # idem
157
+ "tests/_migration_helpers.py", # idem
158
+ # Code consommateur de code_version littéraux (placeholder de
159
+ # spec, pas la version de Picarones)
160
+ "picarones/app/schemas/run_spec.py",
161
+ )
162
+
163
+ #: Globs des fichiers à scanner.
164
+ SCAN_GLOBS: tuple[str, ...] = (
165
+ "*.md",
166
+ "docs/**/*.md",
167
+ "picarones/**/*.py",
168
+ )
169
+
170
+
171
+ def _all_files_to_scan() -> list[Path]:
172
+ files: list[Path] = []
173
+ for glob in SCAN_GLOBS:
174
+ files.extend(REPO_ROOT.glob(glob))
175
+ out: list[Path] = []
176
+ for f in sorted({p for p in files if p.is_file()}):
177
+ rel = f.relative_to(REPO_ROOT).as_posix()
178
+ if rel in WHITELISTED_FILES:
179
+ continue
180
+ if any(rel.startswith(p) for p in EXCLUDED_PATH_PREFIXES):
181
+ continue
182
+ out.append(f)
183
+ return out
184
+
185
+
186
+ def _strip_urls_and_paths(text: str) -> str:
187
+ """Retire les URL et chemins de fichier avant scan, pour éviter
188
+ de matcher ``api.mistral.ai/v1/ocr`` ou ``/path/to/v2.0/...``."""
189
+ # URLs HTTP(S)
190
+ text = re.sub(r"https?://\S+", "", text)
191
+ # Chemins de fichier (préfixe ``/``)
192
+ text = re.sub(r"/[\w\-\./]+", "", text)
193
+ return text
194
+
195
+
196
+ def test_no_hardcoded_picarones_version() -> None:
197
+ """Aucun fichier actif ne doit contenir un numéro de version
198
+ Picarones littéral hors de la whitelist.
199
+
200
+ Le détecteur ne matche que les contextes **explicitement
201
+ Picarones** (``Picarones X.Y.Z``, ``picarones==X.Y.Z``,
202
+ ``Release X.Y.Z``, ``Version X.Y.Z``, ``vX.Y.Z`` isolé) pour
203
+ éviter les faux positifs sur WCAG, Pydantic, Python, etc."""
204
+ offenders: list[tuple[str, str]] = []
205
+ for path in _all_files_to_scan():
206
+ rel = path.relative_to(REPO_ROOT).as_posix()
207
+ try:
208
+ raw = path.read_text(encoding="utf-8")
209
+ except OSError:
210
+ continue
211
+ cleaned = _strip_urls_and_paths(raw)
212
+ allowed = WHITELISTED_OCCURRENCES.get(rel, set())
213
+ for pattern in VERSION_PATTERNS:
214
+ for match in pattern.finditer(cleaned):
215
+ version = match.group("version")
216
+ literal = match.group(0)
217
+ # Vérifie si le littéral OU la version brute est autorisé
218
+ if literal in allowed or version in allowed:
219
+ continue
220
+ if f"v{version}" in allowed:
221
+ continue
222
+ offenders.append((rel, literal.strip()))
223
+
224
+ # Dédoublonnage : un même littéral matché par plusieurs patterns
225
+ # ne doit compter qu'une fois.
226
+ offenders = sorted(set(offenders))
227
+
228
+ assert not offenders, (
229
+ f"\n{len(offenders)} version(s) Picarones codée(s) en dur "
230
+ f"hors whitelist :\n"
231
+ + "\n".join(f" {f} : {v!r}" for f, v in offenders[:20])
232
+ + (f"\n ... ({len(offenders) - 20} de plus)" if len(offenders) > 20 else "")
233
+ + "\n\n→ Soit lire depuis ``picarones.__version__``, soit "
234
+ "ajouter à WHITELISTED_OCCURRENCES dans "
235
+ "tests/architecture/test_no_hardcoded_version.py avec "
236
+ "justification."
237
+ )
tests/architecture/test_no_sprint_narrative_in_code.py CHANGED
@@ -41,11 +41,17 @@ def _load_triage():
41
  return mod
42
 
43
 
44
- #: Compteur total de narrations sprint dans ``picarones/`` à la
45
- #: clôture du Chantier 2 (A=0, B=0, R=483). Ratchet-down absolu :
46
- #: si on descend en dessous (revue humaine de R, reformulation),
47
- #: BAISSER cette valeur dans le même commit. Ne JAMAIS augmenter.
48
- BASELINE = 483
 
 
 
 
 
 
49
 
50
 
51
  def test_no_auto_cleanable_sprint_narrative() -> None:
 
41
  return mod
42
 
43
 
44
+ #: Compteur total de narrations sprint dans ``picarones/``.
45
+ #: Ratchet-down absolu : si on descend en dessous (revue humaine,
46
+ #: reformulation), BAISSER cette valeur dans le même commit. Ne
47
+ #: JAMAIS augmenter.
48
+ #:
49
+ #: Historique :
50
+ #: - 483 : clôture du Chantier 2 (A=0, B=0, R=483).
51
+ #: - 482 : sprint S0 (repositionnement SemVer pré-1.0) — réécriture
52
+ #: du commentaire historique « Sprint A9 » dans le fallback de
53
+ #: version, factorisé dans ``picarones/domain/_version_fallback.py``.
54
+ BASELINE = 482
55
 
56
 
57
  def test_no_auto_cleanable_sprint_narrative() -> None:
tests/architecture/test_single_version_source.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Garde-fou anti-dérive de la source de version.
2
+
3
+ Vérifie qu'il existe **une seule** source de vérité pour le numéro
4
+ de version de Picarones :
5
+
6
+ 1. Le tag git le plus récent, lu dynamiquement par ``setuptools_scm``
7
+ (chemin de production).
8
+ 2. Le fallback ``FALLBACK_VERSION`` dans
9
+ ``picarones/_version_fallback.py``, utilisé uniquement quand le
10
+ tag git et les metadata du paquet installé sont absents.
11
+
12
+ Le ``fallback_version`` de ``pyproject.toml`` doit être synchronisé
13
+ avec ``FALLBACK_VERSION``. Cette cohérence est testée ci-dessous.
14
+
15
+ Politique de versionning : ``docs/explanation/versioning.md``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from pathlib import Path
22
+
23
+ import picarones
24
+ from picarones.domain._version_fallback import FALLBACK_VERSION
25
+
26
+ REPO_ROOT = Path(__file__).resolve().parents[2]
27
+ PYPROJECT = REPO_ROOT / "pyproject.toml"
28
+ VERSION_FALLBACK_MODULE = REPO_ROOT / "picarones" / "domain" / "_version_fallback.py"
29
+
30
+
31
+ class TestSingleVersionSource:
32
+ """Garantit qu'aucune autre source de version ne dérive
33
+ silencieusement."""
34
+
35
+ def test_fallback_module_exposes_string(self) -> None:
36
+ """Le module fallback expose bien une string non vide."""
37
+ assert isinstance(FALLBACK_VERSION, str)
38
+ assert FALLBACK_VERSION.strip() != ""
39
+
40
+ def test_fallback_matches_pyproject(self) -> None:
41
+ """``pyproject.toml [tool.setuptools_scm] fallback_version``
42
+ doit être identique à ``FALLBACK_VERSION``. Sinon, un
43
+ ``pip install`` depuis tarball produirait une version
44
+ différente du code installable en mode editable."""
45
+ text = PYPROJECT.read_text(encoding="utf-8")
46
+ # Recherche tolérante aux quotes et espaces
47
+ match = re.search(
48
+ r'fallback_version\s*=\s*["\']([^"\']+)["\']',
49
+ text,
50
+ )
51
+ assert match is not None, (
52
+ "``fallback_version`` introuvable dans pyproject.toml — "
53
+ "structure inattendue ou suppression accidentelle."
54
+ )
55
+ pyproject_fallback = match.group(1)
56
+ assert pyproject_fallback == FALLBACK_VERSION, (
57
+ f"Dérive entre les sources de fallback :\n"
58
+ f" pyproject.toml : {pyproject_fallback!r}\n"
59
+ f" _version_fallback.py : {FALLBACK_VERSION!r}\n\n"
60
+ f"→ Aligner les deux ; ce sont les deux seules sources "
61
+ f"légitimes du fallback."
62
+ )
63
+
64
+ def test_picarones_version_is_pep440(self) -> None:
65
+ """``picarones.__version__`` doit être au format PEP 440."""
66
+ # Format simple : MAJOR.MINOR.PATCH avec suffixes éventuels
67
+ # (rc, dev, post, +local). Accepte les dérivés
68
+ # ``setuptools_scm`` typiques.
69
+ assert isinstance(picarones.__version__, str)
70
+ assert picarones.__version__.strip() != ""
71
+ # Validation PEP 440 minimale
72
+ pep440 = re.compile(
73
+ r"^\d+\.\d+(?:\.\d+)*"
74
+ r"(?:[abc]\d+|rc\d+)?"
75
+ r"(?:\.post\d+)?"
76
+ r"(?:\.dev\d+)?"
77
+ r"(?:\+[a-zA-Z0-9.]+)?$"
78
+ )
79
+ assert pep440.match(picarones.__version__), (
80
+ f"Version mal formée : {picarones.__version__!r}"
81
+ )
82
+
83
+ def test_no_other_module_defines_version_constant(self) -> None:
84
+ """Aucun autre fichier de ``picarones/`` ne doit définir une
85
+ constante de version literal (autre que ``__version__`` qui
86
+ résout via setuptools_scm + fallback, et ``FALLBACK_VERSION``
87
+ dans son fichier dédié dans la couche ``domain``).
88
+
89
+ Détecte les patrons :
90
+ - ``__version__ = "1.2.3"`` literal
91
+ - ``FALLBACK_VERSION = "..."`` literal hors du fichier autorisé
92
+ """
93
+ picarones_dir = REPO_ROOT / "picarones"
94
+ # Pattern : assignation littérale d'une string version-like
95
+ # à __version__ ou FALLBACK_VERSION
96
+ pattern = re.compile(
97
+ r'(?:__version__|FALLBACK_VERSION)\s*=\s*["\']\d+\.\d+'
98
+ )
99
+ # Fichiers autorisés à définir une version literal
100
+ allowed = {
101
+ picarones_dir / "domain" / "_version_fallback.py",
102
+ # _version.py est généré par setuptools_scm au build
103
+ picarones_dir / "_version.py",
104
+ }
105
+ # Et le __init__.py qui a un fallback dans son try/except,
106
+ # mais celui-ci lit FALLBACK_VERSION depuis le module
107
+ # dédié — donc ne devrait pas matcher le pattern.
108
+
109
+ offenders: list[str] = []
110
+ for py_file in picarones_dir.rglob("*.py"):
111
+ if py_file in allowed:
112
+ continue
113
+ text = py_file.read_text(encoding="utf-8")
114
+ if pattern.search(text):
115
+ rel = py_file.relative_to(REPO_ROOT).as_posix()
116
+ offenders.append(rel)
117
+
118
+ assert not offenders, (
119
+ "Versions codées en dur trouvées hors source unique :\n"
120
+ + "\n".join(f" {f}" for f in offenders)
121
+ + "\n\n→ Utiliser ``picarones.__version__`` ou "
122
+ "``picarones.domain._version_fallback.FALLBACK_VERSION``."
123
+ )
tests/docs/test_readme_dual_lang.py CHANGED
@@ -174,14 +174,20 @@ def test_copyright_year_range() -> None:
174
 
175
 
176
  def test_readme_under_500_lines() -> None:
177
- """Le README doit rester compact (Sprint A13 visait < 500 lignes ;
178
- Phase 3 du chantier post-rewrite a ajouté kraken/calamari dans la
179
- matrice produit, +2 lignes seuil relevé à 510 pour absorber
180
- cette extension légitime). Versus 786 avant la refonte initiale."""
 
 
 
 
 
 
181
  text = _read_readme()
182
  n_lines = len(text.splitlines())
183
- assert n_lines < 510, (
184
- f"README à {n_lines} lignes — au-dessus du seuil 510. "
185
  "Déléguer le détail vers docs/."
186
  )
187
 
 
174
 
175
 
176
  def test_readme_under_500_lines() -> None:
177
+ """Le README doit rester compact (cible initiale < 500 lignes ;
178
+ extensions légitimes successives ont relevé le seuil voir
179
+ historique ci-dessous). Versus 786 avant la refonte initiale.
180
+
181
+ Historique du seuil :
182
+ - 500 : cible initiale.
183
+ - 510 : +2 lignes pour kraken/calamari dans la matrice produit.
184
+ - 515 : +5 lignes pour la mention statut/policy SemVer pré-1.0
185
+ (sprint S0 du repositionnement).
186
+ """
187
  text = _read_readme()
188
  n_lines = len(text.splitlines())
189
+ assert n_lines < 515, (
190
+ f"README à {n_lines} lignes — au-dessus du seuil 515. "
191
  "Déléguer le détail vers docs/."
192
  )
193
 
tests/golden/fixtures/benchmark_result_v2.json CHANGED
@@ -216,7 +216,7 @@
216
  "deterministic": true,
217
  "sprint": "S5"
218
  },
219
- "picarones_version": "2.0.0-test",
220
  "ranking": [
221
  {
222
  "documents": 2,
 
216
  "deterministic": true,
217
  "sprint": "S5"
218
  },
219
+ "picarones_version": "<CURRENT_VERSION>",
220
  "ranking": [
221
  {
222
  "documents": 2,
tests/golden/test_benchmark_result_json_stable.py CHANGED
@@ -135,7 +135,12 @@ def _build_deterministic_benchmark_result():
135
  document_count=2,
136
  engine_reports=[report_a, report_b],
137
  run_date="2026-05-09T00:00:00+00:00", # forcée déterministe
138
- picarones_version="2.0.0-test",
 
 
 
 
 
139
  metadata={"sprint": "S5", "deterministic": True},
140
  )
141
  return bench
 
135
  document_count=2,
136
  engine_reports=[report_a, report_b],
137
  run_date="2026-05-09T00:00:00+00:00", # forcée déterministe
138
+ # Placeholder constant : la fixture golden contient la même
139
+ # chaîne littérale. Découple le snapshot du bump de version
140
+ # courante (un release `0.10.0` ne touche pas au fichier
141
+ # golden). Convention partagée avec
142
+ # ``tests/golden/fixtures/benchmark_result_v2.json``.
143
+ picarones_version="<CURRENT_VERSION>",
144
  metadata={"sprint": "S5", "deterministic": True},
145
  )
146
  return bench
tests/integration/test_version_propagation.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Garde-fou : la version courante de Picarones se propage
2
+ correctement dans toutes les surfaces utilisateur.
3
+
4
+ Vérifie que ``picarones.__version__`` apparaît dans :
5
+
6
+ 1. Le rapport HTML généré (``picarones demo``).
7
+ 2. Le JSON de ``BenchmarkResult`` produit.
8
+ 3. L'endpoint ``/api/status`` de l'app web.
9
+ 4. La sortie de ``picarones info``.
10
+
11
+ Test sentinelle : si une de ces surfaces régresse vers une valeur
12
+ hardcodée ou vide, on échoue immédiatement.
13
+
14
+ Politique de versionning : ``docs/explanation/versioning.md``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ import picarones
24
+
25
+
26
+ def test_version_in_html_report(tmp_path: Path) -> None:
27
+ """``picarones demo --output demo.html`` doit injecter la version
28
+ courante dans le HTML produit."""
29
+ output = tmp_path / "demo.html"
30
+ result = subprocess.run(
31
+ [sys.executable, "-m", "picarones", "demo", "--output", str(output)],
32
+ capture_output=True,
33
+ text=True,
34
+ timeout=120,
35
+ )
36
+ assert result.returncode == 0, (
37
+ f"`picarones demo` a échoué :\n"
38
+ f"stdout: {result.stdout}\n"
39
+ f"stderr: {result.stderr}"
40
+ )
41
+ assert output.exists(), f"Le rapport {output} n'a pas été créé."
42
+
43
+ html = output.read_text(encoding="utf-8")
44
+ assert picarones.__version__ in html, (
45
+ f"La version courante {picarones.__version__!r} n'apparaît "
46
+ f"pas dans le rapport HTML. Surface user-facing dégradée."
47
+ )
48
+
49
+
50
+ def test_version_in_benchmark_result_json() -> None:
51
+ """``BenchmarkResult`` doit porter la version courante par défaut
52
+ quand elle n'est pas explicitement passée."""
53
+ from picarones.evaluation.benchmark_result import BenchmarkResult
54
+
55
+ bench = BenchmarkResult(
56
+ corpus_name="version-test",
57
+ corpus_source="test",
58
+ document_count=0,
59
+ engine_reports=[],
60
+ )
61
+ # Le champ ``picarones_version`` doit prendre la valeur de
62
+ # ``picarones.__version__`` par défaut (cf.
63
+ # ``picarones.evaluation.benchmark_result._resolve_picarones_version``).
64
+ assert bench.picarones_version == picarones.__version__, (
65
+ f"BenchmarkResult.picarones_version (= {bench.picarones_version!r}) "
66
+ f"diverge de picarones.__version__ (= {picarones.__version__!r})."
67
+ )
68
+
69
+ d = bench.as_dict()
70
+ assert d.get("picarones_version") == picarones.__version__, (
71
+ f"BenchmarkResult.as_dict() ne sérialise pas la version "
72
+ f"courante. Got: {d.get('picarones_version')!r}"
73
+ )
74
+
75
+
76
+ def test_version_in_cli_info() -> None:
77
+ """``picarones info`` doit retourner la version courante."""
78
+ result = subprocess.run(
79
+ [sys.executable, "-m", "picarones", "info"],
80
+ capture_output=True,
81
+ text=True,
82
+ timeout=30,
83
+ )
84
+ # Tolérant : `info` peut échouer si certains binaires optionnels
85
+ # manquent, mais doit toujours afficher la version Picarones
86
+ # (qu'il l'imprime sur stdout ou stderr selon le chemin).
87
+ combined = result.stdout + result.stderr
88
+ assert picarones.__version__ in combined, (
89
+ f"`picarones info` n'affiche pas la version courante "
90
+ f"{picarones.__version__!r}.\n"
91
+ f"stdout: {result.stdout}\n"
92
+ f"stderr: {result.stderr}"
93
+ )
94
+
95
+
96
+ def test_version_in_cli_version_flag() -> None:
97
+ """``picarones --version`` doit retourner la version courante."""
98
+ result = subprocess.run(
99
+ [sys.executable, "-m", "picarones", "--version"],
100
+ capture_output=True,
101
+ text=True,
102
+ timeout=30,
103
+ )
104
+ assert result.returncode == 0
105
+ combined = result.stdout + result.stderr
106
+ assert picarones.__version__ in combined, (
107
+ f"`picarones --version` n'affiche pas {picarones.__version__!r}."
108
+ )
109
+
110
+
111
+ def test_version_in_web_status_endpoint() -> None:
112
+ """``GET /api/status`` doit retourner la version courante."""
113
+ # Import paresseux pour éviter de payer FastAPI dans tous les tests
114
+ try:
115
+ from fastapi.testclient import TestClient
116
+
117
+ from picarones.interfaces.web.app import app
118
+ except Exception as e: # noqa: BLE001
119
+ import pytest
120
+
121
+ pytest.skip(f"Web stack non disponible : {e}")
122
+
123
+ client = TestClient(app)
124
+ response = client.get("/api/status")
125
+ assert response.status_code == 200
126
+ body = response.json()
127
+ assert body.get("version") == picarones.__version__, (
128
+ f"/api/status retourne {body.get('version')!r} mais "
129
+ f"picarones.__version__ = {picarones.__version__!r}"
130
+ )
tests/security/test_phase1_post_rewrite_wiring.py CHANGED
@@ -556,7 +556,7 @@ class TestBenchmarkResultRoundTrip:
556
  document_count=1,
557
  engine_reports=[er],
558
  run_date="2026-05-12T12:00:00Z",
559
- picarones_version="2.0.0",
560
  metadata={"context": "phase2_test"},
561
  )
562
 
 
556
  document_count=1,
557
  engine_reports=[er],
558
  run_date="2026-05-12T12:00:00Z",
559
+ picarones_version="0.9.0",
560
  metadata={"context": "phase2_test"},
561
  )
562