From 562cb97ca94c6a3c2363debe61b0860550f5f22e Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 17 Oct 2026 19:52:35 +0000 Subject: [PATCH 1/6] Narrow #1669 to the delivered automation rows; remove closed specs #1655 #1711 The owner's finish line A (2026-10-17) narrows #1669 to the rows the engine renders today: strip fader, strip mute, VCA offset, VCA mute and a following send. One fixture, one oracle pin, one cross-host case. The matrix fixture and pan, trim, filter and EQ rows become phase-2 extensions. D5's single value_at plant cannot hold (the program builder derives jumps from value_before vs value_at), so it becomes three plants. #1670 gets a matching amendment. Co-Authored-By: Claude Opus 5.5 --- ...r-stored-vca-offset-and-mute-automation.md | 796 ------------------ ...ed-automation-across-browser-and-native.md | 222 +++++ ...-automation-on-aarch64-and-across-seeks.md | 15 + ...ves-own-length-across-a-reposition-swap.md | 577 ------------- .../handoffs/stored-automation-1058/README.md | 2 +- 5 files changed, 238 insertions(+), 1374 deletions(-) delete mode 100644 .github/ISSUE_SPECS/1655-render-stored-vca-offset-and-mute-automation.md delete mode 100644 .github/ISSUE_SPECS/1711-keep-a-live-level-moves-own-length-across-a-reposition-swap.md diff --git a/.github/ISSUE_SPECS/1655-render-stored-vca-offset-and-mute-automation.md b/.github/ISSUE_SPECS/1655-render-stored-vca-offset-and-mute-automation.md deleted file mode 100644 index 74ce46492..000000000 --- a/.github/ISSUE_SPECS/1655-render-stored-vca-offset-and-mute-automation.md +++ /dev/null @@ -1,796 +0,0 @@ -# Render stored VCA offset and mute automation - -Proposed by *Research: render stored session automation in the engine, identically on every -platform* (#1058), root's ruling on finding F20 (2026-10-14, README "Root's rulings (2026-10-14)"), -under decision 17 (`docs/rulings/one-control-abstraction-2026-10-13.md`, C3, C6 and C7). A draft for -root to file. Written by the controls coordinator on 2026-10-14. Code anchors verified on -`2e6343a99`. Batch R2, after #1651-#1653. - -## Product outcome - -A producer automates the "vocals" VCA: an `offset_db` ride and `mute` steps. Every member lane -plays the ride and the steps on every host, in the member's own node time, so a member behind a -latent insert hears the VCA move with its own audio. Sends that follow a member's mute follow the -automated VCA mute at their tap. A hand move of an automated VCA offset writes a stored dB offset, -and a hand VCA mute ORs with the automated VCA mute (D5). A VCA automation edit is a carried -rebuild. A session with no VCA automation renders the bits it renders today. - -## Context - -- **The rows.** A VCA has two rows, `offset_db` (1) and `mute` (2), both per lane, family - `StripLevel`; `offset_db` has the fader domain and the ramp row `VcaOffset`, `mute` is a toggle - with the ramp row `VcaMute` (`crates/controls/src/vca.rs:13-72`). A VCA has no render cell: a - change reaches render through its members' effective fader and mute (#1558). `automatable` is - computed (`crates/controls/src/descriptor.rs:124-132`), so both rows are targets today, and - `fixtures/session/v1/builtins-automation.json` holds the entry `vca-ride` (target owner `vca`, - `riders`). Nothing renders it: the classifier masks the whole table - (`crates/host-core/src/live_delta.rs:359-361`). -- **The fader composition.** #1646 D6 (README F16, root's decision): a member's gain is - `checked_fader_gain(vca_compose_db(member, S))`, `S` the `f64` sum of the reaching offsets in - ascending VCA ID, offsets first. #1646 D1 gives each automated fader lane an offsets cell (`S` - as one `f64`, one ramp word); #1649 writes it on every VCA edit; #1650 puts the lane's hand - offset first in `S`. -- **The mute composition.** #1651 (amended): the stage mute of an automated mute lane is - `curve || terms`, `terms = static_mute || reaching VCA mutes || solo term` in one cell word. - #1652 (amended) gives a following send of an automated source lane a seven-word route cell - and evaluates the source curve in the tap's node time. -- **The reach.** Nested VCAs reach members through `session::vca`; the host caps the reach pairs - (`maximum_vca_reach_pairs`; the browser's `MAXIMUM_VCA_REACH_PAIRS`). -- **The shape rule.** Amended #1645: a toggle row takes only `step`. - -## Decisions frozen for this slice - -- **D1. Programs.** Each automated VCA lane compiles once, with #1637's builder: `offset_db` - with grid period and ramp 64, completion `End`, jump row `VcaOffset`; `mute` with jumps only, - row `VcaMute`. The segment table is plan-owned once per VCA lane. Each member lane the VCA reaches - holds its own cursor and event state for it and evaluates it in the member's fader node time - `a(fader)` (#1647), because a VCA has no audio of its own. -- **D2. Offset composition.** A member lane's fader stage runs events when its own fader is - automated or when an automated VCA offset reaches it ("event lanes"). Events are the union of its - own curve's events and each reaching automated VCA's events. At each event: - `S_t = S + c_1(τ) + … + c_m(τ)` in `f64`, the `c_k` the reaching automated VCAs' curve values in - ascending VCA ID, then `gain = checked_fader_gain(vca_compose_db(own, S_t))`. `S` (the cell) holds - the lane's own hand offset, then the static offsets of the reaching VCAs that are not automated, - then the hand offsets of the reaching automated VCAs, each group in ascending VCA ID. `own` is - the member's curve value when its fader is automated, else the cell's `own_db` word (D3). - - A session with no VCA automation has no `c_k` and no `own_db` word: the composition is - #1646's and #1649's, and no bit moves. - - A flat VCA curve at `v` gives the bits of a static VCA offset `v` whenever F16's exactness - condition holds (each nonzero term at least `k·2^-20` dB, so every partial sum is exact); the - order "static terms, then automated terms" can differ from ascending-ID order only in the - degenerate cases F16 lists. Root records it as a class B order under the standing - summation-order ruling, with F16. -- **D3. The cell of a member that is not automated.** An event lane whose own fader is not - automated gets #1646's offsets cell plus one word, `own_db` (the member's static fader dB, - `f32`): four words, 48 bytes in #1312's three slots. A hand fader move on that lane is a live - value, as today, but the classifier writes `own_db` through the cell (#1649 D4's write, ramp - `LiveRamps::resolve(Fader, the edit's ramp)`) instead of a `FaderDb` record, and render jumps at - the next block entry only if the gain bits change (#1649 D3). -- **D4. Mute composition.** A member lane reached by an automated VCA mute uses #1651's - `terms` word whether or not its own mute is automated. Its stage mute is - `own_curve || terms || v_1(τ) || … || v_m(τ)`, the `v_k` the reaching automated VCA mute curves - in the member's node time, with `own_curve` false when the own mute is not automated. `terms` - holds the static mute, the static mutes of every reaching VCA (automated or not: D5) and the solo - term. Events are the curves' discontinuities; only a change emits #1638's `SetMute`, over the - `VcaMute` jump row for a VCA curve event. A following send of such a member uses #1652's - seven-word cell, and its follow term ORs the same `v_k` evaluated in the tap's node time - `τ_tap`, so the send closes on the timeline sample where the member mutes (README A6). -- **D5. Hand edits** (OQ1 = C with root's ruling on level controls). - - `SET_CONTROL` on an automated VCA `offset_db` writes the entry's stored offset (#1650's - field; `takes_offset` gains VCA `offset_db`, per lane), and the classifier rewrites `S` of every - member lane the VCA reaches (#1649 D4's write). This reads root's ruling "send gain is a - level control, so it takes an offset" as covering the VCA offset, a level control too; root - confirms at filing. If root reads it otherwise, the edit stores the fallback and commits - `model_only` (#1648 D3), and gate 4's offset case expects that. - - `SET_CONTROL` on an automated VCA `mute` ORs: the VCA's static mute stays live and is written - into `terms` of each reached member lane (#1651's terms write). - - Edits of a VCA that is not automated, and reach changes that involve only such VCAs, stay - #1649's live `S` write. - - A change of which member lanes an automated VCA reaches (a membership or nesting edit, adding or - removing an automated VCA) needs new event state for the new pairs: a carried rebuild, for the - new-memory reason (decision 14 rule 1). It narrows D15-6's live set for automated VCAs only; - root sees it at filing. -- **D6. Rendering rows.** VCA rows 1 and 2 join #1648 D1's `automation_row_renders`. A VCA - automation edit is a carried rebuild; each member pair's event state carries by `(VCA ID, row, - member strip, lane)`, as #1648 D2 carries a fader cell. -- **D7. Memory.** Per automated VCA lane one program (`32 + 32·n` bytes, A4's header and - segments); per (member lane, automated VCA lane) pair one event state (48 bytes); per event lane - whose own fader is not automated 12 bytes more (D3's word in three slots). Every term is bounded - by the session and the host's reach-pair cap; none reads the song length. -- **D8. Work.** A member lane takes at most `(1 + m)(2⌈q/64⌉ + 3)` fader events per block, `m` the - automated VCA offset lanes that reach it, and `m` more `f64` additions per event. -- **D9. The acked-batch question: can an ack ever precede a drop? No.** Curve events come from the - plan; every check of a transaction precedes the first cell write; a cell write cannot fail. - -## Deliverables - -1. D1-D4 in host-core preparation (programs, member pairs, cells and seeds), the fader stage's - events and mute terms, and the route op's follow term. -2. D5-D6 in `crates/controls` (`takes_offset`, the rendering rows) and the classifier. -3. `docs/SESSION_SCHEMA_V1.md`: VCA rows render; the composition order of D2. - -## Authorized paths - -- `crates/host-core/src/{prepare.rs,live_delta.rs}`, `crates/host-core/tests/{vca.rs,live_delta.rs}` -- `crates/builtins-compiler/src/lib.rs` (the fader stage's VCA events, the `own_db` word and the - mute terms only) -- `crates/graph/src/{lib.rs,runtime.rs}` (the route op's follow term only) -- `crates/controls/src/{automation.rs,vca.rs}` and their tests -- `crates/capi/src/runtime/live_tests.rs`, `hosts/host-web/tests/control_parity.rs`, - `crates/control-plane/tests/vca_automation_realtime.rs` (#1649's binary, extended) -- `docs/SESSION_SCHEMA_V1.md` - -## Non-goals - -- A VCA with an audio path; VCA solo. -- Any change to a session without VCA automation (D2). -- Sends' own rows (#1654). - -## Hazards - -- **Class B order.** D2's order equals ascending-ID order except in F16's degenerate cases; root - records it with F16. Gates use values on the 0.1 dB lattice. -- **Hot files.** `crates/host-core/src/prepare.rs` and `crates/builtins-compiler/src/lib.rs` are - shared with #1651-#1654; root sequences the merge. - -## Objective gates - -Commands: `cargo test --locked -p host-core --features test-support`, `cargo test --locked -p -builtins-compiler --features test-support`, `cargo test --locked -p controls`, `cargo test --locked --p graph --features test-support`, `cargo test --locked -p capi`, `cargo test --locked -p host-web ---features test-support --test control_parity`, `cargo test --locked -p control-plane --features -test-support`, `./target/release/realtime_policy`, `bash scripts/check-cross-targets.sh` (README -F19), clippy `-D warnings`, `cargo fmt --all -- --check`. - -1. **Flat equals static** (C ABI and browser). A flat VCA offset ride at `-6` dB and a flat VCA mute - at 0 render, bit for bit, the session with those static values: for a member with its own fader - ride and for a member with none. -2. **Member node time.** A linear VCA ride reaches two members, one behind the true-peak limiter. - Each member's gain at each grid completion equals `checked_fader_gain(vca_compose_db(own, S + - c(τ)))` computed on the test thread, and the latent member's events fall `L` samples later in - render time. -3. **Mute.** A VCA mute step at a sample that is not a grid sample mutes each member at its node - time over the `VcaMute` ramp; a following send of a member closes at its tap (#1652's rule); - after the ramps the output equals a static-mute plan. -4. **Hand edits.** On an automated VCA: `SET_CONTROL offset_db = +3` gives `live`, stores the - offset and, after the jump, equals a fresh plan with that offset; a hand mute ORs (closes an - open curve, leaves a closed curve closed). A hand fader move on a member with no own automation - gives `live` and one `own_db` write, no `FaderDb`. A membership edit of the automated VCA gives - `rebuild` and completes `exact`; the same edit of a VCA that is not automated gives `live`. -5. **Realtime.** The events, cell reads and writes allocate and free nothing on the render thread - (`bench_support::alloc` thread counters). -6. **No rendered bit moves** for a session without VCA automation: `audit capi` gives the same - `pcm_digest` at base and head (PR evidence); the browser legs pass with unchanged digests. - -## Test value - -- Gate 1 turns red if the VCA curve composes with other arithmetic than the static path. -- Gate 2 turns red if VCA events use the render clock or the VCA's own (non-existent) arrival - instead of each member's. -- Gate 3 turns red if a following send of a member keeps passing audio under an automated VCA mute. -- Gate 4 turns red if a hand VCA move writes the fallback, if a plain member's hand move fights the - VCA curve with a `FaderDb` record, or if an automated VCA's reach change commits live with no - event state for the new pair. - -## Dependencies - -- #1651, #1652 and #1653 (the terms word, the seven-word route cell, the follow evaluation). -- #1649 and #1650 (the offsets cell, its write and the offset field), #1648 D1 - (`automation_row_renders`), #1639 (jump rows), amended #1645 (the shape rule), #1647 - (member node time). -- Batch R2, after #1653. A complete outcome alone; not in a must-land group. - -## Ruling (root, 2026-10-14) - -- **Hand move.** A hand move of an automated VCA `offset_db` writes an offset, the same rule as - fader, trim and send gain (a level control). -- **Summation order.** Static terms are summed first, then the automated VCA curves: a class B - change under the standing summation-order ruling, recorded with F16. -- **Membership of an automated VCA.** A change to which members an automated VCA reaches is a - carried rebuild. Decision 14, rule 3 entry, for automated VCAs only: the reach change needs new - memory (an `own_db` cell word per newly reached lane and the member's curve tables), so it cannot - be a live update. Non-automated VCAs keep D15-6's live set. - -## Amendment (controls coordinator, batch 19 spec check, 2026-10-17) - -Checked against `323433f6a`. #1646-#1653 have landed (batches 17 and 18). The filed text stays; -where it disagrees, this section wins. Rulings R19-V1 and R19-V2 below are open; the rest are fixes. - -**Anchors now.** -- VCA rows: `crates/controls/src/vca.rs:14` (`OFFSET_DB`), `ROWS` `:37-73`, `write` `:111`. - `automatable`: `crates/controls/src/descriptor.rs:154`. The classifier masks the table at - `crates/host-core/src/live_delta.rs:404`; step 3 is `:422-425`. -- `controls` (`crates/controls/src/automation.rs`): `takes_offset` `:51`, `automation_row_renders` - `:400`, `mute_row` `:407`, `fader_row` `:419`, `automated_fader_lanes` `:502`, - `rendered_automation_changed` `:527`, `continued_fader_lanes` `:573`, `continued_mute_lanes` - `:586`. `effective_strip` `crates/controls/src/effective.rs:326-366` (`offsets_sum` `:207`). The - hand-offset write: `crates/controls/src/registry.rs:565` (`builtin_rows::write_fader_offset`), - `ModelView::fader_automation` `crates/controls/src/view.rs:111`, - `OwnerEdits::take_fader_automation` `crates/controls/src/owner_edits.rs:105`, - `mask_fader_offsets` `crates/controls/src/mask.rs:138`. -- builtins-compiler (`crates/builtins-compiler/src/lib.rs`): `OFFSETS_CELL_WORDS` `:719`, - `OffsetsLane` `:727`, `AutomatedFaderCell` `:753`, `AutomatedMuteCell` `:839`, - `LaneFaderAutomation` `:858` (`adopt` `:956`, `mute_terms` `:1062`, `read_offsets` `:1128`), - `offsets_jump` `:1285`, `block_jumps` `:1404`, `FaderCellCursor` `:1458`, `FaderEventDriver` - `:1709` (`new` `:1755`), `settle_adoption` `:2146`, `prepare_fader_automation` `:2618`, - `fader_automation_sizes` `:2582`, `strip_mute_cells` `:5808`. -- `automation::CellState` and `CellEvents`: `crates/automation/src/events.rs:91`, `:217`. -- host-core (`crates/host-core/src/prepare.rs`): `MAXIMUM_VCA_REACH_PAIRS` `:144`, the following - route attach `:2873-2897`, `fader_records` `:4493`, `level_records_between` `:4567`, - `StripChange` `:4637`, `join_level_records` `:4928`, `following_route_lanes` `:5081`, - `join_strips` `:5638`, `join_routes` `:5829`. -- graph: `RouteFollowFlags` `crates/graph/src/lib.rs:1146`; `LiveFollowRoute` - `crates/graph/src/runtime.rs:1287`, `FollowCursors` `:1442`, `follow_walk` `:1649`. -- B17-R14's `automates`: `crates/control-plane/src/control.rs:2651`. - -**How the slice maps onto the landed code.** -- **Event lanes.** A member lane is a fader event lane when its fader is automated or an automated - VCA `offset_db` lane reaches it, and a mute event lane when its mute is automated or an automated - VCA `mute` lane reaches it. Two new `controls` predicates give them (one rule for preparation, - the classifier, the join and the route attach). The own-entry readers of #1651 T2 - (`fader_automation`, `model_fader_offsets`, `mask_fader_offsets`, the listing flags) stay - own-entry only: a member's own fader is not automated by a VCA ride. -- **Program.** The VCA cells join the plan's one program after the mute cells, so a session - without VCA automation builds the same program. One cell per automated VCA lane, shared by - every member. `prepare_fader_automation`'s early return already reads `automation_row_renders`. -- **Slot.** An event lane gets a `LaneFaderAutomation` slot and an offsets cell, also when its own - fader is not automated. Its channel's own source is the cell (own automation) or `own_db`. -- **Driver.** The one `FaderEventDriver` (bank widths, fused pair fallback, scalar oracle: R17-4). - Same `a(n)` and span (R17-2, R17-3); no new node-time context. `block_jumps` also resolves - `LiveRamps::for_row(VcaOffset)` and `for_row(VcaMute)` (equal to the fader and mute lengths - today; resolved by row, not assumed). -- **Mute.** `terms` is already `EffectiveStrip::mute` (static mute, every reaching VCA's static - mute, solo term). So D4 needs no new record and no classifier change: the `Mute` record is the - terms write, and `mute_terms` (the drain branch) composes it with the lane's composed curve. -- **Route.** `following_route_lanes` (the one predicate of the attach and of #1653's record) widens - to mute event lanes. `RouteFollowFlags` keeps its six bits; `automated[lane]` now means "the - lane has a mute curve, own or VCA". No spare bit is used (#1654 keeps bits 6 and up). The route - op evaluates the reaching VCA mute cells at `τ_tap` with its own cursors (`FollowCursors`, - `follow_walk`). `route_mix_column_ramps_block` and the per-column ramps (R18-5) do not change. - #1653's pre-write checks hold: plan and record read the same predicate. -- **FP (R17-7).** The `f64` sums of D2 run on render inside the render entry's `CanonicalFpEnv`; - the seed runs inside preparation's guard; the classifier's `S` inside the commit guard. -- **Classifier.** VCA rows 1 and 2 join `automation_row_renders`, so a VCA entry change is - `Err(LiveRebuild::Automation)` at step 3. The reach rule (D5 last bullet, root's ruling) goes in - `controls::rendered_automation_changed`: it also returns `true` when the `(member strip, lane)` - set that an automated VCA lane reaches differs (membership, nesting, an added or removed VCA). - Step 3's call in `live_delta.rs` does not change (docs only). The fast path (`classify_rows`) - takes `SetControl` only, which cannot change a reach, so it needs no change. -- **Hand edits (D5).** `takes_offset` gains VCA `offset_db`. As for the fader (#1650 D3, D4, - R18-2): `registry::write` sends a hand move on an automated VCA lane to the entry's offset; - `ModelView` and `OwnerEdits` read and take the VCA entry for the in-place edit (R17-1); - the VCA row's value is the offset and the list marks it `automated` (field 35); `mask_values` - copies the entry's offset and leaves the automated lane's static offset (its fallback) as - structure. Without these a write and a read of the same row disagree. -- **`S` (D2).** `effective_strip` puts, on a fader event lane: the own hand offset, then the static - offsets of the reaching VCA lanes that are not automated, then the hand offsets of the automated - ones, each group in ascending VCA ID. With no automated VCA lane this is today's sum, bit for bit. -- **Records (D3).** `level_records_between` and `fader_records` take the fader event lanes as - `automated` (no `FaderDb` there). The offsets record of an event lane carries `own_db` from - `StripChange::own_after` (no new `EffectiveStrip` field). - -**Fixes.** -- **D3 and D7 bytes.** `LatestCell` is const-generic. A slot of three words and a slot of four - words are both 32 bytes (two `AtomicU64` set the alignment), so a four-word cell costs nothing - more. Every offsets cell becomes four words (`OFFSETS_CELL_WORDS = 4`; the `own_db` word is - unread on a lane whose fader is automated). One cell shape, one reader. No edit under - `crates/engine/src/realtime/`. `offsets_cell_bytes` stays 120 (x86-64); the gate measures it. -- **D7 terms.** D7 leaves out that an event lane with no own automation also takes a slot - (`lane_slot_bytes`) and an offsets cell. The per-pair term depends on R19-V1. All terms are - bank payload, charged by `fader_automation_sizes()` and checked as ceilings, never literals. -- **D6 carry.** A carried channel continues verbatim when its own table and each reaching automated - VCA table (in ascending VCA ID) are the same set, bit for bit, and `a(n)` did not move; else it - repositions. This is `continued_cells` (bits 0-3) from the widened `continued_fader_lanes` and - `continued_mute_lanes`; graph passes the byte unchanged. No per-pair carry key is needed, because - a repositioned channel places each cursor again. A lost event lane is released by the join, as - today (`join_level_records`, widened to event lanes). -- **Reposition and #1711 (R18-4 withdrawn).** A VCA offset table edit repositions every reached - member channel. Its landing can cut a live move in flight (a member's `own_db` move, a VCA hand - move), as a fader table edit does. So: land #1711 first and use its deferred jump for every - repositioned channel; a VCA case joins its G1 here (gate 7 below). If #1711 does not land, the - B17-R14 flag stays and `automates` (`control.rs:2651`) widens to VCA `offset_db` entries, so - a VCA-only session's table edit is flagged, never `EXACT` when wrong. A VCA mute table edit - lands only a `SetMute` (the stage keeps `fader_gain`, #1651 T4); U1 fold 1's rule must hold - for the composed mute: a terms record at the swap lands over its own ramp word (gate 8). - -**R19-V1 (ruling needed): how a member channel holds and walks its curves.** D1's per-pair state -(one `CellState` per member and VCA lane) cannot hold a correct composition. Each `CellState` keeps -its own ramp in flight and its own `held` rule, but the stage has one ramp. A member grid event -inside a VCA jump retargets the stage over 64 samples, so the VCA step ends in 64 samples, not in -`J` (and the reverse). Also the driver's arrays are fixed (`MAXIMUM_FADER_CELLS` on the render -stack), and `m` has no compiled bound. -- (a) D1 as filed: per-pair `CellState`, events merged by offset. Small. A VCA step is shortened - whenever the member's own ride moves. Not correct. -- (b) One composite cell per member channel: one `CellState` over the composed curve - `vca_compose_db(own(t), S + c_1(t) + … + c_m(t))` (for mute: `own || v_1 || … || v_m`). - Events at the union of the curves' discontinuities and the shared node-time grid; target, held - and lagging rules on the composed value. A new K-curve event source in `crates/automation` - (`events.rs`, render code, not under `crates/engine/src/realtime/`): per pair one discontinuity - cursor and a cell index (8 bytes) in a boxed slice of the slot, allocated at preparation. With - `m = 0` the driver keeps today's `CellEvents` path, so no fader-only or mute-only bit moves. The - route op uses the same source per lane (OR). D8 holds (events at most the sum of the curves'). - About 8 h more than (a). -- (c) Split: this slice renders VCA `mute` (D4, the reach rule, D5's mute bullet, D6 for row 2); - VCA `offset_db` (D2, D3, D5's offset bullet) moves to a phase-2 successor with (b). -- Recommendation: (b). Fall back to (c) if the offset tranche fails its second fix round. - -**R19-V2 (ruling needed): the reach-pair memory.** The host cap is 16,384 (strip, VCA) pairs -(`MAXIMUM_VCA_REACH_PAIRS`), per lane and per row. Under (b) that is at most about 512 KiB of pair -state per plan; under (a) about 3 MiB. Options: (i) charge it as bank payload under the existing -caps (no new cap); (ii) add a host cap on automated pairs. Recommendation: (i) with (b). - -**Gates (fixes; both hosts and both bank widths).** -- Gates 1-3: both hosts in one binary, `hosts/host-web/tests/control_parity.rs`, at quanta 128, - 100 and 37. The same scripts in `crates/host-core/tests/stored_vca_automation.rs` (new) at - `Backend::Simd8` (`cfg(target_feature = "avx2")`) and `Backend::Simd4`, split and fused, live and - between render calls, and on the scalar oracle; bits equal. Gate 1 adds two VCAs on one member - (one automated, one static) and a member with its own ride. Gate 3 adds a member with no own - mute entry (its send becomes seven-word) and a nested VCA. -- Gate 4 reads: a hand fader move on a member with no own automation gives `live`, one offsets - write carrying `own_db` and no `FaderDb`. Add: the VCA row lists the offset as its value and is - marked `automated`; a replace that changes only an automated VCA lane's static offset is - `rebuild`. In `crates/host-core/tests/live_delta.rs` (run with - `--features host-core/test-support,host-core/control-provider`) and both hosts. -- Gate 5: `crates/control-plane/tests/vca_automation_realtime.rs` gains the curve, a hand VCA move, - a hand member move and a VCA mute, each block `allocations == 0 && frees == 0`. -- Gate 7 (new, Reposition): a VCA ride table edit while a member's hand move (and a VCA hand move) - ramps, heard 1, 12 and 26 blocks: with #1711, every block after the swap equals the - uninterrupted engine and the watermark reports `EXACT`; without #1711, the flag. Both hosts, - both widths. -- Gate 8 (new, carry): a VCA mute table edit with a hand VCA mute in the same transaction lands the - hand mute over its own ramp; added, removed and changed VCA entries each rebuild and carry the - member stage; the swap block allocates nothing. Bank widths (the oracle restarts at a swap, - #1651 T4 departure). -- Gate 6 command: `audit capi` `pcm_digest` stays `bffc21e857b5e134`. Add `cargo test --locked -p - automation --all-features`; `cargo build --locked --release -p realtime-policy && - ./target/release/realtime_policy` (the script is deleted). - -**Authorized paths after the check (this list replaces the filed one).** (R) marks render code. -None is under `crates/engine/src/realtime/`, so no root notice is needed. -- (R) `crates/automation/src/{events.rs,lib.rs}`, `crates/automation/tests/events.rs` (only under - R19-V1 (b); a path extension root approves with the ruling) -- (R) `crates/builtins-compiler/src/lib.rs` (VCA cells, event lanes, slot, offsets cell shape, - driver, drain, adopt, export to host-core) -- (R) `crates/graph/src/runtime.rs` (`FollowCursors`, `follow_walk`, the follow route's carry - only), `crates/graph/src/lib.rs` (`GraphFollowRouteSource`'s cells, docs) -- `crates/controls/src/{automation.rs,vca.rs,effective.rs,registry.rs,view.rs,owner_edits.rs,mask.rs,lib.rs}`, - `crates/controls/tests/{automation.rs,mask_and_diff.rs}` -- `crates/host-core/src/prepare.rs` (event-lane predicates, follow attach, level records, strip - and route joins), `crates/host-core/src/live_delta.rs` (docs only) -- `crates/host-core/tests/{vca.rs,live_delta.rs,stored_vca_automation.rs,route_mute.rs}` -- `crates/capi/src/runtime/live_tests.rs`, `hosts/host-web/tests/control_parity.rs`, - `crates/control-plane/tests/vca_automation_realtime.rs` -- `crates/control-plane/src/control.rs` (only if #1711 does not land: `automates`) -- `docs/SESSION_SCHEMA_V1.md` (`:339-340` the offset rows; the VCA rows render; D2's order), - `docs/REALTIME_MEMORY.md` (the pair term) -- No SDK regeneration: no row class or table changes. - -**Edit order with the batch.** #1711 first (driver adoption, `events.rs`, `control.rs`), then #1654 -(`following_route_lanes`, the route op, `takes_offset` and its test), then this slice. -Program cell order: fader cells, mute cells, #1654's route cells, then the VCA cells (VCA ID, -row, lane), so each earlier session builds its program cell for cell. If #1654 generalizes -`follow_walk` to a table of cursors, this slice adds the VCA mute cursors to that table and -writes no second walk. - -## Rulings (root, batch 19, 2026-10-17) - -These override the batch 19 spec-check amendment where they differ. - -- **R19-2 = (b).** One composed `CellState` per output (a member fader channel, a member mute channel) - over all its curves: the merged walk of `crates/automation/src/compose.rs` (K1, built in batch 19a), - sum for offsets, OR for mutes. The route op adds the VCA mute curves as more cursors in #1654's route - table, ORed into the follow term; no second walk. Fallback (c) if U3 fails its second fix round: VCA - mute ships, VCA offset automation goes to phase 2; root is told. -- **R19-4.** Events of several curves at one offset take the longest jump among them. Gate it. -- **R19-6.** Reach-pair memory is charged as bank payload under the existing caps; no new host cap. A - gate at the cap: one pair over is refused with a typed diagnostic, not a panic. -- **Two PRs.** This issue ships alone in batch 19b, on top of 19a (#1711, K1, #1654). Edit order: every - shared file after 19a. - -## Decision record: V0 (batch 19) - -`crates/controls` only. No render, host-core, graph or builtins-compiler code; no row class or -table change, so no SDK regeneration. - -- **Rows (D6).** `automation_row_renders` admits VCA row 1 (`vca_offset_row`) and row 2 - (`vca_mute_row`). `fader_row` is unchanged, so a VCA entry reaches no fader-only reader. New - accessors: `vca_offset_automation`, `vca_mute_automation` (per lane, first entry in table order; - the strip readers now share one `owner_row_automation` walk). -- **Offset (D5).** `takes_offset` = `fader_row || route_gain_row || vca_offset_row`. The VCA row is - per lane, so `offset_faults` judges each driven lane against the row's own domain - (`[-144, 24]` dB, the fader domain); a lane the entry does not drive holds `+0.0`; the mute row - takes no offset. -- **Owner rule (R17-1).** `registry::write` on an automated VCA `offset_db` lane writes that lane's - entry offset (`vca::write_offset`) and the static offset only on lanes no entry drives; - `vca::resolve` and the list read the offset as the row's value; both VCA rows list - `automated = true` on a driven lane (the `mute` row keeps its static hand mute as its value). - `OwnerEdits::set` on VCA row 1 clones the driving entries; `ModelView::vca_automation` reads the - edited entry. A member's own rows are not marked by a VCA ride. -- **Mask.** `mask_fader_offsets` copies VCA-offset entry offsets (through `takes_offset`); - `mask_values` keeps `into`'s static offset on each automated VCA lane (fallback = structure), - and copies the rest of the VCA list (other lanes, mutes, members) as before. -- **Event lanes.** `fader_event_lanes(model, strip, reach)` and `mute_event_lanes(model, strip, - reach)`: own entry on the row, or a reaching VCA with an entry on the matching VCA row and lane. - `reach` is the caller's VCA reach of the strip (`vca_reach` / `vca_reach_of`), so preparation and - the classifier, which already hold it, build no second table. The own-entry readers - (`fader_automation`, `model_fader_offsets`, `mask_fader_offsets`, the listing flags) stay - own-entry only. -- **Reach rule.** `rendered_automation_changed` also returns `true` when, with equal rendered - entries, the strip IDs reached by some VCA that a rendered entry drives differ (inverse of - `SessionModel::vca_reach`, which reads a model in any strip order). No table is built when no - rendered entry is on a VCA row. -- **`S` (D2).** `EffectiveInputs` gains `vca_offset(vca)` (the driving entries' hand offsets per - lane; `ModelInputs` precomputes it, `LocalInputs` reads the view). `effective_strip` sums: own - hand offset, then static offsets of reaching VCAs whose lane is not automated, then hand offsets - of those whose lane is, each in ascending VCA ID. With no VCA offset entry the second pass does - not run and `S` is today's sum. `fader_db` stays the static composition (a lane an automated VCA - reaches is a fader event lane, which takes no `FaderDb` record). Departure: none from the text; - `EffectiveInputs` is a public trait with no implementor outside the crate. -- **Not in V0** (render tranche): `continued_fader_lanes` / `continued_mute_lanes` are not widened - to the reaching VCA tables (D6 carry). - -Gates (`crates/controls/tests/automation.rs`): `vca_rows_render_and_take_a_per_lane_offset`, -`a_hand_move_on_an_automated_vca_offset_writes_its_offset` (whole and in-place paths, list flags, -`S` on both effective paths), `the_mask_copies_a_vca_offset_and_keeps_its_fallback`, -`event_lanes_take_the_own_entry_and_every_reaching_vca_ride` (lanes, rows, nesting, own-entry -readers), `a_reach_change_of_an_automated_vca_rebuilds` (members added or removed, nesting, a -non-automated VCA, a hand offset, the mute row), `the_offset_sum_puts_the_static_vca_offsets_first` -(values `1, 2^-60, -1` that tell the two orders apart; no VCA offset automation and a VCA mute -entry alone keep every bit of `S` and `fader_db`). - -Commands: `cargo test --locked -p controls` (all green), `cargo clippy --locked -p controls ---all-targets --all-features -- -D warnings`, `cargo check --locked -p controls --all-targets`, -`cargo test --locked -p host-core --features test-support` (467 passed), -`--features test-support,control-provider` (587 passed), `cargo test --locked -p control-plane ---features test-support` (28 passed). No red: no host-core or control-plane test edits a VCA -automation entry or an automated VCA's reach today. - -For V1: a VCA automation edit and an automated VCA's reach change are now step 3 `Automation` -(rebuild) in the classifier; a session whose only rendered entries are VCA entries now enters -`prepare_fader_automation` (it builds no VCA cell yet; tests green); a hand move on an automated -VCA offset diffs as a VCA row 1 change whose value is the offset and moves `S` of each reached -lane, which host-core lowers only on lanes that have an offsets cell until V1 gives every event -lane one. - -## Decision record: V2 (batch 19) - -The fader stage (`crates/builtins-compiler/src/lib.rs`), one additive rule in K1 -(`crates/automation/src/compose.rs`), the carry predicates (`crates/controls/src/automation.rs`). -No edit under `crates/engine/src/realtime/`; no row class or table change (no SDK regeneration). - -- **Program (D1).** The VCA cells follow every route cell, in ascending VCA ID, then row - (`offset_db` with `FADER_CELL_RULE`, `mute` with `MUTE_CELL_RULE`), then lane; one cell per - automated VCA lane, shared by its members (refusal code `host.automation.vca`). The reach is read - only when an entry drives a VCA row, so a session without VCA automation builds its program cell - for cell. Export for V3/V4: `PreparedBuiltinsSession::vca_cells(vca_id) -> Option<(Arc, - VcaCells)>` (`[row][lane]`). `strip_mute_cells` stays own-entry cells (a lane only a VCA mute - reaches reads `None` there). -- **Event lanes (D2, D4, R19-2 = (b)).** Preparation reads `controls::fader_event_lanes` and - `mute_event_lanes`. A lane no automated VCA curve reaches keeps `FaderCurve::Cell` / - `MuteCurve::Cell`: the same `CellEvents` path and bits as before. A reached lane is - `FaderCurve::Composed` (one `automation::Composed` over its own cell, when automated, then - the VCA cells in ascending VCA ID) or `MuteCurve::Composed` (`Composed`, gate cursors, value - `own || v_1 || ... || v_m`; the stage mute is that `|| terms`). The composed value is the fader dB - `vca_compose_db(own, S + c_1 + ... + c_m)` (`composed_db`: `f64` fold, `S` first, inside the - render entry's `CanonicalFpEnv`; the prepared value inside preparation's), `own` the own curve or - `own_db`; the stage gain is `checked_fader_gain` of it. -- **One driver (R17-4).** `FaderEventDriver` (bank, fused pair fallback, scalar oracle) holds a - `ChannelEvents::{Cell, Composed}` per channel: each piece begins the walk (`Composed::start`), the - cut at `E` is a one-sample `Ramp` landing on every cursor (longest jump, R19-4), and the held-ramp - obligation, the R17-10 release, the T3b re-hold release, the held-stage offsets jump and the free - offsets jump act on every cursor (`FaderComposite::each_state`). `settle_adoption` is generic over - `AdoptedCurve` (`CellAdoption`, `ComposedAdoption`): one adoption rule (F0 ramp target, the - deferred jump at the ramp's first free sample, grid hold, clears) for both shapes. - `block_jumps` resolves `LiveRamps::for_row(VcaOffset)` and `for_row(VcaMute)` (slot field - `vca_jumps`); a VCA cursor takes its row's length, the own cursor the fader or mute length. -- **D3.** `OFFSETS_CELL_WORDS = 4`. `FaderOffsetsRecord` gains `own_db: Option` (word 3, a - quiet NaN for `None`); a composed lane whose own fader is not automated takes `Some(own_db)` and - jumps at the block entry only when the gain bits change; `None` keeps it. -- **Carry (D6).** `continued_fader_lanes` / `continued_mute_lanes` now continue a lane only when its - own table (absent in both, or the same) and each reaching automated VCA table (same VCAs, ascending - ID, same segments) continue; with no VCA row automated the rule is the old one. `FaderCurve::carry` - / `MuteCurve::carry`: same shape copies the cell state, or the composed output's state and target - and, with one layout, the cursors; a changed shape takes the cell's ramp or starts fresh (always a - reposition). A composed reposition places every cursor afresh (no target, hears its curve at the - block start), so a deferred jump or a landing reaches every curve. `own_db` is carried like `S` - (`adopted_own_db`, the `Compare` backstop). -- **Memory (R19-6).** Each composed channel charges its boxed curve (`fader_composite_bytes` 128, - `mute_composite_bytes` 112) and its cursor table (`cursor_bytes` 104 per pair) as bank payload and - in the scalar owner row (`FaderAutomationPlan::composed_bytes`), under the existing caps; the - offsets-writer charge widens to fader event lanes. The cap gate is V5's. - -**Departures.** -1. K1 additive rule `Composed::with_held_grid` (opt-in; the route keeps R19-3). Without it a grid - event of one curve retargets the composed lane inside another curve's jump (the R19-V1 (a) - defect: the step ends at the next grid point, not at `J`). With it each retarget reports its ramp - to every value cursor whose own ramp ends earlier, and the cell's own grid rule holds or restarts - it. Also `clear_target`, `output_state`, `set_output` (carry). Test - `compose::tests::a_step_keeps_its_jump_length_while_another_curve_rides_on_a_held_grid_output`. -2. An offsets record on a composed lane takes the single cell's rule (seed, clear the deferral, - every cursor holds to the jump's completion), not `Composed::retarget` (deferral then each - cursor's own `J`), so a composed lane moves as one cell does in holds and deferrals. -3. `own_db` is `Option`: today's host-core record sites write `None` (mechanical, correct for - every lane that has a cell today); V3 writes `Some` on static-own event lanes. -4. `LaneFaderAutomation::new` returns `Result` (the cursor table allocates); bind - (`bound_lane_automation`) panics only on an allocation failure, as a `Box::new` there does. - -**Gates** (`cargo test --locked -p builtins-compiler --all-features`, Simd8 under AVX2 and Simd4): -`a_flat_vca_ride_renders_the_static_vca_offset_bits` (flat ride `-6` dB and flat mute `0` equal the -static VCA bits over 40 blocks, a member with and without its own ride, fallback `+3` unused), -`a_vca_ride_composes_with_each_members_own_value_at_every_grid_event`, -`a_step_inside_another_curves_ride_keeps_its_jump_length` (both ways; red without `with_held_grid`: -`(1024, Grid)` after the step), `coincident_member_and_vca_events_take_the_longest_jump` (fader and -mute, 480/960 both ways), `a_session_without_vca_offset_automation_keeps_its_lone_fader_cells`, -`a_carried_composed_lane_renders_the_uninterrupted_bits` (continued and repositioned, swap inside a -grid ramp and inside a VCA step's jump; red without the cursor carry). `controls`: -`a_member_lane_continues_only_while_its_reaching_vca_tables_do`. - -Commands: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` and no-feature -`cargo check --locked --all-targets` on automation, builtins, builtins-compiler, host-core, controls; -tests automation (all features), builtins (all features), builtins-compiler (all features, 76 lib), -controls, host-core `--features test-support` `stored_fader_automation` (36), `stored_mute_automation` -(13), `successor_swap` (140): all green. Realtime policy: 172 regions in 35 files (171 before: the -composed-curve impls). Sizes (x86-64, measured): `Option` 392 -> 416 B -(`vca_jumps`), `offsets_cell_bytes` 120 (unchanged), `ComposeCursor` 104. - -For V3: write `own_db = Some(static fader dB)` on fader event lanes with no own automation; take the -fader event lanes as `automated` in `level_records_between` / `fader_records` (no `FaderDb` must reach -a composed lane: the drain still applies a `FaderDb` to the stage); `has_offsets` is true on every -fader event lane. For V4: a source lane's VCA mute cells are its reach (ascending VCA ID) mapped -through `vca_cells(..)[1][lane]`, jump row `VcaMute`. - -## Decision record: V3 (batch 19) - -host-core preparation and the classifier (`crates/host-core/src/{prepare.rs,live_delta.rs}`). No -render code, no edit under `crates/engine/src/realtime/`, no `control.rs` edit (#1711 landed in -19a, so `automates` stays), no row class or table change (no SDK regeneration). - -- **Records (D3).** `level_records_between` takes the strip's own faders before and after and a - `FaderEventLanes` (`controls::fader_event_lanes` as `events`, `automated_fader_lanes` as `own`). - `fader_records` reads `events` as its automated lanes, so no `FaderDb` reaches an event lane. An - event lane gets one offsets write when its `S` moves or, on a lane whose own fader is not - automated, when its static own dB moves; that write carries `own_db = Some(own after)` (else - `None`) and the lane's fader length (`LevelRules::lengths`: `resolve(Fader, edit)` for a hand - move). Dense diff, local (fast-path) diff and the strip join all call it with the reach they hold. -- **Clamp.** `level_differs` also compares the own static fader dB: at the effective clamp a hand - move leaves `fader_db` and `S` as they were but a composed lane hears it. On any other lane the - extra "moved" yields no record, so no non-VCA-automation answer changes. -- **VCA hand offset (D5).** `LevelRules::vca_changed` (offset) also compares the driving entry's - hand offset (`vca_offset_moved`: `vca_offset_automation(current)` against - `ModelView::vca_automation(next)`), so members' writes take the VCA edit's length, not the - session's. `S` itself is V0's (`effective_strip`). -- **VCA mute (D4, D5).** No new record: the effective mute (`terms`) change is the `Mute` record, - which V2's composed mute lane takes as `terms`. -- **Reach rule.** No classifier change: V0's `rendered_automation_changed` gives step 3 `Automation`; - step 3's doc now names the VCA rows and the reach rule. -- **Join (D6).** `join_level_records` releases lanes that stop being fader / mute event lanes - (`fader_event_lanes` / `mute_event_lanes` over `reach_before` and `reach`). The carry byte is V2's - widened `continued_*_lanes`, unchanged here. -- **Slot.** V2 already gives every fader event lane its `PreparedFaderCell` and offsets cell - (`attach_offsets` over `cells.fader`); V3 only gates it. -- **Route (D4).** `following_route_lanes` widens `lanes` to `mute_event_lanes` (reach read through - `vca_reach_of` only when an entry drives a VCA `mute` row and an own lane is not automated), so the - attach, #1653's records, the local route scan and `join_routes` read one predicate. The attach - checks `own cell || VCA cells` per lane against it, with the new - `follow_vca_mute_cells(vca_cells, model, source, program) -> Option<[Vec; 2]>`: per source - lane, `PreparedBuiltinsSession::vca_cells(vca)[1][lane]` of each reaching automated VCA mute, in - ascending VCA ID, `None` on a plan/model mismatch. - -**Departure / interim.** Until V4, a following send whose source lane only a VCA mute reaches has -`automated[lane] = true` and no own cell, so `GraphFollowRouteSource::new` refuses it and preparation -fails with `host.route_control.attach` (typed, no panic). V4 passes `follow_vca_mute_cells`' lists -to the source (graph) and relaxes `new`'s `flags.automated == cells` check to `own || VCA`. A lane -with its own mute cell and a VCA mute prepares, and until V4 its send follows only the own curve. - -**Gates** (`crates/host-core/tests/stored_vca_automation.rs`, renders on Simd8 under AVX2, Simd4 and -the scalar oracle, live and between render calls, tail bits equal to a fresh plan, the edit heard, -render allocations and frees 0): `every_fader_event_lane_has_an_offsets_cell` (test-support); -under `control-provider`, full and fast path equal: -`a_hand_move_on_a_member_only_a_vca_ride_reaches_writes_its_own_db`, -`a_hand_move_on_an_automated_vca_offset_rewrites_every_reached_lanes_s`, -`a_hand_mute_of_an_automated_vca_writes_each_reached_lanes_terms`, -`a_reach_change_of_an_automated_vca_rebuilds_and_of_a_plain_vca_is_live`, -`a_hand_move_hidden_by_the_static_clamp_still_writes_own_db`. - -Commands: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` and no-feature -`cargo check --locked --all-targets` on host-core, control-plane, builtins-compiler; tests host-core -`--features test-support` (477 passed) and `test-support,control-provider` (605), control-plane -`--features test-support`, builtins-compiler, capi lib (218): all green. Realtime policy: 172 -regions in 35 files (unchanged). - -For V4: read `follow_vca_mute_cells` in the attach loop (`prepare.rs`, the `follow_cells` push) and -pass its lists into `GraphFollowRouteSource`; jump row `VcaMute`. For V5: host parity, the cap -gate (R19-6), gates 1-3 and 7-8 on both hosts. - -## Decision record: V4 (batch 19) - -The route op's VCA mute follow term (`crates/graph/src/{lib.rs,runtime.rs}`) and the attach in -`crates/host-core/src/prepare.rs` (the `follow_cells` loop and `follow_route_jumps`). No edit under -`crates/engine/src/realtime/`, in `crates/automation` or in any row class or table (no SDK -regeneration). - -- **Source (D4).** `GraphFollowRouteSource::new` takes `vca_mutes: [Vec; 2]` (per source lane, - V3's `follow_vca_mute_cells`, ascending VCA ID; stored boxed) and checks `flags.automated[lane] == - own cell || VCA cells`, every cell in the program. This closes V3's interim refusal: a send whose - lane only a VCA mute reaches now prepares. `with_continued_vca_mutes([bool; 2])` (host-core: - `controls::continued_mute_lanes` of the successor) marks lanes whose VCA mute tables continue. - When the source strip has no own mute cell and the route no row cell, the attach takes the VCA - cells' program (`follow_vca_mute_cells` now takes `&mut Option>`). -- **Jump row (data).** `GraphRouteCursorRow::VcaMute` and `GraphRouteJumpLengths::vca_mute`; - host-core's rule reads `LiveRamps::for_row(VcaMute)`. -- **Walk.** The VCA mute cursors are `Gate` cursors appended after the five role cursors in each - column's one K1 table (`ColumnRoles { fixed, vca_start, vca_count }`); no second walk. The follow - term is `follows_mute && (own curve || v_1 || ... || v_m || terms)` in `column_target`, in the - tap's node time. Because they are gates, a VCA mute fade holds the column's value events and - lands them at its end (R19-3, K1's deferral). The table (and its probe twin) is boxed, allocated - at attach, charged by `Composed::bytes(K)`; the source's lists are added to the producer's - `cursor_bytes`. -- **Carry (D6, U2 folds).** A lane's VCA mute cursors carry verbatim by position (`Continued`) when - the lane continues and `a(tap)` did not move; else they start fresh and are placed at the first - block like the source mute. At the first block the follow curves are one gate: its OR is - compared with the OR the predecessor heard (with the own mute alone this is the old per-cursor - comparison, so no bit moves). A gate move takes the longest own length among the curves that - close the column (for an open, among all follow curves; which were closed is not kept across a - carry), R19-4. A follow curve is "dropped" (#1654 U2 fold 1 MAJOR-3) only when no own or VCA mute - curve is left; a dropped VCA mute lands at once over `VcaMute` at the carry (B19-R7). A seven -> six carry of - a predecessor that heard VCA curves reopens over `max(mute, VcaMute)`. - -**Departure.** In `place_carried` a moved gate now retargets over its own row's length (was the -mute length for every gate; equal for the source and route mutes, so no existing bit moves). - -Gates (`cargo test --locked -p graph --features test-support`, 149 lib tests green), module -`runtime::send_automation_tests`, `VcaMute` = 320 against the mute rule's 200: -`a_vca_mute_step_closes_a_following_send_at_its_tap_time` (a lane only a VCA mute reaches, `a(tap)` -0 and 100: the close lands at render sample `1,000 + a(tap)` over 320), -`a_vca_mute_ors_with_the_source_lanes_own_mute`, `a_vca_unmute_fade_holds_the_columns_value_events_to_its_end` -(R19-3), `a_carried_vca_mute_renders_the_twins_bits_continued_or_repositioned` (red with the -continued cursor's state dropped: block 21), `a_moved_vca_mute_table_lands_over_its_own_length_at_the_first_sample` -(both ways, the OR with a closed own mute, a removed VCA mute); `follow_route_tests`: the source -accepts VCA-only lanes and refuses a VCA cell outside the program and disagreeing flags. - -Commands: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` on graph, host-core, -automation; `cargo check --locked --all-targets` (workspace, no features); tests automation, graph -`--features test-support`, host-core `--features test-support` (all, incl. `route_mute` 26, -`stored_vca_automation`; with `control-provider` 6), capi lib (220), host-web `--features -test-support --test control_parity` (44): all green. Realtime policy: 172 regions in 35 files -(unchanged). `scripts/check-graph-policy.sh`: PASS. Sizes (x86-64, measured): -`LIVE_FOLLOW_ROUTE_OWNER_BYTES` 1,072 -> 1,120, `GraphFollowRouteSource` 120 -> 152, -`GraphFollowRouteControlBinding` 808 -> 872, `GraphFollowRouteControlProducer` 104; charges read -`size_of`. - -For V5: host gates for gate 3 (a following send of a member closes at its tap under a VCA mute, -both hosts, latent tap), a member with no own mute entry, a nested VCA, the carry (continued and -moved VCA mute tables) and the reach-pair cap gate (R19-6). - -## Decision record: V5 (batch 19) - -The objective gates and the doc. Tests and `docs/SESSION_SCHEMA_V1.md` only: no product code, no -edit under `crates/engine/src/realtime/`, no row class or table change (no SDK regeneration). The -gates found no defect. - -- **Hosts.** Every both-host gate is in `hosts/host-web/tests/control_parity.rs`, which drives the - C ABI's exported `miso_engine_v1_*` entries and the browser host in one binary (the amendment's - "both hosts in one binary"); `crates/capi/src/runtime/live_tests.rs` gains nothing (departure - from the brief's file list, not from the gates). Parity runs at quantum 128 and the native width; - widths and quanta are host-core's. -- **Widths.** `crates/host-core/tests/stored_vca_automation.rs` runs Simd8 (when `avx2`), Simd4 and - the scalar oracle (`backends()`), live and between render calls; the carry gates (8) run the bank - widths only (the oracle restarts at a swap, #1651 T4). - -Gates (test names; all green): -1. Flat equals static: host-core `a_flat_vca_ride_and_mute_render_the_static_vca_bits` (quanta 128, - 100, 37; a member with its own ride, one with none and in a second static VCA; fallback +3 - unused); both hosts `a_flat_vca_ride_and_mute_render_the_static_vca_bits_on_both_hosts`. -2. Member node time: host-core `a_vca_ride_reaches_each_member_in_its_own_node_time` (each grid - event's gain equals `checked_fader_gain(vca_compose_db(own, S + c(τ)))` computed on the test - thread; the latent member's events `L` later); both hosts - `a_linear_vca_ride_renders_alike_on_both_hosts` (alike and heard). -3. Mute: host-core `a_vca_mute_step_mutes_each_member_at_its_node_time` (off-grid step at 1,000; - `SetMute` at `1,000` and `1,000 + L` over `VcaMute`; a member with no own mute entry, one with, - a nested VCA; static-mute and open plans after the ramps); both hosts - `a_vca_mute_curve_closes_a_following_send_at_its_latent_tap_on_both_hosts` (also the V4 note: the - send follows a lane only a VCA mute reaches, through a nested VCA, behind a latent tap). -4. Hand edits: both hosts `hand_edits_of_an_automated_vca_commit_as_d5_says_on_both_hosts` (offset - +3 live and heard; hand mute over an open curve heard, over a closed one no bit moves; a hand - move of a member only the ride reaches live and heard; membership edit of the automated VCA - `rebuild` and `EXACT`, no fallback; of a plain VCA `live`) and - `a_hand_move_on_an_automated_vca_offset_is_in_place_as_the_full_path_on_both_hosts` (R17-1; the - document stores the offset). The `own_db` record and "no `FaderDb`" stay V3's classifier gates. -5. Realtime: `crates/control-plane/tests/vca_automation_realtime.rs` - `a_vca_curve_and_hand_edits_over_it_render_allocation_free` (curve, hand VCA move, hand member - move, hand VCA mute and unmute; every block `allocations == 0 && frees == 0`). -6. No bit moves: `./target/release/audit capi` `pcm_digest` = `bffc21e857b5e134`. -7. Reposition: both hosts - `a_vca_ride_table_edit_while_a_level_move_ramps_completes_exactly_on_both_hosts` (a member's - `own_db` move and a VCA hand move of 4,000 samples, heard 1, 12, 26 blocks; a table change after - 1,000,000 repositions every member; each host equals its own twin without the rebuild; `EXACT`). -8. Carry: host-core `a_hand_vca_mute_in_a_vca_mute_curve_rebuild_lands_over_its_own_ramp` (ramp - `M`, 0, 2,000) and `added_removed_and_changed_vca_mute_entries_carry_the_member_stage` (swap - block allocation-free, strip carried); both hosts - `a_hand_vca_mute_in_a_vca_mute_curve_rebuild_lands_over_its_own_ramp_on_both_hosts`. -- R19-4: host-core `coincident_member_and_vca_events_take_one_longest_jump` (one fader jump over - `J`, one `SetMute` over `M`; bits of one curve making the sum). At host level the `VcaOffset` and - `VcaMute` lengths equal the fader and mute lengths, so "longest" is V2's - `coincident_member_and_vca_events_take_the_longest_jump` (480/960) at the stage. -- R19-6: both hosts `the_reach_pair_cap_bounds_vca_automation_alike_on_both_hosts` (256 nested VCAs, - the outermost automated: 64 tracks, 16,384 pairs, boot; 65 tracks refused - `host.resource.vca_reach_pairs` alike, no panic). - -Red without the feature (once each, reverted): gates 1-3 with the VCA reach dropped from -`controls::event_lanes` (VCA curves reach no member): host-core gates 1, 2, 3 red (and gate 8's -carry case); gate 1 and 3 parity red; gate 4 red with the same knockout (a hand VCA move is not -heard: `automated_vca_alike`'s heard assert); gate 7 red with `settle_adoption`'s stage hold -ignored (the move is cut at the swap: the browser twin comparison). Knocking out -`automation_row_renders` for VCA rows alone leaves gates 1-3 green: render reads the event-lane -predicates, the classifier the rows. - -Commands: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` on host-core, -control-plane, host-web; `cargo check --locked --all-targets` (workspace, no features); tests -automation (44), builtins-compiler `--features test-support` (94), graph `--features test-support` -(159), host-core `--features test-support` (485) and `test-support,control-provider` (613), -control-plane `--features test-support` (30), capi lib (220), host-web `--features test-support ---test control_parity` (52): all green. Realtime policy: 172 regions in 35 files (unchanged). - -## Decision record: U3 fold 1 (batch 19) - -U3 MAJOR-1 and NIT-1. No edit under `crates/engine/src/realtime/`, no row class or table change (no -SDK regeneration). - -- **Fix (K1, opt-in).** `automation::Composed::with_jump_release` (new flag, copied by `copy_from`): - every jump the output emits (not a seek landing, not a held event) shortens each restartable value - cursor's hold to the jump's completion (`CellState::release_hold_by`) and drops its deferral - (`clear_deferred_jump`), before the `with_held_grid` hold and the column-jump deferral; a cursor - that pulled ahead is replayed first, as for a hold. A jump that emits nothing changes nothing - (#1711 U1 fold 2). It reaches every hold and deferral the lane put on every cursor: the record - hold in `offsets_jump`, the adoption hold and deferral of `settle_adoption`, and the holds - `with_held_grid` propagates, so a composed lane moves as one cell does in holds and deferrals - (V2 departure 2). -- **Who uses it.** Only the fader composed lanes (`FaderCurve::prepared`: `.with_held_grid() - .with_jump_release()`). The composed mute lanes have gate cursors only. The route column - (`crates/graph`, #1654) does not call it and keeps its T3 rule: a held value follows the column's - current free time and never moves earlier. No graph file changed; graph tests and #1654 gates - unchanged. -- **NIT-1.** `follow_route_flags`' doc lines moved back above it; `follow_vca_mute_cells` keeps its - own doc. - -**Departures.** -1. The host-core live gate compares from the step's jump's end (3,240), not from 0. Before the step - the composed lane and one cell over the sum differ: a record's ramp target composes each curve - toward the ramp's completion clipped at that curve's own next discontinuity, where one cell clips - the sum at the lane's next discontinuity (`FaderComposite::toward` against `curve_toward`). The - jump's end equalizes the gains, and the defect window (3,328 to 9,024) lies after it. - Defined behaviour: on a composed fader lane a record's or an adoption's ramp target composes each - curve toward the ramp's completion, each clipped at its own next discontinuity (K1 - `compose_toward`, #1649 D3), so between the record and another curve's next discontinuity the - lane differs from one cell over the summed curve, and that curve's jump makes them equal; a - successor plan uses the same rule. -2. The both-host gate runs at the native width only (control_parity's rule, V5); the two bank widths - are the host-core twin's. - -**Gates** (red before: the gate with `.with_jump_release()` removed from `FaderCurve::prepared`, or -from the K1 probe; green after): -- K1: `compose::tests::a_step_replaces_a_hold_the_lane_put_on_every_cursor_as_one_cell_does` (every - cursor held to 2,999, then also deferred to 3,000; the composed retargets equal one cell over the - summed curve with the same hold and deferral). Red: `(2944, Grid)` after the step instead of - `(1472, Grid)`. -- host-core live: `stored_vca_automation::classifier:: - a_vca_step_inside_a_hand_moves_ramp_lets_the_own_ride_move_as_one_cell` (own ride on a 2^-12 dB - lattice, VCA step at 3,000, hand VCA move to +3 over 8,000 at 1,024; Simd8, Simd4, scalar, live and - between render calls; same classifier records as the summed single-cell session). Red: first - differing sample 3,328 (Simd8). -- host-core rebuild twin: `stored_fader_automation:: - a_table_step_inside_a_deferred_moves_ramp_releases_every_cursor_of_a_composed_lane` (Simd8 and - Simd4; track 0 also in an automated VCA riding 0 to -6 dB). Red: first differing sample 4,544. -- both hosts: `control_parity:: - a_table_step_inside_a_deferred_moves_ramp_releases_a_composed_lane_alike_on_both_hosts` (the - #1711 body over a session with the VCA ride; four rates). Red: block 39 against the reference at - 44.1 kHz. The single-cell test now calls the shared body `table_step_inside_a_deferred_move`. - -Commands: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` and no-feature -`cargo check --locked --all-targets` on automation, builtins-compiler, host-core, graph, host-web; -tests automation (all features), builtins-compiler `--features test-support` (76 lib), graph -`--features test-support` (149 lib, unchanged), host-core `--features test-support` -`stored_vca_automation` (7; 13 with `control-provider`), `stored_fader_automation` (37), -`stored_mute_automation` (13), `route_mute` (26), `successor_swap` (140), control-plane -`--features test-support` (30), capi lib (220), host-web `--features test-support --test -control_parity` (54): all green. Realtime policy: 172 regions in 35 files (unchanged). diff --git a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md index bf35192f8..a54fca732 100644 --- a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md +++ b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md @@ -155,3 +155,225 @@ Checked against `12d4a4f9d`. - **The fixtures add** a fader offset (#1650), a hand mute over a mute curve (#1651 amended) and a send ride (#1654); depend on #1650 and #1654. - Native pin tests in `hosts/host-web/src/tests.rs` have moved; re-read at filing. + +## Amendment (controls coordinator, batch 20, 2026-10-17): narrowed to the delivered rows + +Checked against `e7722bd19`. The owner narrowed this issue on 2026-10-17. This amendment replaces +D1-D5, the authorized paths, the gates, the test value and the dependencies above, and the +amendment of 2026-10-14. + +### Product outcome (narrowed) + +One fixture session automates the rows that the engine renders today: the strip fader, the strip +mute, the VCA offset, the VCA mute, and a following send that tracks its source's mute and its +VCA's mute. It uses every shape that each row accepts, and it is seeked once while it plays. The +shipped `simd128` artifact (the direct oracle), the native browser host on x86-64 +(`AudioWorkletEngineHost`) and the C ABI render the same bits. There is no new digest owner. The +oracle leg adds one pin beside the existing pins in `expected.json`. + +### What the code accepts (anchors at `e7722bd19`) + +- **Grammar.** An entry is `{id, target, segments, offset}` (`crates/session/src/parse.rs:1569-1604`). + The target is `{owner, owner_id, section, control, lane}` (`:1606-1646`). A segment is + `{shape, start_sample, end_sample, start_value, end_value, unit}` (`:1690`). Sample times are + JSON strings. +- **Rows.** Strip fader: `strip`, `builtins`, control 5. Strip mute: control 6 + (`crates/controls/src/builtin_rows.rs:29-30`). VCA offset: `vca`, `own`, control 1. VCA mute: + control 2 (`crates/controls/src/vca.rs:14-15`). Only these four rows compile to cells + (`crates/controls/src/automation.rs:394-408`). A route-row entry is kept but inert (#1654). +- **Shapes.** `step` is always valid. `linear` needs a continuous row. `exponential` needs a + continuous row above 0 that is not in dB (`crates/controls/src/automation.rs:255-266`). So the + fader and the VCA offset take `step` and `linear`, the two mutes take `step` only (unit + `linear`, values 0 and 1), and `exponential` is refused on all five rows with + `control.automation.shape`. +- **Structure** (`crates/session/src/validate.rs`). Segments are ordered and do not overlap + (`:1023`, `:1031`). Gaps are valid. The jumps of one entry are at least 64 samples apart + (`:1070-1134`). One entry per lane, and `both` claims the two lanes (`:1141-1185`). The hold + rule is in `docs/SESSION_SCHEMA_V1.md:339-345`: a `step` holds `start_value` and takes + `end_value` at `end_sample`. +- **Offsets.** The stored hand-move offset (#1650) is `offset.{left_db,right_db}`. A lane that the + entry drives on the fader or VCA offset row takes a value in `[-144, 24]` dB. Every other lane + holds exactly `+0.0` (`docs/SESSION_SCHEMA_V1.md:363-371`). +- **Known-good shapes.** `fixtures/session/v1/builtins-automation.json` (fader `:174-197`, VCA + offset `:294-318`, VCA members `:106-118`, a following send `:129-148`). Mute entries: + `hosts/host-web/tests/stored_mute_automation.rs:184-207`. VCA mute: + `crates/host-core/tests/stored_vca_automation.rs:161-184`. + +### D1 (replaced). One fixture: `hosts/host-web/tests/browser-v1/automation-session.json` + +Copy the frame of `command-session.json` (48 kHz, quantum 128, profiles, `main-out`, default +smoothing). Then: + +- **Sources.** `src-t1` and `src-t2`, 2 channels, `32f`, `frames` `"48000"`. A seek clamps a + source to `[0, frames]` (`crates/host-core/src/source.rs:37-39`), so the frames must be more + than 37,168. +- **Tracks.** `t1` on `src-t1` and `t2` on `src-t2`. No console slot, no insert, no input + filter. Static faders at 0 dB, all hand mutes `false`. +- **Submix.** `bus`, with the plain builtins of `builtins-automation.json:68-118`. +- **VCA.** `vca`, members `["t1", "t2"]`, static `fader.left_db` `-2.0` (the static offset on the + left lanes), all other static values zero or `false`. +- **Routes.** `t1-main` and `t2-main` (`post_pan` to `main-out`). `t1-bus` (`pre_fader` to `bus`, + `follows_mute: true`). `t2-bus` (`pre_fader` to `bus`, `follows_mute: false`, the + non-following contrast). `bus-main` (`post_pan` to `main-out`). +- **Entries** (values in dB for the dB rows, 0 or 1 for the mutes): + - `t1-fader`, `t1` control 5, lane `both`, offset `{left_db: -1.5, right_db: 0.0}`: + linear 256-2304, 0 to -12; a gap; step 3001-3601, -6 then -9 (3001 and 3601 are not grid + samples); linear 29000-33000, -9 to +3; step 34000-35000, 0 then -3. + - `t1-mute`, `t1` control 6, lane `left`: step 0-1000, 0 then 1; step 1600-4200, 0 then 1; + step 4800-29800, 0 then 1; step 30600-33500, 0 then 1; step 34000-36000, 0 then 0. Muted over + 1000-1600, 4200-4800, 29800-30600 and 33500-34000. + - `t2-fader`, `t2` control 5, lane `right`, offset `{left_db: 0.0, right_db: 0.0}`: linear + 1024-4096, -3 to 0; linear 29900-32000, 0 to -18. + - `vca-offset`, `vca` control 1, lane `right`, offset `{left_db: 0.0, right_db: -0.5}`: linear + 512-3584, 0 to -6; step 4096-4352, +2 then 0; linear 29500-31500, -4 to +2; step 33000-35500, + -3 then -3. + - `vca-mute`, `vca` control 2, lane `both`: step 0-2500, 0 then 1; step 2900-29900, 0 then 1; + step 30100-35000, 0 then 1; step 35500-36000, 0 then 0. Muted over 2500-2900, 29900-30100 + and 35000-35500. +- **What this covers.** A `both` entry and single-lane entries. Lanes driven by their own curve, + by a VCA curve, by both, and plain lanes. Linear segments, steps on and off the grid, gaps, a + hold before the first segment and after the last one, and non-zero stored offsets. Each entry + has a segment that starts before the seek target 30,000 and ends after it, and events after the + seek. The following send closes for the strip mute (left lane only) and for the VCA mute. The + non-following send stays open. +- **Dropped.** The matrix fixture, effect inserts, and every other row (phase 2, below). + +### D2 (replaced). One transcript + +- 96 blocks of 128 frames. Blocks 0-39 render timeline 0-5,119. Before block 40 renders, the host + makes one plain session seek: generation 2, timeline 30,000. Blocks 40-95 render timeline + 30,000-37,167. +- Before each render, submit exactly one 128-frame chunk per source (the oracle's ring holds one + quantum, `direct-oracle.mjs:96`). Block `k < 40`: generation 1, start frame `k*128`. Block + `k >= 40`: generation 2, start frame `30000 + (k-40)*128`. `final` is always false. Do not + submit a generation-1 chunk for block 40. A seek discards stale chunks and needs contiguous new + chunks from the seek frame (`crates/source/src/lib.rs:1162-1168`, `:1355-1381`). +- PCM at source frame `f`: `((f*m) mod 65536 - 32768) / 65536`, with `m` = 40503 (`src-t1` left), + 30011 (`src-t1` right), 52153 (`src-t2` left), 18749 (`src-t2` right). The value is exact in + `f32`. In Rust, compute `(f as u32).wrapping_mul(m) & 0xffff` in integers, then subtract 32768 + and divide by 65536.0 (plain `i32` multiplication overflows). In JavaScript, + `(Math.imul(f, m) >>> 0) & 0xffff` gives the same integer. +- The seek: `miso_engine_web_v1_session_seek(handle, 2, 30000n)` in the oracle + (`hosts/host-web/src/ffi.rs:3351`), `host.session_seek(None, 2, 30000, None)` natively + (`hosts/host-web/src/lib.rs:3756`), and `miso_engine_v1_session_seek(session, 2, 30000)` on the + C ABI (`crates/capi/src/ffi.rs:660`). All three reach `SessionState::session_seek` + (`crates/control-plane/src/control.rs:4327`). A plain seek declares a discontinuity and + publishes a successor plan, on every host alike. Every seek and every submit must return 0. + +### D3 (replaced). One oracle leg and one pin + +- `direct-oracle.mjs` gains `runAutomationTimeline`. It boots `automation-session.json`, runs D2 + and digests the PCM as the other legs do (`pcmSha256`, every left block then every right block). + It records `actual.automationTimeline = {blocks: 96, seekBlock: 40, seekTimelineSample: 30000, + beforeDisposeStatus, pcmF32leSha256}`. Before the print branch, it asserts that the digest + equals `expected.directOracle.nativeAutomationPcmF32leSha256`, as the other legs do + (`:493-576`). +- The pin key is **`directOracle.nativeAutomationPcmF32leSha256`**. Put `automationTimeline` and + the pin after `observationTimeline` in `expected.json`. +- The native pin test is `native_automation_timeline_digest_pins_the_wasm_parity` in + `hosts/host-web/src/tests.rs`, beside its twin at `:2016`. It copies that twin: `include_str!` + of the fixture, `AudioWorkletEngineHost::boot` with `source_ring_frames: QUANTUM`, + `submit_source`, `render_next`, the same digest. It asserts against both pins. +- `serde_pin` (`:2120-2164`) gains two fields: the text after `"nativeAutomationPcmF32leSha256":`, + and the `"pcmF32leSha256":` after the anchor `"automationTimeline"`. +- `scripts/check-browser-expected-resources.py` adds `("automationTimeline", "pcmF32leSha256")` + to `DIGEST_PATHS` (`:121-125`) and says four PCM digests in its docstring (`:33-35`). The two-class + rule covers resource rows only, so the new key needs no class. The oracle (`:599`) and the + script (`:274-283`) compare the whole document, so `expected.json`, the oracle and the script + change in one commit. No other reader breaks: `qualification/qualification.js:1131-1132` and + `run.mjs:204-207` read only the identity pins. + +### D4 (replaced). One cross-host case in `hosts/host-web/tests/control_parity.rs` + +- `the_automation_fixture_renders_alike_on_both_hosts_across_one_session_seek`. The browser side + boots the fixture bytes through `Rig::boot` (`tests/common/mod.rs:363-385`). The C ABI side + parses the same bytes with `parse_session_json` and builds `CAbi::new` + (`control_parity.rs:922-975`). +- It runs D2 with `CAbi::session_seek` (`:4949-4972`) and direct submits, as + `a_composed_stored_mute_renders_alike_on_both_hosts` does (`:3694-3741`). Do not use `step()`: + it hard-codes generation 1 (`common/mod.rs:454-475`). +- It compares every block's two planes with `bits()` (`common/mod.rs:287-289`) and `assert_eq!`. + No digest. The case is under `test-support`. + +### D5 (replaced). The plants (PR evidence, not committed) + +A plain one-ulp change to the result of `value_at` alone is not valid. The program builder +compares `value_before` with `value_at` to find jumps (`crates/automation/src/program.rs:330`), so +the change adds jumps and can make preparation refuse the session. Use these three plants, one at +a time, and record each leg's result: + +- **P1, the evaluator (every row).** Put `f32::from_bits(v.to_bits() + 1)` on every return of + `value_at` (`crates/automation/src/evaluate.rs:18-44`) and of `value_before` + (`crates/automation/src/program.rs:338-358`). The jumps stay the same. A mute value of 0.0 + becomes the least subnormal, so every open mute span, VCA mute span and follow gate closes + (`value != 0.0`: `crates/builtins-compiler/src/lib.rs:3252`; + `crates/graph/src/runtime.rs:1480-1481`, `:2196`). The FP environment keeps subnormals on every + target (`crates/lane/src/fpenv.rs:518-530`). Expected red: the native pin test, the oracle leg, + and G5 (`g5_native_digests_match_pins` and the wasm leg; `crates/automation/src/corpus.rs:245`). + Expected green: D4 (the path is shared). +- **P2, the dB rows.** On a dB row a P1 ulp can disappear (in `db_gain`, or in + `vca_compose_db`). Put `next_up` on the output of `db_gain` + (`crates/builtins/src/lib.rs:6472-6479`). It reaches every fader and VCA offset lane. Expected + red: the native pin test and the oracle leg. +- **P3, one host only (does D4 compare?).** After `state.render` returns `Ok` in + `miso_engine_v1_render_f32_planar` (`crates/capi/src/ffi.rs:1133`), flip the low bit of the + first left sample when `absolute_sample >= 5120`. Expected red: D4 at block 40. Expected green: + the pin test and the oracle leg. + +Hand-written tests that compute their expected values with `value_at` stay green under P1 (for +example `crates/host-core/tests/stored_fader_automation.rs:578`). Do not count them. + +### Authorized paths (replaced) + +- `hosts/host-web/tests/browser-v1/automation-session.json` (new), `direct-oracle.mjs`, + `expected.json`. +- `hosts/host-web/src/tests.rs` (one pin test, `serde_pin`). +- `hosts/host-web/tests/control_parity.rs` (D4). +- `scripts/check-browser-expected-resources.py` (`DIGEST_PATHS` and the docstring only). + +### Objective gates (replaced) + +1. **Browser equals native.** As on main (`.github/workflows/qualification.yml:132`, `:345-348`): + `mkdir -p target/ci/qualification-artifacts target/ci/qualification-named-twin && bash scripts/build-web-audioworklet.sh --named-twin target/ci/qualification-named-twin target/ci/qualification-artifacts`, + then + `bash scripts/check-web-audioworklet.sh --without-metadata-regeneration target/ci/qualification-artifacts target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm` + and `python3 -B scripts/check-browser-expected-resources.py --artifacts target/ci/qualification-artifacts` + pass with the automation leg. `python3 -B scripts/check-browser-expected-resources.py --self-test` + passes. +2. **Native pin and cross-host.** `cargo test --locked -p host-web --features test-support` passes, + with the new pin test and the D4 case. +3. **Plants** (D5, PR evidence): P1, P2 and P3 each give the expected red legs. +4. **Workspace.** The `test-debug-a` step (`qualification.yml:631-638`); G5, + `cargo test --locked --release -p lane -p math -p wasm-gates --features math/lane` (`:705-706`); + `cargo fmt --all -- --check`; + `cargo clippy --locked --workspace --all-targets --all-features -- -D warnings`. + +### Test value (replaced) + +- **The oracle leg and its pin** turn red if the `simd128` artifact renders an event, a + composition (fader with VCA offsets, mute OR, the follow gate) or the landing of a plain session + seek differently from x86-64 native. Today the only artifact-versus-native renders have empty + automation (`session.json:62`, `command-session.json:141`, `observation-session.json:159`). +- **The D4 case** turns red if the C ABI and the browser host prepare the whole fixture or land + its seek differently. Today the only cross-host case with a seek automates the mute row only + (`control_parity.rs:3571`), and the VCA cases do not seek (`:6916-7395`). + +### Dependencies (replaced): all on main + +- #1637 and #1639 (PR #1687), #1645 (PR #1692), #1642 and #1643 (PR #1708), #1646-#1650 (PR + #1712), #1651-#1653 (PR #1715), #1655 (PR #1719). The harness work #1399 and #1560 is closed. + All of these issues are closed on GitHub. +- The fixture has no live edit. A stored offset (#1650) is a field of the entry. A hand mute + (#1651) is the static `fader.*_mute` and would OR to a constant mute, so the fixture keeps it + `false`. The live hand mute over a curve is already compared across hosts at `:3571`. +- #1670 depends on this issue. Its spec names two fixtures and two pin tests; it needs a matching + amendment (one fixture, one pin test). + +### Phase-2 extensions (not in this issue) + +- The matrix fixture `automation-matrix-session.json` and pan and matrix automation (#1657). +- Input trim and polarity (#1658), input HPF and LPF (#1660), parametric EQ (#1667), and every + other effect-parameter row, with effect inserts in the fixture. +- Send-row (route-row) automation (#1654, behind #1718). +- A following send at a latent tap (an insert with latency) in the oracle fixture. +- AArch64 and seek-time invariance stay in #1670. diff --git a/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md b/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md index db6870786..ca8eca8f5 100644 --- a/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md +++ b/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md @@ -119,3 +119,18 @@ automation into per-cell events in node time* arrive through #1669. Checked against `12d4a4f9d`. - No design change. It inherits #1669's added rows. #1019 is still open. Anchors re-read at filing. + +## Amendment (controls coordinator, batch 20, 2026-10-17) + +Checked against `e7722bd19`. The owner narrowed #1669 to the rows that render today (see #1669's +batch 20 amendment). + +- #1669 adds one fixture, `automation-session.json`, one pin test, + `native_automation_timeline_digest_pins_the_wasm_parity`, and one pin, + `directOracle.nativeAutomationPcmF32leSha256`. There is no `automation-matrix-session.json`. + Read "the two automation pin tests", "both fixtures" and "the two pins" in this issue as these + single items. +- D1 and D2 use #1669's narrowed transcript: 96 blocks of 128 frames, a plain session seek to + timeline 30,000 at block 40, generation 2. +- The matrix fixture and the rows that #1657, #1658, #1660 and #1667 add are phase-2 extensions, + as in #1669. diff --git a/.github/ISSUE_SPECS/1711-keep-a-live-level-moves-own-length-across-a-reposition-swap.md b/.github/ISSUE_SPECS/1711-keep-a-live-level-moves-own-length-across-a-reposition-swap.md deleted file mode 100644 index 691680d17..000000000 --- a/.github/ISSUE_SPECS/1711-keep-a-live-level-moves-own-length-across-a-reposition-swap.md +++ /dev/null @@ -1,577 +0,0 @@ -# Keep a live level move's own length across a Reposition swap - -Filed by root from batch 17 (#1648's split, root ruling B17-R13; controls coordinator, 2026-10-16). -It follows the "split a blocking gate early" practice: #1648 closes on its passing gates, and the -in-flight length case moves here. - -## Product outcome - -A live level move (a VCA move over an automated fader, or a hand move stored as the ride's offset) -that is still ramping when a `Reposition` swap adopts (a rebuild whose only change on that lane is -the ride's table) ends when it was due, with its own length, for every `remaining` (the frames -left in its ramp at the swap), including `remaining < J` (`J`, the fader jump length, 960 in the -gate's session). The revision's watermark then reports `OUTCOME_EXACT`. This issue replaces the -conservative fallback flag of B17-R14 with the exact behaviour and deletes that flag path. - -## Context - -- Since #1648 attempt 3 (U3 fold 2), the adoption rule (`settle_adoption`, - `crates/builtins-compiler/src/lib.rs`) holds a free stage's running ramp again, compares the - curve where that ramp ends with the stage's target, and lands a moved target over - `max(J, remaining)` (a one-sample landing piece, then the rest of the block over `J`). -- The defect (verifier, batch 17, at 0fac00ee9): when `remaining < J` the landing is stretched to - `J`. In the gate's setup (a 4,000-sample move from block 4, `a_level_move_across_a_table_change_ - keeps_its_length` in `crates/capi/src/runtime/live_tests.rs`) a move heard 24, 26 and 30 blocks - has 928, 672 and 160 samples left; the rebuilt rig differs from the uninterrupted engine by - 4.8e-4, 3.1e-3 and 2.1e-3 (both table changes, VCA and hand moves). -- Current behaviour (B17-R13 and B17-R14): the move is stretched to `J`. The control plane marks a - rebuild candidate `CandidateOutcome::TransitionFallback` before it publishes it when the - rebuild is a transaction (not a declared discontinuity), the successor's model has at least one - stored fader automation entry (`controls::automation_row_renders`), and the session's level - move window may still ramp. The window (`LevelMoveWindow` in - `crates/control-plane/src/control.rs`) folds every live commit's fader `FaderDb` and offsets - records with a nonzero ramp, and every rebuild commit that changes some kept strip's effective - fader dB or `S` (`controls::effective`, old model against new) with the ramp - `max(fader_samples, the longest explicit ramp of the transaction)`, into one bound: the newest - revision and the longest ramp. "May still ramp" is `watermark.revision < revision || - render_clock < watermark.first_sample + ramp` (a watermark read that gives up counts as "may"). - The watermark and the render clock are read before the reservation. - So the watermark reports `OUTCOME_TRANSITION_FALLBACK`. This is conservative: it is also set - when no move is stretched (any transaction rebuild of a session with fader automation while any - level move may still ramp; see the #1648 decision record "the conservative fallback flag"). -- First thing to find out, before the design work (root's note): why the stage target moves - during a heard move (0.398 at 12 blocks, 0.335 at 24, against 0.3018). This may be a defect of - its own. Measured while landing B17-R13 with a temporary probe: for every heard move on the - moving ride, the stage's `fader_gain` at the swap is not the composed curve at the ramp's end. - So the adoption rule's comparison always differs, and every heard move lands at the swap (over - `remaining`, or `J`). The 4.6e-6 at 12 blocks heard comes from that landing recomputing the - stage's ramp step. -- A second Reposition inside a stretched landing (B17-R13 tranche 5): a flagged rebuild also folds - `(its revision, J)` into the window, so a second table-only rebuild inside the first one's - landing over `J` is flagged too (gate - `a_second_reposition_inside_a_stretched_landing_reports_a_fallback`). D2 deletes this with the - rest of the flag. -- Further over-report: `may_ramp` reads the watermark's newest `first_sample`, so any later - revision restarts the window (a non-level edit, or the flagged rebuild itself). A stream of live - edits keeps it open after one level move. -- A cell's own jump of `J` that is still running at a Reposition swap and is landed over `J` again - is specified D2 behaviour, not a defect: it has no user length, and a fresh plan with the new - table also jumps over `J`. The exception is a stretched live move's landing, which is covered by - the tranche-5 fold today and by D1 after this issue. -- MINOR 4 (from B17-R13, not gated): the comparison point `at = end` in `settle_adoption` has no - gate. It is observable with a heard move on the flat ride (`S` moves, the curve does not), then - a table whose step falls inside the remaining ramp. G2 can use that setup. - -## Decision to make at filing - -- D1. The deferred jump. At a `Reposition` adoption, a free stage's live ramp is not retargeted: - it runs to its end as the uninterrupted engine runs it. The cell makes the D2 comparison and, if - the target moved, the `J` jump at the first free sample after the ramp (the ramp's end), not at - the block's first sample. A small API in `crates/automation` gives the cell's deferred jump - (the node time at which it is due and the comparison it makes there), so the driver needs no - second landing path; the one-sample landing piece and `max(J, remaining)` are deleted. -- D2. The conservative fallback flag of B17-R14 is deleted: `LevelMoveWindow`, `fold_level_move`, - `rebuild_level_ramp`, `rebuild_moves_a_level` and the `set_outcome` call in - `crates/control-plane/src/control.rs`, with `ObservedReservation::set_outcome` if nothing else - uses it. The text that B17-R14 appended is removed from `crates/capi/src/abi.rs`, the C header - comment, `docs/C_ABI_V1_QUALIFICATION.md` and the two d.ts files. Its gates - (`a_reposition_swap_while_a_level_move_ramps_reports_a_fallback` and its host-web parity test) - are replaced by G1. -- D3. A step of the cell's own at the swap block's first sample keeps the B17-R13 rule: the curve - is compared there and the landing is over `J` (gate - `a_step_at_a_reposition_swap_jumps_over_the_fader_length`). - -## Objective gates - -- G1. VCA and hand moves of 4,000 samples, heard 1, 12, 24, 26 and 30 blocks before a table-only - rebuild (the ride split at 10,000, and a change in its last 2,000 samples): every block after - the swap is bit-equal to the uninterrupted engine's (or, where the table's own values differ, - to a fresh plan prepared with the new table and given the same move), allocation-free, adopted, - and the rebuild's watermark advance reports `OUTCOME_EXACT`. -- G2. The comparison point (setup: a heard move on the flat ride, then a table whose step falls - inside the remaining ramp): a heard move whose stage target equals the curve at its ramp's end - keeps its own length with less than `J` left (red if the curve is compared at the block's first - sample). -- G3. The B17-R13 step gate and the #1648 gates stay green; realtime policy count unchanged or - explained. - -## Dependencies - -- #1648 (closed on its gates), B17-R13 and B17-R14 (the conservative flag this issue removes). - -## Amendment (controls coordinator, batch 18, root ruling 2026-10-17) - -- **Added item: gate or delete the owed discharge on a free fader write.** In - `crates/builtins-compiler/src/lib.rs` (about `:1689` on `ad03780ee`), a free live write releases the - hold and sets `self.owed = false`. Deleting that line leaves every test green (batch 18 T1 gate - power check, `scratchpad/c18t1/`). Prove the discharge observable and gate it, or prove it - unobservable and delete it. This slice reworks the same owed and hold machinery at a Reposition, - so the item belongs here, not in a phase-2 placeholder. - -## Amendment (controls coordinator, batch 19 spec check, 2026-10-17) - -Anchors are on `323433f6a`. #1648 is closed; batches 17 and 18 are merged. Every number below was -measured with temporary probes in a detached worktree at `323433f6a` (removed after use). - -### A0. Diagnosis: the stage target does not move; the comparison reads the wrong field (a defect) - -- The stage's ramp target stays at the composed curve at the ramp's end: 0.301812 at every - `remaining` (3872, 2464, 928, 672, 160), VCA and hand moves, both tables. -- `fader_gain` is not the ramp target. Inside a live move's ramp the cell's grid events are held - (`held: true`, `crates/automation/src/events.rs:391-409`). The driver gives them to the stage as - `RememberGain` (`crates/builtins-compiler/src/lib.rs:2026-2043`), and `RememberGain` writes - `fader_gain` with no retarget (`crates/builtins/src/lib.rs:3184`). So `fader_gain` follows the - ride (0.466 heard 1, 0.398 heard 12, 0.335 heard 24, 0.325 heard 26, 0.307 heard 30). -- `bank_hold` puts `fader_gain` in `ChannelHold` (`lib.rs:4032-4041`, field `:1438`), and - `settle_adoption` compares the curve with it (`lib.rs:2196-2198`). On a moving ride it always - differs, so every heard move lands at the swap. -- Fix F0 (smallest): for a free stage (`!held`), `ChannelHold` carries the stage's ramp target - (a new `BuiltinFaderBank` accessor over the kernel's `target_gain`, `builtins/src/lib.rs:3196`, - beside `fader_gain` at `:4639-4649`). A held stage keeps `fader_gain` (the gain an unmute lands - on; correct there). Measured with F0: all 40 cases of the gate setup (VCA and hand, both tables, - heard 1, 12, 24, 26, 30) are bit-equal to the uninterrupted engine over 44 blocks, adopted and - allocation-free. At HEAD: 4.6e-6 (heard 12), 4.8e-4, 3.1e-3, 2.1e-3 (heard 24, 26, 30). -- Rows: the fader stage only. VCA and hand moves both (one offsets-jump path, R17-1), and any - free ramp the cell holds (its own `J` jump or grid ramp). Mute cells are not affected (no gain - comparison). #1655's VCA cells drive the same stage and inherit F0. -- MINOR 4 (`at = end`): with F0, G1 is red if the curve is compared at the block's first sample - (measured: the same 4.8e-4, 3.1e-3, 2.1e-3). G2 as filed cannot be red: on the flat ride - `curve_toward` stops before the step, so `at = start` and `at = end` read the same value. G2 is - replaced (A3). - -### A1. The owed discharge: observable, so keep it and gate it (G4) - -- The line is now `crates/builtins-compiler/src/lib.rs:2104` (`HeldRampObligation::block`, - `:2080-2111`): a free stage at a block entry with no move due. -- `owed` can be true there only when the stage held the gain through the previous block and a - live write freed it at this entry (an exact live unmute). Without the line, the stale `owed` - survives the free blocks and makes a later hold's end `E` land a move over `J` - (`lib.rs:2243`, `moving`) when nothing was remembered in that hold. -- Measured on the C ABI (quantum 128): ride linear 0 to -6 dB over `[0, 1280)`, flat -6 dB to - 2320, then linear. Hand mute (ramp 256) at block 4, exact hand unmute (ramp 0) at block 8, hand - mute (256) at block 12, hand unmute (200) at block 16, so `E` = 2248 with no grid event in the - second hold. Reference: the same rig with only the second hold. With the line, bit-equal from - block 9. Without it, the first difference is block 17 (2.7e-4). - -### A2. D1 against the code (amended) - -- F0 first. An unmoved target then takes no landing: the cell seeds its target and holds the ramp - again (`settle_adoption`, `lib.rs:2174-2202`, already does this when `!differs`). -- A moved target on a free stage with a ramp in flight (and no step at the block's first sample): - no landing at the swap. The cell calls `CellState::defer_jump(at)` with `at` the ramp's first - free sample (`node_time_at(span, remaining)`). The existing deferred path - (`events.rs:456-490`) gives a `Jump` over the block's `J` there, change only. -- One rule change in `crates/automation` (measured as needed): on a restartable cell - (`no_restart` false) grid events before a pending deferral are held. Without it the grid point - 32 samples before the move's end retargets to the new curve (a grid ramp that completes after - the ramp in flight restarts, `events.rs:395-396`), the move is cut short, and change only then - drops the jump. A `no_restart` cell is unchanged. -- The deferral is cleared by `reposition` and a seek landing (both already), and by a new - `CellState::clear_deferred_jump()` that the fader cell calls where the ramp it waits for is - replaced or ends early: the free path of `offsets_jump` (`lib.rs:1346-1352`, a newer live move - keeps its own length), the R17-10 release (`:1920-1926`) and the `release_hold_by` sites - (`:1576`, `:1592`). Reason for the last two: a held grid would freeze the lane after an early - unmute (the #1651 U1 MAJOR-1 defect). -- Kept: the ramped landing at the block's first sample over `J` when no ramp is in flight, when - the stage is held, for D3 (a step at the block's first sample), and for the `Compare` backstop. -- Deleted: `long_landing` and its one-sample landing arm (`lib.rs:1898-1919`, `:1966-2009`) and - `length = length.max(remaining)` (`:2191`). `settle_adoption` no longer returns a length. -- Prototype measured (F0 + `defer_jump` + the held grid): G2's setup (A3) is bit-equal to its - reference at heard 12 (2464 left) and heard 30 (160 left), VCA and hand. At HEAD: 9.4e-3 and - 1.5e-3 from the swap block. With the prototype on, the capi `automated` tests (24), host-core - `stored_fader_automation`, `stored_mute_automation`, `successor_swap`, `session_seek` and the - `automation` and `builtins-compiler` tests stay green. -- No new state: `CellState::deferred` exists (`events.rs:102`, 32 B unchanged). - -### A3. Gates (amended) - -- G1 (unmoved target). The existing gate (`live_tests.rs:9059-9147`) becomes bit-equal (not 1e-3) - for Pending, SameTransaction and heard 1, 12, 24, 26, 30; both tables; VCA and hand. Add: two - table-only rebuilds 5 blocks apart while a 500-sample move ramps (the tranche-5 setup). Every - rebuild's watermark advance reports `OUTCOME_EXACT` and no transition fallback. Simd8 on the - C ABI. Simd4 and Simd8 in `crates/host-core/tests/stored_fader_automation.rs` through a carry - harness with live offsets writes (the pattern of `carry_run`, - `crates/host-core/tests/stored_mute_automation.rs:1129`). The scalar oracle has no plan swap - (`ChannelHold` there, `lib.rs:8234`). Both hosts: the parity test at - `hosts/host-web/tests/control_parity.rs:2933-3012` becomes "a hand move in flight reports - `EXACT` alike on both hosts, same bits". -- G2 (moved target, replaces the flat-ride G2). Base ride flat at -3 dB (`flat()`); move -6 dB - over 4,000 samples from block 4; rebuild to a ride flat at -5 dB to 6,000, then -3 dB; heard 12 - and 30; VCA and hand. Reference: a fresh plan whose table is -3 dB before the move's first free - sample (4,512 here; compute it from the rig), -5 dB to 6,000, then -3 dB, given the same move. - Every block is bit-equal to the reference, allocation-free, adopted, `OUTCOME_EXACT`. Red at - HEAD (above). Case (c): a second live move committed after the swap, before 4,512: bit-equal to - a fresh plan with the new table given both moves, from the second move's last sample on (red if - the deferral is not cleared). Simd8 and Simd4 in host-core as for G1; the C ABI leg; one - both-host parity case. -- G3 as filed. Named: `a_step_at_a_reposition_swap_jumps_over_the_fader_length` - (`live_tests.rs:9186-9289`) and the #1648 carry gates in `automated_carry`. Also the G5 - automation corpus digests (`crates/automation/src/corpus.rs:245`, - `tools/wasm-gates/tests/g5_native_corpus.rs`) do not move. Realtime policy count unchanged or - explained. -- G4 (owed discharge, A1). The A1 setup in `stored_fader_automation.rs` with `acted` on every form - (`FORMS`, `:2092-2100`: Simd8, Simd4, the between-render forms and the scalar oracle), and on - both hosts in `control_parity.rs` (same bits). Test value: red if the discharge line is - deleted. - -### A4. D2 exact deletions - -- `crates/control-plane/src/control.rs`: the `level_moves` field (`:649-651`); - `LevelMoveWindow`, its `may_ramp`, `fold_level_move`, `rebuild_level_ramp`, - `rebuild_moves_a_level` (`:654-735`); the B17-R14 comment paragraph (`:2621-2629`, keep the - #1652 U2a paragraph); `automates`, `in_flight`, `level_fallback`, `moved`, `landing` and the - `rebuild_level` value (`:2640-2672`; the outcome becomes `follow_into_six_word` alone); the fold - after commit (`:2866-2870`); the live commit's `level_ramp` and its fold (`:3519-3539`, - `:3555-3557`). `crates/control-plane/src/compile.rs:1483` (`level_moves: None`). -- Keep `reservation.set_outcome` (`control.rs:2840`) and `ObservedReservation::set_outcome` - (`crates/control-plane/src/plan.rs:199-206`): #1713's six-word fallback uses them. Fix the doc - at `plan.rs:199-200` to name #1713 only. -- The B17-R14 clause: `crates/capi/src/abi.rs:96-97`, `crates/capi/include/miso_engine_v1.h:183-184`, - `docs/C_ABI_V1_QUALIFICATION.md:153-154`, - `hosts/host-web/web/miso-engine-v1-audio-worklet-host.d.ts:782-784` and its generated copy - `sdk/src/browser/shipped-host.d.ts:782-784` (`scripts/check-sdk-generated.sh`). Keep the #1713 - clause. Doc mentions of the window: `crates/controls/src/automation.rs:397-398`, `:414-415`. -- Tests deleted: `a_reposition_swap_while_a_level_move_ramps_reports_a_fallback` - (`live_tests.rs:9297-9368`), `a_second_reposition_inside_a_stretched_landing_reports_a_fallback` - (`:9370-9414`), and - `a_mute_curve_rebuild_while_a_level_move_ramps_completes_exactly_on_both_hosts` - (`control_parity.rs:3014-3114`, its window is gone). The `outcome` helper - (`live_tests.rs:9290-9295`) stays for G1. - -### A5. #1655 (same batch, same fader stage) - -- #1711 lands first. It removes the window that #1655 would otherwise have to extend for VCA - level moves. -- #1711 assumes: (1) the adoption compares the stage's ramp target (F0), which does not depend - on how the gain is composed; (2) the curve side of the comparison is the lane's composed gain at - `at`. Today that is `FaderEventDriver::gain(value, S)` (`lib.rs:2196`). #1655 replaces it with - its one composition `checked_fader_gain(vca_compose_db(own(at), S + Σ c_k(at)))`; (3) one - deferral per lane channel, so one `Jump` fires at the free sample, composed with every cell at - that node time. #1655 names which cell holds it on an event lane with no own fader cell, and - gives its VCA cells the same grid-hold and clear rules. - -### A6. Mapping onto the batch 17-18 code - -- Fader cells and the one event driver: `FaderEventDriver::new` (`lib.rs:1755`), adoption at - `:1898-1919`. Offsets cells and hand moves (`takes_offset`, R17-1): one `offsets_jump` path - (`:1285`). Jump lengths: `BlockJumps` (`:1404`). Node time (R17-3): `take_block_time` - (`:1044`). -- The scalar oracle (R17-4) shares the driver; no swap reaches it. It is in G4 only. -- R17-7, the classifier rows and carry, `LatestCell<7>`, `route_mix_column_ramps_block`, the - #1653 commit write and mute `curve || terms`: not touched. Mute adoption stays - `AutomatedMuteCell::reposition`. -- `controls::fader_row` and `automation_row_renders`: D2 deletes the only control-plane reader of - `fader_row` for the window (`control.rs:2651`). -- The conservative TransitionFallback: B17-R14's is deleted (D2). #1713's stays. - -### A7. Authorized paths - -- Render (realtime policy regions): `crates/builtins-compiler/src/lib.rs`, - `crates/automation/src/events.rs`, `crates/builtins/src/lib.rs` (one accessor). -- Control and docs: `crates/control-plane/src/control.rs`, `crates/control-plane/src/plan.rs`, - `crates/control-plane/src/compile.rs`, `crates/controls/src/automation.rs` (doc only), - `crates/capi/src/abi.rs`, `crates/capi/include/miso_engine_v1.h`, - `docs/C_ABI_V1_QUALIFICATION.md`, `hosts/host-web/web/miso-engine-v1-audio-worklet-host.d.ts`, - `sdk/src/browser/shipped-host.d.ts`. -- Tests: `crates/capi/src/runtime/live_tests.rs`, `hosts/host-web/tests/control_parity.rs`, - `crates/host-core/tests/stored_fader_automation.rs`, - `crates/host-core/tests/support/successor.rs` (only if the carry harness needs a live-write - hook). -- This spec. -- Nothing under `crates/engine/src/realtime/`. - -## Rulings (root, batch 19, 2026-10-17) - -- **R19-1.** Confirmed: for a moved target with `remaining >= J`, the old ramp runs to its end, then - the cell jumps over `J`. Today the ramp is retargeted over `remaining`; D1 changes that. A gate holds - it (red if the ramp is retargeted at the swap). -- **R19-2 (shared with #1655).** The deferred jump acts on one composed `CellState` per output. K1 - (`crates/automation/src/compose.rs`, after this issue) reuses the decisions this issue adds; events.rs - exposes them `pub(crate)`, no copy. -- **Two PRs.** This issue ships in batch 19a with K1 and #1654. - -## Decision record: F1 (batch 19) - -- **F0 shipped.** `BuiltinFaderBank::ramp_target(lane, channel)` (`crates/builtins/src/lib.rs`, over - the kernel's `target_gain`) is new. `ChannelHold::fader_gain` is renamed `stage_gain`. `bank_hold` - fills it with the ramp target on a free stage and with `fader_gain` on a held stage. The - scalar oracle still gives `None`. `settle_adoption` compares the curve with `stage_gain`. No - other behaviour changes: `long_landing`, `length.max(remaining)` and the B17-R14 flag stay - (D1 and D2 are later tranches). -- **G1, C ABI (Simd8).** `a_level_move_across_a_table_change_keeps_its_length` - (`crates/capi/src/runtime/live_tests.rs`) now asserts bit-equality with the uninterrupted engine - over 44 blocks after the swap, not 1e-3. Timings: Pending, SameTransaction, heard 1, 12, 24, 26 - and 30. VCA and hand moves, both tables: 28 cases, all bit-equal, allocation-free, adopted. -- **G1, host-core (Simd8 and Simd4).** New - `a_level_move_in_flight_across_a_table_only_rebuild_keeps_its_bits` - (`crates/host-core/tests/stored_fader_automation.rs`, `test-support`), with its own carry - harness `level_move_run` (the `carry_run` pattern: a plan exchange, a live offsets write at - block 4, a successor prepared on the committed model with the move). Heard 1, 12, 24, 26 and - 30, VCA and hand moves, both tables: every block bit-equal to the uninterrupted engine, - allocation-free, adopted at the swap block, the strip carries. `successor.rs` is not touched. -- **Red without F0** (`bank_hold` back to `fader_gain`, run once, restored): the C ABI gate fails - at "a VCA move, the ride split at 10,000, Heard(12)" (first difference block 25, sample 79); the - host-core gate fails (first difference block 28). -- **Not asserted in F1: `OUTCOME_EXACT`.** The B17-R14 window still flags the heard 24, 26 and 30 - cases (`a_reposition_swap_while_a_level_move_ramps_reports_a_fallback` is unchanged and green). - The outcome assertion of G1 belongs to the tranche that deletes the flag (D2). -- **Departure: the two-rebuild case of A3 is not in F1.** Two table-only rebuilds 5 blocks apart - around a 500-sample move (heard 2) cannot be bit-equal to the uninterrupted engine: the second - rebuild comes after the move's ramp ends, the stage is then free and settled on a grid ramp, and - the split tables' values differ from the original ride in `f32` rounding, so the cell jumps over - `J` (probe: 1.4e-4 to 1.7e-4 with no move at all; 7.7e-5 with only the later rebuild). Its - reference must be a fresh plan with the new table (G1's "where the table's own values differ" - clause), or its claim is the outcome alone. Moved to the D2 tranche, which owns the outcome - assertion and deletes the tranche-5 fallback gate it replaces. -- Gates run: `cargo fmt --all`; clippy `-D warnings` `--all-targets --all-features` on builtins, - builtins-compiler, host-core, capi; `cargo check --locked --all-targets` (no features) on the - same; tests of builtins, builtins-compiler, automation (all features), host-core - `stored_fader_automation` (31), `stored_mute_automation` (13), `successor_swap` (140), - `session_seek`, `browser_successor`, capi lib (209), host-web `control_parity` (30): all green. - -## Decision record: F2 (batch 19) - -- **D1 shipped (A2).** `crates/automation/src/events.rs`: new `CellState::defer_jump(at)`, - `clear_deferred_jump()` and `deferred_jump()` (small `pub` methods on the existing `deferred` - field; no new state, 32 B). One rule change: a restartable cell holds its grid events while a - deferral is pending (`time < deferred`); a `no_restart` cell is unchanged. The existing deferred - path in `next_in_run` fires the jump (a `Jump` over the block's `J`, change only, or always with - a held event outstanding). -- `crates/builtins-compiler/src/lib.rs`: `settle_adoption` returns `bool` (land at the block's - first sample over `J`), not a length. On a `Reposition` of a free stage with a ramp in flight - and no step of the cell's own at the block's first sample, it holds the ramp again, compares the - curve at the ramp's end with the stage's ramp target, and, if it differs, calls - `defer_jump(node_time_at(span, remaining))` (no landing at the swap; R19-1). The landing at the - block's first sample over `J` stays for no ramp in flight, a held stage, D3 and `Compare`. - Deleted: `long_landing`, its one-sample landing arm, `length.max(remaining)` and the `jump` field - of `AdoptionBlock`. `clear_deferred_jump()` is called on `offsets_jump`'s free path, at the - R17-10 release, and at the two `release_hold_by` sites (through a new `release_lane_hold`). - R19-2: the decisions are the `CellState` methods above, for K1 to reuse; `compose.rs` is not - written here. -- **The B17-R14 flag stays** (F4 deletes it), so F2 asserts no `OUTCOME_EXACT`: the G2 and R19-1 - cases are inside the level-move window and still report the transition fallback. -- **G2 (A3).** C ABI (Simd8): `a_moved_target_jumps_at_a_live_moves_first_free_sample` - (`crates/capi/src/runtime/live_tests.rs`). Host-core (Simd8 and Simd4): - `a_moved_target_jumps_over_the_fader_length_at_the_moves_first_free_sample` - (`crates/host-core/tests/stored_fader_automation.rs`; `level_move_run` now takes a list of - `(block, record)` moves and writes a later one to the successor's cells). Flat ride -3 dB, move - -6 dB over 4,000 from block 4, heard 12 and 30, VCA and hand, rebuild to -5 dB to 6,000 then - -3 dB; reference: a fresh plan with -3 dB to 4,512, -5 dB to 6,000, then -3 dB, same move. Every - sample bit-equal, allocation-free, adopted, the strip carries. Case (c): a second move -9 dB over - 1,200 from block 35 (ends 5,679, after 4,512 + `J`): bit-equal to a fresh plan with the - rebuild's table given both moves from 5,679 on; host-core also asserts the lane still moves on - the move's last sample (the length). -- **R19-1 gate.** `a_moved_target_lets_a_live_move_longer_than_the_jump_end_when_it_was_due` (C - ABI and host-core, same names): heard 1 and 12 (3,872 and 2,464 left, both >= `J`), bit-equal - to the fresh plan with the jump at 4,512. -- **Both hosts.** `a_moved_target_defers_its_jump_alike_on_both_hosts` - (`hosts/host-web/tests/control_parity.rs`): hand move heard 3 blocks, rebuild to -5 dB; same - verdicts, replies, watermark and bits over 60 blocks at the four rates. -- **Unit tests** (`events.rs`): `a_deferred_jump_fires_at_its_time_and_holds_the_grid_before_it`, - `a_cleared_deferral_fires_nothing_and_holds_no_grid`. -- **Mutations (each run once, restored).** Land at the swap instead of deferring (the old - behaviour): R19-1 red (C ABI and host-core, VCA move, heard 1) and G2 red (heard 12). No grid - hold while deferring: G2 and R19-1 red (host-core, heard 12 and heard 1). No clear in - `offsets_jump`: G2 case (c) red (host-core length assertion, VCA move, heard 12). Compare at - the block's first sample (`at = end` removed): G2 stays green (on a flat table both points read - -5 dB, as A0 found for the filed G2); G1 host-core is red (heard 1, the ride split at 10,000). - So `at = end` is held by G1, not by G2. -- **F1 note checked: no.** Two table-only rebuilds 5 blocks apart (riding ride, R1 split at - 10,000 to -6 dB, R2 to -9 dB) against a fresh plan prepared with R2's table: with the 500-sample - move (heard 2), bit-equal up to R2, then max 7.7e-5 (block 15, 4 blocks after R2); with no move, - max 1.7e-4 after R1 (block 9) and 1.4e-4 after R2. Cause: at the adoption the stage's grid-ramp - target (taken from the old table's values) and the new table's curve differ in `f32` rounding, - so the cell jumps over `J` (deferred to the grid ramp's end, or at the block's first sample), - where a fresh plan never jumps. Not fixed (outside D1); the D2 tranche's two-rebuild case needs - an outcome-only claim or a phase-2 decision on rounding-only differences. -- Gates run: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` on automation, - builtins, builtins-compiler, host-core, capi (and host-web `--all-targets`); `cargo check --locked - --all-targets` (no features) on the five; tests: automation, builtins-compiler (all features), - host-core `stored_fader_automation` (33), `stored_mute_automation` (13), `successor_swap` (140), - `session_seek`, `browser_successor`, capi lib (211), host-web `control_parity` (31): all green. - Realtime policy: 168 regions (unchanged). - -## Decision record: F3 and F4 (batch 19) - -- **G4 shipped (A1): the owed discharge is kept and gated.** The line `self.owed = false` in - `HeldRampObligation::block` (`crates/builtins-compiler/src/lib.rs`) now has a doc comment that - names the gate. Gates: `an_exact_unmute_that_frees_a_held_lane_discharges_the_owed_move` on the - C ABI (`crates/capi/src/runtime/live_tests.rs`, quantum 128, hand mutes as in A1) and in - `crates/host-core/tests/stored_fader_automation.rs` (`acted`, every one of `FORMS`: Simd8 on - AVX2, Simd4, the between-render forms and the scalar oracle; fader length 20 ms = 960). Both - compare with a rig that takes only the second hold, from block 9 (sample 1,152) on. Red with the - line deleted (run once, restored): C ABI first difference block 17; host-core first difference - at sample 2,248 (`E`). -- **D2 shipped (A4).** `crates/control-plane/src/control.rs`: deleted the `level_moves` field, - `LevelMoveWindow`, `may_ramp`, `fold_level_move`, `rebuild_level_ramp`, `rebuild_moves_a_level`, - the B17-R14 comment, `automates`, `in_flight`, `level_fallback`, `moved`, `landing`, - `rebuild_level`, both folds and the live commit's `level_ramp`. The outcome is now the #1713 - six-word flag alone (a transaction rebuild with `follow_into_six_word`; a discontinuity is - `Exact`). `reservation.set_outcome` and `ObservedReservation::set_outcome` stay (#1713); the doc - of the latter names #1713 only. `compile.rs`: `level_moves: None` deleted. The B17-R14 clause - is removed from `abi.rs`, `miso_engine_v1.h`, `docs/C_ABI_V1_QUALIFICATION.md` and the two d.ts - files (the #1713 clause kept); `crates/controls/src/automation.rs` no longer names the window. -- **Tests deleted:** `a_reposition_swap_while_a_level_move_ramps_reports_a_fallback`, - `a_second_reposition_inside_a_stretched_landing_reports_a_fallback` (C ABI) and - `a_mute_curve_rebuild_while_a_level_move_ramps_completes_exactly_on_both_hosts` (host-web). -- **Outcome checks (G1, G2, R19-1).** A new helper `assert_exact` (the watermark covers the newest - revision, its last advance reads `OUTCOME_EXACT` alone, transition-fallback count 0) is called by - every case of `a_level_move_across_a_table_change_keeps_its_length` (28 cases) and of - `moved_target_case` (G2, its case (c), and R19-1). Host-web: the B17-R14 parity test is now - `a_hand_move_in_flight_across_a_table_only_rebuild_completes_exactly_alike_on_both_hosts` (no - move and a hand move in flight: `EXACT`, no fallback on either host, same bits and watermark at - the four rates), and `a_moved_target_defers_its_jump_alike_on_both_hosts` asserts the same. -- **Two rebuilds 5 blocks apart (A3): outcome only.** - `table_only_rebuilds_around_a_level_move_complete_exactly` (C ABI): with no move (R1, R2) and - with a 500-sample VCA move heard 2 blocks, each rebuild reports `OUTCOME_EXACT` with no - fallback, every block is allocation-free, every successor is adopted. Bits are not compared: - the split tables' values differ from the ride in `f32` rounding, so the adopting cell jumps - over `J` where a fresh plan does not (F2 record: 7.7e-5 to 1.7e-4). A bit claim needs a - phase-2 decision on rounding-only differences at adoption. The no-move case is also the - "a no-live-move rebuild stays EXACT" check. -- **F2 note fixed.** The B17-R13 step gate's doc no longer names "a long landing"; it says the - step one sample after the swap must jump at its own sample (B17-R13's 8.6e-2 measurement kept - as history). The #1713 test's doc and comment no longer name the level-move window ("the flag - marks its own candidate only"). -- **G4 on both hosts.** `an_exact_unmute_discharges_the_owed_move_alike_on_both_hosts` - (`hosts/host-web/tests/control_parity.rs`): the A1 setup on `t1` at the four rates; the - browser and the C ABI give the same verdicts, replies, watermark (`EXACT`, no transition - fallback) and bits in every block, and from block 24 on both equal a pair of rigs that took - only the second hold. Block 24: the first hold's own difference ends by block 21 at every rate - (the session's latency), and the stale obligation's difference spans blocks 21 to 28 at - 44.1 kHz and 25 to 40 at 96 kHz. Red with the line deleted (run once, restored): block 24 at - 44.1 kHz. -- Gates run: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` on control-plane, - capi, host-core, builtins-compiler, host-web, controls; `cargo check --locked --all-targets` (no - features) on the first five; tests: control-plane (`test-support`), capi lib (211 passed, 1 - ignored), host-core `stored_fader_automation` (34), `stored_mute_automation` (13), - `successor_swap` (140), host-web `control_parity` (31 with the G4 parity case): all green. `scripts/check-capi-abi.sh`, - `check-sdk-generated.sh`, `check-sdk-types.sh`: ok. Realtime policy: 168 regions (unchanged). - -## Decision record: U1 fold 1 (batch 19) - -- **MAJOR-1 fixed (A2 clear list).** `crates/automation/src/events.rs`, `next_in_run`: a - discontinuity jump on a restartable cell drops a pending deferral (the jump compares the curve - with the target and replaces the ramp the deferral waits for; the stale deferral's grid hold - froze the lane on the step's target until the deferral's time). A `no_restart` cell is - unchanged. The A2 clear list is now: `reposition`, a seek landing, `clear_deferred_jump()` (the - free path of `offsets_jump`, the R17-10 release, the `release_hold_by` sites) and a - discontinuity jump of a restartable cell. `defer_jump`'s doc says so. -- **Effect on K1** (corrected in U1 fold 2). A value cursor's own discontinuity jump inside a - column-jump deferral drops that cursor's deferral only when the jump emits (it then replaces the - ramp: its grid moves again after its own jump, held by its own ramp by the cell rule); a jump - that emits nothing keeps the deferral, which holds the grid to the column ramp's end and lands - there. The other cursors keep their deferrals. K = 1 still equals `CellEvents` (one rule); the - K1 tests and the allocation test are - green unchanged. -- **MAJOR-1 gate (host-core, Simd8 and Simd4).** - `a_table_step_inside_a_deferred_moves_ramp_drops_the_deferral` - (`crates/host-core/tests/stored_fader_automation.rs`): VCA and hand moves, heard 1 and 4, the - rebuild `[-5 dB step 0..2,000; linear -8 to -14 dB 2,000..8,000; -14 dB]` against a fresh plan - with -3 dB before 2,000: every sample bit-equal, allocation-free, adopted, carried. Red without - the fix: first difference sample 2,944, max 2.9e-2 (all four cases). -- **MAJOR-1, C ABI (Simd8).** `a_table_step_inside_a_deferred_moves_ramp_drops_the_deferral` - (`crates/capi/src/runtime/live_tests.rs`, through `rebuilt_table_case`, which - `moved_target_case` now calls): VCA and hand moves, heard 1 and 4, every sample bit-equal to the - fresh plan, allocation-free, adopted, `OUTCOME_EXACT` with no transition fallback. Red without - the fix (VCA move, heard 1). -- **MAJOR-1, both hosts.** `a_table_step_inside_a_deferred_moves_ramp_lands_alike_on_both_hosts` - (`hosts/host-web/tests/control_parity.rs`): hand move heard 3 blocks, the same rebuild; one - verdict, reply and committed document per edit, the same bits and watermark (`EXACT`, no - fallback), and every block equal to a reference pair prepared with -3 dB before 2,000 that takes - the same move and an unmoved table-only rebuild at the same block (both rebuilds keep the hand - move's stored offset; a rebuild with the default offset cancels the move). Red without the fix - (44.1 kHz, block 26). -- **G2 parity, fixed.** `a_moved_target_defers_its_jump_alike_on_both_hosts` sent its rebuild - with the default offset, which cancelled the hand move (parity held, the deferral was not - exercised). Its rebuild now carries the committed offset (read from both hosts' snapshots, one - document), and every block before the one holding 4,512 equals a pair given the same move and - no rebuild; a later block differs (the jump is heard). Red once with the adoption landing at the - swap instead of deferring (`settle_adoption`, 44.1 kHz, block 10). A fresh-plan reference with a - real step at 4,512 is not bit-equal in this session (first difference about `J / 2` after - 4,512 at every rate: 4,959, 4,998, 5,400 and 5,478). Corrected in U1 fold 2: 4,512 is the - render time; the post-insert console slot `miso.soft-clip` has 31 samples latency, so the - fader's node time is render time less 31 and the deferred jump fires at node 4,481 (correct by - D1). A reference stepped at node 4,481 is bit-equal over every block (U1 fold 2). -- **MINOR-1.** `table_only_rebuilds_around_a_level_move_complete_exactly` is deleted. Its - no-live-move case moved into G1, `a_level_move_across_a_table_change_keeps_its_length`: a - table-only rebuild with no move is adopted and reports `OUTCOME_EXACT` (outcome only: the split - table's rounding differs). The host-web renamed G1 parity test keeps its own no-move check. -- **NIT.** `a_mute_curve_rebuild_while_a_level_move_ramps_renders_alike_on_both_hosts` - (`control_parity.rs`) restores the deleted test's both-host part: verdicts, replies, the same - bits over 40 blocks and the same watermark; no B17-R14 window claim. -- **MINOR-2 gate (host-core, Simd8 and Simd4).** - `a_hand_mute_inside_a_pending_deferral_lands_the_lane_on_the_new_table_at_the_unmute`: G2 - heard 12 (swap 2,048, F 4,512), hand mute at 2,560, unmute at 3,072 or 5,120, ramps 200 and 0. - `level_move_run_muted` adds live mutes to the carry harness. Inside: the rebuild's table is a - linear -5 to -8 dB to 6,000 (a flat table hides a held grid), and from the mute's end on every - sample is bit-equal to a fresh plan with that table. After F (G2's flat table): bit-equal to the - deferred-jump reference everywhere. Mutation (once each): the R17-10 release without its clear - is red (exact unmute); with all three builtins-compiler clears removed the ramped unmute stays - green, because its owed landing at `E` is a seek landing that clears the deferral. The - `release_lane_hold` sites are not reached by this setup. -- Gates run: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` and no-feature - `cargo check --all-targets` on automation, builtins-compiler, host-core, capi and host-web; - tests: automation (43, compose included), builtins-compiler (all features), host-core - `stored_fader_automation` (36), `stored_mute_automation` (13), `successor_swap` (140), capi - lib (211), host-web `control_parity` (33): green. Realtime policy: 171 regions (unchanged). - -## Decision record: U1 fold 2 (batch 19) - -- **MAJOR-A fixed.** `crates/automation/src/events.rs`, `next_in_run`: fold 1 dropped a pending - deferral at every discontinuity of a restartable cell before `jump_event` ran, and - `jump_event` can emit nothing (the curve already has the target's bits, no held event, the ramp - in flight not lagging): then no ramp replaced the one the deferral waits for. A fired deferral - is still spent before the event; a discontinuity of a restartable cell now drops the deferral - only when its jump emits. `defer_jump`'s doc says so. `no_restart` cells are unchanged. -- **Gate (K1).** `an_unchanged_value_jump_keeps_the_gate_deferral` - (`crates/automation/src/compose/tests.rs`): `gate_and` with the unmute at 1,000 over 480; the - gain flat at -5.9921875 to 1,200, then linear -4 to -8 dB over 1,200..1,712, then -8; its - jump at 1,200 targets the curve at 1,455 (-5.9921875, unchanged). The retargets are exactly - `[(1,000, Jump, 480), (1,480, Jump, 256)]`. Red at the fold's base (4533703d3): `[(1,000, Jump, - 480), (1,216, Grid, 64), (1,280, Grid, 64), ..., (1,664, Grid, 64)]` (the grid cuts the gate - jump, R19-3, and the landing at 1,480 is skipped). -- **Builtins single cell: no host-core gate.** In a single builtins cell the deferral comes from - `settle_adoption`, set only when the curve before the next discontinuity `d` (it is compared at - the move's ramp end, clamped before `d`) differs from the stage's target; the cell's target is - the live move's (`seed_target`), and `reposition` cleared `held`. Every grid event in - `[swap, d)` is held (the deferral holds the grid) and the last one, at or after `d - 64`, aims at - the curve at `d - 1`, which differs, so `held` is set and the jump at `d` emits. That holds - whenever a grid point lies in `[swap, d)`: always when the swap block's first node time is on - the 64-sample grid, as in every host-core harness (quantum 128, no latency). The case is - reachable at a swap block whose first node time is off the grid (a quantum - not a multiple of 64, or upstream latency, such as the 31 samples above) with `d` before the - first grid point; there the fold-2 rule is the D1 behaviour (the deferral is kept, so the grid - does not cut the live move short). The U1 fold 2 verifier reached it in the host-web parity session - (quantum 128 with the 31-sample soft-clip latency): bit-equal to a fresh plan at the four rates, - `OUTCOME_EXACT`. No separate gate: the K1 gate goes red on any regression of the shared rule. -- **MINOR-B fixed.** `a_moved_target_defers_its_jump_alike_on_both_hosts` - (`hosts/host-web/tests/control_parity.rs`) now compares every block with a fresh pair whose - table steps at `FREE` in the fader's node time (4 * 128 + 4,000 - `T1_FADER_LATENCY`), given the - same move and, at the rebuild's block, a table-only rebuild that moves nothing (split at - 950,000), both keeping the stored offset: bit-equal over all 60 blocks at the four rates. - `T1_FADER_LATENCY` = 31 is a named constant (the soft-clip slot's descriptor latency; no host - reader reports one strip's upstream latency, the plan reports only the output's, which `t3`'s - limiter raises). Red with it set to 0 (44.1 kHz, block 38). The unrebuilt pair is removed (the - fresh reference subsumes it). -- Gates run: `cargo fmt --all`; clippy `-D warnings --all-targets --all-features` and no-feature - `cargo check --all-targets` on automation and host-web; tests: automation (all), builtins-compiler - (all features), host-core `stored_fader_automation` (36), `stored_mute_automation` (13), - `successor_swap` (140), capi lib (211, 1 ignored), host-web `control_parity` (37): green. - Realtime policy: 171 regions (unchanged). - -## Note (batch 19a, option C) - -- Batch 19a ships #1711 alone (root ruling, option C after the #1654 B19-R8 check). K1, #1654 and - #1655 are not in 19a. -- The U1 fold 2 rule's K1 gate (`an_unchanged_value_jump_keeps_the_gate_deferral`, - `crates/automation/src/compose/tests.rs`) ships with K1 in batch 19b. -- In 19a the rule ("a discontinuity jump on a restartable cell drops a pending deferral only when - the jump emits") is held by the events.rs unit tests and the fader gates. No earlier events.rs - unit test covered the rule, so 19a adds one: - `a_discontinuity_whose_jump_emits_nothing_keeps_a_pending_deferral` - (`crates/automation/src/events.rs`): a restartable cell flat at -6 to 50, then linear -4 to -8 - over 50..562, fixed jump length 257, seeded at -6 and deferred to 201. The jump at 50 aims at - -6 and emits nothing; the grid at 64, 128 and 192 is held and the deferred jump fires at 201. - Red with the fold-1 rule (the clear without `event.is_some()`, run once, restored): the grid - retargets unheld and nothing fires at 201. diff --git a/docs/handoffs/stored-automation-1058/README.md b/docs/handoffs/stored-automation-1058/README.md index 032e6752e..c979fc8aa 100644 --- a/docs/handoffs/stored-automation-1058/README.md +++ b/docs/handoffs/stored-automation-1058/README.md @@ -837,7 +837,7 @@ batch 9 head"). | #1628 | `.github/ISSUE_SPECS/1628-retire-the-automation-canceled-event.md` | Retire the AUTOMATION_CANCELED event and the cancellation path | #1627 | P1 | | #1629 | `.github/ISSUE_SPECS/1629-delete-the-protocol-automation-queue.md` | Delete the protocol automation queue, its records and counters | #1628 | P1 | | #1668 | `.github/ISSUE_SPECS/1668-reserve-the-c-abi-automation-span-limit.md` | Reserve the C ABI's automation span limit | #1629, #1306 | P2 | -| #1669 | `.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md` | Compare stored automation bits across the browser and native hosts | #1653, #1657, #1658, #1660, #1667, #1399 | Q | +| #1669 | `.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md` | Compare stored automation bits across the browser and native hosts (narrowed 2026-10-17 to the delivered rows; pan, trim, filters, EQ and the matrix fixture are phase 2) | #1653, #1655, #1399 | Q | | #1670 | `.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md` | Compare stored automation bits on AArch64 and across seek times | #1669, #1019 | Q | | #1671 | `.github/ISSUE_SPECS/1671-build-the-stored-automation-console-benchmark-row.md` | Build the stored-automation console benchmark row | #1667 | Q | | #1672 | `.github/ISSUE_SPECS/1672-record-the-stored-automation-console-benchmark-baseline.md` | Record the stored-automation console benchmark baseline | #1671 | Q | From 807b474a495811f5cd94e150b42fa41bb129d62b Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 17 Oct 2026 20:03:49 +0000 Subject: [PATCH 2/6] Pin the stored-automation fixture's simd128 render to native (#1669 T1+T2) One fixture automates strip fader, strip mute, VCA offset and VCA mute, with a following and a non-following send, and is seeked once while it plays. The direct oracle's new leg renders it through the shipped simd128 artifact and asserts the native pin before printing. The pin was written only after both digests were measured equal (6f82e466...86cb6). The native test also proves the automation is heard: clearing the entries moves bits before and after the seek. Co-Authored-By: Claude Opus 5.5 --- ...ed-automation-across-browser-and-native.md | 31 ++ hosts/host-web/src/tests.rs | 127 +++++ .../tests/browser-v1/automation-session.json | 514 ++++++++++++++++++ .../tests/browser-v1/direct-oracle.mjs | 82 +++ hosts/host-web/tests/browser-v1/expected.json | 19 +- scripts/check-browser-expected-resources.py | 8 +- 6 files changed, 777 insertions(+), 4 deletions(-) create mode 100644 hosts/host-web/tests/browser-v1/automation-session.json diff --git a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md index a54fca732..6e9e4c0f2 100644 --- a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md +++ b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md @@ -377,3 +377,34 @@ example `crates/host-core/tests/stored_fader_automation.rs:578`). Do not count t - Send-row (route-row) automation (#1654, behind #1718). - A following send at a latent tap (an insert with latency) in the oracle fixture. - AArch64 and seek-time invariance stay in #1670. + +## Decision record: T1+T2 (batch 20) + +- **D1 shipped as written.** `hosts/host-web/tests/browser-v1/automation-session.json` has the + amendment's sources, tracks, submix, VCA, five routes and five entries with no departure. Every + value was accepted: the "0 then 0" and "-3 then -3" steps, the 64-sample jump spacing and the + VCA `both` mute lane all parse, validate and prepare (the native pin test boots the file + verbatim). Track pan is `left -1.0, right 1.0, smoothing_samples 0` and the bus matrix is the + identity with `smoothing_samples 0`, as in `command-session.json` and `builtins-automation.json`. + The two `content` digests are placeholders of the valid form (the engine streams the PCM). +- **D2.** One transcript helper, `automation_transcript` in `hosts/host-web/src/tests.rs`, and its + JS twin in `runAutomationTimeline`: the same integer formula and chunk schedule. Every seek, + submit and render returns 0 and is asserted on both sides. +- **D3.** Pin test `native_automation_timeline_digest_pins_the_wasm_parity`; `serde_pin` reads + `nativeAutomationPcmF32leSha256` and the `pcmF32leSha256` after `"automationTimeline"`. The test + also asserts the automation is heard: the same session with `automation` cleared renders other + bits before the seek (blocks 0-39) and after it (blocks 40-95). +- **Pin discipline.** The native digest and the simd128 oracle digest were read independently + before any pin was written (the oracle run against a scratch copy of `expected.json` with a + probe pin). Both are + `6f82e4668836ea2c7b977f2c322882aea98d73c0dd285caf41e13ccd18186cb6`. The leg's + `beforeDisposeStatus.memoryBytes` is 2,949,120 (one 64 KiB page above the other legs; the + seek's successor plan is control work). +- **Script.** `DIGEST_PATHS` gains `("automationTimeline", "pcmF32leSha256")`; the docstring and the + comment above it say four digests. +- **Gates (all green).** `cargo fmt --all`; `cargo clippy --locked -p host-web --all-targets + --all-features -- -D warnings`; `cargo check --locked -p host-web --all-targets`; + `cargo test --locked -p host-web --features test-support native_` (the four pin tests); + `bash scripts/check-web-audioworklet.sh --without-metadata-regeneration ...`; + `python3 -B scripts/check-browser-expected-resources.py --artifacts target/ci/qualification-artifacts` + and `--self-test` (32 red mutations); `node direct-oracle.mjs` in compare mode. diff --git a/hosts/host-web/src/tests.rs b/hosts/host-web/src/tests.rs index 31bd40ea2..36a965a52 100644 --- a/hosts/host-web/src/tests.rs +++ b/hosts/host-web/src/tests.rs @@ -2113,6 +2113,124 @@ fn native_set_control_timeline_digest_pins_the_wasm_parity() { ); } +/// The #1669 automation transcript (D2): 96 blocks of 128 frames, one plain session seek to +/// timeline 30,000 under generation 2 before block 40, one chunk per source before every render. +/// It returns each block's output (left plane, then right plane). `direct-oracle.mjs`'s +/// `runAutomationTimeline` replays the same integer PCM formula and chunk schedule. +fn automation_transcript(document: &[u8]) -> Vec> { + const QUANTUM: u32 = 128; + const RATE: u32 = 48_000; + const BLOCKS: u64 = 96; + const SEEK_BLOCK: u64 = 40; + const SEEK_TIMELINE: u64 = 30_000; + // `((f*m) mod 65536 - 32768) / 65536`, exact in `f32`, per source and lane. + const SOURCES: [(&[u8], [u32; 2]); 2] = + [(b"src-t1", [40_503, 30_011]), (b"src-t2", [52_153, 18_749])]; + let pcm = |frame: u64, multiplier: u32| -> f32 { + let frame = u32::try_from(frame).expect("frame fits u32"); + let residue = (frame.wrapping_mul(multiplier) & 0xffff) as i32 - 32_768; + residue as f32 / 65_536.0 + }; + + let options = WebBootOptions { + source_ring_frames: QUANTUM, + ..boot_options(QUANTUM) + }; + let mut host = AudioWorkletEngineHost::boot(document, options).unwrap_or_else(|failure| { + panic!("boot: {}", String::from_utf8_lossy(failure.diagnostic())) + }); + let mut blocks = Vec::new(); + for block in 0..BLOCKS { + if block == SEEK_BLOCK { + assert_eq!( + host.session_seek(None, 2, SEEK_TIMELINE, None), + RESULT_OK, + "the session seek" + ); + } + let (generation, start) = if block < SEEK_BLOCK { + (1, block * u64::from(QUANTUM)) + } else { + (2, SEEK_TIMELINE + (block - SEEK_BLOCK) * u64::from(QUANTUM)) + }; + for (source, multipliers) in SOURCES { + let [left, right] = multipliers.map(|multiplier| { + (start..start + u64::from(QUANTUM)) + .map(|frame| pcm(frame, multiplier)) + .collect::>() + }); + let planes: [&[f32]; 2] = [&left, &right]; + assert_eq!( + host.submit_source(source, generation, start, RATE, &planes, QUANTUM, false), + RESULT_OK, + "block {block} submit" + ); + } + assert_eq!(host.render_next(), RESULT_OK, "block {block} render"); + blocks.push(host.output_pcm().expect("prepared output").to_vec()); + } + assert_eq!(host.status().rendered_quanta, BLOCKS); + blocks +} + +/// #1669 D3: the stored-automation timeline's native digest. `tests/browser-v1/automation-session.json` +/// automates every row the engine renders today -- the strip fader (`both` and one lane, with +/// stored offsets), the strip mute (one lane), the VCA offset and the VCA mute -- with a following +/// and a non-following send, and is seeked once while it plays (D2). `direct-oracle.mjs` renders +/// the same session and transcript through the shipped simd128 artifact and asserts its digest +/// equals the pin this test checks. The digest is over little-endian `f32` words (every left +/// block, then every right block), so it is a bit comparison. +/// +/// Test value: red if the simd128 artifact renders an automation event, a composition (fader with +/// VCA offset, mute OR, the follow gate) or the landing of a plain session seek differently from +/// x86-64 native. The other artifact-versus-native renders have empty automation. +#[test] +fn native_automation_timeline_digest_pins_the_wasm_parity() { + use sha2::{Digest, Sha256}; + + const QUANTUM: usize = 128; + // The fixture file is read verbatim, exactly as `direct-oracle.mjs` reads it. + let document = include_str!("../tests/browser-v1/automation-session.json"); + let blocks = automation_transcript(document.as_bytes()); + + // The automation is heard on both sides of the seek: the same session with no entry renders + // other bits before the seek and after it. + let mut plain = parse_session_json(document).expect("the automation fixture"); + plain.automation.clear(); + let plain = automation_transcript( + canonical_session_json(&plain) + .expect("canonical plain session") + .as_bytes(), + ); + assert_ne!( + blocks[..40], + plain[..40], + "automation heard before the seek" + ); + assert_ne!(blocks[40..], plain[40..], "automation heard after the seek"); + + let mut digest = Sha256::new(); + for channel in 0..2_usize { + for output in &blocks { + for sample in &output[channel * QUANTUM..(channel + 1) * QUANTUM] { + digest.update(sample.to_bits().to_le_bytes()); + } + } + } + let native = engine::hex_lower(&digest.finalize()); + + let expected: serde_pin::Pin = + serde_pin::read(include_str!("../tests/browser-v1/expected.json")); + assert_eq!( + native, expected.native_automation, + "native and the pinned wasm automation-timeline digest must agree bit for bit" + ); + assert_eq!( + native, expected.simd128_automation, + "the shipped simd128 artifact renders this automation timeline to the same bits as native" + ); +} + /// A three-field reader for `expected.json`, so the test needs no JSON dependency. /// /// The file is generated by `direct-oracle.mjs` and is machine-formatted, so the two hashes are @@ -2125,6 +2243,8 @@ mod serde_pin { pub(super) simd128_set_control_timeline: String, pub(super) native_observation: String, pub(super) simd128_observation: String, + pub(super) native_automation: String, + pub(super) simd128_automation: String, } fn hex_after(text: &str, key: &str) -> String { @@ -2159,6 +2279,13 @@ mod serde_pin { .expect("expected.json has no observationTimeline")..], "\"pcmF32leSha256\":", ), + native_automation: hex_after(text, "\"nativeAutomationPcmF32leSha256\":"), + simd128_automation: hex_after( + &text[text + .find("\"automationTimeline\"") + .expect("expected.json has no automationTimeline")..], + "\"pcmF32leSha256\":", + ), } } } diff --git a/hosts/host-web/tests/browser-v1/automation-session.json b/hosts/host-web/tests/browser-v1/automation-session.json new file mode 100644 index 000000000..4a1e93648 --- /dev/null +++ b/hosts/host-web/tests/browser-v1/automation-session.json @@ -0,0 +1,514 @@ +{ + "schema_version": 1, + "session_id": "web-browser-automation", + "revision": "1", + "sample_rate_hz": 48000, + "quantum_frames": 128, + "render_profile": { + "id": "native", + "mode": "single_thread" + }, + "output_profile": { + "id": "main", + "channels": 2, + "sample_format": "f32_planar" + }, + "sources": [ + { + "id": "src-t1", + "content": "blake3:08b2490bf336bde77a8e99c5099cf33f98f310f48f015bda3ffe18412427ffc9", + "channels": 2, + "bit_depth": "32f", + "frames": "48000" + }, + { + "id": "src-t2", + "content": "blake3:83a27b09adbc4ad674939525a95bbe671b15423660508249fc86ee9fea30a1be", + "channels": 2, + "bit_depth": "32f", + "frames": "48000" + } + ], + "console": { + "pre_insert": [], + "post_insert": [] + }, + "tracks": [ + { + "id": "t1", + "source_id": "src-t1", + "left_source_channel": 0, + "right_source_channel": 1, + "builtins": { + "left": { + "polarity_invert": false, + "trim_db": 0.0, + "hpf_hz": 0.0, + "lpf_hz": 0.0, + "delay_samples": 0 + }, + "right": { + "polarity_invert": false, + "trim_db": 0.0, + "hpf_hz": 0.0, + "lpf_hz": 0.0, + "delay_samples": 0 + } + }, + "console": [], + "inserts": { + "effects": [] + }, + "fader": { + "left_db": 0.0, + "right_db": 0.0, + "left_mute": false, + "right_mute": false + }, + "pan": { + "left": -1.0, + "right": 1.0, + "smoothing_samples": 0 + } + }, + { + "id": "t2", + "source_id": "src-t2", + "left_source_channel": 0, + "right_source_channel": 1, + "builtins": { + "left": { + "polarity_invert": false, + "trim_db": 0.0, + "hpf_hz": 0.0, + "lpf_hz": 0.0, + "delay_samples": 0 + }, + "right": { + "polarity_invert": false, + "trim_db": 0.0, + "hpf_hz": 0.0, + "lpf_hz": 0.0, + "delay_samples": 0 + } + }, + "console": [], + "inserts": { + "effects": [] + }, + "fader": { + "left_db": 0.0, + "right_db": 0.0, + "left_mute": false, + "right_mute": false + }, + "pan": { + "left": -1.0, + "right": 1.0, + "smoothing_samples": 0 + } + } + ], + "submixes": [ + { + "id": "bus", + "builtins": { + "left": { + "polarity_invert": false, + "trim_db": 0.0, + "hpf_hz": 0.0, + "lpf_hz": 0.0, + "delay_samples": 0 + }, + "right": { + "polarity_invert": false, + "trim_db": 0.0, + "hpf_hz": 0.0, + "lpf_hz": 0.0, + "delay_samples": 0 + } + }, + "console": [], + "inserts": { + "effects": [] + }, + "fader": { + "left_db": 0.0, + "right_db": 0.0, + "left_mute": false, + "right_mute": false + }, + "matrix": { + "ll": 1.0, + "lr": 0.0, + "rl": 0.0, + "rr": 1.0, + "smoothing_samples": 0 + } + } + ], + "vcas": [ + { + "id": "vca", + "fader": { + "left_db": -2.0, + "right_db": 0.0, + "left_mute": false, + "right_mute": false + }, + "members": [ + "t1", + "t2" + ] + } + ], + "control_smoothing": { + "kind": "default" + }, + "outputs": [ + { + "id": "main-out" + } + ], + "routes": [ + { + "id": "t1-main", + "source": { + "kind": "track", + "track_id": "t1", + "tap": "post_pan" + }, + "destination": { + "kind": "output_input", + "output_id": "main-out" + }, + "channel_matrix": { + "ll": 1.0, + "lr": 0.0, + "rl": 0.0, + "rr": 1.0 + }, + "gain_db": 0.0, + "mute": false, + "follows_mute": false + }, + { + "id": "t2-main", + "source": { + "kind": "track", + "track_id": "t2", + "tap": "post_pan" + }, + "destination": { + "kind": "output_input", + "output_id": "main-out" + }, + "channel_matrix": { + "ll": 1.0, + "lr": 0.0, + "rl": 0.0, + "rr": 1.0 + }, + "gain_db": 0.0, + "mute": false, + "follows_mute": false + }, + { + "id": "t1-bus", + "source": { + "kind": "track", + "track_id": "t1", + "tap": "pre_fader" + }, + "destination": { + "kind": "submix_input", + "submix_id": "bus" + }, + "channel_matrix": { + "ll": 1.0, + "lr": 0.0, + "rl": 0.0, + "rr": 1.0 + }, + "gain_db": 0.0, + "mute": false, + "follows_mute": true + }, + { + "id": "t2-bus", + "source": { + "kind": "track", + "track_id": "t2", + "tap": "pre_fader" + }, + "destination": { + "kind": "submix_input", + "submix_id": "bus" + }, + "channel_matrix": { + "ll": 1.0, + "lr": 0.0, + "rl": 0.0, + "rr": 1.0 + }, + "gain_db": 0.0, + "mute": false, + "follows_mute": false + }, + { + "id": "bus-main", + "source": { + "kind": "submix", + "submix_id": "bus", + "tap": "post_pan" + }, + "destination": { + "kind": "output_input", + "output_id": "main-out" + }, + "channel_matrix": { + "ll": 1.0, + "lr": 0.0, + "rl": 0.0, + "rr": 1.0 + }, + "gain_db": 0.0, + "mute": false, + "follows_mute": false + } + ], + "automation": [ + { + "id": "t1-fader", + "target": { + "owner": "strip", + "owner_id": "t1", + "section": "builtins", + "control": 5, + "lane": "both" + }, + "segments": [ + { + "shape": "linear", + "start_sample": "256", + "end_sample": "2304", + "start_value": 0.0, + "end_value": -12.0, + "unit": "db" + }, + { + "shape": "step", + "start_sample": "3001", + "end_sample": "3601", + "start_value": -6.0, + "end_value": -9.0, + "unit": "db" + }, + { + "shape": "linear", + "start_sample": "29000", + "end_sample": "33000", + "start_value": -9.0, + "end_value": 3.0, + "unit": "db" + }, + { + "shape": "step", + "start_sample": "34000", + "end_sample": "35000", + "start_value": 0.0, + "end_value": -3.0, + "unit": "db" + } + ], + "offset": { + "left_db": -1.5, + "right_db": 0.0 + } + }, + { + "id": "t1-mute", + "target": { + "owner": "strip", + "owner_id": "t1", + "section": "builtins", + "control": 6, + "lane": "left" + }, + "segments": [ + { + "shape": "step", + "start_sample": "0", + "end_sample": "1000", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "1600", + "end_sample": "4200", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "4800", + "end_sample": "29800", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "30600", + "end_sample": "33500", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "34000", + "end_sample": "36000", + "start_value": 0.0, + "end_value": 0.0, + "unit": "linear" + } + ], + "offset": { + "left_db": 0.0, + "right_db": 0.0 + } + }, + { + "id": "t2-fader", + "target": { + "owner": "strip", + "owner_id": "t2", + "section": "builtins", + "control": 5, + "lane": "right" + }, + "segments": [ + { + "shape": "linear", + "start_sample": "1024", + "end_sample": "4096", + "start_value": -3.0, + "end_value": 0.0, + "unit": "db" + }, + { + "shape": "linear", + "start_sample": "29900", + "end_sample": "32000", + "start_value": 0.0, + "end_value": -18.0, + "unit": "db" + } + ], + "offset": { + "left_db": 0.0, + "right_db": 0.0 + } + }, + { + "id": "vca-offset", + "target": { + "owner": "vca", + "owner_id": "vca", + "section": "own", + "control": 1, + "lane": "right" + }, + "segments": [ + { + "shape": "linear", + "start_sample": "512", + "end_sample": "3584", + "start_value": 0.0, + "end_value": -6.0, + "unit": "db" + }, + { + "shape": "step", + "start_sample": "4096", + "end_sample": "4352", + "start_value": 2.0, + "end_value": 0.0, + "unit": "db" + }, + { + "shape": "linear", + "start_sample": "29500", + "end_sample": "31500", + "start_value": -4.0, + "end_value": 2.0, + "unit": "db" + }, + { + "shape": "step", + "start_sample": "33000", + "end_sample": "35500", + "start_value": -3.0, + "end_value": -3.0, + "unit": "db" + } + ], + "offset": { + "left_db": 0.0, + "right_db": -0.5 + } + }, + { + "id": "vca-mute", + "target": { + "owner": "vca", + "owner_id": "vca", + "section": "own", + "control": 2, + "lane": "both" + }, + "segments": [ + { + "shape": "step", + "start_sample": "0", + "end_sample": "2500", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "2900", + "end_sample": "29900", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "30100", + "end_sample": "35000", + "start_value": 0.0, + "end_value": 1.0, + "unit": "linear" + }, + { + "shape": "step", + "start_sample": "35500", + "end_sample": "36000", + "start_value": 0.0, + "end_value": 0.0, + "unit": "linear" + } + ], + "offset": { + "left_db": 0.0, + "right_db": 0.0 + } + } + ] +} diff --git a/hosts/host-web/tests/browser-v1/direct-oracle.mjs b/hosts/host-web/tests/browser-v1/direct-oracle.mjs index 05ab941f4..d59e457fc 100644 --- a/hosts/host-web/tests/browser-v1/direct-oracle.mjs +++ b/hosts/host-web/tests/browser-v1/direct-oracle.mjs @@ -461,6 +461,71 @@ async function runObservationTimeline(modulePath, sessionDocument, sourceId, tap }; } +/// Issue #1669 D2/D3: the stored-automation timeline, through the shipped artifact. The same +/// session, source feed and seek the native twin +/// (`tests::native_automation_timeline_digest_pins_the_wasm_parity`) runs. +/// +/// `automation-session.json` automates the strip fader, the strip mute, the VCA offset and the VCA +/// mute, with a following and a non-following send. 96 blocks; before block 40 one plain session +/// seek to timeline 30,000 under generation 2. Before every render each source gets exactly one +/// 128-frame chunk (the ring holds one quantum): generation 1 from `k*128` before the seek, +/// generation 2 from `30000 + (k-40)*128` after it. The PCM at source frame `f` is +/// `((f*m) mod 65536 - 32768) / 65536`, exact in `f32`. +const AUTOMATION_BLOCKS = 96; +const AUTOMATION_SEEK_BLOCK = 40; +const AUTOMATION_SEEK_TIMELINE = 30000; +const AUTOMATION_SOURCES = [["src-t1", [40503, 30011]], ["src-t2", [52153, 18749]]]; +async function runAutomationTimeline(modulePath, sessionDocument) { + const { instance } = await WebAssembly.instantiate(await readFile(modulePath), {}); + const exports = instance.exports; + const handle = boot(exports, sessionDocument); + + // The seek is control work and may grow the memory, so the buffers are re-read at every use. + const feed = (sourceId, multipliers, generation, start) => { + const id = new TextEncoder().encode(sourceId); + const idPointer = exports.miso_engine_web_v1_buffer_ptr(handle, BUFFER_SOURCE_ID); + new Uint8Array(exports.memory.buffer, idPointer, id.byteLength).set(id); + const pcmPointer = exports.miso_engine_web_v1_buffer_ptr(handle, BUFFER_SOURCE_PCM); + const pcm = new Float32Array(exports.memory.buffer, pcmPointer, QUANTUM * 2); + multipliers.forEach((multiplier, lane) => { + for (let index = 0; index < QUANTUM; index += 1) { + const residue = (Math.imul(start + index, multiplier) >>> 0) & 0xffff; + pcm[lane * QUANTUM + index] = (residue - 32768) / 65536; + } + }); + return exports.miso_engine_web_v1_source_submit( + handle, id.byteLength, BigInt(generation), BigInt(start), 2, QUANTUM, 0, + ); + }; + const blocks = []; + for (let block = 0; block < AUTOMATION_BLOCKS; block += 1) { + if (block === AUTOMATION_SEEK_BLOCK) { + assert.equal( + exports.miso_engine_web_v1_session_seek(handle, 2n, BigInt(AUTOMATION_SEEK_TIMELINE)), + 0, + "the session seek", + ); + } + const [generation, start] = block < AUTOMATION_SEEK_BLOCK + ? [1, block * QUANTUM] + : [2, AUTOMATION_SEEK_TIMELINE + (block - AUTOMATION_SEEK_BLOCK) * QUANTUM]; + for (const [sourceId, multipliers] of AUTOMATION_SOURCES) { + assert.equal(feed(sourceId, multipliers, generation, start), 0, `block ${block} submit`); + } + blocks.push(render(exports, handle)); + } + const beforeDisposeStatus = status(exports, handle); + const pcm = [blocks.flatMap((block) => block[0]), blocks.flatMap((block) => block[1])]; + assert.equal(exports.miso_engine_web_v1_dispose(handle), 0); + return { + blocks: AUTOMATION_BLOCKS, + seekBlock: AUTOMATION_SEEK_BLOCK, + seekTimelineSample: AUTOMATION_SEEK_TIMELINE, + beforeDisposeStatus, + pcmF32leSha256: pcmSha256(pcm), + }; +} + async function main() { if (process.argv.length !== 4) { throw new Error("usage: direct-oracle.mjs ARTIFACT_DIRECTORY EXPECTED_JSON"); @@ -574,6 +639,23 @@ async function main() { nativeObservation, "native and simd128 must render this observation timeline to identical bits", ); + // Issue #1669 D3: the stored-automation timeline, pinned the same way and asserted before any + // print. + actual.automationTimeline = await runAutomationTimeline( + artifact, await readFile(path.join(fixtureDirectory, "automation-session.json")), + ); + const nativeAutomation = expected.directOracle?.nativeAutomationPcmF32leSha256; + assert.equal( + typeof nativeAutomation, + "string", + "expected.json must carry the native automation-timeline digest pin", + ); + actual.nativeAutomationPcmF32leSha256 = nativeAutomation; + assert.equal( + actual.automationTimeline.pcmF32leSha256, + nativeAutomation, + "native and simd128 must render this automation timeline to identical bits", + ); if (process.env.MISO_ENGINE_WEB_ORACLE_PRINT === "1") { process.stdout.write(`${JSON.stringify(actual, null, 2)}\n`); return; diff --git a/hosts/host-web/tests/browser-v1/expected.json b/hosts/host-web/tests/browser-v1/expected.json index 409b59f71..0c7717e37 100644 --- a/hosts/host-web/tests/browser-v1/expected.json +++ b/hosts/host-web/tests/browser-v1/expected.json @@ -130,6 +130,23 @@ "unboundReason": 7, "pcmF32leSha256": "4d1412f875886235dd9295ebfb109dfd54a63004eeea6615b7c8711dbc2203e5" }, - "nativeObservationPcmF32leSha256": "4d1412f875886235dd9295ebfb109dfd54a63004eeea6615b7c8711dbc2203e5" + "nativeObservationPcmF32leSha256": "4d1412f875886235dd9295ebfb109dfd54a63004eeea6615b7c8711dbc2203e5", + "automationTimeline": { + "blocks": 96, + "seekBlock": 40, + "seekTimelineSample": 30000, + "beforeDisposeStatus": { + "state": 2, + "lastResult": 0, + "backend": 1, + "sampleRateHz": 48000, + "quantumFrames": 128, + "nextAbsoluteSample": "12288", + "renderedQuanta": "96", + "memoryBytes": 2949120 + }, + "pcmF32leSha256": "6f82e4668836ea2c7b977f2c322882aea98d73c0dd285caf41e13ccd18186cb6" + }, + "nativeAutomationPcmF32leSha256": "6f82e4668836ea2c7b977f2c322882aea98d73c0dd285caf41e13ccd18186cb6" } } diff --git a/scripts/check-browser-expected-resources.py b/scripts/check-browser-expected-resources.py index 9c9717c6c..1de76598c 100755 --- a/scripts/check-browser-expected-resources.py +++ b/scripts/check-browser-expected-resources.py @@ -31,8 +31,9 @@ `hosts/host-web/tests/retained_ceilings.rs`. Everything else in the printed document -- the status snapshots, the result transcripts, the -SET_CONTROL and observation timelines and the three PCM digests, which `direct-oracle.mjs` has already -asserted equal to the native digests before printing -- must equal `expected.json` exactly. +SET_CONTROL, observation and automation timelines and the four PCM digests, which +`direct-oracle.mjs` has already asserted equal to the native digests before printing -- must equal +`expected.json` exactly. `--self-test` proves the comparator discriminates before it is trusted: it fabricates a green document at the ceilings, and then requires every red mutation below -- one byte over a ceiling, @@ -116,12 +117,13 @@ CEILINGS_KEY = "resourceCeilings" -# The three digests `expected.json` pins. Named here so a report that stops carrying one is a +# The four digests `expected.json` pins. Named here so a report that stops carrying one is a # failure rather than a silently skipped comparison. DIGEST_PATHS = ( ("simd128", "pcmF32leSha256"), ("setControlTimeline", "pcmF32leSha256"), ("observationTimeline", "pcmF32leSha256"), + ("automationTimeline", "pcmF32leSha256"), ) From 589403170b98b57af6b687becf83977bfa94ea71 Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 17 Oct 2026 20:05:34 +0000 Subject: [PATCH 3/6] Compare the stored-automation fixture across the C ABI and the browser host (#1669 T3) The new control_parity case renders the batch-20 fixture through the C ABI and through AudioWorkletEngineHost with one session seek at block 40, and compares every block's two planes bit for bit. A one-bit plant in the C ABI read path at block >= 40 turned it red at block 40 (probe reverted). Co-Authored-By: Claude Opus 5.5 --- ...ed-automation-across-browser-and-native.md | 13 +++ hosts/host-web/tests/control_parity.rs | 80 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md index 6e9e4c0f2..f781a5680 100644 --- a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md +++ b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md @@ -408,3 +408,16 @@ example `crates/host-core/tests/stored_fader_automation.rs:578`). Do not count t `bash scripts/check-web-audioworklet.sh --without-metadata-regeneration ...`; `python3 -B scripts/check-browser-expected-resources.py --artifacts target/ci/qualification-artifacts` and `--self-test` (32 red mutations); `node direct-oracle.mjs` in compare mode. + +### T3 + +- **D4 shipped.** `the_automation_fixture_renders_alike_on_both_hosts_across_one_session_seek` in + `hosts/host-web/tests/control_parity.rs` (under `test-support`). The browser side boots the + fixture bytes with `Rig::boot`; the C ABI side builds `CAbi::new` from `parse_session_json` of + the same bytes. Both run the D2 schedule with direct submits and `session_seek`; every seek, + submit and render returns OK, and each block's left and right planes are compared with `bits()`. + Both hosts agree on all 96 blocks. +- **Ring.** `Rig::boot` and `limits()` both use a ring of two quanta, not one; the transcript + submits one quantum per block, so the ring size does not change what renders. +- **Probe (not committed).** Flipping the low bit of the first C ABI sample at blocks >= 40 in the + test's read path turns the case red at block 40, left plane. diff --git a/hosts/host-web/tests/control_parity.rs b/hosts/host-web/tests/control_parity.rs index 472835bd2..59bb54980 100644 --- a/hosts/host-web/tests/control_parity.rs +++ b/hosts/host-web/tests/control_parity.rs @@ -7501,3 +7501,83 @@ fn the_reach_pair_cap_bounds_vca_automation_alike_on_both_hosts() { b"host.resource.vca_reach_pairs\t$\n".to_vec() ); } + +// --------------------------------------------------------------------------------------------- +// The #1669 automation fixture across one session seek (D4) +// --------------------------------------------------------------------------------------------- + +/// `tests/browser-v1/automation-session.json` (the strip fader, strip mute, VCA offset and VCA +/// mute, with a following and a non-following send) runs the #1669 D2 transcript on the browser +/// host and the C ABI: 96 blocks, one plain session seek to timeline 30,000 under generation 2 +/// before block 40, one chunk per source before every render. Every block's two planes have the +/// same bits. +/// +/// Test value: red if the two hosts prepare the whole automation fixture differently or land its +/// session seek differently; the only other cross-host seek case automates the mute row alone, and +/// the VCA cases do not seek. +#[cfg(feature = "test-support")] +#[test] +fn the_automation_fixture_renders_alike_on_both_hosts_across_one_session_seek() { + const BLOCKS: u64 = 96; + const SEEK_BLOCK: u64 = 40; + const SEEK_TIMELINE: u64 = 30_000; + // The D2 PCM, `((f*m) mod 65536 - 32768) / 65536`, exact in `f32`, per source and lane. + const SOURCES: [(&str, [u32; 2]); 2] = + [("src-t1", [40_503, 30_011]), ("src-t2", [52_153, 18_749])]; + let pcm = |frame: u64, multiplier: u32| -> f32 { + let frame = u32::try_from(frame).expect("frame fits u32"); + let residue = (frame.wrapping_mul(multiplier) & 0xffff) as i32 - 32_768; + residue as f32 / 65_536.0 + }; + + let document = include_str!("browser-v1/automation-session.json"); + let model = session::parse_session_json(document).expect("the automation fixture"); + let mut browser = Rig::boot(document.as_bytes(), model.sample_rate_hz); + let mut c_abi = CAbi::new(&model); + let plane = QUANTUM as usize; + for block in 0..BLOCKS { + if block == SEEK_BLOCK { + assert_eq!( + browser.host.session_seek(None, 2, SEEK_TIMELINE, None), + host_web::RESULT_OK, + "browser session seek" + ); + c_abi.session_seek(2, SEEK_TIMELINE, None); + } + let (generation, start) = if block < SEEK_BLOCK { + (1, block * u64::from(QUANTUM)) + } else { + (2, SEEK_TIMELINE + (block - SEEK_BLOCK) * u64::from(QUANTUM)) + }; + for (source, multipliers) in SOURCES { + let [left, right] = multipliers.map(|multiplier| { + (start..start + u64::from(QUANTUM)) + .map(|frame| pcm(frame, multiplier)) + .collect::>() + }); + assert_eq!( + browser.host.submit_source( + source.as_bytes(), + generation, + start, + model.sample_rate_hz, + &[&left, &right], + QUANTUM, + false, + ), + host_web::RESULT_OK, + "block {block}: browser source {source}" + ); + c_abi.submit_planes(source, generation, start, [&left, &right]); + } + assert_eq!( + browser.host.render_next(), + host_web::RESULT_OK, + "block {block}: browser render" + ); + let b = bits(browser.host.output_pcm().expect("browser output")); + let c = c_abi.render(block); + assert_eq!(b[..plane], c[..plane], "block {block}: left plane bits"); + assert_eq!(b[plane..], c[plane..], "block {block}: right plane bits"); + } +} From ac21b2f4d3a1d14227f44cb5bc65a8abe8e4c8d3 Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 17 Oct 2026 20:18:18 +0000 Subject: [PATCH 4/6] Draft TBD55: ceiling the direct oracle's status memory bytes (batch 20 review placeholder) Co-Authored-By: Claude Opus 5.5 --- ...g-the-direct-oracle-status-memory-bytes.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md diff --git a/.github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md b/.github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md new file mode 100644 index 000000000..866650911 --- /dev/null +++ b/.github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md @@ -0,0 +1,58 @@ +# Ceiling the direct oracle's status memory bytes + +Proposed by the controls coordinator from batch 20 (#1669 review, NIT/placeholder; 2026-10-17). +A draft for root to file. Not phase-2 automation work: test hygiene for the browser identity +gate. Code anchors at `589403170`. + +## Product outcome + +The direct oracle's printed `beforeDisposeStatus.memoryBytes` (and any other status field that +is a resource size, not a contract fact) is a ceiling, not an exact pin. A layout change that grows +the Wasm linear memory by one page inside its budget does not turn the gate red; growth past the +budget does. + +## Context + +- `scripts/check-browser-expected-resources.py` splits resource rows into `EXACT_ROWS` and + `CEILING_ROWS` (#1060, owner decision 4: budgets, not exact counts). Everything else in the + printed document must equal `expected.json` exactly (the docstring, `:33-36`). +- The status snapshots carry `memoryBytes`, the Wasm memory size. It falls in "everything else", + so it is pinned to the byte: 2,883,584 in the identity, SET_CONTROL and observation legs + (`hosts/host-web/tests/browser-v1/expected.json:39`, `:49`, `:117`) and 2,949,120 in the batch-20 + automation leg (`:146`). +- AGENTS.md (test value): an exact resource byte count is used only for a wire, ABI or on-disk + format. `memoryBytes` is none of these. The batch-20 reviewer flagged it; the older legs had the + same pin before batch 20. + +## Decisions to make at filing + +- D1. Which status fields are resource sizes (at least `memoryBytes`) and which are contract facts. +- D2. The ceiling rule: reuse the `CEILING_ROWS` rule (value plus 10 %, rounded up to a 64 KiB page + for memory), or a separate status-ceiling table. + +## Deliverables + +- The script classifies the status resource fields as ceilings, with a `--self-test` mutation for + one page over the ceiling. +- `expected.json` carries their ceilings; the oracle prints the measured values. + +## Authorized paths + +- `scripts/check-browser-expected-resources.py`, `hosts/host-web/tests/browser-v1/expected.json`, + and, if the oracle compares the whole document, `hosts/host-web/tests/browser-v1/direct-oracle.mjs`. + +## Objective gates + +1. `python3 -B scripts/check-browser-expected-resources.py --self-test` catches one page over a + status ceiling. +2. `python3 -B scripts/check-browser-expected-resources.py --artifacts target/ci/qualification-artifacts` + passes on the shipped artifact. + +## Test value + +The new self-test mutation turns red if a status resource field stops being ceilinged or grows past +its budget, which the exact pin catches today only as noise on every layout change. + +## Dependencies + +None. #1669 (batch 20) adds the fourth leg it covers. From 30a1dd8d14f72c51b9fa6f19ce536fa7a380c0c7 Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 17 Oct 2026 20:21:10 +0000 Subject: [PATCH 5/6] Let the automation fixture's seek landing be heard (#1669 review fold 1) At timeline 30,000 every member lane sat inside a closed mute, so the fader and VCA offset landing at the seek were only remembered and never heard. The VCA mute steps move so the right lanes are open at the seek; t1's left lane still lands inside its own closed mute. The pin is re-minted from both sides, measured equal (a81d883b...1363). The pin test proves the landing is heard (red on the old fixture). The #1670 amendment states the narrowed seek bound. The self-test gains the automation digest mutation. Co-Authored-By: Claude Opus 5.5 --- ...ed-automation-across-browser-and-native.md | 20 ++++++++-- ...-automation-on-aarch64-and-across-seeks.md | 7 ++++ hosts/host-web/src/tests.rs | 40 +++++++++++++++++++ .../tests/browser-v1/automation-session.json | 8 ++-- hosts/host-web/tests/browser-v1/expected.json | 4 +- scripts/check-browser-expected-resources.py | 1 + 6 files changed, 71 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md index f781a5680..b793816a8 100644 --- a/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md +++ b/.github/ISSUE_SPECS/1669-compare-stored-automation-across-browser-and-native.md @@ -227,9 +227,10 @@ smoothing). Then: - `vca-offset`, `vca` control 1, lane `right`, offset `{left_db: 0.0, right_db: -0.5}`: linear 512-3584, 0 to -6; step 4096-4352, +2 then 0; linear 29500-31500, -4 to +2; step 33000-35500, -3 then -3. - - `vca-mute`, `vca` control 2, lane `both`: step 0-2500, 0 then 1; step 2900-29900, 0 then 1; - step 30100-35000, 0 then 1; step 35500-36000, 0 then 0. Muted over 2500-2900, 29900-30100 - and 35000-35500. + - `vca-mute`, `vca` control 2, lane `both`: step 0-2000, 0 then 1; step 2400-30200, 0 then 1; + step 30400-35000, 0 then 1; step 35500-36000, 0 then 0. Muted over 2000-2400, 30200-30400 + and 35000-35500 (review fold 1, M1: the seek target 30,000 and the step at 3001 are outside + every VCA mute hold, so their landings are heard). - **What this covers.** A `both` entry and single-lane entries. Lanes driven by their own curve, by a VCA curve, by both, and plain lanes. Linear segments, steps on and off the grid, gaps, a hold before the first segment and after the last one, and non-zero stored offsets. Each entry @@ -421,3 +422,16 @@ example `crates/host-core/tests/stored_fader_automation.rs:578`). Do not count t submits one quantum per block, so the ring size does not change what renders. - **Probe (not committed).** Flipping the low bit of the first C ABI sample at blocks >= 40 in the test's read path turns the case red at block 40, left plane. + +### Review fold 1 + +- **M1.** At timeline 30,000 every member lane was inside a closed mute (VCA mute 29900-30100; + t1 left 29800-30600). A mute hold only remembers a seek's value (`docs/SESSION_SCHEMA_V1.md`, + the fader-row paragraph), so the fader and VCA-offset landings were never heard, and the step + at 3001 sat inside the VCA unmute hold. The `vca-mute` segments are now step 0-2000, 2400-30200, + 30400-35000 and 35500-36000 (D1 above is edited in place); t1 left still lands inside a closed + mute. Re-minted pin, measured on both sides independently and equal: + `a81d883b3f83eaf629bd78db149d1a39825c821c6cb2e9a53506341a97ed1363` (native pin test, and the + oracle against the unchanged artifact). The pin test now also asserts the landing is heard: + two copies whose `t1-fader` holds -6 dB or +20 dB over 30000-30064 only render other bits in + block 40. It is red on the fixture before this fold and green after it. The expected-resources self-test gains the `automationTimeline` digest mutation. diff --git a/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md b/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md index ca8eca8f5..570c9fcac 100644 --- a/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md +++ b/.github/ISSUE_SPECS/1670-compare-stored-automation-on-aarch64-and-across-seeks.md @@ -134,3 +134,10 @@ batch 20 amendment). timeline 30,000 at block 40, generation 2. - The matrix fixture and the rows that #1657, #1658, #1660 and #1667 add are phase-2 extensions, as in #1669. +- The narrowed fixture has no insert, so D2's "equal from 256 samples after" bound (a delay's + held jump plus its crossfade) does not apply. The longest ramp a seek leaves is a mute hold + (`mute_ms`, 480 samples) followed by a fader ramp to the curve (`fader_ms`, 960 samples), so + the bound is 1,440 samples after the seek at 48 kHz (`docs/SESSION_SCHEMA_V1.md`, the + fader-row paragraph). A pre-seek unmute ramp still running at the seek carries across it, as + documented. Read "five pin tests" in gate 1 and the hazards as four. The LANE-3 compressor and + limiter hazard does not apply: the narrowed fixture has no insert. diff --git a/hosts/host-web/src/tests.rs b/hosts/host-web/src/tests.rs index 36a965a52..4f235b294 100644 --- a/hosts/host-web/src/tests.rs +++ b/hosts/host-web/src/tests.rs @@ -2208,6 +2208,46 @@ fn native_automation_timeline_digest_pins_the_wasm_parity() { "automation heard before the seek" ); assert_ne!(blocks[40..], plain[40..], "automation heard after the seek"); + // The seek's landing is heard (review fold 1, M1): `t1-fader` holds `value` dB over + // 30000-30064 only, so two values differ only where the seek lands, and block 40 differs. + let landing = |value: f32| { + let mut model = parse_session_json(document).expect("the automation fixture"); + let ride = model + .automation + .iter_mut() + .find(|entry| entry.id.as_str() == "t1-fader") + .expect("t1-fader"); + let segment = |shape, span: (u64, u64), values: (f32, f32)| session::AutomationSegment { + shape, + start_sample: span.0, + end_sample: span.1, + start_value: values.0, + end_value: values.1, + unit: session::ParameterUnit::Db, + }; + let (step, linear) = ( + session::AutomationShape::Step, + session::AutomationShape::Linear, + ); + ride.segments.splice( + 2..3, + [ + segment(step, (29_000, 30_000), (-9.0, value)), + segment(step, (30_000, 30_064), (value, -9.0)), + segment(linear, (30_064, 33_000), (-9.0, 3.0)), + ], + ); + automation_transcript( + canonical_session_json(&model) + .expect("canonical") + .as_bytes(), + ) + }; + assert_ne!( + landing(-6.0)[40], + landing(20.0)[40], + "the seek's fader landing is heard" + ); let mut digest = Sha256::new(); for channel in 0..2_usize { diff --git a/hosts/host-web/tests/browser-v1/automation-session.json b/hosts/host-web/tests/browser-v1/automation-session.json index 4a1e93648..1b1f7ad55 100644 --- a/hosts/host-web/tests/browser-v1/automation-session.json +++ b/hosts/host-web/tests/browser-v1/automation-session.json @@ -475,22 +475,22 @@ { "shape": "step", "start_sample": "0", - "end_sample": "2500", + "end_sample": "2000", "start_value": 0.0, "end_value": 1.0, "unit": "linear" }, { "shape": "step", - "start_sample": "2900", - "end_sample": "29900", + "start_sample": "2400", + "end_sample": "30200", "start_value": 0.0, "end_value": 1.0, "unit": "linear" }, { "shape": "step", - "start_sample": "30100", + "start_sample": "30400", "end_sample": "35000", "start_value": 0.0, "end_value": 1.0, diff --git a/hosts/host-web/tests/browser-v1/expected.json b/hosts/host-web/tests/browser-v1/expected.json index 0c7717e37..5e2a63333 100644 --- a/hosts/host-web/tests/browser-v1/expected.json +++ b/hosts/host-web/tests/browser-v1/expected.json @@ -145,8 +145,8 @@ "renderedQuanta": "96", "memoryBytes": 2949120 }, - "pcmF32leSha256": "6f82e4668836ea2c7b977f2c322882aea98d73c0dd285caf41e13ccd18186cb6" + "pcmF32leSha256": "a81d883b3f83eaf629bd78db149d1a39825c821c6cb2e9a53506341a97ed1363" }, - "nativeAutomationPcmF32leSha256": "6f82e4668836ea2c7b977f2c322882aea98d73c0dd285caf41e13ccd18186cb6" + "nativeAutomationPcmF32leSha256": "a81d883b3f83eaf629bd78db149d1a39825c821c6cb2e9a53506341a97ed1363" } } diff --git a/scripts/check-browser-expected-resources.py b/scripts/check-browser-expected-resources.py index 1de76598c..18093b53a 100755 --- a/scripts/check-browser-expected-resources.py +++ b/scripts/check-browser-expected-resources.py @@ -586,6 +586,7 @@ def apply(pair: tuple) -> tuple: ("the identity-session digest moves", move_digest("simd128"), None), ("the SET_CONTROL-timeline digest moves", move_digest("setControlTimeline"), None), ("the observation-timeline digest moves", move_digest("observationTimeline"), None), + ("the automation-timeline digest moves", move_digest("automationTimeline"), None), ("the render transcript moves", move_status("nextAbsoluteSample"), None), ("the oracle prints a different schema", with_schema("miso.web.browser.v1"), None), # The boot-options vocabulary, and the exact #143 regression this fixture shipped with. From 7f26c106d53eff437b1a4ff2ec814b1ce3669b98 Mon Sep 17 00:00:00 2001 From: BL Date: Sat, 17 Oct 2026 21:04:59 +0000 Subject: [PATCH 6/6] Number the batch 20 placeholder: #1720 Co-Authored-By: Claude Opus 5.5 --- ...md => 1720-ceiling-the-direct-oracle-status-memory-bytes.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/ISSUE_SPECS/{TBD55-ceiling-the-direct-oracle-status-memory-bytes.md => 1720-ceiling-the-direct-oracle-status-memory-bytes.md} (96%) diff --git a/.github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md b/.github/ISSUE_SPECS/1720-ceiling-the-direct-oracle-status-memory-bytes.md similarity index 96% rename from .github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md rename to .github/ISSUE_SPECS/1720-ceiling-the-direct-oracle-status-memory-bytes.md index 866650911..a7903f7cb 100644 --- a/.github/ISSUE_SPECS/TBD55-ceiling-the-direct-oracle-status-memory-bytes.md +++ b/.github/ISSUE_SPECS/1720-ceiling-the-direct-oracle-status-memory-bytes.md @@ -1,7 +1,7 @@ # Ceiling the direct oracle's status memory bytes Proposed by the controls coordinator from batch 20 (#1669 review, NIT/placeholder; 2026-10-17). -A draft for root to file. Not phase-2 automation work: test hygiene for the browser identity +Filed by root. Not phase-2 automation work: test hygiene for the browser identity gate. Code anchors at `589403170`. ## Product outcome