Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,25 @@ 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.
- 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.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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).
Filed by root. 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.
2 changes: 1 addition & 1 deletion docs/handoffs/stored-automation-1058/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
167 changes: 167 additions & 0 deletions hosts/host-web/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,164 @@ 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<Vec<f32>> {
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::<Vec<f32>>()
});
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");
// 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 {
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
Expand All @@ -2125,6 +2283,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 {
Expand Down Expand Up @@ -2159,6 +2319,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\":",
),
}
}
}
Expand Down
Loading
Loading