Claude commited on
Commit
166b00b
·
unverified ·
1 Parent(s): d19d9b9

refactor(domain): Sprint A14-S40 — PipelineSpec migré dans domain/

Browse files

PipelineSpec et PipelineStep sont des types purs (dataclasses pydantic
frozen sans dépendance d'exécution). Le module canonique migre du
cercle 2 (picarones/pipeline/) vers le cercle 1 (picarones/domain/) où
vivent les autres types purs (Artifact, EvaluationView, ProjectionSpec,
ProvenanceRecord).

picarones/domain/pipeline_spec.py
---------------------------------
Module canonique. Contient PipelineStep, PipelineSpec, INITIAL_STEP_ID
inchangés (copie verbatim).

picarones/pipeline/spec.py
--------------------------
Devient un alias de chemin pur :
from picarones.domain.pipeline_spec import (
INITIAL_STEP_ID, PipelineSpec, PipelineStep,
)
+ __all__ identique.

Pas un shim au sens architectural (adaptation d'API incompatible) —
c'est un re-export de convenance pour ne pas casser les 13 callsites
qui importent depuis picarones.pipeline.spec.

picarones/domain/__init__.py
----------------------------
Re-exporte PipelineSpec, PipelineStep, INITIAL_STEP_ID au top-level.
Permet :
from picarones.domain import PipelineSpec # canonique
from picarones.domain.pipeline_spec import PipelineSpec # canonique
from picarones.pipeline.spec import PipelineSpec # alias
from picarones.pipeline import PipelineSpec # convenience

Tests S40 dédiés (5 nouveaux)
-----------------------------
- test_canonical_path_in_domain : import depuis domain.pipeline_spec.
- test_domain_top_level_reexports : import depuis domain.
- test_legacy_pipeline_path_aliased : import depuis pipeline.spec.
- test_all_paths_resolve_to_same_classes : `is` strict — toutes les
classes sont LE MÊME objet (pas une copie).
- test_pipeline_module_init_reexports_too : pipeline/ continue d'exposer.

Tests existants (87 pipeline + 622 domain/integration) : 100 %
préservés.

Tests : 4831 passed, 11 skipped (vs 4826 avant : +5 S40).
Lint : ruff check picarones/ tests/ → All checks passed.

https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP

picarones/domain/__init__.py CHANGED
@@ -57,6 +57,11 @@ from picarones.domain.evaluation_spec import (
57
  EvaluationView,
58
  MetricSpec,
59
  )
 
 
 
 
 
60
  from picarones.domain.projection_spec import ProjectionSpec
61
  from picarones.domain.provenance import ProvenanceRecord
62
  from picarones.domain.run_manifest import RunManifest, utcnow
@@ -87,6 +92,10 @@ __all__ = [
87
  "EvaluationView",
88
  "EvaluationSpec",
89
  "ProjectionSpec",
 
 
 
 
90
  # S17 — Run manifest (pure domain ; RunResult vit dans app/)
91
  "RunManifest",
92
  "utcnow",
 
57
  EvaluationView,
58
  MetricSpec,
59
  )
60
+ from picarones.domain.pipeline_spec import (
61
+ INITIAL_STEP_ID,
62
+ PipelineSpec,
63
+ PipelineStep,
64
+ )
65
  from picarones.domain.projection_spec import ProjectionSpec
66
  from picarones.domain.provenance import ProvenanceRecord
67
  from picarones.domain.run_manifest import RunManifest, utcnow
 
92
  "EvaluationView",
93
  "EvaluationSpec",
94
  "ProjectionSpec",
95
+ # S6 + S40 — Pipeline spec (canonique en domain/ depuis S40)
96
+ "PipelineSpec",
97
+ "PipelineStep",
98
+ "INITIAL_STEP_ID",
99
  # S17 — Run manifest (pure domain ; RunResult vit dans app/)
100
  "RunManifest",
101
  "utcnow",
picarones/domain/pipeline_spec.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """``PipelineStep`` et ``PipelineSpec`` — Sprints A14-S6 / S40.
2
+
3
+ Description **purement déclarative** d'un DAG de transformation
4
+ documentaire. Sérialisable en YAML, versionnable en git, valide
5
+ sans avoir besoin d'instancier les modules concrets.
6
+
7
+ Sprint S40 — migration depuis ``picarones.pipeline.spec``
8
+ ---------------------------------------------------------
9
+ Le module canonique est désormais en cercle 1 (``picarones/domain/``)
10
+ — c'est un type pur qui n'a aucune dépendance d'exécution
11
+ (``picarones/pipeline/`` qui contient le runtime n'est en fait pas
12
+ nécessaire pour décrire la spec). ``picarones.pipeline.spec`` reste
13
+ exposé en re-export pour ne pas casser les callers existants — ce
14
+ n'est pas un shim au sens architectural (adaptation d'une API
15
+ incompatible) mais un alias de chemin.
16
+
17
+ Différence avec l'ancien ``picarones.core.pipeline`` (Sprint 63)
18
+ ----------------------------------------------------------------
19
+ L'ancien ``PipelineStep`` portait un champ ``module: BaseModule``
20
+ — une **instance** d'objet exécutable. Conséquence : la spec
21
+ n'était pas sérialisable en YAML, et un test qui voulait juste
22
+ valider la cohérence des types devait instancier des stubs.
23
+
24
+ Ici, ``PipelineStep`` ne porte qu'un ``adapter_name: str``. Le
25
+ mapping ``nom → instance`` est maintenu par un service applicatif
26
+ (``picarones.app.services.adapter_registry`` au S19) et résolu au
27
+ moment de l'exécution, pas de la spec.
28
+
29
+ Bénéfices :
30
+
31
+ - Le YAML d'une pipeline composée est versionnable en git
32
+ indépendamment de l'environnement Python (BnF peut commit
33
+ ``ocr_llm_alto_remap.yaml`` sans imposer aux contributeurs
34
+ d'avoir tous les SDK installés).
35
+ - ``validate_spec`` peut s'exécuter sans instancier aucun module
36
+ → tests rapides et déterministes.
37
+ - Le rapport de reproductibilité peut citer le YAML exact, le
38
+ commit du code et la version des adapters utilisés —
39
+ séparation propre de la déclaration et de l'implémentation.
40
+
41
+ Anti-sur-ingénierie
42
+ -------------------
43
+ - Pas de typage des ``params`` par adapter ici (chaque adapter
44
+ validera ses propres params au moment de l'exécution).
45
+ - Pas de versioning de spec — un nouveau champ se traduit par un
46
+ rebump pydantic. Si on veut migrer entre versions de schéma,
47
+ on l'ajoutera quand le besoin sera concret.
48
+ - Pas d'``outputs_preferred`` (mapping logique "preferred_text =
49
+ step3.RAW_TEXT"). Reporté quand un caller en aura concrètement
50
+ besoin.
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import re
56
+
57
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
58
+
59
+ from picarones.domain.artifacts import ArtifactType
60
+
61
+
62
+ #: Identifiant d'étape — alphanum + ``_-``. Doit être un nom court
63
+ #: lisible par un humain dans les logs et le rapport.
64
+ _STEP_ID_RE = re.compile(r"^[A-Za-z0-9_\-]+$")
65
+
66
+ #: Sentinel pour ``inputs_from`` qui désigne les artefacts initiaux
67
+ #: fournis au runner (typiquement ``IMAGE``).
68
+ INITIAL_STEP_ID = "__initial__"
69
+
70
+
71
+ class PipelineStep(BaseModel):
72
+ """Une étape déclarative dans un DAG de pipeline.
73
+
74
+ Attributs
75
+ ---------
76
+ id:
77
+ Identifiant unique de l'étape dans la pipeline (alphanum +
78
+ ``_-``). Sert dans les logs, le rapport, et comme cible
79
+ des références ``inputs_from`` des étapes en aval.
80
+ kind:
81
+ Catégorie informationnelle de l'étape (``"ocr"``,
82
+ ``"post_correction"``, ``"alto_remapping"``,
83
+ ``"alto_reconstruction"``, etc.). Pas de validation
84
+ d'enum — c'est un label libre que les services et le
85
+ rapport peuvent grouper. Par convention, en
86
+ ``snake_case``.
87
+ adapter_name:
88
+ Nom de l'adapter dans le registre runtime (résolu par
89
+ ``app/services`` au S19). Convention :
90
+ ``"<provider>:<engine_or_model>"`` (ex : ``"tesseract"``,
91
+ ``"openai:gpt-4o"``, ``"mistral:large"``,
92
+ ``"<vendor>:<custom_module>"``).
93
+ params:
94
+ Paramètres passés à l'adapter au moment de l'exécution.
95
+ Format libre (chaque adapter valide les siens) — typage
96
+ scalaire pour rester sérialisable en YAML.
97
+ input_types:
98
+ Types d'artefacts consommés par l'étape. Validés par
99
+ ``validate_spec`` contre les outputs des étapes antérieures.
100
+ output_types:
101
+ Types d'artefacts produits. Validés au runtime par
102
+ l'executor (qui vérifie que tous les types déclarés sont
103
+ bien dans le dict retourné par l'adapter).
104
+ inputs_from:
105
+ DAG branchant (héritage du Sprint 66). Pour chaque type
106
+ d'entrée, désigne explicitement l'étape source. La chaîne
107
+ spéciale ``"__initial__"`` désigne les entrées initiales
108
+ du runner. Si le dict est vide, l'executor prend la
109
+ version la plus récente de chaque type dans le bag.
110
+ """
111
+
112
+ model_config = ConfigDict(frozen=True, extra="forbid")
113
+
114
+ id: str = Field(min_length=1, max_length=128)
115
+ kind: str = Field(min_length=1, max_length=64)
116
+ adapter_name: str = Field(min_length=1, max_length=256)
117
+ params: dict[str, str | int | float | bool] = Field(default_factory=dict)
118
+ input_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
119
+ output_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
120
+ inputs_from: dict[ArtifactType, str] = Field(default_factory=dict)
121
+
122
+ @field_validator("id")
123
+ @classmethod
124
+ def _validate_step_id(cls, v: str) -> str:
125
+ if not _STEP_ID_RE.match(v):
126
+ from picarones.domain.errors import PicaronesError
127
+ raise PicaronesError(
128
+ f"step id invalide : {v!r}. "
129
+ f"Doit matcher {_STEP_ID_RE.pattern!r} (alphanum + _-)."
130
+ )
131
+ if v == INITIAL_STEP_ID:
132
+ from picarones.domain.errors import PicaronesError
133
+ raise PicaronesError(
134
+ f"step id réservé : {INITIAL_STEP_ID!r} désigne "
135
+ "les entrées initiales du runner."
136
+ )
137
+ return v
138
+
139
+
140
+ class PipelineSpec(BaseModel):
141
+ """DAG déclaratif d'une pipeline composée.
142
+
143
+ Sérialisable en YAML via ``model_dump()`` + ``yaml.safe_dump``,
144
+ chargeable via ``model_validate(yaml.safe_load(text))``. Le
145
+ round-trip est testé.
146
+
147
+ Attributs
148
+ ---------
149
+ name:
150
+ Nom court de la pipeline (utilisé dans les logs, le cache,
151
+ le rapport). Convention ``snake_case``.
152
+ description:
153
+ Phrase courte d'introduction affichée dans le rapport.
154
+ initial_inputs:
155
+ Types d'artefacts qui doivent être fournis par le caller
156
+ au moment de l'exécution. Convention : ``(IMAGE,)`` pour
157
+ une pipeline OCR classique, ``(IMAGE, RAW_TEXT)`` pour
158
+ une post-correction qui part d'un OCR pré-calculé.
159
+ steps:
160
+ Étapes du DAG, ordonnées par dépendance topologique
161
+ d'exécution. Si une étape ``s2`` dépend de ``s1``, alors
162
+ ``s1`` apparaît avant ``s2``. ``validate_spec`` détecte
163
+ les violations.
164
+ """
165
+
166
+ model_config = ConfigDict(frozen=True, extra="forbid")
167
+
168
+ name: str = Field(min_length=1, max_length=128)
169
+ description: str = ""
170
+ initial_inputs: tuple[ArtifactType, ...] = Field(default_factory=tuple)
171
+ steps: tuple[PipelineStep, ...] = Field(default_factory=tuple)
172
+
173
+ def step_by_id(self, step_id: str) -> PipelineStep | None:
174
+ for s in self.steps:
175
+ if s.id == step_id:
176
+ return s
177
+ return None
178
+
179
+
180
+ __all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
picarones/pipeline/spec.py CHANGED
@@ -1,170 +1,25 @@
1
- """``PipelineStep`` et ``PipelineSpec`` — Sprint A14-S6.
2
 
