livepilot 1.12.2 → 1.14.0

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 (37) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +7 -7
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/branches/__init__.py +34 -0
  7. package/mcp_server/branches/types.py +286 -0
  8. package/mcp_server/composer/__init__.py +10 -1
  9. package/mcp_server/composer/branch_producer.py +349 -0
  10. package/mcp_server/composer/tools.py +58 -1
  11. package/mcp_server/evaluation/policy.py +227 -2
  12. package/mcp_server/experiment/engine.py +47 -11
  13. package/mcp_server/experiment/models.py +112 -8
  14. package/mcp_server/experiment/tools.py +502 -38
  15. package/mcp_server/memory/taste_graph.py +84 -11
  16. package/mcp_server/persistence/taste_store.py +21 -5
  17. package/mcp_server/runtime/session_kernel.py +46 -0
  18. package/mcp_server/runtime/tools.py +29 -3
  19. package/mcp_server/server.py +1 -0
  20. package/mcp_server/synthesis_brain/__init__.py +53 -0
  21. package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
  22. package/mcp_server/synthesis_brain/adapters/analog.py +273 -0
  23. package/mcp_server/synthesis_brain/adapters/base.py +86 -0
  24. package/mcp_server/synthesis_brain/adapters/drift.py +271 -0
  25. package/mcp_server/synthesis_brain/adapters/meld.py +261 -0
  26. package/mcp_server/synthesis_brain/adapters/operator.py +292 -0
  27. package/mcp_server/synthesis_brain/adapters/wavetable.py +364 -0
  28. package/mcp_server/synthesis_brain/engine.py +91 -0
  29. package/mcp_server/synthesis_brain/models.py +121 -0
  30. package/mcp_server/synthesis_brain/timbre.py +194 -0
  31. package/mcp_server/synthesis_brain/tools.py +231 -0
  32. package/mcp_server/tools/_conductor.py +144 -0
  33. package/mcp_server/wonder_mode/engine.py +324 -0
  34. package/mcp_server/wonder_mode/tools.py +153 -1
  35. package/package.json +2 -2
  36. package/remote_script/LivePilot/__init__.py +1 -1
  37. package/server.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,224 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.14.0 — Branch-native v2: producer context, synth intelligence, render verify (April 20 2026)
