livepilot 1.9.13 → 1.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +51 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +7 -7
  6. package/bin/livepilot.js +32 -8
  7. package/installer/install.js +21 -2
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
  11. package/livepilot/skills/livepilot-core/SKILL.md +81 -6
  12. package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
  13. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  14. package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
  15. package/livepilot/skills/livepilot-release/SKILL.md +13 -13
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/livepilot_bridge.js +6 -3
  18. package/mcp_server/__init__.py +1 -1
  19. package/mcp_server/curves.py +11 -3
  20. package/mcp_server/evaluation/__init__.py +1 -0
  21. package/mcp_server/evaluation/fabric.py +575 -0
  22. package/mcp_server/evaluation/feature_extractors.py +84 -0
  23. package/mcp_server/evaluation/policy.py +67 -0
  24. package/mcp_server/evaluation/tools.py +53 -0
  25. package/mcp_server/memory/__init__.py +11 -2
  26. package/mcp_server/memory/anti_memory.py +78 -0
  27. package/mcp_server/memory/promotion.py +94 -0
  28. package/mcp_server/memory/session_memory.py +108 -0
  29. package/mcp_server/memory/taste_memory.py +158 -0
  30. package/mcp_server/memory/technique_store.py +2 -1
  31. package/mcp_server/memory/tools.py +112 -0
  32. package/mcp_server/mix_engine/__init__.py +1 -0
  33. package/mcp_server/mix_engine/critics.py +299 -0
  34. package/mcp_server/mix_engine/models.py +152 -0
  35. package/mcp_server/mix_engine/planner.py +103 -0
  36. package/mcp_server/mix_engine/state_builder.py +316 -0
  37. package/mcp_server/mix_engine/tools.py +214 -0
  38. package/mcp_server/performance_engine/__init__.py +1 -0
  39. package/mcp_server/performance_engine/models.py +148 -0
  40. package/mcp_server/performance_engine/planner.py +267 -0
  41. package/mcp_server/performance_engine/safety.py +162 -0
  42. package/mcp_server/performance_engine/tools.py +183 -0
  43. package/mcp_server/project_brain/__init__.py +6 -0
  44. package/mcp_server/project_brain/arrangement_graph.py +64 -0
  45. package/mcp_server/project_brain/automation_graph.py +72 -0
  46. package/mcp_server/project_brain/builder.py +123 -0
  47. package/mcp_server/project_brain/capability_graph.py +64 -0
  48. package/mcp_server/project_brain/models.py +282 -0
  49. package/mcp_server/project_brain/refresh.py +80 -0
  50. package/mcp_server/project_brain/role_graph.py +103 -0
  51. package/mcp_server/project_brain/session_graph.py +51 -0
  52. package/mcp_server/project_brain/tools.py +144 -0
  53. package/mcp_server/reference_engine/__init__.py +1 -0
  54. package/mcp_server/reference_engine/gap_analyzer.py +239 -0
  55. package/mcp_server/reference_engine/models.py +105 -0
  56. package/mcp_server/reference_engine/profile_builder.py +149 -0
  57. package/mcp_server/reference_engine/tactic_router.py +117 -0
  58. package/mcp_server/reference_engine/tools.py +235 -0
  59. package/mcp_server/runtime/__init__.py +1 -0
  60. package/mcp_server/runtime/action_ledger.py +117 -0
  61. package/mcp_server/runtime/action_ledger_models.py +84 -0
  62. package/mcp_server/runtime/action_tools.py +57 -0
  63. package/mcp_server/runtime/capability_state.py +218 -0
  64. package/mcp_server/runtime/safety_kernel.py +339 -0
  65. package/mcp_server/runtime/safety_tools.py +42 -0
  66. package/mcp_server/runtime/tools.py +64 -0
  67. package/mcp_server/server.py +23 -1
  68. package/mcp_server/sound_design/__init__.py +1 -0
  69. package/mcp_server/sound_design/critics.py +297 -0
  70. package/mcp_server/sound_design/models.py +147 -0
  71. package/mcp_server/sound_design/planner.py +104 -0
  72. package/mcp_server/sound_design/tools.py +297 -0
  73. package/mcp_server/tools/_agent_os_engine.py +947 -0
  74. package/mcp_server/tools/_composition_engine.py +1530 -0
  75. package/mcp_server/tools/_conductor.py +199 -0
  76. package/mcp_server/tools/_conductor_budgets.py +222 -0
  77. package/mcp_server/tools/_evaluation_contracts.py +91 -0
  78. package/mcp_server/tools/_form_engine.py +416 -0
  79. package/mcp_server/tools/_motif_engine.py +351 -0
  80. package/mcp_server/tools/_planner_engine.py +516 -0
  81. package/mcp_server/tools/_research_engine.py +542 -0
  82. package/mcp_server/tools/_research_provider.py +185 -0
  83. package/mcp_server/tools/_snapshot_normalizer.py +49 -0
  84. package/mcp_server/tools/agent_os.py +440 -0
  85. package/mcp_server/tools/analyzer.py +18 -0
  86. package/mcp_server/tools/automation.py +25 -10
  87. package/mcp_server/tools/composition.py +563 -0
  88. package/mcp_server/tools/motif.py +104 -0
  89. package/mcp_server/tools/planner.py +144 -0
  90. package/mcp_server/tools/research.py +223 -0
  91. package/mcp_server/tools/tracks.py +18 -3
  92. package/mcp_server/tools/transport.py +10 -2
  93. package/mcp_server/transition_engine/__init__.py +6 -0
  94. package/mcp_server/transition_engine/archetypes.py +167 -0
  95. package/mcp_server/transition_engine/critics.py +340 -0
  96. package/mcp_server/transition_engine/models.py +90 -0
  97. package/mcp_server/transition_engine/tools.py +291 -0
  98. package/mcp_server/translation_engine/__init__.py +5 -0
  99. package/mcp_server/translation_engine/critics.py +297 -0
  100. package/mcp_server/translation_engine/models.py +27 -0
  101. package/mcp_server/translation_engine/tools.py +74 -0
  102. package/package.json +2 -2
  103. package/remote_script/LivePilot/__init__.py +1 -1
  104. package/remote_script/LivePilot/arrangement.py +12 -2
  105. package/requirements.txt +1 -1
