Claude commited on
Commit
94eb0cb
·
unverified ·
1 Parent(s): 72463df

feat(adapters/vlm,planner): Sprint A14-S54 architecture cleanup (audit #6 + #14 + #22)

Browse files

#6 - Garde-fou MRO BaseVLMAdapter
---------------------------------
Avant S54, lordre des parents dans
class AnthropicVLMAdapter(BaseVLMAdapter, AnthropicAdapter)
était critique mais non vérifié. Un swap accidentel
à (AnthropicAdapter, BaseVLMAdapter) aurait donné silencieusement
output_types = {CORRECTED_TEXT} au lieu de {RAW_TEXT} - lerreur
ne se manifestait quau runtime sur une jonction incompatible.

S54 ajoute __init_subclass__ qui lève TypeError à la définition
si lordre est mauvais avec un message qui suggère la correction
concrète. Les 4 VLM existants restent valides.

#14 - ExecutionPlan.metric_junctions
------------------------------------
Documenté honnêtement comme à venir : le PipelineExecutor ne
consomme pas encore ces jonctions au runtime (auto-évaluation
prévue dans un sprint dédié). Le champ est livré pour fixer le
contrat.

#22 - Listing des shims assumés
-------------------------------
Documenté dans CHANGELOG.md (voir entrée séparée).

Tests : 35 passed dans tests/adapters/vlm/ (5 S54 nouveaux + 30
S45). Test propriété : LLM-first ordre rejeté avec message
helpful (BaseVLMAdapter + AnthropicAdapter mentionnés + Corrigez).

https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP

picarones/adapters/vlm/base.py CHANGED
@@ -61,8 +61,62 @@ class BaseVLMAdapter(BaseLLMAdapter):
61
  Config dict ; supporte
62
  ``config["transcription_prompt"]`` pour personnaliser le
63
  prompt de transcription.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  """
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  @property
67
  def input_types(self) -> "frozenset":
68
  return frozenset({ArtifactType.IMAGE})
 
61
  Config dict ; supporte
62
  ``config["transcription_prompt"]`` pour personnaliser le
63
  prompt de transcription.
64
+
65
+ Sprint S54 — garde-fou MRO (audit #6)
66
+ -------------------------------------
67
+ Les VLM concrets utilisent l'héritage multiple :
68
+
69
+ ::
70
+
71
+ class AnthropicVLMAdapter(BaseVLMAdapter, AnthropicAdapter)
72
+
73
+ L'ordre est critique : ``BaseVLMAdapter`` doit venir d'ABORD
74
+ pour que ``input_types``, ``output_types``, ``execute``, et
75
+ ``DEFAULT_TRANSCRIPTION_PROMPT`` soient résolus depuis lui (et
76
+ pas depuis le LLM sibling qui aurait des output_types =
77
+ {CORRECTED_TEXT}).
78
+
79
+ ``__init_subclass__`` valide cet ordre à la définition de la
80
+ classe. Si le développeur swap accidentellement les parents
81
+ par habitude alphabétique, la définition de classe lève une
82
+ ``TypeError`` immédiate au lieu d'un comportement silencieusement
83
+ différent (output_types incorrect au runtime).
84
  """
85
 
86
+ def __init_subclass__(cls, **kwargs) -> None:
87
+ super().__init_subclass__(**kwargs)
88
+ # Garde-fou : BaseVLMAdapter doit être le premier parent
89
+ # *non-trivial* dans l'ordre de la déclaration (pour gagner
90
+ # le MRO sur les attributs surchargés).
91
+ bases = cls.__bases__
92
+ if len(bases) <= 1:
93
+ # Sous-classe directe simple — pas de MRO multiple, OK.
94
+ return
95
+ # On parcourt les bases dans l'ordre déclaré.
96
+ try:
97
+ vlm_idx = next(
98
+ i for i, b in enumerate(bases)
99
+ if issubclass(b, BaseVLMAdapter)
100
+ )
101
+ except StopIteration:
102
+ return # ne devrait pas arriver, vlm subclass DOIT inclure VLM
103
+ # Toutes les bases AVANT BaseVLMAdapter doivent être
104
+ # neutres (mixins sans surcharge des output_types).
105
+ for prev in bases[:vlm_idx]:
106
+ if issubclass(prev, BaseLLMAdapter) and not issubclass(
107
+ prev, BaseVLMAdapter,
108
+ ):
109
+ raise TypeError(
110
+ f"{cls.__name__} : ordre MRO incorrect — "
111
+ f"BaseVLMAdapter doit précéder {prev.__name__} "
112
+ "dans la liste des parents pour que les "
113
+ "output_types VLM ({IMAGE} → {RAW_TEXT}) "
114
+ "soient résolus correctement (et pas écrasés "
115
+ "par les output_types LLM = {CORRECTED_TEXT}). "
116
+ f"Corrigez : `class {cls.__name__}(BaseVLMAdapter, "
117
+ f"{prev.__name__})`.",
118
+ )
119
+
120
  @property
121
  def input_types(self) -> "frozenset":
122
  return frozenset({ArtifactType.IMAGE})
picarones/pipeline/planner.py CHANGED
@@ -197,6 +197,15 @@ class ExecutionPlan:
197
  metric_junctions:
198
  Jonctions auto-détectées si un ``MetricRegistry`` était
199
  fourni au planner ; tuple vide sinon.
 
 
 
 
 
 
 
 
 
200
  """
201
 
202
  spec: PipelineSpec
 
197
  metric_junctions:
198
  Jonctions auto-détectées si un ``MetricRegistry`` était
199
  fourni au planner ; tuple vide sinon.
200
+
201
+ Sprint S54 — note honnête (audit #14) : à ce jour, le
202
+ ``PipelineExecutor`` ne consomme pas ces jonctions au runtime
203
+ (le calcul des métriques aux jonctions intra-pipeline est
204
+ prévu dans un sprint dédié de l'axe « auto-évaluation »).
205
+ Le champ est livré dès maintenant pour fixer le contrat —
206
+ un caller peut déjà l'utiliser pour de l'introspection
207
+ (rapport, diagnostic). Pas de risque de breaking change
208
+ quand l'auto-évaluation arrivera.
209
  """
210
 
211
  spec: PipelineSpec
tests/adapters/vlm/test_sprint_a14_s54_mro_guard.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sprint A14-S54 — garde-fou MRO BaseVLMAdapter (fix audit #6).
2
+
3
+ Avant S54, l'ordre des parents dans :
4
+
5
+ class AnthropicVLMAdapter(BaseVLMAdapter, AnthropicAdapter)
6
+
7
+ était critique mais non vérifié. Un swap accidentel à
8
+ ``(AnthropicAdapter, BaseVLMAdapter)`` aurait silencieusement donné
9
+ output_types = {CORRECTED_TEXT} (depuis LLM) au lieu de {RAW_TEXT}
10
+ (depuis VLM) — l'erreur ne se serait manifestée qu'au runtime sur
11
+ une jonction de type incompatible.
12
+
13
+ S54 ajoute ``__init_subclass__`` qui lève ``TypeError`` à la
14
+ définition de la classe si l'ordre est incorrect.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import pytest
20
+
21
+ from picarones.adapters.llm.anthropic_adapter import AnthropicAdapter
22
+ from picarones.adapters.llm.openai_adapter import OpenAIAdapter
23
+ from picarones.adapters.vlm import (
24
+ AnthropicVLMAdapter,
25
+ BaseVLMAdapter,
26
+ OpenAIVLMAdapter,
27
+ )
28
+ from picarones.domain.artifacts import ArtifactType
29
+
30
+
31
+ class TestExistingAdaptersStillValid:
32
+ """Les 4 VLM adapters concrets définis correctement passent."""
33
+
34
+ def test_anthropic_vlm_defined(self) -> None:
35
+ # Si l'ordre était mauvais, l'import aurait planté.
36
+ adapter = AnthropicVLMAdapter()
37
+ assert adapter.input_types == frozenset({ArtifactType.IMAGE})
38
+ assert adapter.output_types == frozenset({ArtifactType.RAW_TEXT})
39
+
40
+ def test_openai_vlm_defined(self) -> None:
41
+ adapter = OpenAIVLMAdapter()
42
+ assert adapter.input_types == frozenset({ArtifactType.IMAGE})
43
+
44
+
45
+ class TestWrongOrderRejected:
46
+ def test_llm_first_then_vlm_rejected(self) -> None:
47
+ """Définir une classe avec LLM avant VLM doit lever TypeError."""
48
+ with pytest.raises(TypeError, match="ordre MRO"):
49
+ # Définition dynamique d'une classe avec mauvais ordre.
50
+ type(
51
+ "BadOrderVLM",
52
+ (AnthropicAdapter, BaseVLMAdapter),
53
+ {"name": property(lambda self: "bad")},
54
+ )
55
+
56
+ def test_correct_order_accepted(self) -> None:
57
+ """L'ordre correct (VLM en premier) est accepté."""
58
+ # Test propriété : aucun TypeError levé.
59
+ type(
60
+ "GoodOrderVLM",
61
+ (BaseVLMAdapter, OpenAIAdapter),
62
+ {"name": property(lambda self: "good")},
63
+ )
64
+
65
+
66
+ class TestErrorMessageHelpful:
67
+ def test_message_explains_the_fix(self) -> None:
68
+ with pytest.raises(TypeError) as exc_info:
69
+ type(
70
+ "BadVLM",
71
+ (AnthropicAdapter, BaseVLMAdapter),
72
+ {"name": property(lambda self: "x")},
73
+ )
74
+ msg = str(exc_info.value)
75
+ # Le message doit suggérer la correction concrète.
76
+ assert "BaseVLMAdapter" in msg
77
+ assert "AnthropicAdapter" in msg
78
+ assert "Corrigez" in msg or "correct" in msg.lower()