Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/skills/author-session/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ is `schema.unknown_field`; out of range is `numeric.out_of_schema_range` at
| Two discontinuities of one entry less than 64 samples apart, at the later one's `start_sample` or `end_sample` | `automation.discontinuity_spacing` | typed-model |
| A repeated entry `id` | `id.duplicate` | typed-model |
| Segments out of order, overlapping or empty | `automation.out_of_order`, `automation.segment_overlap`, `automation.invalid_range` | typed-model |
| A segment whose unit is not the row's, or a value the row refuses (a fader above 24 dB, a filter above the rate's maximum), at `$.automation[id=<id>].segments[<k>]` | `control.domain` | prepare-effects |
| A `linear` or `exponential` segment on a toggle (polarity, mute) or enumeration row; `exponential` on a dB row or a row whose minimum is not above 0; a `linear` or `exponential` filter segment from or to `0` ("off" is reached only by a `step`) | `control.automation.shape` | prepare-effects |
| An HPF that can reach or pass an enabled LPF on a lane, at any time, with stored automation on either filter, at the LPF entry's (else the HPF entry's) segment | `control.automation.order` | prepare-effects |

## Buses: submix strips and their taps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,82 @@ Checked against `12d4a4f9d`.
`crates/control-plane/src/control.rs`).
- **Dependencies.** #1626, #1637, #1566 (in place of #1335's route). Obsolete draft 03a is not filed; this spec
carries its shape rule. Batch R1.

## Decision record: #1645 (batch 14a)