4
+
5
+ Five-PR follow-up to v1.13.0 that closes the loops the first pass left
6
+ open. Producer context flows through the branch lifecycle via a versioned
7
+ `producer_payload`; synthesis adapters decode algorithm topology instead
8
+ of always targeting the same operator; composer winners commit the full
9
+ resolved plan instead of the audition scaffold; render-verify captures
10
+ audio before/after each branch and feeds spectral movement into the
11
+ hard-rule classifier; and four dedicated MCP tools expose the new
12
+ producers to the LLM.
13
+
14
+ **Tool count**: 398 → **402** (added `analyze_synth_patch`,
15
+ `propose_synth_branches`, `extract_timbre_fingerprint`,
16
+ `propose_composer_branches`). **Domain count**: unchanged at 52.
17
+ **Tests**: 2409 → **2467 passing** (+58 new), 0 regressions.
18
+
19
+ ### New substrate
20
+
21
+ - **`producer_payload: dict` on `BranchSeed`** (PR1) — versioned
22
+ opaque dict producers populate with regeneration / provenance /
23
+ winner-escalation context. Always carries `schema_version` (default 1)
24
+ so older payloads don't break newer readers. Lives on the seed, not
25
+ `compiled_plan`, so analytical-only branches can carry context too.
26
+ Canonical shapes documented per producer (synthesis / composer /
27
+ semantic_move / freeform / technique).
28
+
29
+ - **`BranchSnapshot` gains render-based fields** (PR4) —
30
+ `capture_path`, `loudness`, `spectral_shape`, `fingerprint`. Populated
31
+ only when `run_experiment(render_verify=True)` opts in. Pre-v2
32
+ consumers see no shape change when render-verify is off.
33
+
34
+ ### Synthesis adapters get topology awareness (PR2)
35
+
36
+ - **Wavetable**: position→region classification (sub / mid / bright /
37
+ complex) drives shift direction. Target brightness biases the chosen
38
+ target region — not just freshness scaling.
39
+
40
+ - **Operator**: static `_ALGO_TOPOLOGY` table maps all 11 algorithms
41
+ to their carrier/modulator roles. Targeting picks the modulator with
42
+ the highest Level for the ratio shift; additive algorithms (5, 9)
43
+ fall back to the dominant carrier. No more "always Osc B".
44
+
45
+ - **Analog / Drift / Meld**: single fixed proposers become strategy
46
+ registries. Gates honor `role_hint` ("bass" skips `detune_warmth`,
47
+ "pad" skips `filter_pluck`, silent engines skip `engine_mix_shift`)
48
+ and target fingerprint dimensions (`target.brightness` picks
49
+ `filter_sweep_open` vs `filter_sweep_close`).
50
+
51
+ ### Composer winner escalation (PR3)
52
+
53
+ - Composer seeds now carry their `CompositionIntent` in
54
+ `producer_payload` at emit time. On `commit_experiment`,
55
+ `escalate_composer_branch` rehydrates the intent and runs the full
56
+ `ComposerEngine.compose()` pipeline — Splice / filesystem / browser
57
+ sample resolution — then swaps the scaffold plan for the resolved one
58
+ BEFORE `commit_branch_async` runs it through the async router. The
59
+ scaffold is preserved on `branch.evaluation` for audit.
60
+
61
+ - Graceful fallback when compose yields zero executable layers:
62
+ commit runs the scaffold instead of erroring. User gets tracks +
63
+ scenes they can populate manually, with `composer_escalation.error`
64
+ explaining why escalation couldn't complete.
65
+
66
+ - **Latency note**: composer winner commit now takes 10-30s (Splice +
67
+ filesystem resolution) vs ~0.5s pre-v2. Documented; a progress
68
+ callback version is future work.
69
+
70
+ ### Render-verify + classifier wiring (PR4)
71
+
72
+ - `run_experiment(render_verify=False, render_duration_seconds=2.0)` —
73
+ opt-in per-branch audio capture → offline loudness + spectrum
74
+ analysis → `TimbralFingerprint` extraction. ~2 * duration seconds +
75
+ ~1-2s analysis overhead per branch; default off preserves speed.
76
+
77
+ - New `derive_goal_progress_from_fingerprint(diff, target?)` turns
78
+ TimbralFingerprint diffs into `(goal_progress, measurable_count)`.
79
+ Dimensions below 0.02 epsilon are dropped as noise. With a target:
80
+ sign(target) * diff gives signed progress. Without: magnitude-only
81
+ contribution (branch moved = measurable).
82
+
83
+ - `classify_branch_outcome` accepts `fingerprint_diff` + `timbral_target`
84
+ kwargs. When set and caller didn't supply their own measurable inputs,
85
+ the classifier derives them from the diff. Caller-supplied values
86
+ still take precedence (back-compat). Protection violations still
87
+ trump fingerprint evidence — safety invariant preserved.
88
+
89
+ - `compare_experiments` automatically surfaces `fingerprint_diff` +
90
+ `fingerprint_before` + `fingerprint_after` on each branch's
91
+ `evaluation` dict via the existing pass-through.
92
+
93
+ ### Four new MCP tools (PR5, 398 → 402)
94
+
95
+ - **`analyze_synth_patch(track_index, device_index, role_hint="")`** —
96
+ SynthProfile for any supported native synth. Fetches parameter state
97
+ + display values, hands to the adapter. Opaque fallback for
98
+ non-supported devices.
99
+
100
+ - **`propose_synth_branches(track_index, device_index, target?,
101
+ freshness?, role_hint?)`** — algorithm/topology-aware branch seeds
102
+ with pre-compiled plans. Feeds directly to
103
+ `create_experiment(seeds=..., compiled_plans=...)`.
104
+
105
+ - **`extract_timbre_fingerprint(spectrum?, loudness?, spectral_shape?)`**
106
+ — pure transform from analysis dicts to 9-dimensional
107
+ `TimbralFingerprint`. For callers that already have analysis data.
108
+
109
+ - **`propose_composer_branches(request_text, count=2, freshness=0.65)`**
110
+ — N compositional hypotheses (canonical / energy_shift /
111
+ layer_contrast) with producer_payload-captured intents for
112
+ winner-commit escalation.
113
+
114
+ ### Migration notes for callers
115
+
116
+ - All additions are optional-param / new-function shaped. Pre-v2
117
+ callers see no behavior change unless they opt in.
118
+ - Pre-v2 serialized branches deserialize fine: `producer_payload`
119
+ defaults to `{"schema_version": 1}` when absent.
120
+ - `create_experiment(move_ids=...)` identical behavior.
121
+ - `enter_wonder_mode` response shape stable; `branch_seeds` /
122
+ `compiled_plans_by_seed_id` still additive.
123
+ - `run_experiment(render_verify=False)` default matches v1.13.0
124
+ behavior exactly.
125
+
126
+ ### Known limitations
127
+
128
+ See [`docs/manual/branch-native-migration.md`](docs/manual/branch-native-migration.md#known-limitations)
129
+ for the full list. Headlines:
130
+
131
+ - Wavetable region classification is a coarse heuristic on raw
132
+ `Osc 1 Pos`. Future work: render-based per-wavetable mapping.
133
+ - Composer commit latency (10-30s) with no progress callback yet.
134
+ - Render-verify requires the M4L analyzer bridge + LivePilot_Analyzer
135
+ on master; silently degrades to fast-path when either is missing.
136
+ - End-to-end render-verify path (capture_audio + bridge) is hardware-
137
+ dependent and not unit-tested; wiring is covered via classifier +
138
+ fingerprint derivation tests.
139
+
140
+ ## 1.13.0 — Branch-native architecture (April 20 2026)
141
+
142
+ Twelve-PR migration from "match request → pick move → compile move" to
143
+ "understand intent → generate branches → compile branches → compare".
144
+ The planning layer opens up — Wonder, Preview Studio, Experiment, and
145
+ the new synthesis_brain all share one BranchSeed + CompiledBranch
146
+ contract. Move-first is still available as a targeted flow; branch-native
147
+ is the canonical exploratory path.
148
+
149
+ **No tool count change** (still 398). **Domain count 51 → 52**
150
+ (added `synthesis_brain`). **+175 new tests** across 9 new files
151
+ (2206 → 2387 passing, 1 skipped, 0 failures).
152
+
153
+ ### New substrate (additive, non-breaking)
154
+
155
+ - **`mcp_server/branches/`** (PR1) — shared `BranchSeed` and
156
+ `CompiledBranch` types with `seed_from_move_id` / `freeform_seed` /
157
+ `analytical_seed` factories. `BranchSeed` sources:
158
+ `semantic_move` / `freeform` / `synthesis` / `composer` / `technique`.
159
+ - **SessionKernel creative controls** (PR2) — `freshness`,
160
+ `creativity_profile`, `sacred_elements`, `synth_hints` added as
161
+ optional fields on `SessionKernel` and `get_session_kernel`. Legacy
162
+ callers see zero behavior change.
163
+ - **ExperimentBranch compat shim** (PR3) — `move_id` now optional;
164
+ new `ExperimentBranch.from_seed()` classmethod and
165
+ `create_experiment_from_seeds(seeds=[...], compiled_plans=[...])`
166
+ entry point. Legacy `create_experiment(move_ids=...)` keeps working
167
+ and internally delegates via `seed_from_move_id`.
168
+ - **Creative conductor fork** (PR4) — `classify_request_creative()`
169
+ alongside `classify_request()`. Adds producer selection
170
+ (`branch_sources`, `seed_hints`) based on request content + kernel state.
171
+ - **`interesting_but_failed` branch status** (PR7) — new
172
+ `classify_branch_outcome()` in `evaluation/policy.py`. Exploration
173
+ mode downgrades score / measurable-delta failures to
174
+ `interesting_but_failed`; protection violations still force undo
175
+ (safety invariant).
176
+ - **Per-goal-mode novelty bands** (PR8) — TasteGraph's single
177
+ `novelty_band` is now a view over `novelty_bands["improve"]`; the
178
+ `explore` band lets surprise-me branch generation disconnect from
179
+ conservative improve-mode history. `bypass_taste_in_generation` flag
180
+ makes `rank_moves` return uniform scores.
181
+
182
+ ### Branch-native producers
183
+
184
+ - **Wonder branch assembler** (PR6) — `generate_branch_seeds()` emits
185
+ seeds from four sources: semantic_move, technique (session memory),
186
+ sacred-element inversion (freshness-gated), and corpus hints.
187
+ `enter_wonder_mode` now surfaces `branch_seeds` alongside variants.
188
+ - **`synthesis_brain/` subsystem** (PR9, PR10) — native-synth-aware
189
+ branch production with adapters for Wavetable, Operator, Analog,
190
+ Drift, Meld. `analyze_synth_patch()` / `propose_synth_branches()`
191
+ callable from Python; `extract_timbre_fingerprint()` builds a
192
+ TimbralFingerprint from 8-band spectrum + optional FluCoMa
193
+ descriptors. No MCP tools yet — next release will wire dedicated
194
+ tools and do the tool-count metadata sweep in one pass.
195
+ - **Composer branch producer** (PR11) — `propose_composer_branches()`
196
+ emits N distinct compositional hypotheses from one prompt via three
197
+ strategies (canonical / energy_shift / layer_contrast), gated on
198
+ freshness. Each branch ships a pre-compiled scaffolding plan.
199
+
200
+ ### Docs refactor
201
+
202
+ - **Skills + command guides thinned** (PR5) — `livepilot-core/SKILL.md`
203
+ now presents two peer flows (Flow A targeted / Flow B exploratory)
204
+ instead of one recipe-first pipeline. `arrange` / `beat` / `mix` /
205
+ `sounddesign` commands each add a short Branch-Native section.
206
+ - **Branch status vocabulary** documented including
207
+ `interesting_but_failed` retention semantics.
208
+
209
+ ### Migration notes for callers
210
+
211
+ - All additions are optional-param / new-function shaped. Any code
212
+ reading `branch.move_id` keeps working because `ExperimentBranch`
213
+ mirrors `seed.move_id` there. Any code calling
214
+ `create_experiment(move_ids=...)` keeps its exact behavior.
215
+ - If you have persistent state on disk (`~/.livepilot/taste.json`):
216
+ v1.13 migrates `novelty_band` (flat float) to `novelty_bands` (dict)
217
+ on first read. Old clients reading the file still see the flat field.
218
+ - Tests added across 9 new files — no existing test needed editing
219
+ beyond `test_experiment_engine.py` (which gains PR3 coverage but
220
+ keeps every pre-PR3 test passing).
221
+
3
222
  ## 1.12.2 — Post-release audit reliability fixes (April 18 2026)
4
223
 
5
224
  Six issues surfaced by an immediate post-v1.12.0 deep audit (parallel
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  <p align="center">
19
19
  An agentic production system for Ableton Live 12.<br>
20
- 398 tools. 51 domains. Device atlas. Splice integration. Auto-composition. Spectral perception. Technique memory.
20
+ 402 tools. 52 domains. Device atlas. Splice integration. Auto-composition. Spectral perception. Technique memory.
21
21
  </p>
22
22
 
23
23
  <br>
@@ -79,8 +79,8 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
79
79
  │ └─────────────────┼──────────────────┘ │
80
80
  │ ▼ │
81
81
  │ ┌─────────────────┐ │
82
- │ │ 398 MCP Tools │ │
83
- │ │ 51 domains │ │
82
+ │ │ 402 MCP Tools │ │
83
+ │ │ 52 domains │ │
84
84
  │ └────────┬────────┘ │
85
85
  │ │ │
86
86
  │ Remote Script ──┤── TCP 9878 │
@@ -120,7 +120,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
120
120
 
121
121
  ## The Intelligence Layer
122
122
 
123
- 12 engines sit on top of the 398 tools. They give the AI musical judgment, not just musical execution.
123
+ 12 engines sit on top of the 402 tools. They give the AI musical judgment, not just musical execution.
124
124
 
125
125
  ### SongBrain — What the Song Is
126
126
 
@@ -172,7 +172,7 @@ Every engine follows: **measure before → act → measure after → compare**.
172
172
 
173
173
  ## Tools
174
174
 
175
- 398 tools across 51 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
175
+ 402 tools across 52 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
176
176
 
177
177
  <br>
178
178
 
@@ -360,7 +360,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
360
360
  | Creative Constraints | 5 | constraint activation, reference-inspired variants |
361
361
  | Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
362
362
 
363
- > **[View all 398 tools →](docs/manual/tool-catalog.md)**
363
+ > **[View all 402 tools →](docs/manual/tool-catalog.md)**
364
364
 
365
365
  <br>
366
366
 
@@ -587,7 +587,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
587
587
 
588
588
  | Document | What's inside |
589
589
  |----------|---------------|
590
- | [Manual](docs/manual/index.md) | Complete reference: architecture, all 398 tools, workflows |
590
+ | [Manual](docs/manual/index.md) | Complete reference: architecture, all 402 tools, workflows |
591
591
  | [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
592
592
  | [Device Atlas](docs/manual/device-atlas.md) | 1305 devices indexed — search, suggest, chain building |
593
593
  | [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
Binary file
@@ -95,7 +95,7 @@ function anything() {
95
95
  function dispatch(cmd, args) {
96
96
  switch(cmd) {
97
97
  case "ping":
98
- send_response({"ok": true, "version": "1.12.2"});
98
+ send_response({"ok": true, "version": "1.14.0"});
99
99
  break;
100
100
  case "get_params":
101
101
  cmd_get_params(args);
@@ -1,2 +1,2 @@
1
1
  """LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
2
- __version__ = "1.12.2"
2
+ __version__ = "1.14.0"
@@ -0,0 +1,34 @@
1
+ """Branch-native types — the shared substrate for Wonder, Preview Studio,
2
+ Experiment, and future producers (synthesis_brain, composer).
3
+
4
+ A BranchSeed is a producer-emitted creative intent (pre-compilation).
5
+ A CompiledBranch pairs a seed with a concrete plan ready for the execution
6
+ router. compiled_plan=None ⇒ analytical-only (directional suggestion).
7
+
8
+ This module is pure types + factory helpers. No I/O, no side effects, no
9
+ imports from other mcp_server subsystems.
10
+ """
11
+
12
+ from .types import (
13
+ BranchSeed,
14
+ CompiledBranch,
15
+ BranchSource,
16
+ RiskLabel,
17
+ NoveltyLabel,
18
+ PRODUCER_PAYLOAD_SCHEMA_VERSION,
19
+ seed_from_move_id,
20
+ freeform_seed,
21
+ analytical_seed,
22
+ )
23
+
24
+ __all__ = [
25
+ "BranchSeed",
26
+ "CompiledBranch",
27
+ "BranchSource",
28
+ "RiskLabel",
29
+ "NoveltyLabel",
30
+ "PRODUCER_PAYLOAD_SCHEMA_VERSION",
31
+ "seed_from_move_id",
32
+ "freeform_seed",
33
+ "analytical_seed",
34
+ ]
@@ -0,0 +1,286 @@
1
+ """Branch-native types.
2
+
3
+ Design (see docs/specs/2026-03-17-livepilot-design.md and the branch-native
4
+ migration plan):
5
+
6
+ - Producers (Wonder, synthesis_brain, composer, technique memory) emit
7
+ BranchSeed objects expressing creative intent. A seed carries a hypothesis,
8
+ a source label, a distinctness reason, and novelty/risk labels — but no
9
+ executable plan yet.
10
+ - A compiler turns a seed into a CompiledBranch by attaching a plan. For
11
+ source="semantic_move" seeds, the existing semantic_moves.compiler is used.
12
+ For freeform/synthesis/composer seeds, the producer supplies the plan.
13
+ - CompiledBranch is the canonical post-compilation shape. PreviewVariant
14
+ and ExperimentBranch will migrate to thin wrappers over CompiledBranch
15
+ in later PRs; this PR only introduces the types.
16
+
17
+ compiled_plan=None means analytical_only — the branch is a directional
18
+ suggestion with no executable path. This case already exists in Wonder
19
+ (build_analytical_variant); the branch-native schema promotes it to a
20
+ first-class concept across the whole system.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ from dataclasses import asdict, dataclass, field
27
+ from typing import Literal, Optional
28
+
29
+
30
+ BranchSource = Literal[
31
+ "semantic_move", # compiled via semantic_moves.compiler
32
+ "freeform", # producer supplies the compiled plan directly
33
+ "synthesis", # from synthesis_brain (PR9+)
34
+ "composer", # from composer branch-native path (PR11)
35
+ "technique", # from technique memory replay
36
+ ]
37
+ RiskLabel = Literal["low", "medium", "high"]
38
+ NoveltyLabel = Literal["safe", "strong", "unexpected"]
39
+
40
+
41
+ PRODUCER_PAYLOAD_SCHEMA_VERSION = 1
42
+
43
+
44
+ @dataclass
45
+ class BranchSeed:
46
+ """Pre-compilation creative intent.
47
+
48
+ Distinctness between branches is checked at seed layer — two seeds with
49
+ the same (source, hypothesis_hash, affected_scope_hash) are not distinct.
50
+
51
+ Fields:
52
+ seed_id: stable identifier. For semantic_move seeds, derived from move_id.
53
+ source: producer category.
54
+ move_id: populated when source="semantic_move"; empty otherwise.
55
+ hypothesis: one-line human-readable prediction of what the branch does.
56
+ protected_qualities: dimension names the producer promises not to regress.
57
+ affected_scope: {track_indices, device_paths, section_ids, clip_slots}.
58
+ Session-scope information: which tracks/devices/sections the branch
59
+ affects. Distinct from ``producer_payload`` which carries regeneration
60
+ context.
61
+ distinctness_reason: why this seed is different from siblings in a set.
62
+ risk_label: execution safety tier.
63
+ novelty_label: creative novelty tier — maps to the safe/strong/unexpected UX triptych.
64
+ analytical_only: true ⇒ no plan will be compiled; branch is directional only.
65
+ producer_payload: free-form dict the emitting producer populates with
66
+ whatever it needs for regeneration / provenance / winner escalation.
67
+ Consumers must check ``schema_version`` (defaults to 1) before reading
68
+ specific keys so older serialized branches don't break newer readers.
69
+
70
+ Canonical shapes per producer:
71
+ semantic_move: {schema_version: 1} (empty — move_id is the key)
72
+ freeform / technique: {schema_version: 1} (optional)
73
+ synthesis:
74
+ {schema_version: 1,
75
+ device_name: "Wavetable",
76
+ track_index: int, device_index: int,
77
+ strategy: "osc_position_shift" | "voice_width_variant" | ...,
78
+ topology_hint: {...}} (used by PR4 render-verify to re-target)
79
+ composer:
80
+ {schema_version: 1,
81
+ strategy: "canonical" | "energy_shift" | "layer_contrast",
82
+ intent: {<CompositionIntent.to_dict()>}} (used by PR3 to
83
+ rehydrate for winner-commit escalation)
84
+ """
85
+
86
+ seed_id: str
87
+ source: BranchSource
88
+ move_id: str = ""
89
+ hypothesis: str = ""
90
+ protected_qualities: list[str] = field(default_factory=list)
91
+ affected_scope: dict = field(default_factory=dict)
92
+ distinctness_reason: str = ""
93
+ risk_label: RiskLabel = "low"
94
+ novelty_label: NoveltyLabel = "strong"
95
+ analytical_only: bool = False
96
+ producer_payload: dict = field(
97
+ default_factory=lambda: {"schema_version": PRODUCER_PAYLOAD_SCHEMA_VERSION}
98
+ )
99
+
100
+ def to_dict(self) -> dict:
101
+ d = asdict(self)
102
+ # Guarantee schema_version in the payload even if the dict was
103
+ # mutated to empty by a caller.
104
+ payload = d.get("producer_payload") or {}
105
+ if "schema_version" not in payload:
106
+ payload = dict(payload)
107
+ payload["schema_version"] = PRODUCER_PAYLOAD_SCHEMA_VERSION
108
+ d["producer_payload"] = payload
109
+ return d
110
+
111
+
112
+ @dataclass
113
+ class CompiledBranch:
114
+ """Post-compilation branch — supersedes the move_id-locked ExperimentBranch.
115
+
116
+ Fields:
117
+ branch_id: stable identifier scoped to a branch set.
118
+ seed: the originating BranchSeed (carries source, hypothesis, move_id).
119
+ compiled_plan: execution-router-ready plan dict, or None for analytical-only.
120
+ execution_log: per-step results after run/commit; [{tool, backend, ok, error, result}].
121
+ before_snapshot / after_snapshot: captured session state dicts.
122
+ evaluation: evaluator result dict; shape determined by the evaluator.
123
+ score: composite quality score (0-1).
124
+ status: pending | running | evaluated | committed | discarded
125
+ | interesting_but_failed ← PR7 will gate on this
126
+ created_at_ms / executed_at_ms: timestamps.
127
+ """
128
+
129
+ branch_id: str
130
+ seed: BranchSeed
131
+ compiled_plan: Optional[dict] = None
132
+ execution_log: list = field(default_factory=list)
133
+ before_snapshot: Optional[dict] = None
134
+ after_snapshot: Optional[dict] = None
135
+ evaluation: Optional[dict] = None
136
+ score: float = 0.0
137
+ status: str = "pending"
138
+ created_at_ms: int = 0
139
+ executed_at_ms: int = 0
140
+
141
+ @property
142
+ def move_id(self) -> str:
143
+ """Back-compat convenience — delegates to seed.move_id.
144
+
145
+ Lets callers that currently read branch.move_id keep working once
146
+ ExperimentBranch migrates to wrap CompiledBranch.
147
+ """
148
+ return self.seed.move_id
149
+
150
+ @property
151
+ def analytical_only(self) -> bool:
152
+ """A branch is analytical when the seed says so OR when no plan exists."""
153
+ return self.seed.analytical_only or self.compiled_plan is None
154
+
155
+ def to_dict(self) -> dict:
156
+ d = {
157
+ "branch_id": self.branch_id,
158
+ "seed": self.seed.to_dict(),
159
+ "move_id": self.move_id,
160
+ "score": self.score,
161
+ "status": self.status,
162
+ "analytical_only": self.analytical_only,
163
+ "created_at_ms": self.created_at_ms,
164
+ }
165
+ if self.compiled_plan:
166
+ d["step_count"] = self.compiled_plan.get("step_count", 0)
167
+ d["summary"] = self.compiled_plan.get("summary", "")
168
+ if self.before_snapshot is not None:
169
+ d["before_snapshot"] = self.before_snapshot
170
+ if self.after_snapshot is not None:
171
+ d["after_snapshot"] = self.after_snapshot
172
+ if self.evaluation is not None:
173
+ d["evaluation"] = self.evaluation
174
+ if self.execution_log:
175
+ d["execution_log"] = self.execution_log
176
+ d["steps_ok"] = sum(1 for e in self.execution_log if e.get("ok"))
177
+ d["steps_failed"] = sum(1 for e in self.execution_log if not e.get("ok"))
178
+ return d
179
+
180
+
181
+ # ── Factory helpers ──────────────────────────────────────────────────────
182
+
183
+ def _stable_seed_id(prefix: str, *parts: str) -> str:
184
+ """Deterministic seed_id from parts — no timestamps, stable across runs."""
185
+ seed = "|".join(str(p) for p in parts)
186
+ return f"{prefix}_" + hashlib.sha256(seed.encode()).hexdigest()[:10]
187
+
188
+
189
+ def seed_from_move_id(
190
+ move_id: str,
191
+ seed_id: str = "",
192
+ hypothesis: str = "",
193
+ novelty_label: NoveltyLabel = "strong",
194
+ risk_label: RiskLabel = "low",
195
+ protected_qualities: Optional[list[str]] = None,
196
+ distinctness_reason: str = "",
197
+ producer_payload: Optional[dict] = None,
198
+ ) -> BranchSeed:
199
+ """Build a semantic_move seed — the baseline producer path.
200
+
201
+ Mirrors how wonder_mode.engine.discover_moves already hands moves to
202
+ build_variant. In later PRs, the Wonder distinctness selector will emit
203
+ BranchSeed directly instead of move dicts.
204
+ """
205
+ if not seed_id:
206
+ seed_id = _stable_seed_id("seed", "move", move_id, novelty_label)
207
+ return BranchSeed(
208
+ seed_id=seed_id,
209
+ source="semantic_move",
210
+ move_id=move_id,
211
+ hypothesis=hypothesis or f"Apply {move_id}",
212
+ novelty_label=novelty_label,
213
+ risk_label=risk_label,
214
+ protected_qualities=protected_qualities or [],
215
+ distinctness_reason=distinctness_reason,
216
+ producer_payload=_normalize_payload(producer_payload),
217
+ )
218
+
219
+
220
+ def freeform_seed(
221
+ seed_id: str,
222
+ hypothesis: str,
223
+ affected_scope: Optional[dict] = None,
224
+ protected_qualities: Optional[list[str]] = None,
225
+ distinctness_reason: str = "",
226
+ novelty_label: NoveltyLabel = "strong",
227
+ risk_label: RiskLabel = "medium",
228
+ source: BranchSource = "freeform",
229
+ producer_payload: Optional[dict] = None,
230
+ ) -> BranchSeed:
231
+ """Build a freeform seed — producer has a concrete hypothesis without a move.
232
+
233
+ The compiled plan is attached downstream by the producer; this seed
234
+ carries intent only. Used by synthesis_brain, composer, and any
235
+ producer that doesn't go through semantic_moves.compiler.
236
+ """
237
+ return BranchSeed(
238
+ seed_id=seed_id,
239
+ source=source,
240
+ hypothesis=hypothesis,
241
+ affected_scope=affected_scope or {},
242
+ protected_qualities=protected_qualities or [],
243
+ distinctness_reason=distinctness_reason,
244
+ novelty_label=novelty_label,
245
+ risk_label=risk_label,
246
+ producer_payload=_normalize_payload(producer_payload),
247
+ )
248
+
249
+
250
+ def analytical_seed(
251
+ seed_id: str,
252
+ hypothesis: str,
253
+ source: BranchSource = "freeform",
254
+ protected_qualities: Optional[list[str]] = None,
255
+ producer_payload: Optional[dict] = None,
256
+ ) -> BranchSeed:
257
+ """Build an analytical-only seed — no plan will be compiled.
258
+
259
+ Used when a producer has a directional suggestion but no executable path:
260
+ - Wonder fallback when no move matches (already present as
261
+ build_analytical_variant in wonder_mode.engine).
262
+ - Synthesis brain on opaque devices where parameter state can be read
263
+ but safe mutations cannot be proposed yet.
264
+ """
265
+ return BranchSeed(
266
+ seed_id=seed_id,
267
+ source=source,
268
+ hypothesis=hypothesis,
269
+ protected_qualities=protected_qualities or [],
270
+ analytical_only=True,
271
+ producer_payload=_normalize_payload(producer_payload),
272
+ )
273
+
274
+
275
+ def _normalize_payload(payload: Optional[dict]) -> dict:
276
+ """Ensure a producer_payload always has schema_version set.
277
+
278
+ Producers that don't care about versioning pass None / empty dict and
279
+ get the default schema_version=PRODUCER_PAYLOAD_SCHEMA_VERSION back.
280
+ Producers that carry their own version number have it preserved.
281
+ """
282
+ if not payload:
283
+ return {"schema_version": PRODUCER_PAYLOAD_SCHEMA_VERSION}
284
+ out = dict(payload)
285
+ out.setdefault("schema_version", PRODUCER_PAYLOAD_SCHEMA_VERSION)
286
+ return out
@@ -1 +1,10 @@
1
- """Composer Engine — auto-composition from text prompts via Splice + Sample Engine."""
1
+ """Composer Engine — auto-composition from text prompts via Splice + Sample Engine.
2
+
3
+ PR11 adds branch_producer.propose_composer_branches() for emitting
4
+ multiple section-hypothesis BranchSeeds alongside the existing
5
+ single-plan compose() entry point.
6
+ """
7
+
8
+ from .branch_producer import propose_composer_branches, escalate_composer_branch
9
+
10
+ __all__ = ["propose_composer_branches", "escalate_composer_branch"]