livepilot 1.25.0 → 1.26.1

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 (190) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +9 -9
  3. package/installer/codex.js +87 -9
  4. package/livepilot/.Codex-plugin/plugin.json +8 -0
  5. package/livepilot/.claude-plugin/plugin.json +8 -0
  6. package/livepilot/.mcp.json +8 -0
  7. package/livepilot/agents/livepilot-producer/AGENT.md +314 -0
  8. package/livepilot/commands/arrange.md +47 -0
  9. package/livepilot/commands/beat.md +81 -0
  10. package/livepilot/commands/evaluate.md +49 -0
  11. package/livepilot/commands/memory.md +22 -0
  12. package/livepilot/commands/mix.md +47 -0
  13. package/livepilot/commands/perform.md +42 -0
  14. package/livepilot/commands/session.md +13 -0
  15. package/livepilot/commands/sounddesign.md +58 -0
  16. package/livepilot/rubrics/default_preset_check.md +82 -0
  17. package/livepilot/rubrics/layer_accumulation.md +79 -0
  18. package/livepilot/rubrics/layer_precision.md +79 -0
  19. package/livepilot/rubrics/modulation_presence.md +63 -0
  20. package/livepilot/rubrics/sound_design_depth.md +40 -0
  21. package/livepilot/skills/livepilot-arrangement/SKILL.md +164 -0
  22. package/livepilot/skills/livepilot-composition-engine/SKILL.md +151 -0
  23. package/livepilot/skills/livepilot-composition-engine/references/form-patterns.md +97 -0
  24. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +102 -0
  25. package/livepilot/skills/livepilot-core/SKILL.md +261 -0
  26. package/livepilot/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
  27. package/livepilot/skills/livepilot-core/references/affordances/_schema.md +160 -0
  28. package/livepilot/skills/livepilot-core/references/affordances/devices/auto-filter.yaml +133 -0
  29. package/livepilot/skills/livepilot-core/references/affordances/devices/chorus-ensemble.yaml +91 -0
  30. package/livepilot/skills/livepilot-core/references/affordances/devices/compressor.yaml +98 -0
  31. package/livepilot/skills/livepilot-core/references/affordances/devices/convolution-reverb.yaml +113 -0
  32. package/livepilot/skills/livepilot-core/references/affordances/devices/corpus.yaml +84 -0
  33. package/livepilot/skills/livepilot-core/references/affordances/devices/drift.yaml +105 -0
  34. package/livepilot/skills/livepilot-core/references/affordances/devices/echo.yaml +108 -0
  35. package/livepilot/skills/livepilot-core/references/affordances/devices/eq-eight.yaml +95 -0
  36. package/livepilot/skills/livepilot-core/references/affordances/devices/glue-compressor.yaml +88 -0
  37. package/livepilot/skills/livepilot-core/references/affordances/devices/granulator-iii.yaml +104 -0
  38. package/livepilot/skills/livepilot-core/references/affordances/devices/hybrid-reverb.yaml +83 -0
  39. package/livepilot/skills/livepilot-core/references/affordances/devices/operator.yaml +98 -0
  40. package/livepilot/skills/livepilot-core/references/affordances/devices/ping-pong-delay.yaml +104 -0
  41. package/livepilot/skills/livepilot-core/references/affordances/devices/poli.yaml +98 -0
  42. package/livepilot/skills/livepilot-core/references/affordances/devices/saturator.yaml +98 -0
  43. package/livepilot/skills/livepilot-core/references/affordances/devices/shifter.yaml +77 -0
  44. package/livepilot/skills/livepilot-core/references/affordances/devices/simpler.yaml +113 -0
  45. package/livepilot/skills/livepilot-core/references/affordances/devices/utility.yaml +95 -0
  46. package/livepilot/skills/livepilot-core/references/affordances/devices/vinyl-distortion.yaml +92 -0
  47. package/livepilot/skills/livepilot-core/references/affordances/devices/wavetable.yaml +98 -0
  48. package/livepilot/skills/livepilot-core/references/artist-vocabularies.md +389 -0
  49. package/livepilot/skills/livepilot-core/references/automation-atlas.md +272 -0
  50. package/livepilot/skills/livepilot-core/references/concepts/_schema.md +158 -0
  51. package/livepilot/skills/livepilot-core/references/concepts/artists/akufen.yaml +116 -0
  52. package/livepilot/skills/livepilot-core/references/concepts/artists/aphex-twin.yaml +133 -0
  53. package/livepilot/skills/livepilot-core/references/concepts/artists/arca-sophie.yaml +131 -0
  54. package/livepilot/skills/livepilot-core/references/concepts/artists/autechre.yaml +130 -0
  55. package/livepilot/skills/livepilot-core/references/concepts/artists/basic-channel.yaml +140 -0
  56. package/livepilot/skills/livepilot-core/references/concepts/artists/basinski.yaml +126 -0
  57. package/livepilot/skills/livepilot-core/references/concepts/artists/boards-of-canada.yaml +124 -0
  58. package/livepilot/skills/livepilot-core/references/concepts/artists/burial.yaml +127 -0
  59. package/livepilot/skills/livepilot-core/references/concepts/artists/com-truise-tycho.yaml +121 -0
  60. package/livepilot/skills/livepilot-core/references/concepts/artists/daft-punk.yaml +117 -0
  61. package/livepilot/skills/livepilot-core/references/concepts/artists/dj-premier-rza.yaml +119 -0
  62. package/livepilot/skills/livepilot-core/references/concepts/artists/gas.yaml +134 -0
  63. package/livepilot/skills/livepilot-core/references/concepts/artists/hawtin.yaml +127 -0
  64. package/livepilot/skills/livepilot-core/references/concepts/artists/isolee-luomo.yaml +130 -0
  65. package/livepilot/skills/livepilot-core/references/concepts/artists/j-dilla.yaml +133 -0
  66. package/livepilot/skills/livepilot-core/references/concepts/artists/jeff-mills.yaml +120 -0
  67. package/livepilot/skills/livepilot-core/references/concepts/artists/johannsson-richter.yaml +132 -0
  68. package/livepilot/skills/livepilot-core/references/concepts/artists/madlib.yaml +124 -0
  69. package/livepilot/skills/livepilot-core/references/concepts/artists/moodymann-theo-parrish.yaml +121 -0
  70. package/livepilot/skills/livepilot-core/references/concepts/artists/oneohtrix-point-never.yaml +126 -0
  71. package/livepilot/skills/livepilot-core/references/concepts/artists/photek-source-direct.yaml +120 -0
  72. package/livepilot/skills/livepilot-core/references/concepts/artists/rashad-spinn-traxman.yaml +122 -0
  73. package/livepilot/skills/livepilot-core/references/concepts/artists/robert-henke.yaml +113 -0
  74. package/livepilot/skills/livepilot-core/references/concepts/artists/shackleton.yaml +124 -0
  75. package/livepilot/skills/livepilot-core/references/concepts/artists/skream-mala.yaml +119 -0
  76. package/livepilot/skills/livepilot-core/references/concepts/artists/stars-of-the-lid.yaml +119 -0
  77. package/livepilot/skills/livepilot-core/references/concepts/artists/tim-hecker.yaml +122 -0
  78. package/livepilot/skills/livepilot-core/references/concepts/artists/villalobos.yaml +135 -0
  79. package/livepilot/skills/livepilot-core/references/concepts/genres/ambient.yaml +137 -0
  80. package/livepilot/skills/livepilot-core/references/concepts/genres/boom_bap.yaml +124 -0
  81. package/livepilot/skills/livepilot-core/references/concepts/genres/deep-minimal.yaml +130 -0
  82. package/livepilot/skills/livepilot-core/references/concepts/genres/deep_house.yaml +130 -0
  83. package/livepilot/skills/livepilot-core/references/concepts/genres/detroit_techno.yaml +116 -0
  84. package/livepilot/skills/livepilot-core/references/concepts/genres/disco.yaml +123 -0
  85. package/livepilot/skills/livepilot-core/references/concepts/genres/downtempo.yaml +129 -0
  86. package/livepilot/skills/livepilot-core/references/concepts/genres/drone.yaml +133 -0
  87. package/livepilot/skills/livepilot-core/references/concepts/genres/drum-and-bass.yaml +119 -0
  88. package/livepilot/skills/livepilot-core/references/concepts/genres/dub-techno.yaml +132 -0
  89. package/livepilot/skills/livepilot-core/references/concepts/genres/dub.yaml +129 -0
  90. package/livepilot/skills/livepilot-core/references/concepts/genres/dubstep.yaml +120 -0
  91. package/livepilot/skills/livepilot-core/references/concepts/genres/experimental.yaml +136 -0
  92. package/livepilot/skills/livepilot-core/references/concepts/genres/footwork.yaml +119 -0
  93. package/livepilot/skills/livepilot-core/references/concepts/genres/hip-hop.yaml +132 -0
  94. package/livepilot/skills/livepilot-core/references/concepts/genres/house.yaml +126 -0
  95. package/livepilot/skills/livepilot-core/references/concepts/genres/hyperpop.yaml +128 -0
  96. package/livepilot/skills/livepilot-core/references/concepts/genres/idm.yaml +134 -0
  97. package/livepilot/skills/livepilot-core/references/concepts/genres/lo_fi.yaml +129 -0
  98. package/livepilot/skills/livepilot-core/references/concepts/genres/microhouse.yaml +138 -0
  99. package/livepilot/skills/livepilot-core/references/concepts/genres/minimal-techno.yaml +116 -0
  100. package/livepilot/skills/livepilot-core/references/concepts/genres/modern-classical.yaml +123 -0
  101. package/livepilot/skills/livepilot-core/references/concepts/genres/soul.yaml +125 -0
  102. package/livepilot/skills/livepilot-core/references/concepts/genres/synthwave.yaml +123 -0
  103. package/livepilot/skills/livepilot-core/references/concepts/genres/techno.yaml +123 -0
  104. package/livepilot/skills/livepilot-core/references/concepts/genres/trap.yaml +120 -0
  105. package/livepilot/skills/livepilot-core/references/concepts/genres/uk-garage.yaml +121 -0
  106. package/livepilot/skills/livepilot-core/references/device-atlas/00-index.md +110 -0
  107. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +687 -0
  108. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +753 -0
  109. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +525 -0
  110. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +402 -0
  111. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +963 -0
  112. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +874 -0
  113. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +571 -0
  114. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +714 -0
  115. package/livepilot/skills/livepilot-core/references/device-atlas/synths-native.md +953 -0
  116. package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
  117. package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
  118. package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
  119. package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
  120. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
  121. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
  122. package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
  123. package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
  124. package/livepilot/skills/livepilot-core/references/genre-vocabularies.md +382 -0
  125. package/livepilot/skills/livepilot-core/references/m4l-devices.md +352 -0
  126. package/livepilot/skills/livepilot-core/references/memory-guide.md +178 -0
  127. package/livepilot/skills/livepilot-core/references/midi-recipes.md +402 -0
  128. package/livepilot/skills/livepilot-core/references/mixing-patterns.md +578 -0
  129. package/livepilot/skills/livepilot-core/references/overview.md +300 -0
  130. package/livepilot/skills/livepilot-core/references/pack-knowledge.md +319 -0
  131. package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
  132. package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
  133. package/livepilot/skills/livepilot-core/references/sound-design.md +393 -0
  134. package/livepilot/skills/livepilot-corpus-builder/SKILL.md +379 -0
  135. package/livepilot/skills/livepilot-creative-director/SKILL.md +455 -0
  136. package/livepilot/skills/livepilot-creative-director/references/anti-repetition-rules.md +214 -0
  137. package/livepilot/skills/livepilot-creative-director/references/creative-brief-template.md +222 -0
  138. package/livepilot/skills/livepilot-creative-director/references/hybrid-compilation.md +185 -0
  139. package/livepilot/skills/livepilot-creative-director/references/move-family-diversity-rule.md +258 -0
  140. package/livepilot/skills/livepilot-creative-director/references/phase-6-execution.md +409 -0
  141. package/livepilot/skills/livepilot-creative-director/references/the-four-move-rule.md +192 -0
  142. package/livepilot/skills/livepilot-devices/SKILL.md +213 -0
  143. package/livepilot/skills/livepilot-devices/references/load_browser_item-uri-grammar.md +82 -0
  144. package/livepilot/skills/livepilot-evaluation/SKILL.md +195 -0
  145. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +176 -0
  146. package/livepilot/skills/livepilot-evaluation/references/evaluation-contracts.md +121 -0
  147. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +110 -0
  148. package/livepilot/skills/livepilot-mix-engine/SKILL.md +136 -0
  149. package/livepilot/skills/livepilot-mix-engine/references/mix-critics.md +143 -0
  150. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +105 -0
  151. package/livepilot/skills/livepilot-mixing/SKILL.md +157 -0
  152. package/livepilot/skills/livepilot-notes/SKILL.md +130 -0
  153. package/livepilot/skills/livepilot-performance-engine/SKILL.md +122 -0
  154. package/livepilot/skills/livepilot-performance-engine/references/performance-safety.md +98 -0
  155. package/livepilot/skills/livepilot-release/SKILL.md +151 -0
  156. package/livepilot/skills/livepilot-sample-engine/SKILL.md +117 -0
  157. package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
  158. package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
  159. package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
  160. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +225 -0
  161. package/livepilot/skills/livepilot-sound-design-engine/references/patch-model.md +119 -0
  162. package/livepilot/skills/livepilot-sound-design-engine/references/sound-design-critics.md +118 -0
  163. package/livepilot/skills/livepilot-wonder/SKILL.md +143 -0
  164. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  165. package/m4l_device/LivePilot_Elektron.amxd +0 -0
  166. package/m4l_device/LivePilot_Elektron.maxpat +758 -0
  167. package/m4l_device/livepilot_bridge.js +1 -1
  168. package/m4l_device/livepilot_elektron_bridge.js +82 -0
  169. package/mcp_server/__init__.py +1 -1
  170. package/mcp_server/atlas/tools.py +63 -12
  171. package/mcp_server/audit/checks.py +167 -6
  172. package/mcp_server/audit/state.py +88 -0
  173. package/mcp_server/audit/tools.py +6 -69
  174. package/mcp_server/composer/develop/apply.py +2 -2
  175. package/mcp_server/composer/full/apply.py +32 -6
  176. package/mcp_server/grader/__init__.py +17 -0
  177. package/mcp_server/grader/client.py +647 -0
  178. package/mcp_server/grader/iterator.py +57 -0
  179. package/mcp_server/grader/tools.py +263 -0
  180. package/mcp_server/m4l_bridge.py +5 -0
  181. package/mcp_server/runtime/execution_router.py +6 -0
  182. package/mcp_server/runtime/mcp_dispatch.py +18 -0
  183. package/mcp_server/runtime/remote_commands.py +2 -0
  184. package/mcp_server/server.py +12 -7
  185. package/mcp_server/tools/browser.py +68 -0
  186. package/package.json +20 -5
  187. package/remote_script/LivePilot/__init__.py +1 -1
  188. package/remote_script/LivePilot/server.py +63 -2
  189. package/requirements.txt +24 -3
  190. package/server.json +3 -3
