Picarones / docs /archives /migration /pipeline-convergence-plan.md
Claude
docs(sprint-H.9): archive migration plans + cleanup stale doc paths
2b782d0 unverified
|
Raw
History Blame
16.8 kB

Audit & sub-plan — Convergence PipelineRunner legacy ↔ PipelineExecutor canonique

Note : ce document est l'audit demandé en conclusion de Phase 5 du plan de retrait du legacy (cf. docs/migration/legacy-retirement-plan.md). Il identifie les différences entre les deux designs de pipeline, inventaire les callers, propose 3 stratégies de convergence et recommande un sub-plan d'exécution.


1. État des lieux

Deux designs cohabitent :

1.A Legacy — picarones.evaluation.pipeline (ex-core/pipeline.py)

Sprint 63 (axe B), 607 lignes. Relocalisé en Phase 5.C.batch7 mais non refactoré.

Caractéristiques :

  • PipelineRunner : classe statique avec .run(spec, document, initial_inputs) -> PipelineResult.
  • PipelineSpec : dataclass mutable.
  • PipelineStep : dataclass avec module: BaseModule (instance Python).
  • StepResult : dataclass avec junction_metrics: dict[str, dict[str, Any]].
  • PipelineResult : dataclass avec steps: list[StepResult].
  • Modules : BaseModule ABC consommant des payloads bruts ({ArtifactType: str | dict | list | ...}).
  • Évaluation : compute_at_junction automatique à chaque étape contre la GT du document si GTLevel correspond.
  • Pas de cache d'artefacts.
  • Pas de ExecutionPlan séparé — résolution implicite des inputs au runtime via un bag versionné.

1.B Canonique — picarones.pipeline.executor + planner + protocols

Sprints S6-S7-S28, design rewrite ciblé.