3
- Description **purement déclarative** d'un DAG de transformation
4
- documentaire. Sérialisable en YAML, versionnable en git, valide
5
- sans avoir besoin d'instancier les modules concrets.
 
 
6
 
7
- Différence avec l'ancien ``picarones.core.pipeline`` (Sprint 63)
8
- ----------------------------------------------------------------
9
- L'ancien ``PipelineStep`` portait un champ ``module: BaseModule``
10
- — une **instance** d'objet exécutable. Conséquence : la spec
11
- n'était pas sérialisable en YAML, et un test qui voulait juste
12
- valider la cohérence des types devait instancier des stubs.
13
 
14
- Ici, ``PipelineStep`` ne porte qu'un ``adapter_name: str``. Le
15
- mapping ``nom → instance`` est maintenu par un service applicatif
16
- (``picarones.app.services.adapter_registry`` au S19) et résolu au
17
- moment de l'exécution, pas de la spec.
18
 
19
- Bénéfices :
20
-
21
- - Le YAML d'une pipeline composée est versionnable en git
22
- indépendamment de l'environnement Python (BnF peut commit
23
- ``ocr_llm_alto_remap.yaml`` sans imposer aux contributeurs
24
- d'avoir tous les SDK installés).
25
- - ``validate_spec`` peut s'exécuter sans instancier aucun module
26
- → tests rapides et déterministes.
27
- - Le rapport de reproductibilité peut citer le YAML exact, le
28
- commit du code et la version des adapters utilisés —
29
- séparation propre de la déclaration et de l'implémentation.
30
-
31
- Anti-sur-ingénierie
32
- -------------------
33
- - Pas de typage des ``params`` par adapter ici (chaque adapter
34
- validera ses propres params au moment de l'exécution).
35
- - Pas de versioning de spec — un nouveau champ se traduit par un
36
- rebump pydantic. Si on veut migrer entre versions de schéma,
37
- on l'ajoutera quand le besoin sera concret.
38
- - Pas d'``outputs_preferred`` (mapping logique "preferred_text =
39
- step3.RAW_TEXT"). Reporté quand un caller en aura concrètement
40
- besoin.
41
  """
42
 
43
  from __future__ import annotations
44
 
45
- import re
46
-
47
- from pydantic import BaseModel, ConfigDict, Field, field_validator
48
-
49
- from picarones.domain.artifacts import ArtifactType
50
-
51
-
52
- #: Identifiant d'étape — alphanum + ``_-``. Doit être un nom court
53
- #: lisible par un humain dans les logs et le rapport.
54
- _STEP_ID_RE = re.compile(r"^[A-Za-z0-9_\-]+$")
55
-
56
- #: Sentinel pour ``inputs_from`` qui désigne les artefacts initiaux
57
- #: fournis au runner (typiquement ``IMAGE``).
58
- INITIAL_STEP_ID = "__initial__"
59
-
60
-
61
- class PipelineStep(BaseModel):
62
- """Une étape déclarative dans un DAG de pipeline.
63
-
64
- Attributs
65
- ---------
66
- id:
67
- Identifiant unique de l'étape dans la pipeline (alphanum +
68
- ``_-``). Sert dans les logs, le rapport, et comme cible
69
- des références ``inputs_from`` des étapes en aval.
70
- kind:
71
- Catégorie informationnelle de l'étape (``"ocr"``,
72
- ``"post_correction"``, ``"alto_remapping"``,
73
- ``"alto_reconstruction"``, etc.). Pas de validation
74
- d'enum — c'est un label libre que les services et le
75
- rapport peuvent grouper. Par convention, en
76
- ``snake_case``.
77
- adapter_name:
78
- Nom de l'adapter dans le registre runtime (résolu par
79
- ``app/services`` au S19). Convention :
80
- ``"<provider>:<engine_or_model>"`` (ex : ``"tesseract"``,
81
- ``"openai:gpt-4o"``, ``"mistral:large"``,
82
- ``"<vendor>:<custom_module>"``).
83
- params:
84
- Paramètres passés à l'adapter au moment de l'exécution.
85
- Format libre (chaque adapter valide les siens) — typage
86
- scalaire pour rester sérialisable en YAML.
87
- input_types:
88
- Types d'artefacts consommés par l'étape. Validés par
89
- ``validate_spec`` contre les outputs des étapes antérieures.
90
- output_types:
91
- Types d'artefacts produits. Validés au runtime par
92
- l'executor (qui vérifie que tous les types déclarés sont
93
- bien dans le dict retourné par l'adapter).
94
- inputs_from:
95
- DAG branchant (héritage du Sprint 66). Pour chaque type
96
- d'entrée, désigne explicitement l'étape source. La chaîne
97
- spéciale ``"__initial__"`` désigne les entrées initiales
98
- du runner. Si le dict est vide, l'executor prend la
99
- version la plus récente de chaque type dans le bag.
100
- """
101
-
102
- model_config = ConfigDict(frozen=True, extra="forbid")
103
-
104
- id: str = Field(min_length=1, max_length=128)
105
- kind: str = Field(min_length=1, max_length=64)
106
- adapter_name: str = Field(min_length=1, max_length=256)
107
- params: dict[str, str | int | float | bool] = Field(default_factory=dict)
108
- input_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
109
- output_types: tuple[ArtifactType, ...] = Field(default_factory=tuple)
110
- inputs_from: dict[ArtifactType, str] = Field(default_factory=dict)
111
-
112
- @field_validator("id")
113
- @classmethod
114
- def _validate_step_id(cls, v: str) -> str:
115
- if not _STEP_ID_RE.match(v):
116
- from picarones.domain.errors import PicaronesError
117
- raise PicaronesError(
118
- f"step id invalide : {v!r}. "
119
- f"Doit matcher {_STEP_ID_RE.pattern!r} (alphanum + _-)."
120
- )
121
- if v == INITIAL_STEP_ID:
122
- from picarones.domain.errors import PicaronesError
123
- raise PicaronesError(
124
- f"step id réservé : {INITIAL_STEP_ID!r} désigne "
125
- "les entrées initiales du runner."
126
- )
127
- return v
128
-
129
-
130
- class PipelineSpec(BaseModel):
131
- """DAG déclaratif d'une pipeline composée.
132
-
133
- Sérialisable en YAML via ``model_dump()`` + ``yaml.safe_dump``,
134
- chargeable via ``model_validate(yaml.safe_load(text))``. Le
135
- round-trip est testé.
136
-
137
- Attributs
138
- ---------
139
- name:
140
- Nom court de la pipeline (utilisé dans les logs, le cache,
141
- le rapport). Convention ``snake_case``.
142
- description:
143
- Phrase courte d'introduction affichée dans le rapport.
144
- initial_inputs:
145
- Types d'artefacts qui doivent être fournis par le caller
146
- au moment de l'exécution. Convention : ``(IMAGE,)`` pour
147
- une pipeline OCR classique, ``(IMAGE, RAW_TEXT)`` pour
148
- une post-correction qui part d'un OCR pré-calculé.
149
- steps:
150
- Étapes du DAG, ordonnées par dépendance topologique
151
- d'exécution. Si une étape ``s2`` dépend de ``s1``, alors
152
- ``s1`` apparaît avant ``s2``. ``validate_spec`` détecte
153
- les violations.
154
- """
155
-
156
- model_config = ConfigDict(frozen=True, extra="forbid")
157
-
158
- name: str = Field(min_length=1, max_length=128)
159
- description: str = ""
160
- initial_inputs: tuple[ArtifactType, ...] = Field(default_factory=tuple)
161
- steps: tuple[PipelineStep, ...] = Field(default_factory=tuple)
162
-
163
- def step_by_id(self, step_id: str) -> PipelineStep | None:
164
- for s in self.steps:
165
- if s.id == step_id:
166
- return s
167
- return None
168
-
169
 
170
  __all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
 
1
+ """``PipelineStep`` et ``PipelineSpec`` — re-export depuis ``domain``.
2
 