@@ -0,0 +1,647 @@
1
+ """Mechanical rubric grader.
2
+
3
+ Phase 1 — §7.3 layer accumulation only. Each check is a pure function of
4
+ session state, returning the same shape used by `mcp_server/audit/checks.py`:
5
+
6
+ {
7
+ "id": str,
8
+ "passed": bool,
9
+ "severity": "pass" | "warn" | "fail" | "n/a",
10
+ "summary": str,
11
+ "issues": [{"code": str, "detail": str, "track_index": int | None}, ...],
12
+ "evidence": {...},
13
+ }
14
+
15
+ `evaluate(rubric_id, state)` aggregates per-criterion results into a Verdict.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Callable, Iterable
21
+
22
+ from mcp_server.audit import checks as audit_checks
23
+ from mcp_server.audit.checks import infer_role
24
+
25
+
26
+ _TRACK_COUNT_WARN = 8
27
+ _TRACK_COUNT_FAIL = 12
28
+
29
+ _BURIED_THRESHOLD = 0.15
30
+ _GHOST_KEYWORDS: tuple[str, ...] = ("ghost", "_g ", "_g_", " gh ", "gh_")
31
+
32
+ _ROLE_VOLUME_BANDS: dict[str, tuple[float, float]] = {
33
+ "kick": (0.60, 0.85),
34
+ "bass": (0.60, 0.85),
35
+ "snare": (0.55, 0.80),
36
+ "hat": (0.40, 0.70),
37
+ "perc": (0.40, 0.65),
38
+ "lead": (0.50, 0.80),
39
+ "vox": (0.55, 0.85),
40
+ "pad": (0.25, 0.50),
41
+ "atmos": (0.25, 0.45),
42
+ "fx": (0.30, 0.70),
43
+ "unknown": (0.30, 0.80),
44
+ }
45
+
46
+ # Banned-default detection uses (class_name, name) FINGERPRINTS rather than
47
+ # class names alone. Live's runtime class taxonomy doesn't match user-facing
48
+ # brand names — surveyed live in 2026-05-08 with load_browser_item:
49
+ # Drift → class="Drift", name="Drift" (native)
50
+ # Analog → class="UltraAnalog", name="Analog" (native)
51
+ # Meld → class="InstrumentMeld", name="Meld" (native)
52
+ # Poli → class="MxDeviceInstrument", name="Poli" (M4L wrapper)
53
+ # The fingerprint approach catches all four; the previous flat-set approach
54
+ # only caught Drift.
55
+ _BANNED_DEFAULT_FINGERPRINTS: frozenset[tuple[str, str]] = frozenset({
56
+ ("drift", "drift"),
57
+ ("ultraanalog", "analog"),
58
+ ("instrumentmeld", "meld"),
59
+ ("mxdeviceinstrument", "poli"),
60
+ })
61
+ _BANNED_DEFAULT_ROLES: frozenset[str] = frozenset({"bass", "pad", "lead"})
62
+ _SUBTRACTIVE_EXCEPTION_KEYWORDS: tuple[str, ...] = ("subtractive", "analog sub", "_sub_synth")
63
+
64
+ # Instrument-class set used by the modulation-presence guard (Fix #2).
65
+ # Includes Live's actual runtime class names, not user-facing brand names.
66
+ _INSTRUMENT_CLASSES: frozenset[str] = frozenset({
67
+ "operator", "wavetable", "drift",
68
+ "ultraanalog", # Analog
69
+ "instrumentmeld", # Meld
70
+ "mxdeviceinstrument", # Poli + every other M4L instrument
71
+ "tension", "collision",
72
+ "simpler", "originalsimpler", "sampler", "multisampler",
73
+ "electric", "loungelizard", # Electric → LoungeLizard runtime class
74
+ "drumgroup", "drumrack", "drum rack", "drumgroupdevice",
75
+ "instrumentgroupdevice", "instrumentrack",
76
+ })
77
+
78
+ _MODULATION_REQUIRED_ROLES: frozenset[str] = frozenset({"bass", "pad", "lead", "vox", "atmos"})
79
+
80
+
81
+ def _is_ghost(name: str) -> bool:
82
+ n = (name or "").lower()
83
+ return any(kw in n for kw in _GHOST_KEYWORDS)
84
+
85
+
86
+ def _track_volume(track: dict) -> float | None:
87
+ mixer = track.get("mixer") or {}
88
+ vol = mixer.get("volume")
89
+ return float(vol) if vol is not None else None
90
+
91
+
92
+ def _check_track_count_within_limit(state: dict) -> dict:
93
+ tracks = list(state.get("tracks") or [])
94
+ n = len(tracks)
95
+ if n <= _TRACK_COUNT_WARN:
96
+ severity = "pass"
97
+ summary = f"{n} tracks — within sustainable range (≤{_TRACK_COUNT_WARN})"
98
+ issues: list[dict] = []
99
+ elif n < _TRACK_COUNT_FAIL:
100
+ severity = "warn"
101
+ summary = f"{n} tracks — approaching §7.3 ceiling (warn at >{_TRACK_COUNT_WARN}, fail at ≥{_TRACK_COUNT_FAIL})"
102
+ issues = [{
103
+ "code": "track_count_high",
104
+ "detail": f"{n} tracks present. Consider deleting 1–{n - _TRACK_COUNT_WARN} weakest layers before adding more.",
105
+ "track_index": None,
106
+ }]
107
+ else:
108
+ severity = "fail"
109
+ summary = f"{n} tracks — exceeds §7.3 ceiling (≥{_TRACK_COUNT_FAIL}). 5–6 great layers > {n} mediocre."
110
+ issues = [{
111
+ "code": "track_count_exceeded",
112
+ "detail": f"{n} tracks present. §7.3 demands fewer, better layers — delete the weakest until ≤{_TRACK_COUNT_WARN}.",
113
+ "track_index": None,
114
+ }]
115
+ return {
116
+ "id": "track_count_within_limit",
117
+ "passed": severity in ("pass", "warn"),
118
+ "severity": severity,
119
+ "summary": summary,
120
+ "issues": issues,
121
+ "evidence": {"track_count": n, "warn_threshold": _TRACK_COUNT_WARN, "fail_threshold": _TRACK_COUNT_FAIL},
122
+ }
123
+
124
+
125
+ def _check_no_extreme_buried_track(state: dict) -> dict:
126
+ tracks = list(state.get("tracks") or [])
127
+ buried_non_ghost: list[dict] = []
128
+ buried_ghost: list[dict] = []
129
+ for t in tracks:
130
+ vol = _track_volume(t)
131
+ if vol is None or vol >= _BURIED_THRESHOLD:
132
+ continue
133
+ entry = {"index": t.get("index"), "name": t.get("name"), "volume": round(vol, 3)}
134
+ if _is_ghost(t.get("name") or ""):
135
+ buried_ghost.append(entry)
136
+ else:
137
+ buried_non_ghost.append(entry)
138
+
139
+ if not buried_non_ghost:
140
+ return {
141
+ "id": "no_extreme_buried_track",
142
+ "passed": True,
143
+ "severity": "pass",
144
+ "summary": (
145
+ "No buried tracks below 0.15"
146
+ if not buried_ghost
147
+ else f"{len(buried_ghost)} buried track(s) all ghost-tagged — OK"
148
+ ),
149
+ "issues": [],
150
+ "evidence": {"buried_non_ghost": [], "buried_ghost": buried_ghost},
151
+ }
152
+
153
+ return {
154
+ "id": "no_extreme_buried_track",
155
+ "passed": False,
156
+ "severity": "fail",
157
+ "summary": f"{len(buried_non_ghost)} non-ghost track(s) at volume < {_BURIED_THRESHOLD} — delete or feature them",
158
+ "issues": [
159
+ {
160
+ "code": "extreme_buried_track",
161
+ "detail": f"Track '{e['name']}' at volume {e['volume']}. §7.3: delete it or feature it, don't bury it.",
162
+ "track_index": e["index"],
163
+ }
164
+ for e in buried_non_ghost
165
+ ],
166
+ "evidence": {"buried_non_ghost": buried_non_ghost, "buried_ghost": buried_ghost, "threshold": _BURIED_THRESHOLD},
167
+ }
168
+
169
+
170
+ def _check_role_volume_hierarchy(state: dict) -> dict:
171
+ tracks = list(state.get("tracks") or [])
172
+ out_of_band: list[dict] = []
173
+ in_band_count = 0
174
+ skipped_unknown = 0
175
+ for t in tracks:
176
+ vol = _track_volume(t)
177
+ if vol is None:
178
+ continue
179
+ role = infer_role(t.get("name") or "", t.get("devices") or [])
180
+ # No role inferred → no expected band → skip. Live's default fader
181
+ # is 0.85 (unity); applying any band to unrecognised tracks fires
182
+ # false positives on every fresh project.
183
+ if role == "unknown":
184
+ skipped_unknown += 1
185
+ continue
186
+ band = _ROLE_VOLUME_BANDS.get(role) or _ROLE_VOLUME_BANDS["unknown"]
187
+ if band[0] <= vol <= band[1]:
188
+ in_band_count += 1
189
+ continue
190
+ out_of_band.append({
191
+ "index": t.get("index"),
192
+ "name": t.get("name"),
193
+ "role": role,
194
+ "volume": round(vol, 3),
195
+ "band": [band[0], band[1]],
196
+ "direction": "above" if vol > band[1] else "below",
197
+ })
198
+
199
+ if not out_of_band:
200
+ if in_band_count == 0 and skipped_unknown > 0:
201
+ summary = f"No role-tagged tracks to check ({skipped_unknown} skipped as unknown role)"
202
+ else:
203
+ summary = f"All {in_band_count} role-tagged track(s) within role volume band"
204
+ if skipped_unknown:
205
+ summary += f" ({skipped_unknown} unknown-role skipped)"
206
+ return {
207
+ "id": "role_volume_hierarchy",
208
+ "passed": True,
209
+ "severity": "pass",
210
+ "summary": summary,
211
+ "issues": [],
212
+ "evidence": {
213
+ "in_band": in_band_count,
214
+ "out_of_band": [],
215
+ "skipped_unknown": skipped_unknown,
216
+ },
217
+ }
218
+
219
+ return {
220
+ "id": "role_volume_hierarchy",
221
+ "passed": True, # advisory — flag, don't block
222
+ "severity": "warn",
223
+ "summary": (
224
+ f"{len(out_of_band)} role-tagged track(s) outside role volume band (advisory)"
225
+ + (f" ({skipped_unknown} unknown-role skipped)" if skipped_unknown else "")
226
+ ),
227
+ "issues": [
228
+ {
229
+ "code": "role_volume_off_band",
230
+ "detail": (
231
+ f"Track '{e['name']}' (role={e['role']}) at volume {e['volume']} — "
232
+ f"{e['direction']} expected band {e['band']}. "
233
+ f"{'Pad/atmos shouldn’t dominate.' if e['direction'] == 'above' and e['role'] in ('pad', 'atmos') else ''}"
234
+ f"{'Anchor role too quiet — should carry.' if e['direction'] == 'below' and e['role'] in ('kick', 'bass', 'vox') else ''}"
235
+ ).strip(),
236
+ "track_index": e["index"],
237
+ }
238
+ for e in out_of_band
239
+ ],
240
+ "evidence": {
241
+ "in_band": in_band_count,
242
+ "out_of_band": out_of_band,
243
+ "skipped_unknown": skipped_unknown,
244
+ },
245
+ }
246
+
247
+
248
+ def _first_instrument_device(devices: list[dict]) -> dict | None:
249
+ for d in devices or []:
250
+ cls = (d.get("class_name") or "").lower()
251
+ if cls in _INSTRUMENT_CLASSES:
252
+ return d
253
+ return None
254
+
255
+
256
+ def _is_banned_default_fingerprint(device: dict) -> bool:
257
+ """Match (class_name, name) against the banned-default fingerprint set.
258
+
259
+ Fires only when both class AND device-display-name match a banned synth's
260
+ default-loaded state. A preset-applied device has device.name set to the
261
+ preset stem, so it falls out of the fingerprint set automatically.
262
+ """
263
+ cls = (device.get("class_name") or "").strip().lower()
264
+ name = (device.get("name") or "").strip().lower()
265
+ return (cls, name) in _BANNED_DEFAULT_FINGERPRINTS
266
+
267
+
268
+ def _is_subtractive_exception(track_name: str) -> bool:
269
+ n = (track_name or "").lower()
270
+ return any(kw in n for kw in _SUBTRACTIVE_EXCEPTION_KEYWORDS)
271
+
272
+
273
+ def _check_no_banned_default_instruments(state: dict) -> dict:
274
+ tracks = list(state.get("tracks") or [])
275
+ violations: list[dict] = []
276
+ skipped_exceptions: list[dict] = []
277
+ for t in tracks:
278
+ role = infer_role(t.get("name") or "", t.get("devices") or [])
279
+ if role not in _BANNED_DEFAULT_ROLES:
280
+ continue
281
+ instr = _first_instrument_device(t.get("devices") or [])
282
+ if not instr:
283
+ continue
284
+ if not _is_banned_default_fingerprint(instr):
285
+ continue
286
+ entry = {
287
+ "index": t.get("index"),
288
+ "name": t.get("name"),
289
+ "role": role,
290
+ "class_name": instr.get("class_name"),
291
+ "device_name": instr.get("name"),
292
+ }
293
+ if _is_subtractive_exception(t.get("name") or ""):
294
+ skipped_exceptions.append(entry)
295
+ else:
296
+ violations.append(entry)
297
+
298
+ if not violations:
299
+ return {
300
+ "id": "no_banned_default_instruments",
301
+ "passed": True,
302
+ "severity": "pass",
303
+ "summary": (
304
+ "No banned-default synths on melodic-role tracks"
305
+ if not skipped_exceptions
306
+ else f"All banned-default loads explicitly tagged as subtractive ({len(skipped_exceptions)} skipped)"
307
+ ),
308
+ "issues": [],
309
+ "evidence": {"violations": [], "subtractive_exceptions": skipped_exceptions},
310
+ }
311
+
312
+ return {
313
+ "id": "no_banned_default_instruments",
314
+ "passed": False,
315
+ "severity": "fail",
316
+ "summary": f"{len(violations)} melodic-role track(s) using banned-default synth (§1)",
317
+ "issues": [
318
+ {
319
+ "code": "banned_default_instrument",
320
+ "detail": (
321
+ f"Track '{v['name']}' (role={v['role']}) starts with default-loaded "
322
+ f"{v['class_name']}. §1: hunt the library — atlas_search, search_browser, "
323
+ "or sample-based / granular / physical-modeling source. "
324
+ "Tag track name with 'subtractive' if this is a deliberate analog choice."
325
+ ),
326
+ "track_index": v["index"],
327
+ }
328
+ for v in violations
329
+ ],
330
+ "evidence": {"violations": violations, "subtractive_exceptions": skipped_exceptions},
331
+ }
332
+
333
+
334
+ def _check_melodic_layers_have_motion(state: dict) -> dict:
335
+ """§4 — every melodic/harmonic layer should have ≥1 form of motion.
336
+
337
+ State must populate per track:
338
+ - modulation_count: int (sum of mod-matrix non-zero entries)
339
+ - has_clip_automation: bool
340
+
341
+ Tracks missing both keys are reported as `unknown` and the check
342
+ degrades to n/a if no track has either signal.
343
+ """
344
+ tracks = list(state.get("tracks") or [])
345
+ static_tracks: list[dict] = []
346
+ moving_tracks: list[dict] = []
347
+ unknown_tracks: list[dict] = []
348
+
349
+ for t in tracks:
350
+ role = infer_role(t.get("name") or "", t.get("devices") or [])
351
+ if role not in _MODULATION_REQUIRED_ROLES:
352
+ continue
353
+ mod_count = t.get("modulation_count")
354
+ has_auto = t.get("has_clip_automation")
355
+ if mod_count is None and has_auto is None:
356
+ unknown_tracks.append({
357
+ "index": t.get("index"), "name": t.get("name"), "role": role,
358
+ })
359
+ continue
360
+ has_motion = (mod_count or 0) > 0 or bool(has_auto)
361
+ entry = {
362
+ "index": t.get("index"),
363
+ "name": t.get("name"),
364
+ "role": role,
365
+ "modulation_count": mod_count or 0,
366
+ "has_clip_automation": bool(has_auto),
367
+ }
368
+ (moving_tracks if has_motion else static_tracks).append(entry)
369
+
370
+ n_checked = len(moving_tracks) + len(static_tracks)
371
+
372
+ if n_checked == 0:
373
+ return {
374
+ "id": "melodic_layers_have_motion",
375
+ "passed": True,
376
+ "severity": "n/a",
377
+ "summary": (
378
+ "No melodic-role tracks present"
379
+ if not unknown_tracks
380
+ else f"Modulation data missing for {len(unknown_tracks)} melodic-role track(s)"
381
+ ),
382
+ "issues": [],
383
+ "evidence": {"moving": [], "static": [], "unknown": unknown_tracks},
384
+ }
385
+
386
+ if not static_tracks:
387
+ return {
388
+ "id": "melodic_layers_have_motion",
389
+ "passed": True,
390
+ "severity": "pass",
391
+ "summary": f"All {n_checked} melodic-role layer(s) have modulation or automation",
392
+ "issues": [],
393
+ "evidence": {"moving": moving_tracks, "static": [], "unknown": unknown_tracks},
394
+ }
395
+
396
+ return {
397
+ "id": "melodic_layers_have_motion",
398
+ "passed": True,
399
+ "severity": "warn",
400
+ "summary": (
401
+ f"{len(static_tracks)} of {n_checked} melodic-role layer(s) static "
402
+ "(no modulation routings, no clip automation)"
403
+ ),
404
+ "issues": [
405
+ {
406
+ "code": "static_melodic_layer",
407
+ "detail": (
408
+ f"Track '{t['name']}' (role={t['role']}) has 0 modulation routings "
409
+ "and no clip automation. §4: add LFO routing, mod-matrix entry, or "
410
+ "automation curve. Static MIDI at default velocity ≈ 'didn't try'."
411
+ ),
412
+ "track_index": t["index"],
413
+ }
414
+ for t in static_tracks
415
+ ],
416
+ "evidence": {"moving": moving_tracks, "static": static_tracks, "unknown": unknown_tracks},
417
+ }
418
+
419
+
420
+ def _aggregate_per_track(
421
+ *,
422
+ criterion_id: str,
423
+ state: dict,
424
+ args_for_track: Callable[[dict, dict, str], tuple | None],
425
+ check_fn: Callable[..., dict],
426
+ pass_summary: str,
427
+ ) -> dict:
428
+ """Run an audit check function per track, aggregate into one verdict.
429
+
430
+ args_for_track(state, track, role) returns the args tuple for check_fn,
431
+ or None to skip the track entirely. The audit check's own n/a returns
432
+ are filtered out at aggregation time so they don't drag down the result.
433
+ """
434
+ tracks = list(state.get("tracks") or [])
435
+ per_track: list[dict] = []
436
+ for t in tracks:
437
+ role = infer_role(t.get("name") or "", t.get("devices") or [])
438
+ args = args_for_track(state, t, role)
439
+ if args is None:
440
+ continue
441
+ try:
442
+ result = check_fn(*args)
443
+ except Exception as exc:
444
+ per_track.append({
445
+ "track_index": t.get("index"),
446
+ "name": t.get("name"),
447
+ "role": role,
448
+ "severity": "n/a",
449
+ "summary": f"check failed: {type(exc).__name__}",
450
+ "issues": [],
451
+ "evidence": {},
452
+ })
453
+ continue
454
+ per_track.append({
455
+ "track_index": t.get("index"),
456
+ "name": t.get("name"),
457
+ "role": role,
458
+ **result,
459
+ })
460
+
461
+ actionable = [r for r in per_track if r["severity"] != "n/a"]
462
+
463
+ if not actionable:
464
+ return {
465
+ "id": criterion_id,
466
+ "passed": True,
467
+ "severity": "n/a",
468
+ "summary": "No checkable tracks (data missing or no applicable role)",
469
+ "issues": [],
470
+ "evidence": {"per_track": per_track},
471
+ }
472
+
473
+ has_fail = any(r["severity"] == "fail" for r in actionable)
474
+ has_warn = any(r["severity"] == "warn" for r in actionable)
475
+
476
+ if has_fail:
477
+ rubric_severity, passed = "fail", False
478
+ elif has_warn:
479
+ rubric_severity, passed = "warn", True
480
+ else:
481
+ rubric_severity, passed = "pass", True
482
+
483
+ issues: list[dict] = []
484
+ for r in actionable:
485
+ if r["severity"] in ("warn", "fail"):
486
+ for issue in r.get("issues") or []:
487
+ issues.append({
488
+ "code": issue.get("code", ""),
489
+ "detail": f"Track '{r['name']}' (role={r['role']}): {issue.get('detail', '')}",
490
+ "track_index": r["track_index"],
491
+ })
492
+
493
+ n_pass = sum(1 for r in actionable if r["severity"] == "pass")
494
+ n_warn = sum(1 for r in actionable if r["severity"] == "warn")
495
+ n_fail = sum(1 for r in actionable if r["severity"] == "fail")
496
+ summary = (
497
+ pass_summary
498
+ if rubric_severity == "pass"
499
+ else f"{len(actionable)} checked: {n_pass} pass, {n_warn} warn, {n_fail} fail"
500
+ )
501
+
502
+ return {
503
+ "id": criterion_id,
504
+ "passed": passed,
505
+ "severity": rubric_severity,
506
+ "summary": summary,
507
+ "issues": issues,
508
+ "evidence": {"per_track": per_track},
509
+ }
510
+
511
+
512
+ def _check_timbre_per_track(state: dict) -> dict:
513
+ return _aggregate_per_track(
514
+ criterion_id="timbre_per_track",
515
+ state=state,
516
+ args_for_track=lambda s, t, role: (role, t.get("fingerprint")),
517
+ check_fn=audit_checks.check_timbre,
518
+ pass_summary="All checked tracks have role-appropriate spectral shape",
519
+ )
520
+
521
+
522
+ def _check_sequence_per_track(state: dict) -> dict:
523
+ return _aggregate_per_track(
524
+ criterion_id="sequence_per_track",
525
+ state=state,
526
+ args_for_track=lambda s, t, role: (role, t.get("notes_per_clip") or []),
527
+ check_fn=audit_checks.check_sequence,
528
+ pass_summary="All MIDI tracks meet sequence bar (humanization + ghosts + variation)",
529
+ )
530
+
531
+
532
+ def _check_stereo_per_track(state: dict) -> dict:
533
+ return _aggregate_per_track(
534
+ criterion_id="stereo_per_track",
535
+ state=state,
536
+ args_for_track=lambda s, t, role: (role, t),
537
+ check_fn=audit_checks.check_stereo,
538
+ pass_summary="No anti-pattern panning detected",
539
+ )
540
+
541
+
542
+ def _check_masking_per_track(state: dict) -> dict:
543
+ masking_report = state.get("masking_report")
544
+ return _aggregate_per_track(
545
+ criterion_id="masking_per_track",
546
+ state=state,
547
+ args_for_track=lambda s, t, role: (t.get("index"), masking_report) if masking_report else None,
548
+ check_fn=audit_checks.check_masking,
549
+ pass_summary="No detected cross-track masking collisions",
550
+ )
551
+
552
+
553
+ def _modulation_args(state: dict, track: dict, role: str) -> tuple | None:
554
+ """Skip tracks that have no instrument-class device.
555
+
556
+ `audit_checks.check_modulation` returns 'no_movement' on empty-device
557
+ tracks because routings=0 — but there's nothing on the track to
558
+ modulate. Audio tracks, FX-only buses, and fresh empty tracks are
559
+ not candidates for §4. Pre-filter here.
560
+ """
561
+ devices = track.get("devices") or []
562
+ if _first_instrument_device(devices) is None:
563
+ return None
564
+ return (
565
+ role,
566
+ devices,
567
+ bool(track.get("has_clip_automation")),
568
+ int(track.get("wavetable_mod_routings", 0)),
569
+ )
570
+
571
+
572
+ def _check_modulation_per_track(state: dict) -> dict:
573
+ return _aggregate_per_track(
574
+ criterion_id="modulation_per_track",
575
+ state=state,
576
+ args_for_track=_modulation_args,
577
+ check_fn=audit_checks.check_modulation,
578
+ pass_summary="All instrument tracks have ≥1 modulation routing or automation",
579
+ )
580
+
581
+
582
+ def _check_params_per_track(state: dict) -> dict:
583
+ return _aggregate_per_track(
584
+ criterion_id="params_per_track",
585
+ state=state,
586
+ args_for_track=lambda s, t, role: (role, t.get("devices") or []),
587
+ check_fn=audit_checks.check_params,
588
+ pass_summary="All instrument tracks show evidence of parameter programming (§2)",
589
+ )
590
+
591
+
592
+ def _check_effects_per_track(state: dict) -> dict:
593
+ return _aggregate_per_track(
594
+ criterion_id="effects_per_track",
595
+ state=state,
596
+ args_for_track=lambda s, t, role: (role, t.get("devices") or []),
597
+ check_fn=audit_checks.check_effects,
598
+ pass_summary="Required effects categories present per role",
599
+ )
600
+
601
+
602
+ _RUBRICS: dict[str, list[Callable[[dict], dict]]] = {
603
+ "layer_accumulation": [
604
+ _check_track_count_within_limit,
605
+ _check_no_extreme_buried_track,
606
+ _check_role_volume_hierarchy,
607
+ ],
608
+ "default_preset_check": [
609
+ _check_no_banned_default_instruments,
610
+ ],
611
+ "modulation_presence": [
612
+ _check_melodic_layers_have_motion,
613
+ ],
614
+ "layer_precision": [
615
+ _check_timbre_per_track,
616
+ _check_sequence_per_track,
617
+ _check_stereo_per_track,
618
+ _check_masking_per_track,
619
+ _check_modulation_per_track,
620
+ _check_params_per_track,
621
+ _check_effects_per_track,
622
+ ],
623
+ "sound_design_depth": [
624
+ _check_params_per_track,
625
+ ],
626
+ }
627
+
628
+
629
+ def evaluate(rubric_id: str, state: dict[str, Any]) -> dict[str, Any]:
630
+ """Run all checks for a rubric, return aggregated verdict.
631
+
632
+ Raises KeyError if rubric_id is unknown.
633
+ """
634
+ checks = _RUBRICS[rubric_id]
635
+ results = [check(state) for check in checks]
636
+ blocking_failed = any(
637
+ not r["passed"] and r["severity"] == "fail" for r in results
638
+ )
639
+ return {
640
+ "rubric_id": rubric_id,
641
+ "passed": not blocking_failed,
642
+ "criteria": results,
643
+ }
644
+
645
+
646
+ def list_rubrics() -> list[str]:
647
+ return list(_RUBRICS.keys())