Caractéristiques :

  • PipelineExecutor : classe instanciable avec adapter_resolver injecté + planner optionnel + artifact_store optionnel.
  • Méthode run(spec, document, initial_inputs, context) -> PipelineResult (compat S7) qui plan-then-execute.
  • Méthode canonique run_plan(plan, document, initial_inputs, context) qui consomme un ExecutionPlan pré-calculé.
  • PipelineSpec : Pydantic immutable (picarones.domain.pipeline_spec), sérialisable YAML.
  • PipelineStep : Pydantic immutable avec adapter_name: str (pas d'instance — résolution applicative).
  • ExecutionPlan : produit du PipelinePlanner — porte StepInputBinding explicites + MetricJunction détectées.
  • StepResult : Pydantic immutable avec produced_artifacts: dict[str, str] (map ArtifactType.value → Artifact.id).
  • PipelineResult : Pydantic immutable avec artifacts: tuple[Artifact, ...].
  • Adapters : StepExecutor Protocol (runtime-checkable) consommant des Artifact typés ({ArtifactType: Artifact(uri, content_hash, provenance)}).
  • Cache d'artefacts via ArtifactCachePort (Sprint S29 + S47).
  • RunContext Pydantic injecté à chaque execute() — document_id, code_version, pipeline_name, workspace_uri.

2. Différences API détaillées

Dimension Legacy (evaluation.pipeline) Canonique (pipeline.executor)
Construction classe statique instance avec deps injectées
Spec dataclass mutable Pydantic immutable, YAML-sérialisable
Step porte module: BaseModule porte adapter_name: str
Résolution adapters implicite (instance dans spec) explicite (adapter_resolver callable)
Résolution inputs implicite (last-producer-wins) explicite (StepInputBinding)
Validation spec au runtime au planning (PipelinePlanner)
Type passé aux modules payload brut (str, dict, list…) Artifact typé
Provenance absente ProvenanceRecord automatique
Hash de contenu absent Artifact.content_hash SHA-256
Cache inter-runs absent ArtifactCachePort
RunContext absent injecté à chaque step
Évaluation auto vs GT oui, à chaque step non (sortie : artefacts seulement)
junction_metrics dans StepResult absent du runtime, calculé à part
Représentation des étapes objets Python uniquement YAML + Python

3. Inventaire des callers

3.A Legacy (evaluation.pipeline)

Production (4 fichiers) :

  • picarones/__init__.py — re-export de PipelineRunner, PipelineSpec, PipelineStep, StepResult, PipelineResult dans l'API publique.
  • picarones/evaluation/pipeline_benchmark.py — orchestre l'exécution corpus-wide via PipelineRunner.run().
  • picarones/evaluation/pipeline_comparison.py — compare N PipelineSpec via run_pipeline_benchmark.
  • picarones/measurements/pipeline_spec_loader (Phase 7.D — module supprimé) — chargeait des YAML legacy en PipelineSpec + PipelineStep legacy.

Tests : 7 fichiers de tests directs (test_sprint63_*, test_sprint64_*, test_sprint65_*, test_sprint66_*, test_sprint67_*, test_sprint68_*, etc.).

3.B Canonique (pipeline.executor)

Production : 0 caller production (le rewrite n'a pas encore de service applicatif qui consomme l'executor canonique en mode mono-document).

Tests : 9 fichiers de tests directs. Tests-only à ce jour.

Conclusion sur les callers : le legacy est en production, le canonique est test-only. La convergence doit migrer le legacy sans casser les 7+4 = 11 fichiers tests/prod existants.


4. Stratégies de convergence

4.A Wrapper legacy → canonique

Le legacy PipelineRunner.run(spec, document, initial_inputs) devient un adaptateur qui :

  1. Convertit la PipelineSpec legacy (dataclass + module instance) en PipelineSpec canonique (Pydantic + adapter_name).
  2. Wrappe chaque BaseModule en StepExecutor Protocol.
  3. Convertit les payloads bruts en Artifact (uri inline, content_hash calculé).
  4. Injecte un adapter_resolver ad hoc qui retourne les wrappers.
  5. Invoque PipelineExecutor.run(spec, document, initial_inputs, context).
  6. Reconvertit le PipelineResult canonique en PipelineResult legacy avec junction_metrics calculées à partir des artefacts produits.

Avantages :

  • Préserve l'API legacy → 0 caller cassé en production.
  • Unifie le moteur d'exécution → 1 seul code path à maintenir.
  • Cohérent avec la philosophie "no breaking change for callers".

Inconvénients :

  • 200-400 LOC de glue (conversion bidirectionnelle de types).
  • Coût de performance : double conversion à chaque step.
  • Le double modèle Artifact/payload reste visible côté modules (le wrapper masque mais le concept demeure).

Effort : 2-3 sessions.

4.B Migration complète

Migrer chaque caller legacy vers l'API canonique :

  1. pipeline_benchmark : passe de PipelineRunner.run à PipelineExecutor.run_plan. Les StepAggregate doivent accepter la nouvelle structure StepResult (Pydantic).
  2. pipeline_comparison : idem.
  3. pipeline_spec_loader : produit des PipelineSpec Pydantic au lieu de dataclass. Plus de module instance — juste adapter_name.
  4. __init__.py : ré-exporte le canonique.
  5. Tests : 7 fichiers à refactorer (mock adapters → StepExecutor Protocol, payloads → Artifact).

Avantages :

  • 1 seul design. Le legacy disparaît complètement.
  • Pas de glue ni de double conversion.
  • Conforme à la cible architecturale du rewrite.

Inconvénients :

  • Massive : ~2500 LOC à toucher entre prod + tests.
  • Le contrat des modules tiers (BaseModuleStepExecutor) change. Un caller externe (BnF, HF Space) qui utilise PipelineRunner.run casse silencieusement.
  • Risque de régression non détectée sur les ~7 tests sprints axe B (les fixtures sont volumineuses).
  • Évaluation auto vs GT (legacy : à chaque step) doit être ré-implémentée comme une post-étape canonique.

Effort : 5-7 sessions.

4.C Cohabitation documentée

État actuel. Document explicitement que les deux designs sont volontaires. Convergence reportée à un sprint dédié quand un caller institutionnel l'exigera (BnF demande un YAML déclaratif non-instanciable, ou HF Space veut le cache d'artefacts).