@@ -0,0 +1,167 @@
1
+ """Transition archetype library — 7 curated transition patterns.
2
+
3
+ Each archetype encodes a musically validated approach to section
4
+ boundaries, with use cases, risk profile, devices, gestures, and
5
+ verification cues.
6
+
7
+ Zero I/O.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .models import TransitionArchetype, TransitionBoundary
13
+
14
+
15
+ # ── Archetype Library ─────────────────────────────────────────────────
16
+
17
+ TRANSITION_ARCHETYPES: dict[str, TransitionArchetype] = {
18
+ "subtractive_inhale": TransitionArchetype(
19
+ name="subtractive_inhale",
20
+ description="Pull energy back before impact — strip elements to create anticipation, then release into the new section.",
21
+ use_cases=["build_to_drop", "verse_to_chorus", "pre_peak_tension"],
22
+ risk_profile="low",
23
+ devices=["Auto Filter", "Utility", "Compressor"],
24
+ gestures=["inhale", "conceal", "release"],
25
+ verification=[
26
+ "Energy dips before boundary bar",
27
+ "At least 2 tracks reduce volume or filter cutoff",
28
+ "New section feels louder by contrast, not by gain",
29
+ ],
30
+ ),
31
+ "fill_and_reset": TransitionArchetype(
32
+ name="fill_and_reset",
33
+ description="Drum fill or rhythmic intensification, then clean downbeat — classic transition that signals change without ambiguity.",
34
+ use_cases=["verse_to_chorus", "chorus_to_verse", "any_section_change"],
35
+ risk_profile="low",
36
+ devices=["Drum Rack", "Simpler", "Beat Repeat"],
37
+ gestures=["punctuate", "release"],
38
+ verification=[
39
+ "Fill occupies last 1-2 bars before boundary",
40
+ "Downbeat of new section is rhythmically clean",
41
+ "Fill density doesn't overwhelm the arrangement",
42
+ ],
43
+ ),
44
+ "tail_throw": TransitionArchetype(
45
+ name="tail_throw",
46
+ description="Delay or reverb throw on the last hit of outgoing section — creates continuity while the new section enters.",
47
+ use_cases=["phrase_cadence", "section_end_punctuation", "dub_style_transition"],
48
+ risk_profile="low",
49
+ devices=["Delay", "Reverb", "Echo"],
50
+ gestures=["punctuate", "drift"],
51
+ verification=[
52
+ "Send level spikes on last beat/bar of outgoing section",
53
+ "Tail decays naturally into new section",
54
+ "Tail doesn't mask arrival of new elements",
55
+ ],
56
+ ),
57
+ "width_bloom": TransitionArchetype(
58
+ name="width_bloom",
59
+ description="Narrow the stereo field before the boundary, then widen at arrival — creates a sense of opening up.",
60
+ use_cases=["verse_to_chorus", "breakdown_to_drop", "section_expansion"],
61
+ risk_profile="medium",
62
+ devices=["Utility", "Auto Pan", "Chorus-Ensemble", "Wider"],
63
+ gestures=["conceal", "reveal"],
64
+ verification=[
65
+ "Stereo width measurably narrows before boundary",
66
+ "Width expands at or just after boundary bar",
67
+ "Mono compatibility preserved during narrow phase",
68
+ ],
69
+ ),
70
+ "harmonic_suspend": TransitionArchetype(
71
+ name="harmonic_suspend",
72
+ description="Suspend chord (sus2/sus4) or dominant 7th at section end, resolve on arrival — harmonic tension drives the transition.",
73
+ use_cases=["key_change", "chord_progression_pivot", "harmonic_lift"],
74
+ risk_profile="medium",
75
+ devices=["Instrument Rack", "Wavetable", "Operator"],
76
+ gestures=["lift", "release"],
77
+ verification=[
78
+ "Suspension or dominant chord appears in last 1-2 bars",
79
+ "Resolution lands on downbeat of new section",
80
+ "Voice leading is smooth (no large parallel jumps)",
81
+ ],
82
+ ),
83
+ "impact_vacuum": TransitionArchetype(
84
+ name="impact_vacuum",
85
+ description="Everything cuts to silence (or near-silence), brief pause, then full impact — maximum contrast transition.",
86
+ use_cases=["build_to_drop", "pre_peak_climax", "dramatic_reentry"],
87
+ risk_profile="high",
88
+ devices=["Utility", "Gate", "Compressor"],
89
+ gestures=["conceal", "release"],
90
+ verification=[
91
+ "Clear silence or near-silence for at least half a bar",
92
+ "Impact arrival is immediate and full",
93
+ "Silence doesn't feel like a mistake or glitch",
94
+ ],
95
+ ),
96
+ "delayed_foreground_handoff": TransitionArchetype(
97
+ name="delayed_foreground_handoff",
98
+ description="Old lead element fades while new lead enters underneath — overlapping handoff avoids abrupt role change.",
99
+ use_cases=["lead_change", "hook_rotation", "verse_vocal_to_instrumental_hook"],
100
+ risk_profile="medium",
101
+ devices=["Utility", "Auto Filter", "EQ Eight"],
102
+ gestures=["conceal", "reveal", "handoff"],
103
+ verification=[
104
+ "Outgoing lead fades over 2-4 bars across boundary",
105
+ "Incoming lead enters before outgoing fully exits",
106
+ "No frequency masking between overlapping leads",
107
+ ],
108
+ ),
109
+ }
110
+
111
+
112
+ # ── Archetype Selection ───────────────────────────────────────────────
113
+
114
+ # Maps (from_type, to_type) pairs to preferred archetypes.
115
+ _SECTION_PAIR_PREFERENCES: dict[tuple[str, str], list[str]] = {
116
+ ("build", "drop"): ["subtractive_inhale", "impact_vacuum"],
117
+ ("verse", "chorus"): ["fill_and_reset", "width_bloom", "subtractive_inhale"],
118
+ ("chorus", "verse"): ["tail_throw", "delayed_foreground_handoff"],
119
+ ("intro", "verse"): ["delayed_foreground_handoff", "fill_and_reset"],
120
+ ("breakdown", "drop"): ["impact_vacuum", "subtractive_inhale"],
121
+ ("breakdown", "chorus"): ["width_bloom", "subtractive_inhale"],
122
+ ("verse", "bridge"): ["harmonic_suspend", "tail_throw"],
123
+ ("bridge", "chorus"): ["subtractive_inhale", "width_bloom"],
124
+ ("chorus", "bridge"): ["tail_throw", "harmonic_suspend"],
125
+ ("pre_chorus", "chorus"): ["subtractive_inhale", "fill_and_reset"],
126
+ ("verse", "pre_chorus"): ["fill_and_reset", "tail_throw"],
127
+ ("drop", "breakdown"): ["tail_throw", "delayed_foreground_handoff"],
128
+ ("chorus", "outro"): ["tail_throw", "delayed_foreground_handoff"],
129
+ ("verse", "outro"): ["tail_throw", "delayed_foreground_handoff"],
130
+ }
131
+
132
+
133
+ def select_archetype(boundary: TransitionBoundary) -> TransitionArchetype:
134
+ """Pick the best-fit archetype for a given boundary.
135
+
136
+ Strategy:
137
+ 1. Check section-type pair preferences.
138
+ 2. Fall back to energy-delta heuristics.
139
+ """
140
+ pair = (boundary.from_type, boundary.to_type)
141
+
142
+ # Check explicit section-pair preferences
143
+ if pair in _SECTION_PAIR_PREFERENCES:
144
+ preferred = _SECTION_PAIR_PREFERENCES[pair]
145
+ return TRANSITION_ARCHETYPES[preferred[0]]
146
+
147
+ # Energy-delta heuristics
148
+ ed = boundary.energy_delta
149
+
150
+ # Large energy increase — needs preparation
151
+ if ed > 0.3:
152
+ return TRANSITION_ARCHETYPES["subtractive_inhale"]
153
+
154
+ # Large energy decrease — needs graceful exit
155
+ if ed < -0.3:
156
+ return TRANSITION_ARCHETYPES["tail_throw"]
157
+
158
+ # Moderate increase — fill works universally
159
+ if ed > 0.1:
160
+ return TRANSITION_ARCHETYPES["fill_and_reset"]
161
+
162
+ # Moderate decrease — handoff preserves continuity
163
+ if ed < -0.1:
164
+ return TRANSITION_ARCHETYPES["delayed_foreground_handoff"]
165
+
166
+ # Flat energy — width bloom adds interest without energy change
167
+ return TRANSITION_ARCHETYPES["width_bloom"]
@@ -0,0 +1,340 @@
1
+ """Transition Engine critics — 5 boundary-specific critics.
2
+
3
+ Detect transition quality issues: boundary clarity, payoff strength,
4
+ overtelegraphing, energy redirection, and gesture fit.
5
+
6
+ All pure computation, zero I/O.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import asdict, dataclass, field
12
+
13
+ from .models import TransitionBoundary, TransitionPlan, TransitionScore
14
+
15
+
16
+ # ── TransitionIssue ───────────────────────────────────────────────────
17
+
18
+
19
+ @dataclass
20
+ class TransitionIssue:
21
+ """A single detected transition issue."""
22
+
23
+ issue_type: str = ""
24
+ critic: str = ""
25
+ severity: float = 0.0 # 0.0-1.0
26
+ confidence: float = 0.0 # 0.0-1.0
27
+ boundary: dict = field(default_factory=dict)
28
+ evidence: str = ""
29
+ recommended_moves: list[str] = field(default_factory=list)
30
+
31
+ def to_dict(self) -> dict:
32
+ return asdict(self)
33
+
34
+
35
+ # ── Boundary Clarity Critic ───────────────────────────────────────────
36
+
37
+
38
+ def run_boundary_clarity_critic(
39
+ boundary: TransitionBoundary,
40
+ ) -> list[TransitionIssue]:
41
+ """Detect unclear boundaries — no energy or density change."""
42
+ issues: list[TransitionIssue] = []
43
+
44
+ abs_energy = abs(boundary.energy_delta)
45
+ abs_density = abs(boundary.density_delta)
46
+
47
+ # Both energy and density flat — boundary is invisible
48
+ if abs_energy < 0.05 and abs_density < 0.05:
49
+ issues.append(TransitionIssue(
50
+ issue_type="invisible_boundary",
51
+ critic="boundary_clarity",
52
+ severity=0.7,
53
+ confidence=0.75,
54
+ boundary=boundary.to_dict(),
55
+ evidence=(
56
+ f"Neither energy (delta={boundary.energy_delta:.2f}) nor density "
57
+ f"(delta={boundary.density_delta:.2f}) changes at boundary bar "
58
+ f"{boundary.boundary_bar}"
59
+ ),
60
+ recommended_moves=[
61
+ "add_fill_at_boundary",
62
+ "vary_density_before_arrival",
63
+ "insert_breath_before_downbeat",
64
+ ],
65
+ ))
66
+
67
+ # Only density changes but energy is flat — structural but not felt
68
+ if abs_energy < 0.05 and abs_density >= 0.1:
69
+ issues.append(TransitionIssue(
70
+ issue_type="structural_only_boundary",
71
+ critic="boundary_clarity",
72
+ severity=0.4,
73
+ confidence=0.60,
74
+ boundary=boundary.to_dict(),
75
+ evidence=(
76
+ f"Density shifts (delta={boundary.density_delta:.2f}) but energy "
77
+ f"is flat (delta={boundary.energy_delta:.2f}) — transition may feel "
78
+ f"like track muting, not a musical boundary"
79
+ ),
80
+ recommended_moves=[
81
+ "add_energy_gesture_at_boundary",
82
+ "automate_filter_or_volume_sweep",
83
+ ],
84
+ ))
85
+
86
+ return issues
87
+
88
+
89
+ # ── Payoff Critic ─────────────────────────────────────────────────────
90
+
91
+
92
+ def run_payoff_critic(
93
+ boundary: TransitionBoundary,
94
+ score: TransitionScore,
95
+ ) -> list[TransitionIssue]:
96
+ """Detect weak payoff — high energy arrival without reward."""
97
+ issues: list[TransitionIssue] = []
98
+
99
+ # High energy increase but low payoff score
100
+ if boundary.energy_delta > 0.2 and score.payoff_strength < 0.4:
101
+ issues.append(TransitionIssue(
102
+ issue_type="weak_payoff",
103
+ critic="payoff",
104
+ severity=0.7,
105
+ confidence=0.70,
106
+ boundary=boundary.to_dict(),
107
+ evidence=(
108
+ f"Energy rises by {boundary.energy_delta:.2f} at bar "
109
+ f"{boundary.boundary_bar} but payoff_strength is only "
110
+ f"{score.payoff_strength:.2f} — arrival doesn't feel earned"
111
+ ),
112
+ recommended_moves=[
113
+ "add_pre_arrival_subtraction",
114
+ "increase_contrast_at_boundary",
115
+ "add_impact_element_at_downbeat",
116
+ ],
117
+ ))
118
+
119
+ # Build section into drop/chorus with low payoff
120
+ build_types = {"build", "pre_chorus"}
121
+ peak_types = {"drop", "chorus"}
122
+ if (boundary.from_type in build_types
123
+ and boundary.to_type in peak_types
124
+ and score.payoff_strength < 0.5):
125
+ issues.append(TransitionIssue(
126
+ issue_type="anticlimactic_arrival",
127
+ critic="payoff",
128
+ severity=0.8,
129
+ confidence=0.75,
130
+ boundary=boundary.to_dict(),
131
+ evidence=(
132
+ f"{boundary.from_type} -> {boundary.to_type} transition at bar "
133
+ f"{boundary.boundary_bar} has payoff {score.payoff_strength:.2f} "
134
+ f"— peak section should feel like a reward"
135
+ ),
136
+ recommended_moves=[
137
+ "deepen_subtraction_in_build",
138
+ "add_impact_vacuum",
139
+ "widen_stereo_at_arrival",
140
+ ],
141
+ ))
142
+
143
+ return issues
144
+
145
+
146
+ # ── Overtelegraphing Critic ───────────────────────────────────────────
147
+
148
+
149
+ def run_overtelegraphing_critic(
150
+ plan: TransitionPlan,
151
+ ) -> list[TransitionIssue]:
152
+ """Detect transitions that use too many gestures — trying too hard."""
153
+ issues: list[TransitionIssue] = []
154
+
155
+ total_gestures = len(plan.lead_in_gestures) + len(plan.arrival_gestures)
156
+
157
+ # Too many gestures — overproduced transition
158
+ if total_gestures > 5:
159
+ issues.append(TransitionIssue(
160
+ issue_type="overtelegraphed_transition",
161
+ critic="overtelegraphing",
162
+ severity=min(1.0, 0.3 + (total_gestures - 5) * 0.15),
163
+ confidence=0.65,
164
+ boundary=plan.boundary.to_dict(),
165
+ evidence=(
166
+ f"Transition uses {total_gestures} gestures "
167
+ f"({len(plan.lead_in_gestures)} lead-in, "
168
+ f"{len(plan.arrival_gestures)} arrival) — more FX != better transition"
169
+ ),
170
+ recommended_moves=[
171
+ "remove_weakest_gesture",
172
+ "simplify_to_one_lead_in_and_one_arrival",
173
+ "trust_the_arrangement",
174
+ ],
175
+ ))
176
+
177
+ # High-risk archetype with many gestures — doubly obvious
178
+ if plan.archetype.risk_profile == "high" and total_gestures > 3:
179
+ issues.append(TransitionIssue(
180
+ issue_type="high_risk_overloaded",
181
+ critic="overtelegraphing",
182
+ severity=0.6,
183
+ confidence=0.60,
184
+ boundary=plan.boundary.to_dict(),
185
+ evidence=(
186
+ f"High-risk archetype '{plan.archetype.name}' combined with "
187
+ f"{total_gestures} gestures — dramatic archetype needs fewer "
188
+ f"gestures, not more"
189
+ ),
190
+ recommended_moves=[
191
+ "reduce_to_core_gestures_only",
192
+ "let_archetype_do_the_work",
193
+ ],
194
+ ))
195
+
196
+ return issues
197
+
198
+
199
+ # ── Energy Redirection Critic ─────────────────────────────────────────
200
+
201
+
202
+ def run_energy_redirection_critic(
203
+ boundary: TransitionBoundary,
204
+ ) -> list[TransitionIssue]:
205
+ """Detect boundaries where energy doesn't redirect enough."""
206
+ issues: list[TransitionIssue] = []
207
+
208
+ abs_energy = abs(boundary.energy_delta)
209
+
210
+ # Section types that demand energy change
211
+ high_contrast_pairs = {
212
+ ("build", "drop"), ("breakdown", "drop"),
213
+ ("breakdown", "chorus"), ("pre_chorus", "chorus"),
214
+ }
215
+ low_contrast_pairs = {
216
+ ("verse", "verse"), ("chorus", "chorus"),
217
+ }
218
+
219
+ pair = (boundary.from_type, boundary.to_type)
220
+
221
+ # High-contrast pair with low energy change — transition falls flat
222
+ if pair in high_contrast_pairs and abs_energy < 0.15:
223
+ issues.append(TransitionIssue(
224
+ issue_type="flat_high_contrast_transition",
225
+ critic="energy_redirection",
226
+ severity=0.7,
227
+ confidence=0.70,
228
+ boundary=boundary.to_dict(),
229
+ evidence=(
230
+ f"{boundary.from_type} -> {boundary.to_type} expects significant "
231
+ f"energy change but delta is only {boundary.energy_delta:.2f}"
232
+ ),
233
+ recommended_moves=[
234
+ "increase_energy_contrast",
235
+ "subtract_before_arrival",
236
+ "add_elements_at_arrival",
237
+ ],
238
+ ))
239
+
240
+ # Same section type repeating with large energy change — unintentional
241
+ if pair in low_contrast_pairs and abs_energy > 0.3:
242
+ issues.append(TransitionIssue(
243
+ issue_type="unexpected_energy_shift",
244
+ critic="energy_redirection",
245
+ severity=0.4,
246
+ confidence=0.55,
247
+ boundary=boundary.to_dict(),
248
+ evidence=(
249
+ f"Repeating section type '{boundary.from_type}' has energy delta "
250
+ f"of {boundary.energy_delta:.2f} — may feel inconsistent"
251
+ ),
252
+ recommended_moves=[
253
+ "normalize_energy_across_repeats",
254
+ "differentiate_section_types_if_intentional",
255
+ ],
256
+ ))
257
+
258
+ return issues
259
+
260
+
261
+ # ── Gesture Fit Critic ────────────────────────────────────────────────
262
+
263
+
264
+ def run_gesture_fit_critic(
265
+ plan: TransitionPlan,
266
+ ) -> list[TransitionIssue]:
267
+ """Detect mismatches between archetype and section types."""
268
+ issues: list[TransitionIssue] = []
269
+
270
+ boundary = plan.boundary
271
+ archetype = plan.archetype
272
+
273
+ # Check if the section pair matches any of the archetype's use cases
274
+ pair_tags = {
275
+ f"{boundary.from_type}_to_{boundary.to_type}",
276
+ boundary.from_type,
277
+ boundary.to_type,
278
+ }
279
+ use_case_match = any(
280
+ tag in uc or uc in tag
281
+ for tag in pair_tags
282
+ for uc in archetype.use_cases
283
+ )
284
+
285
+ if not use_case_match:
286
+ issues.append(TransitionIssue(
287
+ issue_type="archetype_section_mismatch",
288
+ critic="gesture_fit",
289
+ severity=0.5,
290
+ confidence=0.55,
291
+ boundary=boundary.to_dict(),
292
+ evidence=(
293
+ f"Archetype '{archetype.name}' (use_cases={archetype.use_cases}) "
294
+ f"doesn't match section pair {boundary.from_type} -> "
295
+ f"{boundary.to_type}"
296
+ ),
297
+ recommended_moves=[
298
+ "select_different_archetype",
299
+ "customize_gestures_for_section_pair",
300
+ ],
301
+ ))
302
+
303
+ # High-risk archetype for low-energy transitions — overkill
304
+ if archetype.risk_profile == "high" and abs(boundary.energy_delta) < 0.15:
305
+ issues.append(TransitionIssue(
306
+ issue_type="overkill_archetype",
307
+ critic="gesture_fit",
308
+ severity=0.5,
309
+ confidence=0.60,
310
+ boundary=boundary.to_dict(),
311
+ evidence=(
312
+ f"High-risk archetype '{archetype.name}' used for low-contrast "
313
+ f"transition (energy_delta={boundary.energy_delta:.2f}) — "
314
+ f"dramatic technique for a subtle moment"
315
+ ),
316
+ recommended_moves=[
317
+ "use_lower_risk_archetype",
318
+ "increase_energy_contrast_if_dramatic_intent",
319
+ ],
320
+ ))
321
+
322
+ return issues
323
+
324
+
325
+ # ── Run All ───────────────────────────────────────────────────────────
326
+
327
+
328
+ def run_all_transition_critics(
329
+ boundary: TransitionBoundary,
330
+ plan: TransitionPlan,
331
+ score: TransitionScore,
332
+ ) -> list[TransitionIssue]:
333
+ """Run all 5 transition critics and return combined issues."""
334
+ issues: list[TransitionIssue] = []
335
+ issues.extend(run_boundary_clarity_critic(boundary))
336
+ issues.extend(run_payoff_critic(boundary, score))
337
+ issues.extend(run_overtelegraphing_critic(plan))
338
+ issues.extend(run_energy_redirection_critic(boundary))
339
+ issues.extend(run_gesture_fit_critic(plan))
340
+ return issues
@@ -0,0 +1,90 @@
1
+ """Transition Engine state models — all dataclasses with to_dict().
2
+
3
+ Pure data structures for boundary analysis, transition planning,
4
+ archetype selection, and scoring.
5
+
6
+ Zero I/O.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import asdict, dataclass, field
12
+
13
+
14
+ # ── Boundary ──────────────────────────────────────────────────────────
15
+
16
+
17
+ @dataclass
18
+ class TransitionBoundary:
19
+ """The junction between two adjacent sections."""
20
+
21
+ from_section_id: str = ""
22
+ to_section_id: str = ""
23
+ boundary_bar: int = 0
24
+ from_type: str = "unknown"
25
+ to_type: str = "unknown"
26
+ energy_delta: float = 0.0 # positive = energy rises
27
+ density_delta: float = 0.0 # positive = density rises
28
+
29
+ def to_dict(self) -> dict:
30
+ return asdict(self)
31
+
32
+
33
+ # ── Archetype ─────────────────────────────────────────────────────────
34
+
35
+
36
+ @dataclass
37
+ class TransitionArchetype:
38
+ """A curated transition pattern with risk profile and verification cues."""
39
+
40
+ name: str = ""
41
+ description: str = ""
42
+ use_cases: list[str] = field(default_factory=list)
43
+ risk_profile: str = "low" # "low", "medium", "high"
44
+ devices: list[str] = field(default_factory=list)
45
+ gestures: list[str] = field(default_factory=list)
46
+ verification: list[str] = field(default_factory=list)
47
+
48
+ def to_dict(self) -> dict:
49
+ return asdict(self)
50
+
51
+
52
+ # ── Plan ──────────────────────────────────────────────────────────────
53
+
54
+
55
+ @dataclass
56
+ class TransitionPlan:
57
+ """A concrete plan for executing a transition at a boundary."""
58
+
59
+ boundary: TransitionBoundary = field(default_factory=TransitionBoundary)
60
+ archetype: TransitionArchetype = field(default_factory=TransitionArchetype)
61
+ lead_in_gestures: list[dict] = field(default_factory=list)
62
+ arrival_gestures: list[dict] = field(default_factory=list)
63
+ payoff_estimate: float = 0.0 # 0.0-1.0
64
+
65
+ def to_dict(self) -> dict:
66
+ return {
67
+ "boundary": self.boundary.to_dict(),
68
+ "archetype": self.archetype.to_dict(),
69
+ "lead_in_gestures": list(self.lead_in_gestures),
70
+ "arrival_gestures": list(self.arrival_gestures),
71
+ "payoff_estimate": self.payoff_estimate,
72
+ }
73
+
74
+
75
+ # ── Score ─────────────────────────────────────────────────────────────
76
+
77
+
78
+ @dataclass
79
+ class TransitionScore:
80
+ """Multi-dimensional quality score for a transition."""
81
+
82
+ boundary_clarity: float = 0.0 # 0.0-1.0
83
+ payoff_strength: float = 0.0 # 0.0-1.0
84
+ energy_redirection: float = 0.0 # 0.0-1.0
85
+ identity_preservation: float = 0.0 # 0.0-1.0
86
+ cliche_risk: float = 0.0 # 0.0-1.0 (lower is better)
87
+ overall: float = 0.0 # 0.0-1.0
88
+
89
+ def to_dict(self) -> dict:
90
+ return asdict(self)