- **Shipped, per the amendment.** `controls::automation_diagnostics` (`crates/controls/src/automation.rs`)
gains the shape rule and the curve form of the input filter order rule; `ControlError` gains
`AutomationShape` (`control.automation.shape`) and `AutomationOrder` (`control.automation.order`).
`controls` gains a normal dependency on `automation` and calls `automation::value_at` for every
evaluated value. No new call site: preparation, the classifier's step 2 and `session-validator`
stage `prepare-effects` already call the row rule, so browser and C ABI refuse alike.
- **Shape rule, every row.** A segment is reported once at `.segments[<k>]`: `control.domain`
first, else `control.automation.shape`. Toggle or enumeration row: `step` only. `exponential`:
continuous, `minimum > 0`, unit not dB. A row with a `disabled_value`: a `linear` or
`exponential` segment has neither value equal to it.
- **Order rule.** As D2, per strip and lane (a `both` entry drives both lanes). Departures, each
for the code's truth: (1) the value at a sub-interval's right end is the left limit, evaluated on
the defining segment alone (`value_at(&[segment], b)`), because `value_at` on the whole table at
a segment's `end` returns the next segment's `start_value` when one begins there; (2) the rule
skips a lane whose HPF or LPF entry has its own fault (target, domain or shape), so it never reads
a curve that ramps through "off", and a fault is reported once; (3) a `both` entry failing both
lanes at one segment is reported once (identical code and path). Stage index: the row rule runs
at stage index 4 (`prepare-effects`), not 3 as gate 3 says.
- **Gates.**
- 1 and 2: `crates/controls/tests/automation.rs`
`builtin_rows_refuse_a_segment_by_unit_domain_and_shape`,
`input_filter_curves_keep_the_hpf_below_the_lpf` (adds one case: the LPF ride against an HPF
that stays on, refused in the gap's hold at the LPF entry's segment 0). PASS.
- 3: `tools/session-validator/tests/validate.rs` two `MUTATIONS` rows (fader ride in `linear`
units -> `control.domain`, polarity flip as `linear` -> `control.automation.shape`, both stage
index 4). PASS; `fixtures_distinguish_schema_examples_from_launch_effects` and `skill` PASS.
- 7: `crates/host-core/tests/live_delta.rs` `an_automation_the_builtin_rows_refuse_needs_a_rebuild`;
`crates/capi/src/runtime/live_tests.rs`
`a_builtin_automation_the_rows_refuse_is_refused_before_any_ack` (shape and order refusals on a
playing engine: `RESULT_COMPILE_REJECTED`, preparation's diagnostic, no revision, candidate or
event, next three blocks bit-identical). PASS.
- 6: `cargo test` for controls, session-validator, host-core (`test-support,control-provider`;
the `live_delta` target is `cfg(feature = "control-provider")`), protocol, builtins-compiler,
capi: PASS. `check-workspace-policy.sh`, `check-builtins-policy.sh`, `check-cross-targets.sh`
(no new `memset_pattern16`; the #1018 expected failures unchanged), `realtime-policy`, clippy on
the touched crates with `-D warnings`, `cargo fmt --check`: PASS.
- 5: no render code changed; the `audit capi` digest comparison is left to the coordinator's PR
evidence.
- **Docs.** `docs/SESSION_SCHEMA_V1.md` names both codes, the shape rule and the curve order rule;
the `author-session` skill's automation refusal table gains three rows at `prepare-effects`.
- **Fold (batch 14a).** The gap the first pass found is closed here: a static HPF or LPF edit that
crosses an automated curve with the automation table unchanged classified live, so the committed
model could break the order rule. `controls` gains `filter_statics_changed_under_automation`
(a strip's static HPF or LPF bits differ and `next` automates either filter of that strip) and
`filter_under_automation` (an address is such a strip's HPF or LPF). The classifier's step 2
(`crates/host-core/src/live_delta.rs`) also runs `automation_diagnostics` on `next` when the
first holds, and refuses with `LiveRebuild::AutomationTarget`, as the #1631 activity path does;
the fast path (`classify_rows`, used by the C ABI and the browser through `control-plane`)
answers `None` for a row the second names, so the full classifier judges it. Tests:
`a_static_filter_edit_that_crosses_an_automated_curve_needs_a_rebuild` (host-core `live_delta`;
red without the step-2 change: `Ok([("eq0", [], None)])` against `Err(AutomationTarget)`) and
`a_static_lpf_edit_that_crosses_an_automated_hpf_is_refused_before_any_ack` (capi; a
`SET_CONTROL` of the LPF to 500 Hz under an HPF ride to 900 Hz: red without the fix, and red
again with the step-2 change but without the fast-path fallback, both times `RESULT_OK` (0)
against `RESULT_COMPILE_REJECTED` (5); a move to 2 kHz commits live). #1660 keeps only the
classification of input-filter edits on an automated strip, not this refusal.

**Review fold (batch 14a).**

- **Root ruling (2026-10-15): the order rule has a reach.** The input-filter order rule is checked
on each sub-interval widened by the cell's reach, 64 samples. That is #1637's grid lookahead
(`crates/automation/src/events.rs`, `target_at`) and the filter's jump ramp. So stored automation
is never accepted and then refused in render. The reviewer's counterexample is now a refused
case: the HPF holds 900 Hz on [0,1000) and then steps to 100; the LPF is linear from 1000 to 300
on [1000,1010). Before the widening, each piece was ordered, but the LPF target at sample 950 is
its value at 1014, 300 Hz, below the HPF's 900 Hz. `docs/SESSION_SCHEMA_V1.md` states the rule
with the reach.
- **#1660 must keep its reach at or below 64 samples**, or widen this rule in the same change.
- **The reach is 128 samples (coordinator, within the ruling).** Widening each curve by its
64-sample reach means a rendered pair can read stored values up to 128 samples apart: render
leads a curve by up to 64 (grid lookahead) and lags it by up to 64 (jump ramp), on either filter.
`ORDER_REACH` is 128; with 64 the test `input_filter_curves_keep_their_order_within_renders_reach`
is red (an LPF fall 100 samples after the HPF's step is accepted). #1660 must keep each of its two
reaches at or below 64 samples, or widen this rule in the same change.
- **Path.** The fold edits `crates/host-core/src/live_delta.rs`. That file is outside the original
authorized paths. The coordinator approved the edit.
13 changes: 13 additions & 0 deletions .github/ISSUE_SPECS/1660-render-stored-input-filter-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,16 @@ Checked against `12d4a4f9d`.
yet" and the dependency on root's confirmation are met. D4's per-block bound is the bound F1's
condition 2 names; its gate stays. The contract text is #1659's (D5 of its batch-10
amendment), for owner review.

## Amendment (controls coordinator, batch 14a)

- **#1645's fold now refuses a static HPF or LPF edit that crosses an automated curve.** It does so
in the classifier's step 2 and in the fast path, with `LiveRebuild::AutomationTarget` and code
`control.automation.order`. The names in this spec, `LiveRebuild::Domain`, `builtin.filter.order`
and `input_filter_order_diagnostics`, are superseded by those.
- **#1660 keeps only the classification of input-filter edits on an automated strip** (the
automation edits that are carried rebuilds, and the record rules of #1648).
- **The reach.** Root ruled on 2026-10-15 that #1645's order rule is checked widened by each
curve's 64-sample reach, which compares stored values up to 128 samples apart (`ORDER_REACH`).
#1660's grid lookahead and filter jump ramp must each stay at or below 64 samples, or the rule
must widen in the same change.
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,8 @@ Checked against `12d4a4f9d`.
takes no live record (A10, #1665), so a piece stages each cell at most once. No #1306 term.
- **D3 also keeps** the bypass crossfade step of #1341a per block.
- **Dependencies.** #1345 has landed; #1611 and #1621 replace #1306 here.

## Amendment (batch 14a)

- **D1's `Point`.** D1 says `start_sample = end_sample`. After #1686, `PreparedAutomationSpan` is
`{channel, parameter_index, sample, value}`: a `Point` has one `sample`, the event's `r0 + offset`.
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,94 @@ that must name a strip that exists at boot.
## Size

Half a day.

## Decision record: #1683 (batch 14a)

**Decisions taken (implementer's proposal, for the coordinator's ruling).**

- **Where it lives: a core-owned monitor row.** Every strip (track and submix) has an own row
`master` (control ID 4, toggle, `scope: monitor`, family `setting`, fixed motion, not
automatable). Like `solo` it is host-local: held in the control plane's monitor overlay
(`controls::MonitorOverlay::master`), never saved, never in a snapshot, and a write is a
monitor-only `SET_CONTROL` that spends no revision and replies path `monitor`. It is held by
strip ID, so a strip added or removed before the master never moves it. At most one strip is
the master: designating one ends the other's designation; clearing a strip that is not the
master changes nothing. Both hosts reach it through the same `SESSION_TRANSACTION_APPLY`; the
SDK's generated builders gain `strip.master(enabled)`.
- **A strip that does not exist yet: refused before commit.** The write is checked by
`controls::check_monitor` like any address; an unknown strip is `AddressUnknown`. A
structural transaction that removes the master strip ends the designation
(`MonitorOverlay::retain_strips_of`), as a removed strip keeps no solo.

**What shipped.**

- `controls`: `MonitorOverlay` gains `master`, `is_master`, `set_master`, `same_solo`;
`MonitorWrite` gains `row: MonitorRow { Solo, Master }` and its `soloed` field is now `on`;
`MonitorOverlay::after` applies each write to its row, and only solo writes choose the ramp.
- `protocol`: a later monitor write replaces an earlier one of the same strip *and row*.
- `host-core`: the classifiers compare solos only for the effective values (a master change
moves none) and mark the owners whose master row changed, so the catalog refreshes them. The
preparation resolves the master once, before allocating: a first plan from
`HostLiveControlRequest::master_track` (the browser's boot word, by index, refused
`host.observation.master_track` when it names no strip, as before); a successor from
`SuccessorBase::monitor.next` by ID. A successor request that names an index is refused with
the same code. The boot catalog lists the boot word's designation.
- `control-plane`: the boot plan's `master_track` seeds the session's overlay
(`PreparedRuntime::master_track`); `SessionState::master_strip()` reads it.
- `host-web`: the boot word designates the first plan only (successor requests carry `None`);
after every successful control command `ReadyOwnership::sync_master` resolves the core's
master by ID in the rendering plan's tables and in each replacement's that waits for adoption,
and writes `PlanShape::master_track` and the meter header's `master_track_plus_one`. The meter
fold is unchanged.

**Departures.** None from frozen text. The boot word `liveControlMasterTrackPlusOne` stays: it
seeds the core's overlay, so a boot designation and a later `SET_CONTROL` are one state. It also
fixes a latent defect: a successor plan no longer reuses the boot index (after a strip was added
before the master, it named another strip, or refused the rebuild).

**Gates.**

1. Browser: `structural_apply::a_master_designated_after_an_empty_boot_reads_as_a_boot_designation`
(`hosts/host-web/src/tests.rs`): empty boot refuses the boot word; a rebuild adds `t0`
(compressor); `SET_CONTROL master` on `t0` applies on path `monitor` at the same revision;
the header names strip 1 and the master word is `t0`'s own, bit for bit; a later rebuild adds
`a0` before it and the master follows `t0` to strip 2; an unknown strip is refused; clearing
reads absent. C ABI parity row:
`both_hosts_designate_the_master_strip_after_an_empty_boot_alike`
(`hosts/host-web/tests/control_parity.rs`): same verdicts, control list and state pages and
snapshots on both hosts for designate, move, refuse and clear.
Core: `the_master_designation_is_one_monitor_row_held_by_strip_id`
(`crates/controls/tests/monitor.rs`), `a_successor_takes_its_master_from_the_overlay_by_strip_id`
(`crates/host-core/tests/strip_meters.rs`); `list_yields_every_expected_address_once_in_ruled_order`
updated for the new row.
2. Suites (all green): `cargo test -p controls`, `-p protocol -p control-plane --all-features`,
`-p host-core --all-features`, `-p host-web --all-features`, `-p capi --all-features`,
`-p conformance`; `check-sdk-generated.sh`, `check-sdk-types.sh`; realtime policy ok (149
regions); clippy `-D warnings` and fmt on the touched crates. The real-browser qualification
was not run here (no browser matrix in this worktree); the boot word it uses is unchanged.

**Moved pins (measured).**

- `hosts/host-web/tests/browser-v1/expected.json`, simd128 `initialStatus` and
`beforeDisposeStatus` `memoryBytes`: 2,818,048 -> 2,883,584 (one 64 KiB page), reason #1683
(one more own row per strip in the catalog and the overlay's master). Measured with
`check-browser-expected-resources.py` on this tree; the base `616106095` measures 2,818,048.
- `hosts/host-web/tests/fixtures/control-list.json`: blessed (`MISO_ENGINE_BLESS_FIXTURES=1`), four
`master` rows added, one per strip of the parity session.
- SDK regenerated: `sdk/assets/miso-engine-v1-parameter-metadata.json`,
`sdk/src/generated/controls.ts`, `sdk/src/generated/control-builders.ts`. No wire change, so no
schema hash moved.

**Departures (batch 14a, appended).** "Departures: none" above needs one line. The C ABI lists,
accepts and reads back the `master` row, but nothing on the C ABI consumes it yet. Under decision 17
C1 this is correct: the designation is core state, readable on both hosts. Its reading on the C ABI
is the successor #1690 ("Read the master strip's meter on the C ABI").

**Review fold (batch 14a).**

- **The master takes effect at adoption.** `sync_master` updates only the pending shapes once a
replacement is published, so the rendering plan keeps its master until the swap.
- **A solo and a master write on one strip in one transaction both apply.** The replace rule of
`protocol` is per strip and row.
- **The boot word with no observation taps is refused; the row is accepted with a null reading.**
The two are documented side by side.
Loading
Loading