livepilot 1.10.4 → 1.10.6

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 (74) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +148 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +6 -6
  6. package/livepilot/.Codex-plugin/plugin.json +2 -2
  7. package/livepilot/.claude-plugin/plugin.json +2 -2
  8. package/livepilot/skills/livepilot-core/SKILL.md +4 -4
  9. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  10. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  11. package/livepilot/skills/livepilot-release/SKILL.md +5 -5
  12. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  13. package/m4l_device/livepilot_bridge.js +12 -1
  14. package/manifest.json +3 -3
  15. package/mcp_server/__init__.py +1 -1
  16. package/mcp_server/composer/sample_resolver.py +10 -6
  17. package/mcp_server/composer/tools.py +10 -6
  18. package/mcp_server/connection.py +6 -1
  19. package/mcp_server/creative_constraints/tools.py +9 -8
  20. package/mcp_server/experiment/engine.py +9 -5
  21. package/mcp_server/experiment/tools.py +9 -9
  22. package/mcp_server/hook_hunter/tools.py +14 -9
  23. package/mcp_server/m4l_bridge.py +11 -0
  24. package/mcp_server/memory/taste_graph.py +7 -2
  25. package/mcp_server/mix_engine/tools.py +8 -3
  26. package/mcp_server/musical_intelligence/tools.py +15 -10
  27. package/mcp_server/performance_engine/tools.py +6 -2
  28. package/mcp_server/preview_studio/tools.py +21 -15
  29. package/mcp_server/project_brain/tools.py +18 -10
  30. package/mcp_server/reference_engine/tools.py +7 -5
  31. package/mcp_server/runtime/capability_probe.py +10 -4
  32. package/mcp_server/runtime/tools.py +8 -2
  33. package/mcp_server/sample_engine/tools.py +394 -33
  34. package/mcp_server/semantic_moves/tools.py +5 -1
  35. package/mcp_server/server.py +10 -9
  36. package/mcp_server/services/motif_service.py +9 -3
  37. package/mcp_server/session_continuity/tools.py +7 -3
  38. package/mcp_server/session_continuity/tracker.py +9 -8
  39. package/mcp_server/song_brain/tools.py +17 -12
  40. package/mcp_server/splice_client/client.py +19 -6
  41. package/mcp_server/stuckness_detector/tools.py +8 -5
  42. package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
  43. package/mcp_server/tools/_agent_os_engine/critics.py +134 -0
  44. package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
  45. package/mcp_server/tools/_agent_os_engine/models.py +132 -0
  46. package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
  47. package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
  48. package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
  49. package/mcp_server/tools/_composition_engine/__init__.py +67 -0
  50. package/mcp_server/tools/_composition_engine/analysis.py +174 -0
  51. package/mcp_server/tools/_composition_engine/critics.py +522 -0
  52. package/mcp_server/tools/_composition_engine/gestures.py +230 -0
  53. package/mcp_server/tools/_composition_engine/harmony.py +70 -0
  54. package/mcp_server/tools/_composition_engine/models.py +193 -0
  55. package/mcp_server/tools/_composition_engine/sections.py +371 -0
  56. package/mcp_server/tools/_perception_engine.py +18 -11
  57. package/mcp_server/tools/agent_os.py +23 -15
  58. package/mcp_server/tools/analyzer.py +166 -7
  59. package/mcp_server/tools/automation.py +6 -1
  60. package/mcp_server/tools/composition.py +25 -16
  61. package/mcp_server/tools/devices.py +10 -6
  62. package/mcp_server/tools/motif.py +7 -2
  63. package/mcp_server/tools/planner.py +6 -2
  64. package/mcp_server/tools/research.py +13 -10
  65. package/mcp_server/transition_engine/tools.py +6 -1
  66. package/mcp_server/translation_engine/tools.py +8 -6
  67. package/mcp_server/wonder_mode/engine.py +8 -3
  68. package/mcp_server/wonder_mode/tools.py +29 -21
  69. package/package.json +2 -2
  70. package/remote_script/LivePilot/__init__.py +1 -1
  71. package/requirements.txt +6 -0
  72. package/livepilot.mcpb +0 -0
  73. package/mcp_server/tools/_agent_os_engine.py +0 -947
  74. package/mcp_server/tools/_composition_engine.py +0 -1530
