Spaces:
Running
chore: finir le retrait execution_mode (audit du commit precedent)
Browse filesAudit du commit 5e13c0d a revele 13 defauts ; ce commit les corrige.
## Zombies de code production
- adapters/ocr/pero_ocr.py : retire les 2 mentions ProcessPool
qui motivaient le lazy-init du parser. Le pattern reste valable
(eviter de charger PyTorch au constructeur), seul son rationale
fictif disparait.
- adapters/ocr/calamari.py : ajoute la justification GIL/TensorFlow
(asymetrie avec tesseract/pero/kraken corrigee).
## Mensonges documentation actifs
- docs/explanation/architecture.md : runner.py n'est plus decrit
comme "ProcessPool/ThreadPool" mais "ThreadPool unique".
- docs/reference/specification.md : la table "4.2 Moteurs OCR
livres" ne dit plus "CPU (ProcessPool)" / "IO (ThreadPool)"
mais decrit le profil d'execution reel (sous-processus C, PyTorch,
appel HTTP).
- docs/operations/deployment-institutional.md : le sizing institutionnel
ne parle plus de "3 GB RAM par worker ProcessPool" ni de
"ProcessPool 8 workers" mais bien du ThreadPool reel et de
max_in_flight.
## Rechutes soft sur l'option B
- pipeline/runner.py : la docstring retire la phrase "redeviendra
recevable... commit 047ab1b" qui contredisait le choix YAGNI.
- specification.md (section 4.1) : meme nettoyage, la phrase
"un dispatch multi-pool avait ete prototype puis suspendu"
remplacee par une instruction de revue de PR.
- README.md : "all supported adapters release the GIL" recadre en
"today's adapters happen to release the GIL" pour ne pas faire
croire a un invariant du projet.
- CHANGELOG : retire l'enumeration des cas d'usage hypothetiques
(overnorm, voter, MUFI) ; ces fonctionnalites ne sont pas sur
la roadmap publique, les nommer dans un changelog est du wishful
thinking architectural.
## Inconsistances introduites
- test_doc_paths.py : commentaire au-dessus de BROKEN_PATHS_BASELINE
raconte maintenant la transition 6 -> 5 et liste les 5 chemins
restants (etait reste a "6 chemins" apres modification de la
constante).
## Garde-fou trop etroit
- test_no_execution_mode_resurrection.py : scanne maintenant aussi
README.md, CLAUDE.md et docs/*.md (hors docs/archive/) en plus du
code Python. Limite assumee (symbole exact, pas concept renomme)
explicitee dans la docstring. CHANGELOG.md et test_doc_paths.py
whitelistes (mentions historiques legitimes).
## Mensonge oublie dans conftest.py
- tests/conftest.py : le workaround tqdm._monitor invoquait
"ProcessPoolExecutor du runner Picarones" pour justifier la
desactivation. Le runner n'est plus ProcessPool. Commentaire
reecrit pour assumer que la cause racine a pu disparaitre et
qu'il faudra revalider au prochain passage.
## Resultat
- 6058 tests passent (idem, 0 regression).
- ruff check vert.
- +55 LOC nets (essentiellement docstring guardrail elargie + recits
honnetes des choix dans la doc).
https://claude.ai/code/session_01B93huMjNh4CG2rNcexgDeL
- CHANGELOG.md +1 -4
- README.md +6 -4
- docs/explanation/architecture.md +1 -1
- docs/operations/deployment-institutional.md +5 -3
- docs/reference/specification.md +11 -11
- picarones/adapters/ocr/calamari.py +2 -0
- picarones/adapters/ocr/pero_ocr.py +5 -5
- picarones/pipeline/runner.py +8 -8
- tests/architecture/test_doc_paths.py +4 -2
- tests/architecture/test_no_execution_mode_resurrection.py +61 -10
- tests/conftest.py +12 -13
|
@@ -118,10 +118,7 @@ L'attribut était un mensonge structurel. L'historique git portait
|
|
| 118 |
une tentative de dispatch multi-pool (``MultiDomainCorpusRunner``,
|
| 119 |
commit ``047ab1b``) qui a été suspendue (``cd67184``). Aucun adapter
|
| 120 |
actuel n'est GIL-bound en pratique — chacun délègue à du C qui relâche
|
| 121 |
-
le GIL.
|
| 122 |
-
(détecteur d'over-normalisation, voter d'ensemble, MUFI scorer en
|
| 123 |
-
step de pipeline plutôt qu'en métrique), la décision sera explicite
|
| 124 |
-
plutôt que portée par un attribut décoratif.
|
| 125 |
|
| 126 |
---
|
| 127 |
|
|
|
|
| 118 |
une tentative de dispatch multi-pool (``MultiDomainCorpusRunner``,
|
| 119 |
commit ``047ab1b``) qui a été suspendue (``cd67184``). Aucun adapter
|
| 120 |
actuel n'est GIL-bound en pratique — chacun délègue à du C qui relâche
|
| 121 |
+
le GIL. Retirer plutôt que documenter un attribut zombi : YAGNI.
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
---
|
| 124 |
|
|
@@ -432,10 +432,12 @@ CONTRIBUTING, SECURITY, ACCESSIBILITY, and the
|
|
| 432 |
`logger.warning("[module] degraded feature: %s", e)`.
|
| 433 |
- One canonical home per module — circle dependency direction
|
| 434 |
enforced by tests.
|
| 435 |
-
- The `CorpusRunner` is thread-only by design
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
| 439 |
- Hardcoded UI strings forbidden — always go through i18n
|
| 440 |
(cf. [`docs/developer/extending-i18n.md`](docs/developer/extending-i18n.md)).
|
| 441 |
|
|
|
|
| 432 |
`logger.warning("[module] degraded feature: %s", e)`.
|
| 433 |
- One canonical home per module — circle dependency direction
|
| 434 |
enforced by tests.
|
| 435 |
+
- The `CorpusRunner` is thread-only by design. Today's adapters
|
| 436 |
+
(OCR via C binary, LLM/VLM via httpx, ML via PyTorch/TensorFlow)
|
| 437 |
+
happen to release the GIL during their blocking call, which is
|
| 438 |
+
why a single thread pool works. A future adapter doing pure-Python
|
| 439 |
+
CPU work would not benefit from parallelism — that's a constraint
|
| 440 |
+
for adapter authors to verify, not a global invariant.
|
| 441 |
- Hardcoded UI strings forbidden — always go through i18n
|
| 442 |
(cf. [`docs/developer/extending-i18n.md`](docs/developer/extending-i18n.md)).
|
| 443 |
|
|
@@ -102,7 +102,7 @@ Orchestration mono-document d'une pipeline composée :
|
|
| 102 |
| `executor.py` | `PipelineExecutor` — exécute un `PipelineSpec` step par step, capture `StepResult`, filtre outputs sur `step.output_types` |
|
| 103 |
| `planner.py` | `PipelinePlanner` — résout les `inputs_from`, valide la spec, calcule les métriques aux jonctions |
|
| 104 |
| `validation.py` | Validation statique d'une `PipelineSpec` (types s'enchaînent, pas de cycle) |
|
| 105 |
-
| `runner.py` | `CorpusRunner` — orchestration corpus-wide
|
| 106 |
| `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
|
| 107 |
| `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
|
| 108 |
| `llm_pipeline_builder.py` | `make_ocr_llm_pipeline_spec` (3 modes : text_only, text_and_image, zero_shot) |
|
|
|
|
| 102 |
| `executor.py` | `PipelineExecutor` — exécute un `PipelineSpec` step par step, capture `StepResult`, filtre outputs sur `step.output_types` |
|
| 103 |
| `planner.py` | `PipelinePlanner` — résout les `inputs_from`, valide la spec, calcule les métriques aux jonctions |
|
| 104 |
| `validation.py` | Validation statique d'une `PipelineSpec` (types s'enchaînent, pas de cycle) |
|
| 105 |
+
| `runner.py` | `CorpusRunner` — orchestration corpus-wide (ThreadPool unique), backpressure, timeout, cancellation |
|
| 106 |
| `cache.py`, `cache_helpers.py`, `cache_protocol.py` | Reprise par hash via `ArtifactCachePort` |
|
| 107 |
| `yaml_io.py` | Sérialisation YAML déterministe d'une `PipelineSpec` |
|
| 108 |
| `llm_pipeline_builder.py` | `make_ocr_llm_pipeline_spec` (3 modes : text_only, text_and_image, zero_shot) |
|
|
@@ -17,8 +17,10 @@
|
|
| 17 |
- **Python 3.11 ou 3.12** (3.13 informationnel).
|
| 18 |
- **Tesseract OCR ≥ 5.3** (avec packs `fra`, `lat`, `eng` au
|
| 19 |
minimum).
|
| 20 |
-
- **3 GB RAM
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
- **5 GB de disque** pour l'application + 50 GB recommandés pour
|
| 23 |
les uploads et la base SQLite des jobs.
|
| 24 |
|
|
@@ -272,7 +274,7 @@ uniquement** (jamais sur le filesystem en clair). Voir [`SECURITY.md`](../../SEC
|
|
| 272 |
| Charge | Configuration |
|
| 273 |
|---|---|
|
| 274 |
| < 5 jobs/h, < 5 utilisateurs | Mono-instance, SQLite, 2 vCPU / 4 GB RAM |
|
| 275 |
-
| 5–50 jobs/h, < 20 utilisateurs | Mono-instance, SQLite, 4 vCPU / 8 GB RAM,
|
| 276 |
| > 50 jobs/h | Multi-instance derrière LB, PostgreSQL centralisé, NFS uploads |
|
| 277 |
| > 500 jobs/h | Considérer un orchestrateur de tâches dédié (Celery + Redis), hors scope Picarones |
|
| 278 |
|
|
|
|
| 17 |
- **Python 3.11 ou 3.12** (3.13 informationnel).
|
| 18 |
- **Tesseract OCR ≥ 5.3** (avec packs `fra`, `lat`, `eng` au
|
| 19 |
minimum).
|
| 20 |
+
- **3 GB RAM** pour un worker FastAPI exécutant `max_in_flight=4`
|
| 21 |
+
documents en parallèle dans le ThreadPool du `CorpusRunner`
|
| 22 |
+
(profil mémoire dominé par Pillow + jiwer + les modèles OCR locaux
|
| 23 |
+
chargés une fois par instance).
|
| 24 |
- **5 GB de disque** pour l'application + 50 GB recommandés pour
|
| 25 |
les uploads et la base SQLite des jobs.
|
| 26 |
|
|
|
|
| 274 |
| Charge | Configuration |
|
| 275 |
|---|---|
|
| 276 |
| < 5 jobs/h, < 5 utilisateurs | Mono-instance, SQLite, 2 vCPU / 4 GB RAM |
|
| 277 |
+
| 5–50 jobs/h, < 20 utilisateurs | Mono-instance, SQLite, 4 vCPU / 8 GB RAM, `max_in_flight=8` |
|
| 278 |
| > 50 jobs/h | Multi-instance derrière LB, PostgreSQL centralisé, NFS uploads |
|
| 279 |
| > 500 jobs/h | Considérer un orchestrateur de tâches dédié (Celery + Redis), hors scope Picarones |
|
| 280 |
|
|
@@ -308,23 +308,23 @@ ses `input_types` et `output_types` et implémente
|
|
| 308 |
`execute(inputs, params, context, control) -> dict[ArtifactType, Artifact]`.
|
| 309 |
|
| 310 |
Le `CorpusRunner` orchestre l'exécution via un `ThreadPoolExecutor`
|
| 311 |
-
unique pour tous les adapters. Les adapters
|
| 312 |
binaire C, LLM/VLM via httpx, ML via PyTorch/TensorFlow) relâchent
|
| 313 |
le GIL pendant leur travail bloquant, donc un thread pool donne
|
| 314 |
-
les performances attendues. Un
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
GIL-bound
|
| 318 |
|
| 319 |
### 4.2 Moteurs OCR livrés
|
| 320 |
|
| 321 |
-
| Moteur | Type |
|
| 322 |
|---|---|---|---|
|
| 323 |
-
| **Tesseract 5** | Local CLI |
|
| 324 |
-
| **Pero OCR** | Local Python |
|
| 325 |
-
| **Mistral OCR** | Cloud API |
|
| 326 |
-
| **Google Vision** | Cloud API |
|
| 327 |
-
| **Azure Doc Intelligence** | Cloud API |
|
| 328 |
|
| 329 |
Quand un moteur expose ses confidences natives, le runner calcule
|
| 330 |
automatiquement les métriques de calibration (ECE, MCE, reliability
|
|
|
|
| 308 |
`execute(inputs, params, context, control) -> dict[ArtifactType, Artifact]`.
|
| 309 |
|
| 310 |
Le `CorpusRunner` orchestre l'exécution via un `ThreadPoolExecutor`
|
| 311 |
+
unique pour tous les adapters. Les adapters actuels (OCR via
|
| 312 |
binaire C, LLM/VLM via httpx, ML via PyTorch/TensorFlow) relâchent
|
| 313 |
le GIL pendant leur travail bloquant, donc un thread pool donne
|
| 314 |
+
les performances attendues. Un adapter qui ferait du calcul Python
|
| 315 |
+
pur ne profiterait pas de la parallélisation : c'est à l'auteur de
|
| 316 |
+
l'adapter de vérifier en revue de PR que son `execute()` n'est pas
|
| 317 |
+
GIL-bound avant de prétendre tourner en parallèle.
|
| 318 |
|
| 319 |
### 4.2 Moteurs OCR livrés
|
| 320 |
|
| 321 |
+
| Moteur | Type | Profil dominant | Confidence native exposée ? |
|
| 322 |
|---|---|---|---|
|
| 323 |
+
| **Tesseract 5** | Local CLI | Sous-processus C (GIL relâché) | ✅ Sprint 47 (`image_to_data`) |
|
| 324 |
+
| **Pero OCR** | Local Python | Inférence PyTorch (GIL relâché) | ✅ Sprint 48 (`transcription_confidence` ligne) |
|
| 325 |
+
| **Mistral OCR** | Cloud API | Appel HTTP | ✅ Sprint 49 (quand disponible côté API) |
|
| 326 |
+
| **Google Vision** | Cloud API | Appel HTTP | ✅ Sprint 50 (`Word.confidence` en mode `DOCUMENT_TEXT_DETECTION`) |
|
| 327 |
+
| **Azure Doc Intelligence** | Cloud API | Appel HTTP | ✅ Sprint 51 (`Word.confidence`) |
|
| 328 |
|
| 329 |
Quand un moteur expose ses confidences natives, le runner calcule
|
| 330 |
automatiquement les métriques de calibration (ECE, MCE, reliability
|
|
@@ -9,6 +9,8 @@ Calamari est un OCR open-source basé TensorFlow / Keras, conçu pour
|
|
| 9 |
les imprimés historiques et la transcription ligne par ligne.
|
| 10 |
Modèles disponibles via OCR-D, Wikisource, et le hub Calamari.
|
| 11 |
Particulièrement performant en ensemble (vote multi-modèles).
|
|
|
|
|
|
|
| 12 |
|
| 13 |
Configuration
|
| 14 |
-------------
|
|
|
|
| 9 |
les imprimés historiques et la transcription ligne par ligne.
|
| 10 |
Modèles disponibles via OCR-D, Wikisource, et le hub Calamari.
|
| 11 |
Particulièrement performant en ensemble (vote multi-modèles).
|
| 12 |
+
L'inférence passe par TensorFlow (extension C++) qui relâche le GIL,
|
| 13 |
+
donc le ``CorpusRunner`` thread-only suffit.
|
| 14 |
|
| 15 |
Configuration
|
| 16 |
-------------
|
|
@@ -36,9 +36,9 @@ Anti-sur-ingénierie
|
|
| 36 |
-------------------
|
| 37 |
- Pas de support GPU explicite (Pero OCR le gère via la config).
|
| 38 |
- Pas de retry, pas d'extraction de confidences (à ajouter quand un caller en aura besoin).
|
| 39 |
-
- ``_parser`` lazy-init
|
| 40 |
-
|
| 41 |
-
|
| 42 |
"""
|
| 43 |
|
| 44 |
from __future__ import annotations
|
|
@@ -96,8 +96,8 @@ class PeroOCRAdapter(BaseOCRAdapter):
|
|
| 96 |
self._name = name
|
| 97 |
self._config_path = Path(config_path)
|
| 98 |
# Le parser est instancié paresseusement au premier execute()
|
| 99 |
-
#
|
| 100 |
-
#
|
| 101 |
self._parser: Any = None
|
| 102 |
|
| 103 |
@property
|
|
|
|
| 36 |
-------------------
|
| 37 |
- Pas de support GPU explicite (Pero OCR le gère via la config).
|
| 38 |
- Pas de retry, pas d'extraction de confidences (à ajouter quand un caller en aura besoin).
|
| 39 |
+
- ``_parser`` lazy-init : le modèle PyTorch n'est chargé qu'au
|
| 40 |
+
premier ``execute()``, pas au constructeur — permet d'instancier
|
| 41 |
+
l'adapter sans pénalité même si le doc n'est jamais exécuté.
|
| 42 |
"""
|
| 43 |
|
| 44 |
from __future__ import annotations
|
|
|
|
| 96 |
self._name = name
|
| 97 |
self._config_path = Path(config_path)
|
| 98 |
# Le parser est instancié paresseusement au premier execute()
|
| 99 |
+
# — le constructeur reste léger et l'on évite de charger les
|
| 100 |
+
# modèles PyTorch tant qu'aucun document n'est traité.
|
| 101 |
self._parser: Any = None
|
| 102 |
|
| 103 |
@property
|
|
@@ -25,14 +25,14 @@ avec trois propriétés critiques que l'ancien
|
|
| 25 |
|
| 26 |
Limites assumées
|
| 27 |
----------------
|
| 28 |
-
- **Pool d'exécution unique : ThreadPool.**
|
| 29 |
-
|
| 30 |
-
PyTorch/TensorFlow) délèguent leur travail bloquant
|
| 31 |
-
natif qui relâche le GIL, donc un thread pool unique
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
- **Cancel coopératif sur les in-flight.** Quand ``cancel_event``
|
| 37 |
est signalé, le runner appelle ``trigger_cancel()`` sur le
|
| 38 |
``RunControl`` de chaque doc en cours : les adapters qui ont
|
|
|
|
| 25 |
|
| 26 |
Limites assumées
|
| 27 |
----------------
|
| 28 |
+
- **Pool d'exécution unique : ThreadPool.** Choix de conception
|
| 29 |
+
assumé. Les adapters actuels (OCR via binaire C, LLM/VLM via
|
| 30 |
+
httpx, ML via PyTorch/TensorFlow) délèguent leur travail bloquant
|
| 31 |
+
à du code natif qui relâche le GIL, donc un thread pool unique
|
| 32 |
+
suffit en pratique. Un adapter qui ferait du calcul Python pur
|
| 33 |
+
ne profiterait pas de la parallélisation — c'est sa charge de
|
| 34 |
+
vérifier que son ``execute()`` n'est pas GIL-bound (cf. revue de
|
| 35 |
+
PR) avant de prétendre tourner en parallèle.
|
| 36 |
- **Cancel coopératif sur les in-flight.** Quand ``cancel_event``
|
| 37 |
est signalé, le runner appelle ``trigger_cancel()`` sur le
|
| 38 |
``RunControl`` de chaque doc en cours : les adapters qui ont
|
|
@@ -162,11 +162,13 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
| 162 |
#: pré-v2.0 ont été consolidés sous ``docs/archive/``. Les 35
|
| 163 |
#: chemins cassés qu'ils portaient sortent du périmètre actif (cf.
|
| 164 |
#: ``EXCLUDED_PATH_PREFIXES`` ci-dessous).
|
|
|
|
|
|
|
|
|
|
| 165 |
#:
|
| 166 |
-
#: Les
|
| 167 |
#: - CHANGELOG.md (4) : refs Sprint H.4/H.6 dans la section
|
| 168 |
#: migration v2.0 (intouchables sans réécrire l'historique 2.0) ;
|
| 169 |
-
#: - SPECS.md (1) : exemple YAML legacy à corriger en Phase 2 ou v2.1 ;
|
| 170 |
#: - docs/explanation/architecture.md (1) : ref historique au shim
|
| 171 |
#: ``picarones/pipeline/spec.py`` supprimé en Sprint S7.
|
| 172 |
BROKEN_PATHS_BASELINE = 5
|
|
|
|
| 162 |
#: pré-v2.0 ont été consolidés sous ``docs/archive/``. Les 35
|
| 163 |
#: chemins cassés qu'ils portaient sortent du périmètre actif (cf.
|
| 164 |
#: ``EXCLUDED_PATH_PREFIXES`` ci-dessous).
|
| 165 |
+
#: Retrait ``execution_mode`` (mai 2026) : 6 → 5. La section 4.1 de
|
| 166 |
+
#: ``docs/reference/specification.md`` ne pointe plus vers
|
| 167 |
+
#: ``picarones/adapters/legacy_engines/base.py`` (path supprimé).
|
| 168 |
#:
|
| 169 |
+
#: Les 5 chemins restants sont dans la doc active :
|
| 170 |
#: - CHANGELOG.md (4) : refs Sprint H.4/H.6 dans la section
|
| 171 |
#: migration v2.0 (intouchables sans réécrire l'historique 2.0) ;
|
|
|
|
| 172 |
#: - docs/explanation/architecture.md (1) : ref historique au shim
|
| 173 |
#: ``picarones/pipeline/spec.py`` supprimé en Sprint S7.
|
| 174 |
BROKEN_PATHS_BASELINE = 5
|
|
@@ -21,8 +21,25 @@ Ce test bloque sa réintroduction silencieuse. Si tu le vois échouer :
|
|
| 21 |
profite pas du thread pool), réintroduire l'attribut **avec** le
|
| 22 |
dispatch effectif dans le runner — sinon c'est une fausse promesse.
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"""
|
| 27 |
|
| 28 |
from __future__ import annotations
|
|
@@ -34,20 +51,54 @@ import pytest
|
|
| 34 |
|
| 35 |
_FORBIDDEN_TOKENS = ("execution_mode", "ExecutionMode")
|
| 36 |
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
_SELF = Path(__file__).resolve()
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
for py in root.rglob("*.py"):
|
| 46 |
if py.resolve() == _SELF:
|
| 47 |
continue
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
assert not offenders, (
|
| 52 |
f"{token!r} a été réintroduit dans : "
|
| 53 |
+ ", ".join(offenders)
|
|
|
|
| 21 |
profite pas du thread pool), réintroduire l'attribut **avec** le
|
| 22 |
dispatch effectif dans le runner — sinon c'est une fausse promesse.
|
| 23 |
|
| 24 |
+
Portée du scan
|
| 25 |
+
--------------
|
| 26 |
+
Le test couvre :
|
| 27 |
+
|
| 28 |
+
- ``picarones/`` et ``tests/`` (fichiers ``*.py``) — empêche la
|
| 29 |
+
réintroduction du symbole dans le code et les tests ;
|
| 30 |
+
- ``README.md``, ``CLAUDE.md``, ``CHANGELOG.md`` et ``docs/``
|
| 31 |
+
(``*.md``) — empêche la réintroduction de la **promesse** dans
|
| 32 |
+
la documentation active, là où le drift est historiquement le
|
| 33 |
+
plus dommageable.
|
| 34 |
+
|
| 35 |
+
Le ``CHANGELOG.md`` peut légitimement mentionner le symbole pour
|
| 36 |
+
relater le retrait — l'entrée concernée est donc whitelistée.
|
| 37 |
+
|
| 38 |
+
Limite assumée : on ne détecte que le symbole exact ``execution_mode``
|
| 39 |
+
/ ``ExecutionMode``. Un attribut renommé (``runtime_mode``,
|
| 40 |
+
``exec_profile``…) passerait ce test ; il appartient au revieweur de
|
| 41 |
+
ne pas réintroduire le concept sous un autre nom sans le dispatch
|
| 42 |
+
effectif.
|
| 43 |
"""
|
| 44 |
|
| 45 |
from __future__ import annotations
|
|
|
|
| 51 |
|
| 52 |
_FORBIDDEN_TOKENS = ("execution_mode", "ExecutionMode")
|
| 53 |
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 54 |
+
_PY_ROOTS = (_REPO_ROOT / "picarones", _REPO_ROOT / "tests")
|
| 55 |
+
_DOC_ROOTS = (_REPO_ROOT / "docs",)
|
| 56 |
+
_DOC_FILES = (
|
| 57 |
+
_REPO_ROOT / "README.md",
|
| 58 |
+
_REPO_ROOT / "CLAUDE.md",
|
| 59 |
+
)
|
| 60 |
_SELF = Path(__file__).resolve()
|
| 61 |
|
| 62 |
+
#: Fichiers qui mentionnent légitimement les symboles retirés pour
|
| 63 |
+
#: documenter leur disparition (CHANGELOG, ratchet doc-paths).
|
| 64 |
+
#: Le whitelister garde le test utile sans bloquer l'historique narratif.
|
| 65 |
+
_WHITELIST: frozenset[Path] = frozenset({
|
| 66 |
+
(_REPO_ROOT / "CHANGELOG.md").resolve(),
|
| 67 |
+
(_REPO_ROOT / "tests" / "architecture" / "test_doc_paths.py").resolve(),
|
| 68 |
+
})
|
| 69 |
|
| 70 |
+
#: ``docs/archive/`` capture l'historique pré-0.9.0 (sprints, ADRs
|
| 71 |
+
#: rejetées, plans de migration). Réécrire ces archives serait une
|
| 72 |
+
#: réécriture d'historique.
|
| 73 |
+
_EXCLUDED_DOC_PREFIXES: tuple[str, ...] = ("docs/archive/",)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _iter_scanned_files():
|
| 77 |
+
for root in _PY_ROOTS:
|
| 78 |
for py in root.rglob("*.py"):
|
| 79 |
if py.resolve() == _SELF:
|
| 80 |
continue
|
| 81 |
+
yield py
|
| 82 |
+
for root in _DOC_ROOTS:
|
| 83 |
+
for md in root.rglob("*.md"):
|
| 84 |
+
rel = md.relative_to(_REPO_ROOT).as_posix()
|
| 85 |
+
if any(rel.startswith(p) for p in _EXCLUDED_DOC_PREFIXES):
|
| 86 |
+
continue
|
| 87 |
+
yield md
|
| 88 |
+
for path in _DOC_FILES:
|
| 89 |
+
if path.exists():
|
| 90 |
+
yield path
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
@pytest.mark.parametrize("token", _FORBIDDEN_TOKENS)
|
| 94 |
+
def test_no_execution_mode_resurrection(token: str) -> None:
|
| 95 |
+
offenders: list[str] = []
|
| 96 |
+
for path in _iter_scanned_files():
|
| 97 |
+
if path.resolve() in _WHITELIST:
|
| 98 |
+
continue
|
| 99 |
+
text = path.read_text(encoding="utf-8")
|
| 100 |
+
if token in text:
|
| 101 |
+
offenders.append(str(path.relative_to(_REPO_ROOT)))
|
| 102 |
assert not offenders, (
|
| 103 |
f"{token!r} a été réintroduit dans : "
|
| 104 |
+ ", ".join(offenders)
|
|
@@ -50,20 +50,19 @@ os.environ.setdefault("PICARONES_RATE_LIMIT_PER_HOUR", "0")
|
|
| 50 |
|
| 51 |
|
| 52 |
# (3) Désactivation préventive du thread daemon de tqdm.
|
| 53 |
-
#
|
| 54 |
-
# ``tqdm._monitor``
|
| 55 |
-
# ``
|
| 56 |
-
#
|
| 57 |
-
# l'interpréteur après ``=== passed ===``. Le ``_python_exit`` de
|
| 58 |
-
# ``concurrent.futures.process`` essaie de joindre les workers du
|
| 59 |
-
# pool, mais le thread monitor de tqdm bloque la sortie globale —
|
| 60 |
-
# le hang dépasse le timeout GNU configuré dans ci.yml (9 min) et
|
| 61 |
-
# le job échoue avec exit code 124.
|
| 62 |
#
|
| 63 |
-
#
|
| 64 |
-
#
|
| 65 |
-
#
|
| 66 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
try:
|
| 68 |
from tqdm import tqdm as _tqdm
|
| 69 |
|
|
|
|
| 50 |
|
| 51 |
|
| 52 |
# (3) Désactivation préventive du thread daemon de tqdm.
|
| 53 |
+
# Workaround historique pour un hang de shutdown observé en CI
|
| 54 |
+
# Python 3.12+ (ubuntu-latest) : ``tqdm._monitor`` reste bloqué et
|
| 55 |
+
# empêche la sortie de l'interpréteur après ``=== passed ===``, ce
|
| 56 |
+
# qui faisait dépasser le timeout GNU de ci.yml (9 min, exit 124).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
#
|
| 58 |
+
# Le diagnostic originel pointait une interaction avec
|
| 59 |
+
# ``ProcessPoolExecutor`` du runner historique ; ce runner a été
|
| 60 |
+
# remplacé par un ``ThreadPoolExecutor`` (cf.
|
| 61 |
+
# ``picarones/pipeline/runner.py``), donc la cause racine peut
|
| 62 |
+
# avoir disparu — à revalider la prochaine fois que ce workaround
|
| 63 |
+
# est révisé. En attendant, ``monitor_interval=0`` désactive le
|
| 64 |
+
# polling thread de tqdm (sans valeur en CI où stdout est captured)
|
| 65 |
+
# et reste inoffensif.
|
| 66 |
try:
|
| 67 |
from tqdm import tqdm as _tqdm
|
| 68 |
|