Avantages :

  • 0 risque de régression maintenant.
  • Permet de continuer le retrait du legacy sur les autres paquets (Phases 6-11) sans buter sur ce sujet complexe.
  • Le canonique reste prêt pour le jour où il sera vraiment nécessaire.

Inconvénients :

  • 2 designs à maintenir.
  • L'objectif "core/ vide" du retrait du legacy n'est pas totalement atteint : evaluation/pipeline.py reste un module "legacy-style" en cercle 2.
  • Risque que le canonique reste mort-né si personne ne le réclame.

Effort : 0 (juste documentation).


5. Recommandation

Mise à jour 2026-05 : l'utilisateur a précisé que le projet est en stand-by jusqu'à la fin de la migration complète et que la rétrocompat de l'API publique n'est pas une contrainte. Cela élimine l'avantage principal de la stratégie 4.A (wrapper) et rend la stratégie 4.B (migration complète) recommandée :

Stratégie 4.B — Migration complète est la voie cible.

Bénéfices avec contrainte API levée :

  • 1 seul design final, plus de wrapper interne à maintenir.
  • Le contrat des modules tiers (BaseModuleStepExecutor) peut changer sans gérer la rétrocompat.
  • Les Artifact typés (provenance, content_hash, uri) deviennent natifs partout — pas de double conversion.

Risques résiduels :

  • ~2500 LOC à toucher entre prod + tests.
  • L'évaluation auto vs GT (legacy : à chaque step) doit être ré-implémentée comme une post-étape canonique.
  • Risque de régression sur les ~7 tests sprints axe B (fixtures volumineuses).
  • Plusieurs sessions de travail nécessaires (5-7 sessions).

6. Découvertes additionnelles (audit complémentaire)

L'audit initial parlait de 4 callers de production de PipelineRunner. Une investigation plus poussée révèle un écosystème legacy plus large, qui doit être inclus dans le plan :

6.A Legacy engines (picarones/engines/, ~1500 LOC)

5 modules OCR legacy qui héritent de BaseOCREngine (lui-même extension de BaseModule) :

  • engines/base.py:BaseOCREngine
  • engines/tesseract.py:TesseractEngine (177 l)
  • engines/pero_ocr.py:PeroOCREngine (182 l)
  • engines/mistral_ocr.py:MistralOCREngine (231 l)
  • engines/google_vision.py:GoogleVisionEngine (256 l)
  • engines/azure_doc_intel.py:AzureDocIntelEngine

Équivalents canoniques existent dans picarones/adapters/ocr/ (TesseractAdapter, PeroOCRAdapter, etc.) et implémentent déjà StepExecutor. Mais les noms de classes et les APIs publiques diffèrent — pas un simple shim.

Callers production des engines legacy :

  • picarones/web/benchmark_utils.py
  • picarones/pipelines/base.py (lui-même legacy, Phase 6)

6.B Legacy LLM (picarones/llm/, ~67 LOC)

Déjà migré : tous les fichiers sont des shims qui ré-exportent depuis picarones/adapters/llm/. Rien à faire.

6.C Legacy modules officiels (picarones/modules/)

  • modules/alto_text_to_mono_region.py:TextToAltoMonoRegion (310 LOC) — extension de BaseModule.

Pas d'équivalent canonique à ce jour. Cible documentée : picarones/formats/alto/baseline_reconstruction.py ou picarones/evaluation/projectors/text_to_alto.py (cf. Phase 7 du plan de retrait).

6.D Sémantique des payloads vs Artifacts