@@ -0,0 +1,522 @@
1
+ """Part of the _composition_engine package — extracted from the single-file engine.
2
+
3
+ Pure-computation core, no external deps. Callers should import from the package
4
+ facade (e.g. `from mcp_server.tools._composition_engine import X`), which
5
+ re-exports everything from these sub-modules.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from dataclasses import asdict, dataclass, field
12
+ from enum import Enum
13
+ from typing import Any, Optional
14
+
15
+ from .models import SectionType, RoleType, SectionNode, PhraseUnit, RoleNode, CompositionIssue, HarmonyField
16
+
17
+ def run_form_critic(sections: list[SectionNode]) -> list[CompositionIssue]:
18
+ """Critique the overall form/structure of the arrangement."""
19
+ issues = []
20
+ if not sections:
21
+ issues.append(CompositionIssue(
22
+ issue_type="no_sections",
23
+ critic="form",
24
+ severity=0.8,
25
+ confidence=1.0,
26
+ evidence="No sections detected in the arrangement",
27
+ recommended_moves=["create_sections", "add_scene_structure"],
28
+ ))
29
+ return issues
30
+
31
+ # 1. Too few sections for a full track
32
+ if len(sections) < 3:
33
+ issues.append(CompositionIssue(
34
+ issue_type="too_few_sections",
35
+ critic="form",
36
+ severity=0.6,
37
+ confidence=0.8,
38
+ evidence=f"Only {len(sections)} section(s) detected",
39
+ recommended_moves=["section_expansion", "add_contrast_section"],
40
+ ))
41
+
42
+ # 2. No energy arc (all sections similar energy)
43
+ if len(sections) >= 2:
44
+ energies = [s.energy for s in sections]
45
+ energy_range = max(energies) - min(energies)
46
+ if energy_range < 0.15:
47
+ issues.append(CompositionIssue(
48
+ issue_type="flat_energy_arc",
49
+ critic="form",
50
+ severity=0.7,
51
+ confidence=0.75,
52
+ evidence=f"Energy range: {energy_range:.2f} (all sections similar density)",
53
+ recommended_moves=["vary_track_count", "add_build_section", "create_breakdown"],
54
+ ))
55
+
56
+ # 3. No contrast between adjacent sections
57
+ for i in range(1, len(sections)):
58
+ prev = sections[i - 1]
59
+ curr = sections[i]
60
+ energy_diff = abs(curr.energy - prev.energy)
61
+ density_diff = abs(curr.density - prev.density)
62
+ if energy_diff < 0.1 and density_diff < 0.1:
63
+ issues.append(CompositionIssue(
64
+ issue_type="no_adjacent_contrast",
65
+ critic="form",
66
+ severity=0.5,
67
+ confidence=0.7,
68
+ scope={"sections": [prev.section_id, curr.section_id]},
69
+ evidence=f"Sections '{prev.name or prev.section_id}' and '{curr.name or curr.section_id}' have similar energy/density",
70
+ recommended_moves=["thin_one_section", "add_element_to_one", "vary_automation"],
71
+ ))
72
+
73
+ # 4. First section too dense (reveals too much too early)
74
+ if sections and sections[0].density > 0.7:
75
+ issues.append(CompositionIssue(
76
+ issue_type="intro_too_dense",
77
+ critic="form",
78
+ severity=0.5,
79
+ confidence=0.65,
80
+ scope={"section_id": sections[0].section_id},
81
+ evidence=f"First section density: {sections[0].density:.2f} (reveals too much)",
82
+ recommended_moves=["remove_elements_from_intro", "defer_reveal"],
83
+ ))
84
+
85
+ return issues
86
+
87
+ def run_section_identity_critic(
88
+ sections: list[SectionNode],
89
+ roles: list[RoleNode],
90
+ ) -> list[CompositionIssue]:
91
+ """Critique individual section identity and clarity."""
92
+ issues = []
93
+
94
+ for section in sections:
95
+ section_roles = [r for r in roles if r.section_id == section.section_id]
96
+ foreground_count = sum(1 for r in section_roles if r.foreground)
97
+
98
+ # 1. No clear foreground element
99
+ if foreground_count == 0 and len(section_roles) > 0:
100
+ issues.append(CompositionIssue(
101
+ issue_type="no_foreground",
102
+ critic="section_identity",
103
+ severity=0.6,
104
+ confidence=0.70,
105
+ scope={"section_id": section.section_id},
106
+ evidence=f"Section '{section.name or section.section_id}' has {len(section_roles)} tracks but none inferred as foreground",
107
+ recommended_moves=["assign_lead_role", "add_hook_element"],
108
+ ))
109
+
110
+ # 2. Too many foreground voices
111
+ if foreground_count > 3:
112
+ issues.append(CompositionIssue(
113
+ issue_type="too_many_foregrounds",
114
+ critic="section_identity",
115
+ severity=0.5,
116
+ confidence=0.65,
117
+ scope={"section_id": section.section_id},
118
+ evidence=f"Section has {foreground_count} foreground elements (max recommended: 3)",
119
+ recommended_moves=["background_some_elements", "thin_section", "use_automation_to_rotate"],
120
+ ))
121
+
122
+ # 3. Section type mismatch — e.g., "chorus" less energetic than "verse"
123
+ # (Compare against adjacent sections of different type)
124
+
125
+ # Cross-section type check
126
+ choruses = [s for s in sections if s.section_type == SectionType.CHORUS]
127
+ verses = [s for s in sections if s.section_type == SectionType.VERSE]
128
+ if choruses and verses:
129
+ chorus_energy = max(s.energy for s in choruses)
130
+ verse_energy = max(s.energy for s in verses)
131
+ if chorus_energy <= verse_energy:
132
+ issues.append(CompositionIssue(
133
+ issue_type="chorus_not_stronger_than_verse",
134
+ critic="section_identity",
135
+ severity=0.6,
136
+ confidence=0.60,
137
+ evidence=f"Chorus energy ({chorus_energy:.2f}) <= verse energy ({verse_energy:.2f})",
138
+ recommended_moves=["add_elements_to_chorus", "thin_verse", "add_chorus_hook"],
139
+ ))
140
+
141
+ return issues
142
+
143
+ def run_phrase_critic(phrases: list[PhraseUnit]) -> list[CompositionIssue]:
144
+ """Critique phrase structure within sections."""
145
+ issues = []
146
+
147
+ if len(phrases) < 2:
148
+ return issues
149
+
150
+ # 1. All phrases identical length (no variation)
151
+ lengths = [p.length_bars() for p in phrases]
152
+ unique_lengths = set(lengths)
153
+ if len(unique_lengths) == 1 and len(phrases) > 3:
154
+ issues.append(CompositionIssue(
155
+ issue_type="uniform_phrase_lengths",
156
+ critic="phrase",
157
+ severity=0.4,
158
+ confidence=0.60,
159
+ evidence=f"All {len(phrases)} phrases are {lengths[0]} bars — no structural variation",
160
+ recommended_moves=["extend_one_phrase", "add_pickup", "truncate_for_surprise"],
161
+ ))
162
+
163
+ # 2. No cadence detected (all cadence_strength near 0)
164
+ weak_cadences = [p for p in phrases if p.cadence_strength < 0.2]
165
+ if len(weak_cadences) > len(phrases) * 0.7:
166
+ issues.append(CompositionIssue(
167
+ issue_type="weak_cadences",
168
+ critic="phrase",
169
+ severity=0.5,
170
+ confidence=0.55,
171
+ evidence=f"{len(weak_cadences)}/{len(phrases)} phrases have weak cadence (< 0.2)",
172
+ recommended_moves=["add_resolution_notes", "create_turnaround", "vary_last_bar"],
173
+ ))
174
+
175
+ # 3. No variation between adjacent phrases
176
+ no_variation = sum(1 for p in phrases if not p.has_variation)
177
+ if no_variation >= len(phrases) - 1 and len(phrases) > 2:
178
+ issues.append(CompositionIssue(
179
+ issue_type="no_phrase_variation",
180
+ critic="phrase",
181
+ severity=0.5,
182
+ confidence=0.60,
183
+ evidence=f"{no_variation}/{len(phrases)} phrases identical to their neighbor",
184
+ recommended_moves=["add_fill", "vary_notes", "create_response_phrase"],
185
+ ))
186
+
187
+ return issues
188
+
189
+
190
+ # ── Transition Critic (Round 1) ──────────────────────────────────────
191
+ def run_transition_critic(
192
+ sections: list[SectionNode],
193
+ roles: list[RoleNode],
194
+ harmony_fields: Optional[list[HarmonyField]] = None,
195
+ ) -> list[CompositionIssue]:
196
+ """Analyze boundaries between adjacent sections for transition quality."""
197
+ issues = []
198
+ if len(sections) < 2:
199
+ return issues
200
+
201
+ harmony_map = {}
202
+ if harmony_fields:
203
+ harmony_map = {hf.section_id: hf for hf in harmony_fields}
204
+
205
+ for i in range(1, len(sections)):
206
+ prev = sections[i - 1]
207
+ curr = sections[i]
208
+
209
+ # 1. Hard cut — no energy or density change at boundary
210
+ energy_delta = abs(curr.energy - prev.energy)
211
+ density_delta = abs(curr.density - prev.density)
212
+
213
+ if energy_delta < 0.05 and density_delta < 0.05:
214
+ issues.append(CompositionIssue(
215
+ issue_type="hard_cut_transition",
216
+ critic="transition",
217
+ severity=0.5,
218
+ confidence=0.70,
219
+ scope={"from": prev.section_id, "to": curr.section_id},
220
+ evidence=f"No energy/density change between '{prev.name or prev.section_id}' and '{curr.name or curr.section_id}'",
221
+ recommended_moves=["add_transition_fx", "create_fill", "vary_density_at_boundary"],
222
+ ))
223
+
224
+ # 2. No pre-arrival subtraction before high-energy section
225
+ if curr.energy > 0.7 and prev.energy > 0.6:
226
+ issues.append(CompositionIssue(
227
+ issue_type="no_pre_arrival_subtraction",
228
+ critic="transition",
229
+ severity=0.6,
230
+ confidence=0.65,
231
+ scope={"from": prev.section_id, "to": curr.section_id},
232
+ evidence=f"High-energy section '{curr.name or curr.section_id}' (E={curr.energy:.2f}) not preceded by subtraction (prev E={prev.energy:.2f})",
233
+ recommended_moves=["thin_preceding_section", "add_breakdown_before_peak", "inhale_gesture"],
234
+ ))
235
+
236
+ # 3. Groove break — rhythmic elements drop out at boundary
237
+ prev_rhythm = {r.track_index for r in roles
238
+ if r.section_id == prev.section_id
239
+ and r.role in (RoleType.KICK_ANCHOR, RoleType.RHYTHMIC_TEXTURE)}
240
+ curr_rhythm = {r.track_index for r in roles
241
+ if r.section_id == curr.section_id
242
+ and r.role in (RoleType.KICK_ANCHOR, RoleType.RHYTHMIC_TEXTURE)}
243
+
244
+ if prev_rhythm and not curr_rhythm:
245
+ issues.append(CompositionIssue(
246
+ issue_type="groove_break_at_transition",
247
+ critic="transition",
248
+ severity=0.5,
249
+ confidence=0.60,
250
+ scope={"from": prev.section_id, "to": curr.section_id},
251
+ evidence=f"All rhythmic elements ({len(prev_rhythm)} tracks) drop out at '{curr.name or curr.section_id}'",
252
+ recommended_moves=["carry_one_rhythm_element", "add_transition_percussion"],
253
+ ))
254
+
255
+ # 4. Harmonic non-sequitur — key change without voice-leading support
256
+ prev_hf = harmony_map.get(prev.section_id)
257
+ curr_hf = harmony_map.get(curr.section_id)
258
+
259
+ if prev_hf and curr_hf and prev_hf.key and curr_hf.key:
260
+ if prev_hf.key != curr_hf.key:
261
+ # Key change: check if it's prepared
262
+ if prev_hf.resolution_potential < 0.5 and curr_hf.instability > 0.5:
263
+ issues.append(CompositionIssue(
264
+ issue_type="harmonic_non_sequitur",
265
+ critic="transition",
266
+ severity=0.6,
267
+ confidence=0.55,
268
+ scope={"from": prev.section_id, "to": curr.section_id},
269
+ evidence=f"Key change {prev_hf.key} → {curr_hf.key} without harmonic preparation",
270
+ recommended_moves=["add_pivot_chord", "use_chromatic_mediant", "prepare_with_dominant"],
271
+ ))
272
+
273
+ # 5. Weak build — energy rises but no role rotation
274
+ if curr.energy > prev.energy + 0.2:
275
+ prev_fg = {r.track_index for r in roles
276
+ if r.section_id == prev.section_id and r.foreground}
277
+ curr_fg = {r.track_index for r in roles
278
+ if r.section_id == curr.section_id and r.foreground}
279
+
280
+ if prev_fg == curr_fg and prev_fg:
281
+ issues.append(CompositionIssue(
282
+ issue_type="weak_build",
283
+ critic="transition",
284
+ severity=0.4,
285
+ confidence=0.55,
286
+ scope={"from": prev.section_id, "to": curr.section_id},
287
+ evidence=f"Energy rises but same foreground voices ({len(prev_fg)} tracks) — no role rotation",
288
+ recommended_moves=["rotate_foreground_voice", "add_new_element", "handoff_gesture"],
289
+ ))
290
+
291
+ return issues
292
+
293
+
294
+ # ── Emotional Arc Critic (Round 3) ──────────────────────────────────
295
+ def run_emotional_arc_critic(
296
+ sections: list[SectionNode],
297
+ harmony_fields: Optional[list["HarmonyField"]] = None,
298
+ ) -> list[CompositionIssue]:
299
+ """Analyze whether the arrangement tells an emotional story.
300
+
301
+ Builds a tension curve from energy, harmonic instability, and density,
302
+ then checks for common arc problems: monotone, all-climax, build
303
+ without payoff, no resolution.
304
+ """
305
+ issues = []
306
+ if len(sections) < 3:
307
+ return issues # Need enough sections to judge an arc
308
+
309
+ # Build tension curve: composite of energy, density, and harmonic instability
310
+ harmony_map = {}
311
+ if harmony_fields:
312
+ harmony_map = {hf.section_id: hf for hf in harmony_fields}
313
+
314
+ tension_curve: list[float] = []
315
+ for section in sections:
316
+ energy_component = section.energy
317
+ density_component = section.density
318
+
319
+ # Add harmonic instability if available
320
+ hf = harmony_map.get(section.section_id)
321
+ instability = hf.instability if hf else 0.3 # neutral default
322
+
323
+ tension = (energy_component * 0.5 + density_component * 0.3 + instability * 0.2)
324
+ tension_curve.append(round(tension, 3))
325
+
326
+ # 1. Monotone arc — tension doesn't vary enough
327
+ tension_range = max(tension_curve) - min(tension_curve)
328
+ if tension_range < 0.15:
329
+ issues.append(CompositionIssue(
330
+ issue_type="monotone_arc",
331
+ critic="emotional_arc",
332
+ severity=0.7,
333
+ confidence=0.70,
334
+ evidence=f"Tension range: {tension_range:.2f} — arrangement feels static",
335
+ recommended_moves=[
336
+ "add_breakdown_section", "create_energy_contrast",
337
+ "thin_one_section", "add_build_before_peak",
338
+ ],
339
+ ))
340
+
341
+ # 2. All-climax — high tension everywhere, no rest
342
+ high_tension_count = sum(1 for t in tension_curve if t > 0.7)
343
+ if high_tension_count > len(tension_curve) * 0.6:
344
+ issues.append(CompositionIssue(
345
+ issue_type="all_climax",
346
+ critic="emotional_arc",
347
+ severity=0.6,
348
+ confidence=0.65,
349
+ evidence=f"{high_tension_count}/{len(tension_curve)} sections have tension > 0.7 — no rest",
350
+ recommended_moves=[
351
+ "add_low_energy_section", "create_breakdown",
352
+ "reduce_density_in_verse", "strip_back_intro",
353
+ ],
354
+ ))
355
+
356
+ # 3. Build without payoff — tension rises then doesn't reach peak
357
+ peak_idx = tension_curve.index(max(tension_curve))
358
+ if peak_idx < len(tension_curve) - 1:
359
+ # Check if there's a build (rising tension) before the peak
360
+ has_build = False
361
+ for i in range(1, peak_idx + 1):
362
+ if tension_curve[i] > tension_curve[i - 1] + 0.1:
363
+ has_build = True
364
+ break
365
+
366
+ if not has_build and len(tension_curve) > 4:
367
+ issues.append(CompositionIssue(
368
+ issue_type="no_clear_build",
369
+ critic="emotional_arc",
370
+ severity=0.5,
371
+ confidence=0.55,
372
+ evidence="No gradual tension increase before peak — peak arrives without anticipation",
373
+ recommended_moves=[
374
+ "add_build_section", "tension_ratchet_gesture",
375
+ "gradual_element_addition", "harmonic_tint_rise",
376
+ ],
377
+ ))
378
+
379
+ # 4. No resolution — tension stays high at the end
380
+ if len(tension_curve) >= 3:
381
+ final_tension = tension_curve[-1]
382
+ peak_tension = max(tension_curve)
383
+ if final_tension > peak_tension * 0.8 and peak_tension > 0.5:
384
+ issues.append(CompositionIssue(
385
+ issue_type="no_resolution",
386
+ critic="emotional_arc",
387
+ severity=0.5,
388
+ confidence=0.60,
389
+ evidence=f"Final tension ({final_tension:.2f}) nearly as high as peak ({peak_tension:.2f}) — no release",
390
+ recommended_moves=[
391
+ "add_outro", "create_energy_drop_at_end",
392
+ "outro_decay_dissolve_gesture", "strip_elements_gradually",
393
+ ],
394
+ ))
395
+
396
+ # 5. Peak too early — climax in first third
397
+ if peak_idx < len(tension_curve) / 3 and len(tension_curve) > 4:
398
+ issues.append(CompositionIssue(
399
+ issue_type="peak_too_early",
400
+ critic="emotional_arc",
401
+ severity=0.5,
402
+ confidence=0.55,
403
+ evidence=f"Peak tension at section {peak_idx + 1}/{len(tension_curve)} — climax in first third",
404
+ recommended_moves=[
405
+ "move_peak_elements_later", "add_second_bigger_climax",
406
+ "reorder_sections", "save_hook_reveal_for_later",
407
+ ],
408
+ ))
409
+
410
+ return issues
411
+
412
+
413
+ # ── Cross-Section Critic (Round 4) ──────────────────────────────────
414
+ def run_cross_section_critic(
415
+ sections: list[SectionNode],
416
+ roles: list[RoleNode],
417
+ harmony_fields: Optional[list["HarmonyField"]] = None,
418
+ motif_count: int = 0,
419
+ ) -> list[CompositionIssue]:
420
+ """Reason across the entire arrangement for cross-section coherence.
421
+
422
+ Checks that the arrangement works as a whole, not just per-section:
423
+ - Clear reveal order (elements shouldn't all appear at once)
424
+ - Foreground voice rotation (same lead everywhere = fatigue)
425
+ - Harmonic pacing (rapid key changes everywhere = chaos)
426
+ - Element variety across sections
427
+ """
428
+ issues = []
429
+ if len(sections) < 3:
430
+ return issues
431
+
432
+ # 1. All elements appear from the start — no reveal order
433
+ if len(sections) >= 3:
434
+ first_active = set(sections[0].tracks_active)
435
+ second_active = set(sections[1].tracks_active)
436
+ third_active = set(sections[2].tracks_active)
437
+ if first_active == second_active == third_active and first_active:
438
+ issues.append(CompositionIssue(
439
+ issue_type="no_reveal_order",
440
+ critic="cross_section",
441
+ severity=0.6,
442
+ confidence=0.65,
443
+ evidence=f"First 3 sections all have same {len(first_active)} active tracks — no staggered reveal",
444
+ recommended_moves=[
445
+ "defer_elements_to_later_sections", "strip_intro",
446
+ "create_reveal_sequence", "mute_tracks_in_early_sections",
447
+ ],
448
+ ))
449
+
450
+ # 2. Same foreground voices in every section — no rotation
451
+ fg_by_section: list[set[int]] = []
452
+ for section in sections:
453
+ fg = {r.track_index for r in roles
454
+ if r.section_id == section.section_id and r.foreground}
455
+ fg_by_section.append(fg)
456
+
457
+ if len(fg_by_section) >= 3:
458
+ all_same = all(fg == fg_by_section[0] for fg in fg_by_section[1:])
459
+ if all_same and fg_by_section[0]:
460
+ issues.append(CompositionIssue(
461
+ issue_type="no_foreground_rotation",
462
+ critic="cross_section",
463
+ severity=0.5,
464
+ confidence=0.60,
465
+ evidence=f"Same foreground voices ({len(fg_by_section[0])} tracks) in all {len(sections)} sections",
466
+ recommended_moves=[
467
+ "alternate_lead_voice", "handoff_gesture_between_sections",
468
+ "mute_lead_in_bridge", "introduce_new_hook_element",
469
+ ],
470
+ ))
471
+
472
+ # 3. Harmonic monotony — same key across all sections
473
+ if harmony_fields:
474
+ keys = [hf.key for hf in harmony_fields if hf.key]
475
+ if len(keys) >= 3 and len(set(keys)) == 1:
476
+ issues.append(CompositionIssue(
477
+ issue_type="harmonic_monotony",
478
+ critic="cross_section",
479
+ severity=0.4,
480
+ confidence=0.50,
481
+ evidence=f"All {len(keys)} sections in same key ({keys[0]}) — consider modulation",
482
+ recommended_moves=[
483
+ "modulate_for_bridge", "use_chromatic_mediant",
484
+ "borrow_from_parallel_key", "transpose_final_chorus",
485
+ ],
486
+ ))
487
+
488
+ # 4. Harmonic chaos — different key in every section
489
+ unique_keys = set(keys)
490
+ if len(unique_keys) > len(keys) * 0.7 and len(keys) >= 4:
491
+ issues.append(CompositionIssue(
492
+ issue_type="harmonic_chaos",
493
+ critic="cross_section",
494
+ severity=0.5,
495
+ confidence=0.45,
496
+ evidence=f"{len(unique_keys)} different keys across {len(keys)} sections — hard to follow",
497
+ recommended_moves=[
498
+ "consolidate_to_two_keys", "use_pivot_chords",
499
+ "establish_home_key", "group_related_sections",
500
+ ],
501
+ ))
502
+
503
+ # 5. No motif development (if motifs exist but aren't varied)
504
+ if motif_count > 0:
505
+ # Check if sections have varying density (proxy for development)
506
+ densities = [s.density for s in sections]
507
+ unique_densities = len(set(round(d, 1) for d in densities))
508
+ if unique_densities <= 2 and len(sections) > 4:
509
+ issues.append(CompositionIssue(
510
+ issue_type="static_arrangement",
511
+ critic="cross_section",
512
+ severity=0.4,
513
+ confidence=0.50,
514
+ evidence=f"Only {unique_densities} distinct density levels across {len(sections)} sections with {motif_count} motifs",
515
+ recommended_moves=[
516
+ "vary_motif_density_per_section", "fragment_motif_in_bridge",
517
+ "augment_motif_in_outro", "register_shift_for_variety",
518
+ ],
519
+ ))
520
+
521
+ return issues
522
+