3
+ Sprint A14-S40 a migré le module canonique vers
4
+ ``picarones.domain.pipeline_spec`` (cercle 1, types purs). Ce
5
+ module reste un alias de chemin pour ne pas casser les callers
6
+ existants — ce n'est pas un shim au sens architectural
7
+ (adaptation d'une API incompatible) mais une convenance de chemin.
8
 
9
+ Les nouveaux callers doivent importer directement depuis
10
+ ``picarones.domain`` :
 
 
 
 
11
 
12
+ ::
 
 
 
13
 
14
+ from picarones.domain import PipelineSpec, PipelineStep, INITIAL_STEP_ID
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  """
16
 
17
  from __future__ import annotations
18
 
19
+ from picarones.domain.pipeline_spec import (
20
+ INITIAL_STEP_ID,
21
+ PipelineSpec,
22
+ PipelineStep,
23
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  __all__ = ["PipelineStep", "PipelineSpec", "INITIAL_STEP_ID"]
tests/domain/test_sprint_a14_s40_pipeline_spec_in_domain.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sprint A14-S40 — PipelineSpec migré dans domain/.
2
+
3
+ Vérifie que :
4
+
5
+ 1. ``picarones.domain.pipeline_spec`` est le module canonique.
6
+ 2. ``picarones.domain`` re-exporte ``PipelineSpec``, ``PipelineStep``,
7
+ ``INITIAL_STEP_ID``.
8
+ 3. ``picarones.pipeline.spec`` continue d'exposer les mêmes classes
9
+ (alias de chemin pour la rétrocompat).
10
+ 4. Les deux chemins d'import retournent **la même classe**
11
+ (``is`` strict, pas seulement ``==``).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+
17
+ def test_canonical_path_in_domain() -> None:
18
+ """``picarones.domain.pipeline_spec`` expose les classes canoniques."""
19
+ from picarones.domain.pipeline_spec import (
20
+ INITIAL_STEP_ID,
21
+ PipelineSpec,
22
+ PipelineStep,
23
+ )
24
+ assert PipelineSpec is not None
25
+ assert PipelineStep is not None
26
+ assert INITIAL_STEP_ID == "__initial__"
27
+
28
+
29
+ def test_domain_top_level_reexports() -> None:
30
+ """``picarones.domain`` re-exporte au top-level."""
31
+ from picarones.domain import (
32
+ INITIAL_STEP_ID,
33
+ PipelineSpec,
34
+ PipelineStep,
35
+ )
36
+ assert PipelineSpec is not None
37
+ assert PipelineStep is not None
38
+ assert INITIAL_STEP_ID == "__initial__"
39
+
40
+
41
+ def test_legacy_pipeline_path_aliased() -> None:
42
+ """``picarones.pipeline.spec`` reste un alias de chemin."""
43
+ from picarones.pipeline.spec import (
44
+ INITIAL_STEP_ID,
45
+ PipelineSpec,
46
+ PipelineStep,
47
+ )
48
+ assert PipelineSpec is not None
49
+ assert PipelineStep is not None
50
+ assert INITIAL_STEP_ID == "__initial__"
51
+
52
+
53
+ def test_all_paths_resolve_to_same_classes() -> None:
54
+ """Les imports depuis les 3 emplacements pointent vers le MÊME objet."""
55
+ from picarones.domain import PipelineSpec as DomainSpec
56
+ from picarones.domain import PipelineStep as DomainStep
57
+ from picarones.domain import INITIAL_STEP_ID as DomainInitial
58
+ from picarones.domain.pipeline_spec import PipelineSpec as CanonSpec
59
+ from picarones.domain.pipeline_spec import PipelineStep as CanonStep
60
+ from picarones.pipeline.spec import PipelineSpec as LegacySpec
61
+ from picarones.pipeline.spec import PipelineStep as LegacyStep
62
+ from picarones.pipeline.spec import INITIAL_STEP_ID as LegacyInitial
63
+
64
+ # is strict — toutes les classes pointent vers le même objet.
65
+ assert DomainSpec is CanonSpec
66
+ assert DomainSpec is LegacySpec
67
+ assert DomainStep is CanonStep
68
+ assert DomainStep is LegacyStep
69
+ assert DomainInitial == LegacyInitial
70
+
71
+
72
+ def test_pipeline_module_init_reexports_too() -> None:
73
+ """``picarones.pipeline`` continue d'exposer pour rétrocompat."""
74
+ from picarones.pipeline import PipelineSpec, PipelineStep, INITIAL_STEP_ID
75
+ from picarones.domain.pipeline_spec import (
76
+ PipelineSpec as CanonSpec,
77
+ PipelineStep as CanonStep,
78
+ )
79
+ assert PipelineSpec is CanonSpec
80
+ assert PipelineStep is CanonStep
81
+ assert INITIAL_STEP_ID == "__initial__"