La conversion BaseModule.processStepExecutor.execute n'est pas triviale parce que :

  • Le legacy passe des payloads bruts :
    • ArtifactType.IMAGEstr (chemin du fichier image)
    • ArtifactType.RAW_TEXTstr (contenu textuel inline)
    • ArtifactType.ALTO_XMLstr (contenu XML inline)
    • ArtifactType.ENTITIESlist[dict]
  • Le canonique passe des Artifact Pydantic immutables :
    • uri (filesystem ou URI distant)
    • content_hash (SHA-256)
    • provenance (ProvenanceRecord)
    • pas de champ content direct — le contenu se lit via uri.

Pour les tests legacy qui injectent du contenu inline (mock modules retournant "hello"), il faut soit :

  1. Persister le contenu dans un fichier temporaire et pointer artifact.uri dessus.
  2. Ajouter une convention data: URI pour le contenu inline.
  3. Étendre Artifact avec un champ inline_payload: bytes | None optionnel.

Décision recommandée : option 1 (fichier temporaire), parce qu'elle préserve la sémantique « un artefact a toujours un identifiant filesystem » et permet le cache/provenance proprement.


7. Sub-plan d'exécution révisé (stratégie 4.B)

Sub-phase 7.A — Migration des adapters concrets

Bouclage de la migration des adapters legacy (engines/llm/modules) vers les canoniques avant de toucher aux pipeline runners.

Étapes :

  1. engines/ → shims pointant vers adapters/ocr/ (avec alias de classes : TesseractEngine = TesseractAdapter, etc.).
  2. Mise à jour des callers de engines/ à utiliser adapters/ocr/ directement.
  3. modules/alto_text_to_mono_region.py → migré vers picarones/evaluation/projectors/text_to_alto.py (canonique en StepExecutor).
  4. Suppression du shim engines/.

Effort : 2-3 sessions.

Sub-phase 7.B — Migration des callers PipelineRunner

Une fois les adapters unifiés sur StepExecutor :

  1. pipeline_spec_loader : produit des picarones.domain.pipeline_spec.PipelineSpec (Pydantic) avec adapter_name: str au lieu d'instances.
  2. pipeline_benchmark : consomme PipelineExecutor.run_plan. StepAggregate accepte StepResult Pydantic canonique.
  3. pipeline_comparison : idem.
  4. __init__.py : ré-exporte les canoniques.

Effort : 2 sessions.

Sub-phase 7.C — Refactor des tests

Les 7 fichiers de tests legacy axe B (sprints 63-68 + 95) :

  • Mocks BaseModule → mocks StepExecutor Protocol.
  • Payloads bruts → Artifact (avec helper make_inline_artifact(content, type_) pour réduire le boilerplate).
  • Document legacy → DocumentRef canonique.
  • Fixtures junction_metrics → ré-implémentation via post-étape canonique.

Effort : 1-2 sessions.

Sub-phase 7.D — Suppression du legacy

  1. Suppression de evaluation/pipeline.PipelineRunner, PipelineSpec, PipelineStep, StepResult, PipelineResult (le legacy).
  2. Suppression de domain/module_protocol.BaseModule.
  3. Le module evaluation/pipeline.py réduit à _artifact_type_to_gt_level ou supprimé totalement.
  4. core/pipeline.py (shim) supprimé.
  5. core/modules.py (shim) supprimé.

Effort : 0.5 session (suppression mécanique).


8. Total effort révisé (stratégie 4.B)

Sub-phase Description Effort
7.A Migration adapters concrets 2-3 sessions
7.B Migration callers PipelineRunner 2 sessions
7.C Refactor des tests 1-2 sessions
7.D Suppression du legacy 0.5 session
Total Migration complète 5-8 sessions

9. Ordre d'exécution recommandé

L'ordre bottom-up est plus sûr : à chaque étape, les tests restent verts.

Sub-phase 7.A (adapters) → Sub-phase 7.B (orchestration) →
Sub-phase 7.C (tests) → Sub-phase 7.D (suppression)

L'ordre top-down (start by removing PipelineRunner, then fix everything that breaks) est plus risqué mais plus rapide si on accepte une période de tests rouges.

Recommandation : bottom-up, par étapes verticales testables.