diff --git a/.github/ISSUE_SPECS/709-publish-one-lane-symmetry-witness-bank-per-rack-stage.md b/.github/ISSUE_SPECS/709-publish-one-lane-symmetry-witness-bank-per-rack-stage.md new file mode 100644 index 000000000..e28eaf827 --- /dev/null +++ b/.github/ISSUE_SPECS/709-publish-one-lane-symmetry-witness-bank-per-rack-stage.md @@ -0,0 +1,425 @@ +# Publish one lane-symmetry witness bank per rack stage + +## Authority and smallest closable outcome + +Parent: #559 RT6. Companion tracker: #560. Base and current main are +`898bdc94b0143288049397629f3afeded384f8c2`. + +Lane A owns this low-level render-path slice. Lane B independently owns #705 and +all AudioWorklet qualification/pin work. #703 is a passive SOURCE PASS parent, +not a second implementation slot. This issue and active lane-B #705 occupy the +two shared implementation slots. + +Sol HIGH coordinates. Astra XHIGH implements because this changes render-time +low-level dispatch. A separate Astra LOW reviewer performs scope review and the +adversarial verdict for every implementation attempt. Agents are bounded +assignees and do not own the issue. + +The smallest closable outcome replaces the collapse witness's `lanes × slots` +virtual calls at the **`BankChain` → `BankStage` boundary** with one fixed-width +lane-witness publication per visited rack stage, then combines those witnesses +locally. Nested processor forwarding outside `rack` is an unchanged residual. +The issue changes no DSP arithmetic, render ordering, queue semantics, PCM, +public ABI, session schema, effect contract, artifact, pin, benchmark, or +performance budget. + +## Exact-path ownership + +Product and inline tests: + +```text +crates/rack/src/lib.rs +``` + +Decision record: + +```text +.github/ISSUE_SPECS/709-publish-one-lane-symmetry-witness-bank-per-rack-stage.md +``` + +Coordinator-only concise disposition rows in #559/#560 are synchronized outside +the implementation checkout. No other path is owned. If a correct solution +requires another crate, generated artifact, benchmark framework, policy change, +or public contract, stop and split it into a new stateless issue. + +## Current fact pattern + +`BankStage::lane_symmetry(lane)` is a virtual query. `BankChain::lane_symmetry` +walks every active slot for one lane, while `all_lanes_symmetric`, +`all_lanes_preserve_agreement`, `symmetry_counters`, and +`active_lane_eligibility` walk lanes through that method. On the armed collapse +path, the dispatch therefore performs a virtual call for each visited +lane/slot pair even though lane width is fixed at four or eight and each stage +already holds event-maintained witness state. + +Production `BuiltinStage` implementations outside this issue's ownership can +forward `lane_symmetry` into their own boxed processor traits. #709 does not +remove or measure that nested dispatch. A bulk `BankStage` default must preserve +each existing implementation's `lane_symmetry` override through the concrete +implementation's default-method instantiation; it may not substitute an +unconditional declined bank for already classified external stages. + +`BankChain::run` must continue to call every active slot's `begin_block` before +reading a witness. That ordering makes an admitted live-channel record affect +the first sample of the drained block and prevents an addressed one-channel +retarget from being rendered collapsed. Designed terms may remain prepared or +cached; live terms must remain current after the drain. + +The original audit location and cost classification are hypotheses, not current +measurements. Acceptance is structural and behavioral. No timing or speedup +claim is authorized. + +## Product contract + +1. A rack stage publishes the witnesses for the fixed maximum of eight lanes + through one stage-level `BankStage` virtual query. A private + `[ChannelSymmetryWitness; 8]` or byte-equivalent representation is acceptable; + it must not allocate. The supplied valid width and slot-active mask bound the + publication: W4 never queries lanes 4–7, and identity lanes never cause a + query that the existing walk skipped. +2. An active collapse decision invokes each visited active `BankStage` at most + once to obtain its lane witnesses across both eligibility and subsequent + agreement-preservation evaluation. It must not perform a second aggregate + query when eligibility declines. The chain combines the single aggregate + locally for W4, W8, partial cohorts, identity slots, inactive lanes, + eligibility, and agreement preservation. +3. `begin_block` remains complete and ordered before witness publication. A live + update drained for block N changes block N's eligibility. +4. Identity slots contribute nothing. Inactive lanes remain declined and never + become accidental collapse candidates. The stage-level default delegates only + the supplied valid, active lanes to the implementation's existing + `lane_symmetry` override, preserving external classified stages. An + implementation that never classified `lane_symmetry` still inherits its + conservative declined result. +5. Mono-collapse arming, forced-off behavior, recovery, `channels_agree`, + desymmetrization, transition counters, gather/scatter selection, fault + propagation, and observation/report/latency state retain their existing + behavior and order. +6. Render remains allocation/free, lock, I/O, logging, syscall, and structural + mutation free. No new data-dependent unbounded work is permitted. +7. Existing public evidence helpers retain their results. A private helper may + share the aggregate, but no public API expansion is required. + +## Attempt and checkpoint discipline + +The current five-attempt maximum applies. Each attempt is one coherent +implementation pass followed by one separate Astra LOW adversarial verdict. A +failed prerequisite, compile, test, policy, portability, allocation, behavior, +or evidence gate stops that attempt. No gate may be weakened. After attempt five +fails, preserve evidence and hard-stop this issue; no disguised sixth attempt. + +Astra XHIGH edits only the owned product path and this issue record. It stops at +a coherent compiling checkpoint for Sol to audit, commit, and push before any +revision. Raw targets, compiler streams, binaries, and temporary evidence remain +outside Git. + +## Objective gates + +Before implementation, Astra LOW must confirm exact main/branch/spec/GitHub +identity, exact path ownership, live slot independence from #705, and that the +current source still has the stated virtual-call shape. + +Each implementation attempt must provide: + +1. Focused W4 and W8 tests proving exact lane-witness results for full and partial + cohorts, inactive lanes, identity slots, mixed eligible/ineligible stages, + live-channel drain changes, and conservative defaults. +2. Instrumented private test stages proving the entire armed collapse decision, + including a declining eligibility result followed by agreement-preservation + evaluation, uses at most one `BankChain` → `BankStage` bulk virtual call per + visited active stage. The gate also proves W4 never queries lanes 4–7 and + identity lanes introduce no query. It must fail under restoration of the old + outer-boundary `lanes × slots` dispatch shape. Nested builtin-processor + forwarding is explicitly outside this structural claim. +3. Existing mono-collapse PCM/state/transition/fault tests, including disengage, + recovery, forced-off, observations, bypass, and exact operation order. +4. A render allocation gate covering the changed armed and declining paths. +5. `cargo test -p rack` in debug and release-unwind, strict Clippy for `rack`, + formatting and exact diff review. +6. Workspace policy, realtime dependency policy, realtime mutation gates, and + supported native x86-64-v3 plus Wasm scalar/simd128 compilation appropriate + to the changed crate. + +Astra LOW independently reviews the exact pushed checkpoint, reruns the +claim-discriminating focused gates, and records PASS or FAIL. Source PASS grants +no timing, allocation reduction beyond the tested render gate, artifact, pin, +PR, merge, or delivery claim. + +## Delivery boundary + +After SOURCE PASS, Sol performs exact-head/current-main review. If the rack +change is in the browser artifact dependency closure, lane B alone owns any +separately numbered AudioWorklet applicability/qualification and pin decision; +this lane-A issue remains passive while that bounded work runs and does not +consume an implementation slot. + +Required PR qualification, guarded live head/base merge, post-main +qualification, exact GitHub synchronization, and clean delivered-worktree +removal follow the repository workflow. Failed worktrees, branches, commits, +and evidence are preserved. + +## Scope review record + +Initial Astra LOW review at pushed brief `757bb598` returned **SCOPE FAIL** +without implementation or attempt consumption. The source fact pattern and +rack-only boundary were valid, but the virtual-call claim accidentally included +nested builtin-processor forwarding outside the owned path. This amendment +limits the claim to `BankChain` → `BankStage`, preserves external overrides, +bounds publication by valid width and the slot-active mask, and requires one +aggregate to serve both eligibility and agreement preservation. Fresh Astra LOW +exact-head review is required before implementation. + +## Attempt 1 — FAIL + +Astra XHIGH supplied the implementation at clean, pushed source checkpoint +`95af1e99d564a0e8795500c1bb46b916b18de13d`. Raw logs and Cargo targets are +preserved at `/tmp/issue709-attempt1-28LQihgQ`; no timing, performance, +allocation reduction, artifact, or delivery claim is made. + +Commands ran from `/home/bl/misofm/engine-rt6-stage-symmetry-witnesses` with +`CARGO_TARGET_DIR=/tmp/issue709-attempt1-28LQihgQ/target`: + +| Exact command after the target-directory assignment | Result | Log | +| --- | --- | --- | +| `cargo test --locked -p rack stage_witness_bank -- --nocapture` | Exit 0; all 3 claim-discriminating inline tests passed | `01-focused.log` | +| `cargo test --locked -p rack` | Exit 0; 33 inline, 10 console-bank and 4 re-engagement tests passed | `02-debug.log` | +| `CARGO_PROFILE_RELEASE_PANIC=unwind cargo test --locked -p rack --release` | Exit 0; the same 47 tests passed | `03-release.log` | +| `cargo clippy --locked -p rack --all-targets -- -D warnings` | Exit 101; blocking `clippy::collapsible_if` in the new inline test's `WitnessStage::begin_block`, `crates/rack/src/lib.rs:5092` | `04-clippy.log` | + +The attempt stopped at that first failed gate without a fix or retry. The +render-allocation harness, workspace/realtime policies and mutation gates, +native/Wasm portability builds, final formatting gate and exact diff review +were not completed. No allocation-harness files were created. + +Astra LOW (`/root/issue709_astra_low_scope`) recorded **Attempt 1 FAIL** on that +clean, pushed checkpoint: the Clippy log confirms the blocker; allocation, +policy and portability qualification remain incomplete; **no SOURCE PASS**. +No additional blocking source finding emerged. The reviewer authorized +Attempt 2 solely to combine the nested conditional without suppression, +update this decision record, and rerun the complete required gate sequence +within the same owned paths, preserving Attempt 1 evidence and stopping at +the first failure. + +## Attempt 2 — implementation gates complete; review pending + +Astra XHIGH applied only the combined conditional, without suppression, after +Attempt 1. The correction tranche passed formatting, the three focused tests +and strict Clippy (`01-fmt.log`, `02-focused.log`, `03-clippy.log`) before Sol +checkpointed and pushed `dbe7ea7be2bfd26da3cd20084179aa3b4a017bca`. + +The complete sequence below was then restarted against that clean, frozen +checkpoint. Source SHA-256 is +`266133f6870f2bf1bfc8d923125605ac840dba9fe3e1ddcb998420a3c822b676`. +All 20 gates completed without retry. Only this decision record was edited +afterward; the product source stayed frozen. + +Commands ran from `/home/bl/misofm/engine-rt6-stage-symmetry-witnesses`. +In the table, `E` expands to `/tmp/issue709-attempt2-nTfNNWlH`, `B` to +`898bdc94b0143288049397629f3afeded384f8c2`, and `H` to +`dbe7ea7be2bfd26da3cd20084179aa3b4a017bca`. Unless overridden in a row, +`CARGO_TARGET_DIR="$E/target"`. Logs are under `E`; `full-results.jsonl` +preserves each literal argv, environment override, source commit and exit. + +| Command | Result | Log | +| --- | --- | --- | +| `cargo test --locked -p rack stage_witness_bank -- --nocapture` | Exit 0; 3 focused tests | `full-01-focused.log` | +| `cargo test --locked -p rack` | Exit 0; 33 inline + 10 console-bank + 4 re-engagement tests | `full-02-debug.log` | +| `CARGO_PROFILE_RELEASE_PANIC=unwind cargo test --locked -p rack --release` | Exit 0; same 47 tests | `full-03-release.log` | +| `cargo clippy --locked -p rack --all-targets -- -D warnings` | Exit 0; unchanged configuration warnings noted below | `full-04-clippy.log` | +| `cargo fmt --check` | Exit 0 | `full-05-fmt.log` | +| `CARGO_TARGET_DIR="$E/allocation-target" cargo run --offline --release --manifest-path "$E/allocation-harness/Cargo.toml"` | Exit 0; detector liveness and all 24 armed render calls passed | `full-06-allocation.log` | +| `bash scripts/check-workspace-policy.sh` | Exit 0 | `full-07-workspace-policy.log` | +| `bash scripts/check-rack-policy.sh` | Exit 0; dependency/safety boundary | `full-08-rack-policy.log` | +| `bash scripts/check-realtime-policy.sh` | Exit 0; 43 marked regions in 12 files | `full-09-realtime-policy.log` | +| `bash scripts/test-rack-policy.sh` | Exit 0 | `full-10-rack-mutations.log` | +| `bash scripts/test-realtime-policy.sh` | Exit 0 | `full-11-realtime-mutations.log` | +| `bash scripts/check-realtime-audit-leak.sh` | Exit 0 | `full-12-audit-leak-policy.log` | +| `bash scripts/test-realtime-audit-leak.sh` | Exit 0 | `full-13-audit-leak-mutations.log` | +| `cargo build --locked --release -p rack --target x86_64-unknown-linux-gnu` | Exit 0; repository x86-64-v3 AVX2/FMA pin | `full-14-native.log` | +| `CARGO_TARGET_DIR="$E/wasm-scalar" cargo build --locked --release -p rack --target wasm32-unknown-unknown` | Exit 0 | `full-15-wasm-scalar.log` | +| `CARGO_TARGET_DIR="$E/wasm-simd128" CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS="-C target-feature=+simd128" cargo build --locked --release -p rack --target wasm32-unknown-unknown` | Exit 0 | `full-16-wasm-simd128.log` | +| `CARGO_TARGET_DIR="$E/mutation-target" cargo test --offline --manifest-path "$E/old-dispatch-mutation/Cargo.toml" --lib tests::stage_witness_bank_entire_armed_decision_publishes_once_even_when_declining -- --exact --nocapture` | Expected test exit 101 at the required bulk-call assertion; mutation validator exit 0 | `full-17-old-dispatch-mutation.log` | +| `git rev-parse HEAD`; `git status --porcelain=v1`; `git diff --check "$B" "$H"`; `git diff --name-only "$B" "$H"`; `git diff --stat "$B" "$H"`; `git diff "$B" "$H" -- crates/rack/src/lib.rs .github/ISSUE_SPECS/709-publish-one-lane-symmetry-witness-bank-per-rack-stage.md` | All exits 0; clean exact head, only the two owned paths; complete diff reviewed | `full-18-exact-diff.log` | +| `cargo test --locked -p builtins --test mono_collapse` | Exit 0; 2 existing report/state tests | `full-19-builtin-mono-report.log` | +| `cargo test --locked -p host-core --test effect_observation every_bank_lane_publishes_its_own_reduction -- --exact` | Exit 0; existing per-lane observation test | `full-20-observation.log` | + +The implementation publishes one eight-entry witness array per active stage, +bounded by `BankWidth` and the prepared slot mask. Existing stage overrides +remain the default's source; unclassified stages still decline. The armed +path publishes after all drains and reuses that aggregate for eligibility and +agreement maintenance. The forced-off path retains its lazy publication after +disengagement. The former activity-count test now expects ten constant slot +checks rather than eight: the two additional checks guard stage publications; +its zero lane-mask-inspection assertion remains intact. + +The three inline tests cover exact terms, public evidence-helper results, +full/partial W4/W8 cohorts, identity and inactive lanes, default/external-style +overrides, all-drains-before-publication, and the complete armed eligible, +`UNBYPASSED`-declined and `LIVE`-declined decisions. Each visited stage is +asserted to publish once across the whole decision, with no direct outer +per-lane query. The external mutation restores lane/slot virtual calls only +in the aggregate; its single run compiled and failed specifically because +bulk counts were `[0, 0, 0]` instead of `[1, 1, 0]`. The authoritative checkout +was never mutated. Original/mutated sources, manifest and exact diff are under +`$E/old-dispatch-mutation/`; `mutation-input-sha256.json` records their hashes. +The mutation diff SHA-256 is +`42232d23c7c4d73a60d928f4c7b4dc672fc40ed9d34ebc32b33300664d17636a`. + +The external allocation harness uses the public rack API and the inherited +stage bulk default. It first proves alloc/zeroed-alloc/realloc/free detector +liveness (three allocation and three free observations), then runs 12 cases: +W4/W8 × full/partial cohorts × eligible/bypass-declined/live-declined outcomes. +Each case audits its initial collapsed block and its next block after a drain +changes the witness. All 24 armed calls allocate/free zero times, with exact +collapse, agreement and transition assertions. Harness sources, manifest, +lockfile and binaries remain outside Git. Input SHA-256 values are: + +- `$E/allocation-harness/Cargo.toml`: + `b346af2a8a154577fc1e5a77c60e5c62352051a9fe4b76a70d96864c41f19292`. +- `$E/allocation-harness/src/main.rs`: + `ce3e96abff03e4b047c24a192d3c4a3876c0a7d0423586335dd68ad96efe2e1f`. + +Clippy exits successfully but emits the pre-existing unreachable-path +configuration warnings at `clippy.toml:81–82`; no suppression or configuration +change was made. Allocation evidence is limited to the tested render paths; +it is not a syscall trace. The Wasm evidence is crate compilation, not browser +execution. Nested builtin-processor forwarding remains unchanged. There is +no timing, speedup, artifact, pin, merge, delivery or **SOURCE PASS** claim. +Astra LOW's independent adversarial review and Sol's delivery workflow remain +pending; both attempt evidence directories are preserved. + +## Attempt 2 — Astra LOW SOURCE PASS + +Astra LOW (`/root/issue709_astra_low_scope`) returned **SOURCE PASS** at exact +clean pushed head `ac6cb00ebf2a53ac65ca01a7c6b03855421fe3fb` for product source +`dbe7ea7be2bfd26da3cd20084179aa3b4a017bca`. The reviewer verified upstream +identity, exact GitHub body parity, both owned paths, the complete Attempt 2 +evidence, the allocation harness and the old-dispatch mutation. Fresh review +evidence is preserved at `/tmp/issue709-low-review-99sx3syb`. + +With a fresh external target, the reviewer reran: + +| Command | Result | +| --- | --- | +| `cargo test --locked -p rack stage_witness_bank -- --nocapture` | Exit 0; all 3 discriminating tests passed | +| `cargo clippy --locked -p rack --all-targets -- -D warnings` | Exit 0 | +| `cargo run --offline --release --manifest-path /tmp/issue709-attempt2-nTfNNWlH/allocation-harness/Cargo.toml` | Exit 0; detector liveness and all 24 allocation/free-free render calls passed | + +The adversarial review found no blocker. The defaulted Rust trait method keeps +existing implementations and overrides source-compatible and changes neither +the exported ABI nor the effect contract. The reviewer confirmed width/mask +bounds, conservative defaults, drain ordering, aggregate reuse, forced-off and +recovery ordering, and the mutation gate's exact discrimination. + +Sol fetched origin after the verdict. `origin/main` remains +`898bdc94b0143288049397629f3afeded384f8c2`, the issue's original base and the +branch merge base, with no current-main drift. The branch diff remains limited +to the two owned paths. Because `rack` is in the browser artifact dependency +closure through graph/compiler/host composition, retained AudioWorklet identity +cannot be assumed. Lane B alone owns any separately numbered applicability, +qualification and pin action. This lane-A source PASS grants no timing, speedup, +artifact, pin, PR, merge or delivery claim and does not authorize lane A to +touch AudioWorklet state. + +## Current-main combined delivery coordination + +User routing: Astra LOW implementation, Astra XHIGH scope/verification. +Independent XHIGH scope PASS permits combined delivery with #722 from delivered +main878db254. Admit only the separately accepted #709 and #722 deltas. #709's +rack changes remain owned by #709 and are not repairs or expanded authority +under exhausted #714 or qualification successor #722. All #722 production +additions remain unchanged. Verify the shared rack file as the composition of +accepted deltas; no additional production correction is authorized. Stop on +conflicts or unexpected source drift. Preserve #713 resident-input machinery. + +Root checkpoints each history merge separately. Then run the existing rack +suite, meter tests, graph resident/executor selectors and compiler resident +selectors in debug and release-unwind, formatting and realtime policy. Retarget +and rerun only the existing #709 and #722 allocation harnesses once to establish +joint-checkout behavior, including live detectors. No new harness/full campaign. +XHIGH integration PASS precedes one root-owned shared artifact qualification +and PR/main pair. Each issue retains separate acceptance and closure evidence. +No timing, full-RT10, nested-forwarding removal or publication claim. + +## Preserved-history integration checkpoint + +Accepted #709 history4c165c1f merged cleanly onto delivered878db254. Only the +owned rack/spec paths enter this checkpoint. The admitted source patch matches +the accepted branch patch; #713 resident-input machinery is preserved and +stage witness publication remains after drains in common run_with_input. +No new production correction. Joint integration tests await the separately +checkpointed #722 merge; this is not integration or delivery PASS. + +## Joint current-main qualification checkpoint + +Joint source cbc3c4f7917d5704396fa904b68e0c5c021da610 passed all twelve focused +commands. Each debug/release-unwind profile passed rack50, meter9, graph +resident2, executor/order1 and compiler resident2 tests. Formatting and realtime +policy passed. Existing #709 allocation harness proved detector3/3 and24 +allocation/free-free render calls. Existing #722 harness proved detector1/1, +zero allocations/frees on accepted and declined paths, correct dispatch counts +and queue overflow/publication counters. Copied harness sources/locks are +unchanged; locked offline metadata resolves their local dependencies to the +joint checkout. Actual commands, environments, exits and source identities +are external at `/tmp/joint709722-gates-itstq384/`. No retry or source edit. + +Independent XHIGH SOURCE COMPOSITION PASS reconstructs9573fe16 as the disjoint +union of accepted #709 and current-main/#713 edits, then removes the exact +three #722 rack insertion blocks from joint source to recover9573fe16 byte +for byte. Five other files exactly match accepted #722; merge parents and +clean source verified. Final joint receipt review precedes shared artifact +qualification and PR/main delivery. + +## Joint integration PASS and provisional artifact + +Independent Astra XHIGH accepts joint headb823abb0: exact source composition, +all twelve focused gate receipts, both retargeted allocation harnesses and +tested source identities verified without reruns. Root's single repin-report +invocation then exited0 with digest `ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b`. +This is recorded as a provisional, unqualified pin; ordinary six-file build +and shared artifact qualification remain mandatory. Probe argv/environment, +source head, status and full outputs are external at +`/tmp/rack-meter-artifact-ydlz4bpc/probe.*`. No performance or delivery claim. + +## Shared artifact qualification checkpoint + +One ordinary build with repin mode unset reproduced ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b +and the exact six-file set. All five non-Wasm payload hashes match delivered +#710. Every existing qualification command passed once against this artifact. +Let A=/tmp/rack-meter-artifact-ydlz4bpc/artifact: + +```sh +bash scripts/build-web-audioworklet.sh "$A" +bash scripts/check-web-audioworklet.sh "$A" +python3 -B scripts/check-browser-expected-resources.py --artifacts "$A" +bash scripts/test-web-audioworklet.sh +bash scripts/check-sdk-types.sh +bash scripts/check-sdk-headless.sh "$A" +bash scripts/sdk-package.sh check "$A" +npm --prefix hosts/host-web/qualification run qualify -- --artifacts "$A" --browser all --record-matrix --candidate-commit cbc3c4f7917d5704396fa904b68e0c5c021da610 --self-test-mutations +node hosts/host-web/qualification/generate-matrix.mjs --check +``` + +Exact argv, source heads, external target/temp paths, exits, timestamps and +full outputs are preserved at `/tmp/rack-meter-artifact-ydlz4bpc/`, including +`qualification-commands.json` and `artifact-sha256.json`. Static render closure, +boot memory budget, native/browser resource/PCM parity and26 resource negative +controls passed. SDK headless188/188 and publishable-tarball gates passed. +Chromium151.0.7922.34, Firefox153.0 and WebKit26.5 passed all existing gates. +Matrix changes only source/digest lineage; browser floors, resource expectations +and fixture digests are unchanged. No retry, new harness, compiler capture, +performance or physical-device claim. + +Both #709/#722 cite this shared artifact evidence. Final XHIGH artifact review, +exact-head PR qualification, guarded merge and exact-main qualification remain +required before synchronized closure. No registry publication. + +## Independent shared artifact PASS + +Astra XHIGH accepts exact pushed401a09f4: product source unchanged from joint +PASSb823abb0; all actual six-file hashes, probe/ordinary/pin agreement, five +unchanged non-Wasm identities, eight successful qualification receipts and +lineage-only matrix diff verified. Both live issue bodies matched the specs. +No rerun or blocker. This appended verdict changes only decision records; +required exact-head PR qualification, guarded merge and exact-main qualification +remain before synchronized closure. diff --git a/.github/ISSUE_SPECS/714-observe-resident-bank-output-in-built-in-meters.md b/.github/ISSUE_SPECS/714-observe-resident-bank-output-in-built-in-meters.md new file mode 100644 index 000000000..86864c264 --- /dev/null +++ b/.github/ISSUE_SPECS/714-observe-resident-bank-output-in-built-in-meters.md @@ -0,0 +1,467 @@ +One-line summary: Let eligible built-in meter observers consume the final resident +AoSoA output of a successfully executed bank at the existing observation point, +while retaining the current scalar meter arithmetic and planar fallback. + +## Status, authority, and dependency + +This is a queued, placement-only child of #349 RT10, coordinated by lane A under +#559/#560. The opening baseline is main +`b1f9128f3e06532afdfc16aad661c4b2deb5dea1`; reconciled tracker authority is +`1a6bb3d12f900445e9aa9ebc65589d804ea59bce`. Astra XHIGH supplied the read-only +scope and delivery audit. Sol must approve the brief and Astra LOW must review +its scope before implementation authority exists. + +Implementation is blocked until #713's qualified product source +`7576d1b6c794f01df4abeb7256a8309d45879b21` is integrated into main with its +required delivery evidence. Its SOURCE PASS record is `bd10831e`; that is +source qualification, not merged delivery. After integration, record the exact +new main and dependency merge, revalidate the source anchors and finite preflight +below, and obtain explicit implementation authorization. Do not cherry-pick, +modify, requalify, or otherwise consume #713's passive worktree during this brief. +This is an intra-lane-A source dependency, not a cross-lane dependency or a new +lane-B obligation. The queued issue consumes no active implementation slot. + +Lane B alone owns AudioWorklet qualification, artifact disposition and pins. +This brief grants no authority over those paths or #705's execution. It makes no +timing, speedup, improvement, load-elimination, full-RT10, or delivery claim. + +## Delivery audit and inherited evidence + +RT10 is still open. #203 recorded costly planar meter observation; its closure +does not establish resident metering. #516/#519/#520 delivered coherent meter +publication and selective metric/observer preparation through +[PR #521](https://github.com/misofm/engine/pull/521), reviewed head +`6567ef4d639dce05e359096e106588aa95c0602b`, merge +`be781895decc72328f727dcd816b8b40a2ab6051`. That merge is an ancestor of this +brief's main baseline. Required [PR qualification 34034182930](https://github.com/misofm/engine/actions/runs/34034182930) +and [post-main qualification 34034536132](https://github.com/misofm/engine/actions/runs/34034536132) +completed successfully at those respective exact heads. They qualify that prior +delivery; they are not evidence for this child. The historical metering timing +invocation failed warmup; no speedup is inherited. + +Current source still routes `MeterObserver::observe` in +`crates/builtins-compiler/src/lib.rs:4311` to planar +`MeterAccumulator::observe` in `crates/builtins/src/lib.rs:3783`. +`crates/graph/src/runtime.rs:1630` acquires planar arena input for each observer; +`observe_segment`/`observe_selected_segment` at builtins lines 4000/4048 retain +scalar per-channel loops. Relevant issue/PR history and current source show no +resident AoSoA meter implementation. The original RT10 comment explicitly +separates placement from optional vectorization. + +#713's accepted resident-input behavior, observer/send semantics, and resource +accounting are inherited constraints. Its review evidence is preserved at +`/tmp/issue713-astra-low-review-wcrdc9ai/verdict.json` and implementation evidence +at `/tmp/issue713-attempt4-_dmwn7zs/`. Reuse only evidence whose exact source and +applicability are proved after integration; this child must earn its own gates. + +## Smallest closable product slice + +At the existing post-execution observation point, offer each eligible observer +an immutable view of its actual final bank-output lane. The built-in meter +adapter accepts it and runs the same meter state machine over strided resident +words. Other observers and all ineligible cases retain planar acquisition. +The child keeps scatter, observer dispatch order, per-meter scalar scans and +all arithmetic. A separate read of resident scratch is still a read: placement +alone does not prove fewer total loads or a faster render. + +Do not insert metering as an executing DSP stage, move publication before a +processor completes, batch observers across failure boundaries, precompute or +publish another observer's state, materialize planar meter scratch, or change +bank membership/merge eligibility. Existing `BankChain::run`, #713's +`run_with_resident_input`, and graph `chains_into` contracts remain intact. + +## Exact ownership + +At the opening checkpoint, only this numbered specification is owned. Concise +replacement updates to the existing #559/#560 disposition and current-lane +paragraphs are coordination work, not implementation authority. + +After the dependency, scope approval, preflight, and implementation authorization, +the complete product/test allowlist is: + +- `crates/rack/src/lib.rs`: borrowed final-output view and focused rack tests. +- `crates/graph/src/lib.rs`: optional resident-observer hook and focused tests. +- `crates/graph/src/runtime.rs`: admission, safe field borrowing, ordered dispatch, + physical discriminator, and focused tests. +- `crates/builtins/src/lib.rs`: shared planar/resident meter body and validation. +- `crates/builtins/tests/meter.rs`: differential coverage using the existing corpus. +- `crates/builtins-compiler/src/lib.rs`: built-in meter adapter, real prepared-plan + integration coverage, and existing resource-boundary checks. +- This numbered specification: scope, attempt ledger, evidence and decisions. + +The named compiler adapter path is part of this lane-A child, not a transfer of +compiler-wide ownership. Root records its exact-path availability at activation. +No graph-compiler, `program.rs`, lane kernel, manifest, policy, host, artifact, +pin, benchmark, or generic harness changes are allowed. Keep temporary harness +source/manifests, mutation checkouts, targets and raw evidence outside Git. + +## Finite pre-implementation gates + +After #713 is integrated, freeze the exact method/type signatures in this record +and complete one bounded preflight before product edits: + +1. Demonstrate ordinary safe disjoint borrowing of immutable bank scratch and + mutable member observer bindings at `Runtime::observe_unit`. The rack view + contains only borrowed final left/right words and checked frame/width/lane + shape. Its lifetime cannot outlive the chain borrow. No unsafe, retained + reference, additional PCM buffer, or callback from inside `BankChain::run`. +2. Freeze a backward-compatible optional method on `GraphRuntimeObserver` that + returns `None` to decline without mutation and `Some(Result<(), RenderError>)` + after accepting. The default declines. An accepted error propagates directly; + it must never cause planar retry. A separate immutable resident block carries + the checked lane view and first-sample time. Existing `observe` remains intact. + The builtins adapter bridges to a checked strided meter input without adding + a builtins-to-rack or builtins-to-graph dependency. +3. Prove final logical right-plane validity after full-prefix and partial-prefix + collapse, decline and recovery. At the opening baseline, `run` copies left + into right at the collapse seam and then runs the dual suffix before scatter; + use the actual final planes, not a previous-collapse flag or a blanket + left-for-right substitution. The integrated source must retain that proof. +4. Record native and Wasm scalar/SIMD128 size/alignment, retained total, prepare + peak and largest-allocation deltas for affected owners. The intended delta + is zero: borrowed views and a default trait method require no retained table, + flag or scratch. Reconcile existing authoritative estimates and one-byte + resource-boundary rejections. If any nonzero retained/layout delta or needed + accounting change cannot be proved within the allowlist, stop and split the + prerequisite; do not approximate, omit, or expand ownership. + +A failed or unresolved preflight is a scope blocker, not permission to improvise +an API, add state or start fixtures. Report the exact seam and required owner. +No implementation attempt is consumed by a read-only scope/preflight review. + +## Eligibility and order contract + +Graph owns the proof that this unit successfully executed in the current block, +that the observed node/member corresponds to the chain's final output, and that +its active lane and frame range match the view. Rack validates local dimensions +and bounds; it cannot certify graph freshness or observer identity from caller +assertions alone. Keep these responsibilities explicit in code and tests. + +The first positive form is a normal emitted bank with an exact final-output +member mapping. Direct and observed-alias bindings at that same final output +remain positive, including multiple meters and mixed meter/custom observers. +An extra planar reader or send does not disqualify an otherwise identical final +output: scatter still occurs. Send transform, crossfeed, delay/PDC and fanout +continue to read their original planar data in their original order. + +Scalar units, intermediate members of merged chains, retired/non-emitted units, +shape mismatch, stale prior-block data, and output redirects/route-fold forms +without an established identical output mapping take the ordinary planar path. +No broader eligibility is required by this child. Partial populations are valid +when the exact active lane is proved; inactive lanes and out-of-frame words are +never meter input. No new maximum track count or runtime SIMD dispatch. + +Preserve `execute(A) -> observe(A) -> execute(B)`, member ordering, stable-handle +ordering after direct/alias merge, and first-error short circuit. Process failure +prevents observations; observer failure prevents later observers and successor +execution. Accepted resident failure does not retry or mutate twice. Ordinary +fallback acquires the same planar slices and executes the same observer once. +PCM bits, render reports, processor/automation state, bypass, collapse transitions, +command drains, PDC cursors, sends and reductions remain unchanged. + +## Meter and realtime invariants + +Preserve independent left/right state and increasing sample order per lane: +scalar `f64` square/add energy, RMS publication, D8 comparison/select peak law, +normal-or-zero sanitization, exact clip/sanitize counts, hold/decay and subnormal +flush placement. No energy reassociation, `f32` substitution, new `f64` lanes, +horizontal reduction or cross-channel shortcut. + +Preserve all metric selections and disabled-field zeros, silence behavior, +partial/multiple windows, zero-frame input, first-sample/time-overflow validation, +discontinuity and reset state, interval/cumulative counters, generation, sequence, +queue-drop accounting, and publication order. Validate new input shape before +any meter mutation; existing planar validation/error behavior remains unchanged. +The prepared bounded SPSC queues and consumer ownership remain the same. + +Render remains allocation/free/lock/syscall/I/O/logging-free, bounded and safe. +No preparation or structural plan mutation moves into observation. The bank view +is read-only and does not expose mutable audio or retained scratch ownership. + +## Discriminating evidence and gate order + +Reuse existing fixtures; do not grow a second fixture framework. Record exact +source HEAD, owned-file hashes, argv, environment, exits, logs, and limitations +under a fresh external per-attempt directory. Stop at the first unexpected +failure, preserve it, and request the next adversarial verdict; do not retry or +repair within that attempt. + +1. Formatting, smallest focused compile, existing `cargo test --locked -p + builtins --test meter`, and focused rack/graph/builtins-compiler tests first. + Pause for root's exact-path checkpoint as soon as the coherent tranche + compiles and focused tests pass; root commits/pushes before broader work. +2. Compare current planar reference and resident candidate for Scalar/W4/W8, + differing lanes/channels, partial populations, poisoned inactive/right and + out-of-frame storage, signal/silence, NaN/Inf/subnormals/signed zero, window + boundaries, all metric selections, reset/discontinuity and queue overflow. + Pin every snapshot field (float bits), reports and subsequent state, not just + aggregate PCM. Exercise mono, partial-prefix collapse, decline and recovery. +3. Use a real prepared-plan fixture to compare direct/alias/multiple/mixed + observers, nonunity crossfeed and delayed-PDC sends, extra readers, scalar and + retired/intermediate/incompatible controls. An ordered failing-observer trace + must prove that later meters publish nothing and the successor does not run. +4. A private test-only discriminator must prove accepted production meters obtain + resident input without the old planar acquisition, while declines still take + it exactly once. A structural validator must cover GraphExecutor execution/ + observation order and the single intended production dispatch site. Restore + old planar dispatch in one external physical mutation: semantic/reference + comparisons must remain meaningful, while the resident-path assertion fails + specifically. An admission/order-bypass control must fail the structural gate. + Preserve input/diff/command/output, restore exact source and rerun the decisive + gate. An unexpected pass or unrelated failure stops the attempt. +5. Run a real prepared-plan render allocation/free gate with a live positive + detector, both accepted and declined paths, and meter emission/queue overflow. + Use the existing allocation support from an external harness; setup and drops + occur outside the counted render interval. No new unsafe allowlist entry. +6. Run affected rack/graph/builtins/builtins-compiler debug and release-unwind + suites (`cargo test --locked -p `, then with `--release` and + `CARGO_PROFILE_RELEASE_PANIC=unwind`), strict all-target/all-feature Clippy + (`cargo clippy --locked -p --all-targets --all-features -- -D warnings`), + and `cargo fmt --all -- --check`. Run `bash scripts/check--policy.sh` + and `bash scripts/test--policy.sh` for each of workspace, rack, graph, + builtins, lane and realtime; rack/graph/builtins also enforce dependency + boundaries. Run `bash scripts/check-realtime-audit-leak.sh` and + `bash scripts/test-realtime-audit-leak.sh`, then + `bash scripts/check-unfused-seal.sh` and its `--self-test`. Inspect the + affected dependency closure against `docs/REALTIME_DEPENDENCY_POLICY.md`. + No policy changes are authorized. +7. For packages rack, graph, builtins and builtins-compiler, run + `cargo build --locked --release --lib -p rack -p graph -p builtins -p builtins-compiler` + natively with the unchanged `.cargo/config.toml` x86-64-v3 AVX2/FMA flags. + Run the same command with `--target wasm32-unknown-unknown` twice, first with + `RUSTFLAGS='-C target-feature=-simd128'`, then with + `RUSTFLAGS='-C target-feature=+simd128'`, using distinct external + `CARGO_TARGET_DIR` values. Run `bash scripts/check-wasm-realtime-atomics.sh` + with an explicit external target-directory argument and + `bash scripts/test-wasm-realtime-atomics.sh`. Record exact commands/flags. + Compilation does not claim target execution. +8. Review exact current base/head, owned-path diff, source identities, resource + accounting, retained/peak/largest deltas and unchanged dependency contracts. + Astra LOW reviews the source and evidence; SOURCE PASS is not delivery. + Later delivery requires exact reviewed-head CI, guarded merge/current-base + review, successful post-main qualification and synchronized issue closure. + Artifact disposition, where required, remains exclusively lane B's workflow. + +## Successor boundary and acceptance + +This child closes only when eligible production built-in meters use resident +input at the unchanged observation point, all declines/errors preserve their +original behavior, and the finite semantic/physical/realtime/resource/target +gates pass. Full RT10 remains open. Across-track vector meter state, optional +scatter/load fusion, RT10/IO8 master-peak consolidation, and performance evidence +belong to separately scoped successors. New `f64` lane arithmetic requires an +owner ruling. No host, AudioWorklet, artifact, pin, timing or benchmark work is +part of this child, and no load-elimination or improvement claim follows from it. + +## Decision record and attempt ledger + +Opening scope: Astra XHIGH, 2026-09-10. Fetched origin; main remains +`b1f9128f3e06532afdfc16aad661c4b2deb5dea1`. All 349 numbered main specs had a +matching issue number in the 472-entry all-state issue roster; the active #713 +and tracker bodies were reconciled read-only. No existing resident-meter child +was found. GitHub atomically assigned #714 on creation, titled +"Observe resident bank output in built-in meters". The matching local file is +`.github/ISSUE_SPECS/714-observe-resident-bank-output-in-built-in-meters.md`; +its body and title/number must be verified against GitHub before reporting the +opening checkpoint. Opening audit evidence is outside Git at +`/tmp/issue-rt10-scope-ue4bs0nj/`. + +| Stage | Status | Evidence/next authorization | +|---|---|---| +| Opening scope | Astra LOW PASS at reviewed head `5cff0d4740ccec8f815820743c93fc627a7d3893` | Placement-only scope accepted; #713 integration blocks implementation | +| Dependency/API/resource preflight | Astra XHIGH PREFLIGHT PASS; Astra LOW independent PREFLIGHT PASS | Evidence and frozen-signature findings recorded below; no attempt consumed | +| Implementation attempts 1-5 | Attempts 1-2 FAIL; consumed; Astra LOW verdicts recorded below | Attempt 3 is limited to scoping the call-count validator to the intended observer dispatcher while retaining admission, mutation, and one-dispatch obligations; stop at the first unexpected failure | +| Qualification/delivery | Not started | No product, test, SOURCE PASS, artifact or delivery credit | + +Scope review: Astra LOW PASS at `5cff0d4740ccec8f815820743c93fc627a7d3893`, +against main `b1f9128f3e06532afdfc16aad661c4b2deb5dea1` and tracker +`6063cd58d3a966db94d9fb728d422ed860f3a734`. This records scope approval only: +there is no implementation authorization, attempt consumption, or credit. The +#713 dependency, integration gate, lane-B ownership boundary, and all finite +preflight requirements remain unchanged. + +No implementation, build, test, mutation, timing, benchmark, artifact or pin work +was performed for the opening scope. Future verdicts and failed gates must be +recorded candidly without overwriting earlier attempt evidence. + +## Current integration baseline — #713 dependency delivered + +The #713 dependency is delivered through PR #718 at merge +`0df770b0f1f002b563246db70b71a3991fe8a8c7`, with parents `0dc06633` and +`be50921a`. Required run `34453900286` and verdict job `102797510876` passed; +post-main run `34454589139` and verdict job `102799403320` passed at the exact +merge. The current integration baseline is main +`0df770b0f1f002b563246db70b71a3991fe8a8c7`; #713's accepted product/test +identity remains `7576d1b6c794f01df4abeb7256a8309d45879b21`, and RT9 remains +partial. The dependency blocker is cleared, and the read-only API/resource +preflight against this baseline has passed independent Astra LOW review; Attempt +1 is authorized within the existing allowlist and focused gate order. The exact +existing allowlist and all +no-claim boundaries remain unchanged; no product, test, timing, performance, +artifact, pin, SOURCE PASS, or full-RT10 claim is made. + +## Astra XHIGH PREFLIGHT PASS — Attempt 1 authorized + +Astra XHIGH completed the read-only API/resource preflight at evidence root +`/tmp/issue714-preflight-qxddd3h5`; its manifest SHA-256 is +`f41d01a56c79898f7670bba34d2ffd431351c341b133111c6fe21f5599aa1b71`. +Frozen signatures were taken from `FROZEN_API.md`. The findings retain the safe +borrow ownership, E0506, final-right, default-decline, accepted-error, and lazy +fallback requirements. Native, Wasm scalar, and Wasm SIMD128 resource accounting +recorded zero retained, peak, largest, and allocation deltas. Wasm remains +compile-only for this preflight. Astra LOW independently returned PREFLIGHT +PASS. Zero implementation attempts were consumed; Attempt 1 is explicitly +authorized to Astra XHIGH within the existing allowlist and focused gate order. +This records no artifact, pin, timing, performance, SOURCE PASS, or full-RT10 +claim. + +## Astra XHIGH ATTEMPT 1 FAIL — Astra LOW verdict + +Astra XHIGH's first unexpected focused-gate failure is preserved at evidence +root `/tmp/issue714-attempt1-8ba98eid`; the manifest SHA-256 is +`026876d4642eefd8d46b627e8187f2d7e695cf2a37bd304348b6108a82d59cae`. +Astra LOW independently returned an ATTEMPT 1 FAIL verdict. The exact ordered +commands and exits were: + + rustfmt --edition 2024 --config-path rustfmt.toml --config skip_children=true crates/rack/src/lib.rs crates/graph/src/lib.rs crates/graph/src/runtime.rs crates/builtins/src/lib.rs crates/builtins/tests/meter.rs crates/builtins-compiler/src/lib.rs # exit 0 + cargo check --locked -p builtins-compiler --lib --features test-support # exit 0 + cargo test --locked -p builtins --test meter # exit 0; 9/9 passed + cargo test --locked -p rack --lib resident_output_lane # exit 101; E0382 + +The rack failure is the active `active` borrow after the suffix loop consumed +it with `active.into_boxed_slice()` in the prior suffix iteration. The failure +consumed Attempt 1; no later graph or compiler selectors, broader suites, +policy, mutation, allocation, target, timing, benchmark, artifact, or pin +gates ran. The candid failed checkpoint contains exactly these six authorized +dirty files: `crates/builtins-compiler/src/lib.rs`, `crates/builtins/src/lib.rs`, +`crates/builtins/tests/meter.rs`, `crates/graph/src/lib.rs`, +`crates/graph/src/runtime.rs`, and `crates/rack/src/lib.rs`. It earns no +qualification, delivery, timing, performance, artifact, pin, or full-RT10 +credit; the tree is authorized for this candid failed checkpoint. + +Attempt 2 is authorized only to clone or reconstruct the fixture's active mask +per suffix, then resume the existing focused gate order. It must stop at the +first unexpected failure. No product or API expansion, artifact, pin, timing, +benchmark, or broader repair is authorized by this record. + +## Astra XHIGH ATTEMPT 2 FAIL — Astra LOW verdict + +Astra XHIGH's second focused pass is preserved at evidence root +`/tmp/issue714-attempt2-pb4c9_pt`; the manifest SHA-256 is +`7dcfe07119a16378b56fad29f66c45697938a7ffc7f49eae1ef22f38063afc85`. +The one-line rack fixture clone completed; rustfmt exited 0 and the rack +fixture selector exited 0 with 1/1 passed. The graph behavioral test passed, +but the structural-valid-source check failed because its call-count validator +counted an unrelated existing processor `.observe_resident(` in addition to the +intended meter dispatch. No later gate ran and this attempt earns no +qualification, delivery, timing, performance, artifact, pin, or full-RT10 +credit. Astra LOW's verdict is that the validator has scoping fragility; the +production checkpoint is unchanged and suitable as a candid failed checkpoint. + +Attempt 3 is authorized only to scope the call-count validator to the intended +observer dispatcher while retaining the admission, mutation, and one-dispatch +obligations, then resume the existing focused gates and stop at the first +unexpected failure. No production or API change, artifact, pin, timing, +benchmark, or broader repair is authorized. + + +## Recovery checkpoint and Attempt 4 authorization + +The owner transferred coordination to this session. Astra LOW implements; +Astra XHIGH scopes and verifies, superseding earlier prospective model routing. +The five-attempt policy delivered in #707 applies; Attempts 1–3 remain consumed. + +Independent Astra XHIGH recovery review confirmed Attempt 3 FAIL from +`/tmp/issue714-attempt3-4eobg44i/VERDICT.md`. The preserved validator correction +passed graph resident-meter tests (2/2) and the execution-order selector (1/1), +then the builtins-compiler fixture failed E0616 on private `PreparedGraphPlan.routes`. +No later gate ran. Current runtime.rs matches the preserved implementation diff; +all six owned source hashes and manifest entries verify. Manifest SHA-256: +`2a3d5642a89ca6feb9776c88c9969e208ff4baab9367a784d0a305ce3dc275fa`. +This is a candid failed checkpoint, with no source or delivery PASS. + +Attempt 4 may change only the private builtins-compiler fixture: supply its +nonunity crossfeed transform during PreparedGraphPlanParts construction through +a private parameterized helper, preserving the old wrapper's default behavior, +and remove the illegal post-construction private-field mutation. Public API and +production behavior stay unchanged. Run formatting and the existing compiler +resident-meter selector first; checkpoint the coherent correction before +continuing the original finite qualification gates. Stop at first unexpected +failure and preserve its result. Astra XHIGH independently reviews the attempt. + +## Attempt 4 focused checkpoint + +Astra LOW repaired only private compiler fixture construction: the route +transform is supplied through PreparedGraphPlanParts; existing fixture callers +retain their default transform. Formatting passed and +`cargo test --locked -p builtins-compiler --features test-support --lib resident_meter` +passed both tests. Exact commands, exits, source identity, diff and logs are +preserved in `/tmp/issue714-attempt4-1kttzoal/`. This is a focused checkpoint, +not SOURCE PASS. Astra XHIGH now owns adversarial verification under the user's +latest routing; the original remaining finite gates and stop-on-failure rule apply. + +## Attempt 4 verification incomplete; fifth attempt bounded + +Astra XHIGH stopped on an external mutation-driver setup assertion (exit 1): +`let resident = if eligible` occurs three times in runtime.rs because test +strings repeat the production token. The driver incorrectly required one global +occurrence. No product failure was demonstrated. Evidence and exact failed +command are `/tmp/issue714-attempt4-review-tll88otr/VERDICT.md` and adjacent +`unexpected-setup-command.py`, status, logs and final-source record. + +Independent tests passed: builtins meter 9/9, rack resident output 1/1, graph +resident meter 2/2, compiler resident meter 2/2 and actual executor structural +selector 1/1. An extra guessed selector selected zero tests and earns no credit. +The physical planar mutation retained 9/9 meter semantics and failed the intended +acquisition assertion with `[1,0,0]` rather than `[0,1,1]`. Shared source stayed +clean; the external source was restored exactly before the setup failure. +Restored decisive reruns, allocation, remaining policies/suites/targets and +resource reconciliation are incomplete. No SOURCE PASS or delivery credit. + +Attempt 5 is the final attempt under the user-approved ceiling. Root authorizes +only external verification-driver correction and completion of the original +finite gates on unchanged product source. Scope admission mutation to the actual +production observe_unit body, excluding embedded validator strings; require an +exact single-site diff and restoration. Preflight actual selectors, script +arguments and the existing-support allocation harness before executing their +gates. Reuse existing fixtures and resource probes; no new evidence framework, +production correction, policy weakening, timing or artifact work. Stop at the +first unexpected failure, preserve it and return an adversarial verdict. A fifth +failure requires a candid hard stop and bounded rescope, never a sixth retry. + +## Attempt 5 FAIL — frozen at the hard stop + +Astra XHIGH stopped at the required realtime policy gate on unchanged product +source `b0f80bc4fbf39c7a2121daa6caa3b9d4eb7283f9` (product hashes equal +ad9685fc). `bash scripts/check-realtime-policy.sh` exited 1: +`crates/graph/src/runtime.rs has unmatched realtime policy markers`. The +structural fixture embeds two complete END-marker strings at lines 4144/4152; +the raw scanner counts eight BEGIN and ten END lines, despite eight actual +paired regions. No render arithmetic or allocation failure is demonstrated. + +Evidence: `/tmp/issue714-attempt5-review-aj9srv1e/VERDICT.md`, exact per-command +receipts/logs and source copies; manifest SHA-256 +471ff0b7722872d670114f0499996bd379d3f2681cd889153f26dd237e616848. +Earned gates: admission/order physical negative controls and exact restored +reruns; live detector and accepted/declined prepared renders with zero +allocations/frees, eight queued plus one later publication and two drops; +268 tests in each debug/release-unwind profile; all four strict Clippy suites; +formatting; workspace/rack/graph/builtins/lane policies and self-tests. + +Realtime self-tests, audit-leak, unfused seal, native/Wasm build/atomics and +current candidate layout measurements did not run after the failure. Fifteen +retained owner definitions and preparation allocation contracts remain unchanged, +but this does not grant the unexecuted gates. No SOURCE PASS or delivery. + +All five attempts are consumed. Implementation is frozen; no sixth repair or +retry is authorized. An independently scoped, bounded successor may correct +only the structural fixture delimiter expressions while preserving production +and policy behavior, then earn its stated remaining qualification. Preserve +all earlier failures and gate evidence; do not silently restart this feature. + +## Superseded by bounded qualification successor #722 + +#714 remains exhausted at five failed attempts. #722 owns only the two +structural-test delimiter corrections and completion of inherited qualification +and delivery. This issue is closed as superseded, not delivered; no capability +or parent-finding credit is awarded until #722 actually delivers. diff --git a/.github/ISSUE_SPECS/722-repair-resident-meter-structural-fixture-and-qualify-the-frozen-implementation.md b/.github/ISSUE_SPECS/722-repair-resident-meter-structural-fixture-and-qualify-the-frozen-implementation.md new file mode 100644 index 000000000..60c98c2b3 --- /dev/null +++ b/.github/ISSUE_SPECS/722-repair-resident-meter-structural-fixture-and-qualify-the-frozen-implementation.md @@ -0,0 +1,239 @@ +## Authority and bounded outcome + +Parent finding: #559 RT10; lane-B delivery coordinator #560. Supersedes exhausted +#714 after five candid failed attempts. Independent Astra XHIGH scoping PASS +authorizes this qualification successor. #714 earns no SOURCE PASS or delivery +credit; its frozen history and evidence remain intact. User routing is Astra LOW +implementation and Astra XHIGH verification. At most two active issue slots; +#710 delivery and this successor are the active pair. + +Adopt unchanged product identity ad9685fcfa6631a42123e2b5c2d90ab28e810c20, +preserved at b0f80bc4fbf39c7a2121daa6caa3b9d4eb7283f9 and terminal checkpoint +f51ea518. The smallest correction is exactly two test-only delimiter expressions +in crates/graph/src/runtime.rs. Compose each existing reserved END delimiter +from constant fragments, preserving its exact computed value while avoiding +a raw reserved-marker occurrence in the test literal. The scanner currently +sees eight real BEGIN/END pairs plus two test END matches and correctly refuses +the resulting 8/10 inventory. + +## Ownership and prohibitions + +Owned source: only the two structural-test delimiter expressions in +`crates/graph/src/runtime.rs`. No production dispatch, actual policy directive, +structural assertion, policy script, marker floor, owner layout, allocation, +DSP arithmetic, dependency or public API change. No other product repair is +authorized. This is not a sixth implementation attempt on #714. + +The successor owns its numbered spec. Lane-B root additionally owns the +existing delivery-only pin/lineage paths after #710 is delivered: + +- hosts/host-web/web/miso-engine-v1-audio-worklet-artifact.sha256 +- hosts/host-web/qualification/results.json +- hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md + +All six original #714 feature paths enter integration only as the frozen, +reviewed inherited implementation. Do not expand this successor into general +feature work, new harnesses, benchmark campaigns, compiler captures or policy +relaxation. Any production repair or resource redesign stops this scope. + +## Inherited evidence and finite gates + +The original #714 product contract and unearned acceptance obligations remain +mandatory; this issue narrows editable scope, not product correctness. Inherit +A4 evidence /tmp/issue714-attempt4-review-tll88otr and A5 evidence +/tmp/issue714-attempt5-review-aj9srv1e, manifest SHA-256 +471ff0b7722872d670114f0499996bd379d3f2681cd889153f26dd237e616848. +Successful behavior/reference tests, planar dispatch negative control, accepted +and declined zero-allocation renders with live detector and queue overflow, +268 tests in each debug/release profile, four strict Clippy suites, formatting +and earlier policies remain individually earned. They do not retroactively +turn exhausted #714 into SOURCE PASS. Prove inherited source identity first. + +1. Astra LOW changes only the two delimiter expressions. Run formatting, the + existing resident/admission/executor structural selectors in debug and + release-unwind, then realtime policy and its self-test. Preserve exact + commands/exits externally; stop on first unexpected failure. Pause when + focused checks pass for root exact-path commit/push and issue synchronization. +2. Astra XHIGH verifies production bytes, actual directives and computed + delimiter are unchanged; the only source diff is the two expressions. + Reuse the existing physical admission/order controls to prove both violations + remain rejected and restore exact source. No new harness. +3. Complete only the unrun mandatory checks: realtime audit-leak check/self-test, + unfused seal/self-test, native release libraries for rack/graph/builtins/ + builtins-compiler, Wasm scalar and simd128 release builds using distinct + external targets and explicit flags, Wasm realtime atomics check/self-test, + existing per-target layout/resource reconciliation. Preserve the original + zero retained/peak/largest/allocation delta and one-byte cap-rejection gates. + Reuse existing A5 gate7/layout drivers after checking their paths and source + identity. No repeated full feature test campaign absent a new concern. +4. XHIGH reviews inherited plus new evidence and records SOURCE PASS only after + every remaining obligation passes. The five-attempt ceiling applies to this + materially reduced scope; no automatic expansion after a failure. + +## Integration, artifact qualification and closure + +Wait for #710 to deliver, then integrate the frozen feature plus two-expression +correction onto delivered main. Current-base XHIGH review and focused resident +tests must establish compatibility; conflicts or product drift return to scope +review before edits. Lane B alone owns artifact qualification and pinning. + +Use one existing-builder repin-report invocation; if needed checkpoint its +observed digest as provisional. Build the ordinary six-file artifact once into +a fresh external empty directory. Run existing static/render/boot, native/browser +resource parity, hermetic host, SDK types/headless/package and Chromium/Firefox/ +WebKit qualification with the existing matrix recorder. Only actual successful +results may update source/digest lineage; preserve non-Wasm payload identities, +resource expectations and browser floors. No artifact rebuild retries, new +framework, timing or registry publication. + +After independent XHIGH source/artifact PASS, require exact-head PR qualification, +guarded current-base merge, successful exact-main qualification, synchronized +issue closure and removal of clean delivered worktrees. Preserve branches, +history and external evidence. Parent RT10 remains broader than this resident +meter input slice; no load-elimination, speedup or full-RT10 claim follows. + +## Status + +Scoped before implementation. #714 is frozen at terminal FAIL; successor +attempts have not started. Independent XHIGH scope PASS supports only this +bounded correction and remaining qualification. + +## Attempt 1 focused checkpoint + +Astra LOW changed exactly the two test delimiter expressions to constant +concatenations, preserving their computed value. Formatting, resident meter +2/2, admission 1/1 and executor-order 1/1 selectors passed in both debug and +release-unwind. Realtime policy and its self-test passed. Nine actual command +receipts, argv/environment/exits and full outputs are preserved at +`/tmp/issue722-attempt1-n4fv8jyj/`. Implementation paused for root checkpoint. +No production or policy change and no SOURCE PASS is claimed. Independent +XHIGH review and the listed remaining gates follow this pushed checkpoint. + +## Astra XHIGH SOURCE PASS + +Independent XHIGH accepts exact clean pushed48168771a9680542b6e6837772e7232e6dbe9a78. +The two computed delimiters, production prefix, eight actual policy regions and +other five inherited source paths are unchanged. A4/A5 manifests and all earned +gates revalidated; exhausted #714 retains its failed verdicts. + +All remaining gates passed: external admission/order negatives and exact +restored resident/admission/order checks; realtime audit-leak check/self-test; +unfused seal/self-test (62 controls); native release libraries with unchanged +AVX2/FMA flags; separate Wasm release libraries using explicit -simd128 and ++simd128; Wasm realtime atomic check/self-test; existing native/Wasm scalar/ +Wasm SIMD128 layout probes. All fifteen owner layouts match prior values on +all three targets, with zero retained-total, prepare-peak, largest-allocation +and allocation-count deltas. Existing cap/one-byte rejection evidence applies. +No claim of zero static-code or transient-stack growth is made. + +Exact commands/flags, nineteen independent receipts (seventeen zero exits and +two specifically expected negatives), inherited receipts, source identities, +layout measurements and resource reconciliation are external at +`/tmp/issue722-review-galtwo33/VERDICT.md` and adjacent records. Manifest SHA-256: +677246c28c521cfbfe5ca33a3d1359e34188537caddb8aac4ad65d18d2c74324. +No unexpected failure, new harness, full repeated campaign or compiler-IR +capture. Integration, artifact qualification and required CI remain root's +delivery steps. #710 is now delivered at878db254 with both required runs PASS. + +## Current-main combined delivery coordination + +User routing: Astra LOW implementation, Astra XHIGH scope/verification. +Independent XHIGH scope PASS permits combined delivery with #722 from delivered +main878db254. Admit only the separately accepted #709 and #722 deltas. #709's +rack changes remain owned by #709 and are not repairs or expanded authority +under exhausted #714 or qualification successor #722. All #722 production +additions remain unchanged. Verify the shared rack file as the composition of +accepted deltas; no additional production correction is authorized. Stop on +conflicts or unexpected source drift. Preserve #713 resident-input machinery. + +Root checkpoints each history merge separately. Then run the existing rack +suite, meter tests, graph resident/executor selectors and compiler resident +selectors in debug and release-unwind, formatting and realtime policy. Retarget +and rerun only the existing #709 and #722 allocation harnesses once to establish +joint-checkout behavior, including live detectors. No new harness/full campaign. +XHIGH integration PASS precedes one root-owned shared artifact qualification +and PR/main pair. Each issue retains separate acceptance and closure evidence. +No timing, full-RT10, nested-forwarding removal or publication claim. + +## Preserved-history joint integration checkpoint + +Accepted #722 history58a2cbc1 merged cleanly after #709 checkpoint9573fe16 on +delivered878db254. Five non-rack source files exactly match accepted #722; +rack contains exactly the accepted #709/#722 deltas with #713 machinery +preserved. No conflict, new production correction or whitespace error. Both +imports are separate recoverable checkpoints. Joint focused and allocation +gates remain pending; no integration or delivery PASS is claimed here. + +## Joint current-main qualification checkpoint + +Joint source cbc3c4f7917d5704396fa904b68e0c5c021da610 passed all twelve focused +commands. Each debug/release-unwind profile passed rack50, meter9, graph +resident2, executor/order1 and compiler resident2 tests. Formatting and realtime +policy passed. Existing #709 allocation harness proved detector3/3 and24 +allocation/free-free render calls. Existing #722 harness proved detector1/1, +zero allocations/frees on accepted and declined paths, correct dispatch counts +and queue overflow/publication counters. Copied harness sources/locks are +unchanged; locked offline metadata resolves their local dependencies to the +joint checkout. Actual commands, environments, exits and source identities +are external at `/tmp/joint709722-gates-itstq384/`. No retry or source edit. + +Independent XHIGH SOURCE COMPOSITION PASS reconstructs9573fe16 as the disjoint +union of accepted #709 and current-main/#713 edits, then removes the exact +three #722 rack insertion blocks from joint source to recover9573fe16 byte +for byte. Five other files exactly match accepted #722; merge parents and +clean source verified. Final joint receipt review precedes shared artifact +qualification and PR/main delivery. + +## Joint integration PASS and provisional artifact + +Independent Astra XHIGH accepts joint headb823abb0: exact source composition, +all twelve focused gate receipts, both retargeted allocation harnesses and +tested source identities verified without reruns. Root's single repin-report +invocation then exited0 with digest `ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b`. +This is recorded as a provisional, unqualified pin; ordinary six-file build +and shared artifact qualification remain mandatory. Probe argv/environment, +source head, status and full outputs are external at +`/tmp/rack-meter-artifact-ydlz4bpc/probe.*`. No performance or delivery claim. + +## Shared artifact qualification checkpoint + +One ordinary build with repin mode unset reproduced ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b +and the exact six-file set. All five non-Wasm payload hashes match delivered +#710. Every existing qualification command passed once against this artifact. +Let A=/tmp/rack-meter-artifact-ydlz4bpc/artifact: + +```sh +bash scripts/build-web-audioworklet.sh "$A" +bash scripts/check-web-audioworklet.sh "$A" +python3 -B scripts/check-browser-expected-resources.py --artifacts "$A" +bash scripts/test-web-audioworklet.sh +bash scripts/check-sdk-types.sh +bash scripts/check-sdk-headless.sh "$A" +bash scripts/sdk-package.sh check "$A" +npm --prefix hosts/host-web/qualification run qualify -- --artifacts "$A" --browser all --record-matrix --candidate-commit cbc3c4f7917d5704396fa904b68e0c5c021da610 --self-test-mutations +node hosts/host-web/qualification/generate-matrix.mjs --check +``` + +Exact argv, source heads, external target/temp paths, exits, timestamps and +full outputs are preserved at `/tmp/rack-meter-artifact-ydlz4bpc/`, including +`qualification-commands.json` and `artifact-sha256.json`. Static render closure, +boot memory budget, native/browser resource/PCM parity and26 resource negative +controls passed. SDK headless188/188 and publishable-tarball gates passed. +Chromium151.0.7922.34, Firefox153.0 and WebKit26.5 passed all existing gates. +Matrix changes only source/digest lineage; browser floors, resource expectations +and fixture digests are unchanged. No retry, new harness, compiler capture, +performance or physical-device claim. + +Both #709/#722 cite this shared artifact evidence. Final XHIGH artifact review, +exact-head PR qualification, guarded merge and exact-main qualification remain +required before synchronized closure. No registry publication. + +## Independent shared artifact PASS + +Astra XHIGH accepts exact pushed401a09f4: product source unchanged from joint +PASSb823abb0; all actual six-file hashes, probe/ordinary/pin agreement, five +unchanged non-Wasm identities, eight successful qualification receipts and +lineage-only matrix diff verified. Both live issue bodies matched the specs. +No rerun or blocker. This appended verdict changes only decision records; +required exact-head PR qualification, guarded merge and exact-main qualification +remain before synchronized closure. diff --git a/crates/builtins-compiler/src/lib.rs b/crates/builtins-compiler/src/lib.rs index 5e742e7e2..2ffc2ffb1 100644 --- a/crates/builtins-compiler/src/lib.rs +++ b/crates/builtins-compiler/src/lib.rs @@ -4317,6 +4317,26 @@ impl GraphRuntimeObserver for MeterObserver { builtins::MeterObservationError::LaneLength => RenderError::InvalidEnvelope, }) } + + fn observe_resident( + &mut self, + block: graph::GraphResidentObservationBlock<'_>, + ) -> Option> { + Some( + builtins::MeterInput::strided( + block.lane.left(), + block.lane.right(), + block.lane.frames() as usize, + block.lane.width().lanes() as usize, + block.lane.lane(), + ) + .and_then(|input| self.0.observe_input(input, block.first_sample)) + .map_err(|error| match error { + builtins::MeterObservationError::SampleTimeOverflow => RenderError::TimeOverflow, + builtins::MeterObservationError::LaneLength => RenderError::InvalidEnvelope, + }), + ) + } } fn render_error(error: BuiltinParameterError) -> RenderError { @@ -5469,6 +5489,24 @@ mod tests { fn track_graph_variant( n: usize, variant: BoundaryVariant, + ) -> (PreparedGraphPlan, Vec) { + track_graph_variant_with_route_transform( + n, + variant, + RouteTransform { + gain: 0.5, + ll: 1.0, + lr: 0.0, + rl: 0.0, + rr: 1.0, + }, + ) + } + + fn track_graph_variant_with_route_transform( + n: usize, + variant: BoundaryVariant, + route_transform: RouteTransform, ) -> (PreparedGraphPlan, Vec) { let envelope = RenderEnvelope { sample_rate: SampleRateHz(48_000), @@ -5769,13 +5807,7 @@ mod tests { ) { vec![PreparedRoute { node: route, - transform: RouteTransform { - gain: 0.5, - ll: 1.0, - lr: 0.0, - rl: 0.0, - rr: 1.0, - }, + transform: route_transform, }] } else { Vec::new() @@ -10374,6 +10406,282 @@ mod tests { } } + #[cfg(feature = "test-support")] + #[test] + fn resident_meter_prepared_plans_match_planar_with_aliases_mixed_observers_and_delayed_sends() { + struct FailingObserver; + impl GraphRuntimeObserver for FailingObserver { + fn observe(&mut self, _block: GraphObservationBlock<'_>) -> Result<(), RenderError> { + Err(RenderError::InvalidEnvelope) + } + fn observe_resident( + &mut self, + _block: graph::GraphResidentObservationBlock<'_>, + ) -> Option> { + Some(Err(RenderError::InvalidEnvelope)) + } + } + struct CountSink(Arc); + impl GraphRuntimeProcessor for CountSink { + fn process(&mut self, _block: GraphBindingBlock<'_>) -> Result<(), RenderError> { + self.0.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + } + let run = |backend, n, variant, disabled, fail| { + let compiled = n_track_session(n); + let config = MeterConfig { + period_frames: NonZeroU32::new(17).unwrap(), + peak_hold_frames: 3, + peak_decay_db_per_second: 24.0, + queue_capacity: NonZeroUsize::new(2).unwrap(), + reset_generation: 714, + }; + let selected = n - 1; + let mut requests = vec![MeterRequest { + handle: handle(20), + track_id: track_name(selected), + tap: MeterTap::PostFader, + config, + }]; + if variant == BoundaryVariant::AliasObserved { + requests.push(MeterRequest { + handle: handle(1), + track_id: track_name(selected), + tap: MeterTap::PostDynamic, + config, + }); + } + let builtins = prepare_session_builtins(&compiled, &requests, caps()).unwrap(); + let (mut graph, levels) = track_graph_variant_with_route_transform( + n, + variant, + RouteTransform { + gain: 0.625, + ll: 0.75, + lr: -0.25, + rl: 0.125, + rr: 0.5, + }, + ); + if variant == BoundaryVariant::SelectedSend { + let edge_id = GraphEdgeId::RouteDestination { + route_id: StableGraphId::parse("proof-send").unwrap(), + }; + graph.inserted_delays.push(graph::InsertedDelay { + node: GraphNodeId::CompensationDelay { + edge_id: Box::new(edge_id.clone()), + }, + edge_id, + samples: effect_contract::LatencySamples(7), + }); + } + let classes = SessionPoolClasses::from_session(&compiled); + let mut artifact = + builtins.into_graph_artifact_with_banks(graph, (), backend, &levels, &classes); + let node = GraphNodeId::TrackStage { + track_id: StableGraphId::parse(&track_name(selected)).unwrap(), + stage: TrackStage::PostFader, + }; + let extra = MeterAccumulator::prepare(handle(30), config, 48_000).unwrap(); + artifact + .builtin_observers + .push(GraphNodeObserverBinding::new( + node.clone(), + 30, + Box::new(MeterObserver(extra.accumulator)), + )); + artifact.meter_consumers.push(MeterConsumer { + handle: handle(30), + track_id: track_name(selected).into_boxed_str(), + tap: MeterTap::PostFader, + consumer: extra.consumer, + }); + if fail { + artifact + .builtin_observers + .push(GraphNodeObserverBinding::new( + node.clone(), + 22, + Box::new(FailingObserver), + )); + } + let capture = Arc::new(std::sync::Mutex::new(Vec::new())); + artifact + .builtin_observers + .push(GraphNodeObserverBinding::new( + node, + 25, + Box::new(Capture(Arc::clone(&capture))), + )); + let envelope = artifact.graph.envelope; + let mut nodes: Vec<_> = (0..n) + .map(|index| { + GraphNodeBinding::new( + GraphNodeId::TrackStage { + track_id: StableGraphId::parse(&track_name(index)).unwrap(), + stage: TrackStage::Input, + }, + Box::new(SeededInput { + seed: 714 + index as u64, + symmetric: false, + nonfinite: true, + }), + ) + }) + .collect(); + let sink_calls = Arc::new(AtomicUsize::new(0)); + nodes.push(GraphNodeBinding::new( + GraphNodeId::Output { + output_id: StableGraphId::parse("main-out").unwrap(), + }, + Box::new(CountSink(Arc::clone(&sink_calls))), + )); + let mut bound = artifact + .into_bound(GraphRuntimeBindings { + envelope, + nodes, + observers: Vec::new(), + }) + .unwrap_or_else(|error| panic!("resident meter bind: {}", error.code)); + graph::test_only_meter_input_reset(disabled); + let mut pcm = Vec::new(); + let mut snapshots = Vec::new(); + let mut reports = Vec::new(); + for block in 0..if fail { 1 } else { 5 } { + let mut words = vec![0.0; HARNESS_QUANTUM as usize * 2]; + let report = bound.plan.render( + engine::realtime::RenderIo { + input: None, + output: engine::realtime::PlanarBufferMut::try_new( + &mut words, + 2, + HARNESS_QUANTUM as usize, + HARNESS_QUANTUM as usize, + ) + .unwrap(), + }, + engine::realtime::RenderTime { + absolute_sample: block * u64::from(HARNESS_QUANTUM), + }, + ); + assert_eq!(report.is_err(), fail); + reports.push(report); + pcm.extend(words.into_iter().map(f32::to_bits)); + for meter in &mut bound.meter_consumers { + while let Ok(snapshot) = meter.consumer.try_pop() { + snapshots.push(snapshot); + } + } + } + let counts = graph::test_only_meter_input_counts(); + graph::test_only_meter_input_reset(false); + let observed = capture.lock().unwrap().clone(); + if fail { + assert!(observed.is_empty(), "later custom observer must not run"); + assert!(!snapshots.is_empty()); + assert!( + snapshots + .iter() + .all(|snapshot| snapshot.handle == handle(20)), + "later direct and alias meters must publish nothing" + ); + assert_eq!( + sink_calls.load(Ordering::Relaxed), + 0, + "successor must not execute" + ); + } else { + assert!(observed.iter().any(|word| *word != 0)); + assert!( + snapshots + .iter() + .any(|snapshot| snapshot.cumulative_dropped_snapshots > 0) + ); + assert_eq!(sink_calls.load(Ordering::Relaxed), 5); + } + (pcm, observed, snapshots, counts, reports) + }; + for backend in [Backend::Scalar, Backend::Simd4, Backend::Simd8] { + for n in [3, 4, 5, 8, 9] { + for variant in [ + BoundaryVariant::Plain, + BoundaryVariant::AliasObserved, + BoundaryVariant::SelectedSend, + ] { + let a = run(backend, n, variant, false, false); + let b = run(backend, n, variant, true, false); + assert_eq!(a.0, b.0, "output: {backend:?}/{n}/{variant:?}"); + assert_eq!(a.1, b.1); + assert_eq!(a.4, b.4); + assert_eq!(a.2, b.2); + for (a, b) in a.2.iter().zip(&b.2) { + for (a, b) in [(a.left, b.left), (a.right, b.right)] { + assert_eq!(a.sample_peak.to_bits(), b.sample_peak.to_bits()); + assert_eq!(a.rms.to_bits(), b.rms.to_bits()); + assert_eq!(a.energy.to_bits(), b.energy.to_bits()); + assert_eq!(a.held_peak.to_bits(), b.held_peak.to_bits()); + } + } + assert_eq!(b.3[1..], [0, 0]); + if backend == Backend::Scalar { + assert_eq!(a.3[1..], [0, 0]); + } else { + assert!( + a.3[2] >= 10, + "both production meters must accept resident input: {backend:?}/{n}/{variant:?}: {:?}", + a.3 + ); + assert!(a.3[1] > a.3[2], "the mixed custom observer declines"); + // Mixed declines share one planar acquisition at this final member. + assert_eq!(a.3[0], b.3[0]); + } + } + } + let a = run(backend, 3, BoundaryVariant::AliasObserved, false, true); + let b = run(backend, 3, BoundaryVariant::AliasObserved, true, true); + assert_eq!((&a.0, &a.1, &a.2, &a.4), (&b.0, &b.1, &b.2, &b.4)); + assert_eq!( + a.3, + if backend == Backend::Scalar { + [1, 0, 0] + } else { + [0, 2, 2] + } + ); + assert_eq!(b.3, [1, 0, 0]); + } + } + + #[cfg(feature = "test-support")] + #[test] + fn resident_meter_accepted_plan_avoids_planar_acquisition_and_matches_decline() { + for backend in [Backend::Simd4, Backend::Simd8] { + let mut candidate = + prepared_pair_graph_fixture(true, true, false, false, None, backend, 9); + let mut reference = + prepared_pair_graph_fixture(true, true, false, false, None, backend, 9); + candidate.plan.arm_mono_collapse(&|_| true); + reference.plan.arm_mono_collapse(&|_| true); + for (block, forced) in [false, true, false].into_iter().enumerate() { + candidate.plan.force_mono_collapse_off(forced); + reference.plan.force_mono_collapse_off(forced); + graph::test_only_meter_input_reset(false); + let a = render_bound(&mut candidate, block as u64 * u64::from(HARNESS_QUANTUM)); + assert_eq!(graph::test_only_meter_input_counts(), [0, 1, 1]); + graph::test_only_meter_input_reset(true); + let b = render_bound(&mut reference, block as u64 * u64::from(HARNESS_QUANTUM)); + assert_eq!(graph::test_only_meter_input_counts(), [1, 0, 0]); + assert_eq!(a, b); + assert_eq!( + candidate.meter_consumers[0].consumer.try_pop(), + reference.meter_consumers[0].consumer.try_pop() + ); + graph::test_only_meter_input_reset(false); + } + } + } + fn host_dispatch() -> Backend { Backend::current() } diff --git a/crates/builtins/src/lib.rs b/crates/builtins/src/lib.rs index 9e3346cc1..ee690e0e5 100644 --- a/crates/builtins/src/lib.rs +++ b/crates/builtins/src/lib.rs @@ -3649,6 +3649,60 @@ pub enum MeterObservationError { LaneLength, SampleTimeOverflow, } + +/// Checked, borrowed left/right input for a single meter lane. +#[derive(Clone, Copy)] +pub struct MeterInput<'a> { + left: &'a [f32], + right: &'a [f32], + frames: usize, + stride: usize, + lane: usize, +} + +impl<'a> MeterInput<'a> { + // REALTIME_POLICY_BEGIN + /// Validate both exact plane lengths before any meter state can change. + /// Empty planes accept zero frames with a valid stride/lane. + /// + /// # Errors + /// [`MeterObservationError::LaneLength`] for any invalid or overflowing shape. + pub fn strided( + left: &'a [f32], + right: &'a [f32], + frames: usize, + stride: usize, + lane: usize, + ) -> Result { + if stride == 0 + || lane >= stride + || frames.checked_mul(stride) != Some(left.len()) + || right.len() != left.len() + { + return Err(MeterObservationError::LaneLength); + } + Ok(Self { + left, + right, + frames, + stride, + lane, + }) + } + + fn samples( + &self, + words: &'a [f32], + start: usize, + end: usize, + ) -> impl Iterator + Clone { + let lane = self.lane; + words[start * self.stride..end * self.stride] + .chunks_exact(self.stride) + .map(move |frame| frame[lane]) + } + // REALTIME_POLICY_END +} #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct MeterLaneSnapshot { pub sample_peak: f32, @@ -3786,14 +3840,25 @@ impl MeterAccumulator { right: &[f32], first_sample: u64, ) -> Result<(), MeterObservationError> { - if left.len() != right.len() { - return Err(MeterObservationError::LaneLength); - } - let len = match u64::try_from(left.len()) + let input = MeterInput::strided(left, right, left.len(), 1, 0)?; + self.observe_input(input, first_sample) + } + + /// Observe checked planar or strided words with the same scalar window state machine. + /// + /// # Errors + /// [`MeterObservationError::SampleTimeOverflow`] before any state mutation if the + /// block would run past `u64::MAX`. + pub fn observe_input( + &mut self, + input: MeterInput<'_>, + first_sample: u64, + ) -> Result<(), MeterObservationError> { + let len = match u64::try_from(input.frames) .ok() .and_then(|len| first_sample.checked_add(len)) { - Some(_) => left.len(), + Some(_) => input.frames, None => return Err(MeterObservationError::SampleTimeOverflow), }; if self @@ -3843,8 +3908,12 @@ impl MeterAccumulator { && self.right.held == 0.0 && self.left.hold_remaining == window.hold_frames && self.right.hold_remaining == window.hold_frames - && left[..len].iter().all(|sample| *sample == 0.0) - && right[..len].iter().all(|sample| *sample == 0.0); + && input + .samples(input.left, 0, len) + .all(|sample| sample == 0.0) + && input + .samples(input.right, 0, len) + .all(|sample| sample == 0.0); let mut offset = 0; while offset < len { let take = ((period - self.frames) as usize).min(len - offset); @@ -3860,14 +3929,14 @@ impl MeterAccumulator { if self.metrics == MeterMetricSet::ALL { observe_segment( &mut self.left, - &left[offset..end], + input.samples(input.left, offset, end), window, &mut self.cumulative_clipped, &mut self.cumulative_sanitized, ); observe_segment( &mut self.right, - &right[offset..end], + input.samples(input.right, offset, end), window, &mut self.cumulative_clipped, &mut self.cumulative_sanitized, @@ -3875,7 +3944,7 @@ impl MeterAccumulator { } else { observe_selected_segment( &mut self.left, - &left[offset..end], + input.samples(input.left, offset, end), window, self.metrics, &mut self.cumulative_clipped, @@ -3883,7 +3952,7 @@ impl MeterAccumulator { ); observe_selected_segment( &mut self.right, - &right[offset..end], + input.samples(input.right, offset, end), window, self.metrics, &mut self.cumulative_clipped, @@ -3999,7 +4068,7 @@ fn clear_interval(lane: &mut MeterLane) { /// is forbidden on any path whose bits are pinned. fn observe_segment( lane: &mut MeterLane, - samples: &[f32], + samples: impl Iterator + Clone, window: MeterWindow, cumulative_clipped: &mut u64, cumulative_sanitized: &mut u64, @@ -4010,7 +4079,7 @@ fn observe_segment( let mut hold_remaining = lane.hold_remaining; let mut clipped = 0_u64; let mut sanitized = 0_u64; - for sample in samples.iter().copied() { + for sample in samples.clone() { let invalid = !normal_or_zero(sample); let sample = if invalid { 0.0 } else { sample }; sanitized += u64::from(invalid); @@ -4047,7 +4116,7 @@ fn observe_segment( /// selection stays on [`observe_segment`] so its arithmetic order and published bits do not move. fn observe_selected_segment( lane: &mut MeterLane, - samples: &[f32], + samples: impl Iterator + Clone, window: MeterWindow, metrics: MeterMetricSet, cumulative_clipped: &mut u64, @@ -4055,7 +4124,7 @@ fn observe_selected_segment( ) { if metrics.contains(MeterMetricSet::SAMPLE_PEAK) { let mut peak = lane.peak; - for sample in samples.iter().copied() { + for sample in samples.clone() { let sample = if normal_or_zero(sample) { sample } else { 0.0 }; let absolute = sample.abs(); peak = if absolute > peak { absolute } else { peak }; @@ -4064,7 +4133,7 @@ fn observe_selected_segment( } if metrics.contains(MeterMetricSet::ENERGY_RMS) { let mut energy = lane.energy; - for sample in samples.iter().copied() { + for sample in samples.clone() { let sample = if normal_or_zero(sample) { sample } else { 0.0 }; #[cfg(test)] meter_work_probe::ENERGY.fetch_add(1, core::sync::atomic::Ordering::Relaxed); @@ -4075,7 +4144,7 @@ fn observe_selected_segment( if metrics.contains(MeterMetricSet::COUNTS) { let mut clipped = 0_u64; let mut sanitized = 0_u64; - for sample in samples.iter().copied() { + for sample in samples.clone() { #[cfg(test)] meter_work_probe::COUNTS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); let invalid = !normal_or_zero(sample); @@ -4091,7 +4160,7 @@ fn observe_selected_segment( if metrics.contains(MeterMetricSet::HELD_PEAK) { let mut held = lane.held; let mut hold_remaining = lane.hold_remaining; - for sample in samples.iter().copied() { + for sample in samples.clone() { #[cfg(test)] meter_work_probe::HELD.fetch_add(1, core::sync::atomic::Ordering::Relaxed); let sample = if normal_or_zero(sample) { sample } else { 0.0 }; diff --git a/crates/builtins/tests/meter.rs b/crates/builtins/tests/meter.rs index 6af7ba702..6106af6e0 100644 --- a/crates/builtins/tests/meter.rs +++ b/crates/builtins/tests/meter.rs @@ -437,3 +437,177 @@ fn silence_against_a_nonzero_held_peak_still_decays() { ); assert_eq!(second.left.energy, 0.0, "and still adds no energy"); } + +fn assert_snapshot_bits(actual: MeterSnapshot, expected: MeterSnapshot) { + assert_eq!(actual, expected); + for (a, b) in [(actual.left, expected.left), (actual.right, expected.right)] { + assert_eq!(a.sample_peak.to_bits(), b.sample_peak.to_bits()); + assert_eq!(a.rms.to_bits(), b.rms.to_bits()); + assert_eq!(a.energy.to_bits(), b.energy.to_bits()); + assert_eq!(a.held_peak.to_bits(), b.held_peak.to_bits()); + } +} + +#[test] +fn resident_meter_matches_planar_bits_for_every_selection_stride_and_state_transition() { + let hostile = [ + 0.0, + -0.0, + 0.25, + -0.75, + 1.0, + -1.5, + f32::NAN, + f32::INFINITY, + f32::NEG_INFINITY, + f32::from_bits(1), + f32::from_bits(0x8000_0001), + 0.03125, + ]; + for stride in [1, 4, 8] { + for lane in 0..stride { + for selection in 1..=MeterMetricSet::ALL.bits() { + let config = MeterConfig { + period_frames: NonZeroU32::new(5).unwrap(), + peak_hold_frames: 2, + peak_decay_db_per_second: 120.0, + queue_capacity: NonZeroUsize::new(2).unwrap(), + reset_generation: 714, + }; + let prepare = || { + MeterAccumulator::prepare_selected( + MeterHandle(NonZeroU64::new(1).unwrap()), + config, + 48_000, + MeterMetricSet::from_bits_retain(selection), + ) + .unwrap() + }; + let mut planar = prepare(); + let mut resident = prepare(); + let mut time = 3_u64; + for block in 0..18 { + if block == 7 { + time += 17; + } + if block == 10 || block == 13 { + let reset = if block == 10 { + BuiltinResetKind::DiscontinuityKeepTargets + } else { + BuiltinResetKind::FullToPrepared + }; + planar.accumulator.reset(reset); + resident.accumulator.reset(reset); + } + let frames = [0, 1, 4, 13, 2, 5][block % 6]; + let left: Vec<_> = (0..frames) + .map(|f| { + if block > 13 { + -0.0 + } else { + hostile[(f + block + lane) % hostile.len()] + } + }) + .collect(); + let right: Vec<_> = (0..frames) + .map(|f| { + if block > 13 { + 0.0 + } else { + hostile[(f * 3 + lane + 1) % hostile.len()] + } + }) + .collect(); + // Unselected lanes and the unused capacity behind the validated view are poison. + let mut bank_left = vec![f32::from_bits(0x7fc0_0714); (frames + 3) * stride]; + let mut bank_right = vec![f32::NEG_INFINITY; (frames + 3) * stride]; + for f in 0..frames { + bank_left[f * stride + lane] = left[f]; + bank_right[f * stride + lane] = right[f]; + } + let input = MeterInput::strided( + &bank_left[..frames * stride], + &bank_right[..frames * stride], + frames, + stride, + lane, + ) + .unwrap(); + assert_eq!( + resident.accumulator.observe_input(input, time), + planar.accumulator.observe(&left, &right, time), + ); + time += frames as u64; + // Delayed drains force queue overflow as well as partial/multiple windows. + if block % 3 == 2 || block == 17 { + loop { + match (resident.consumer.try_pop(), planar.consumer.try_pop()) { + (Ok(a), Ok(b)) => assert_snapshot_bits(a, b), + (Err(a), Err(b)) => { + assert_eq!(a, b); + break; + } + _ => panic!("publication count differs"), + } + } + } + } + } + } + } +} + +#[test] +fn resident_meter_shape_and_time_errors_precede_mutation_and_empty_input_matches_planar() { + for (left, right, frames, stride, lane) in [ + (&[][..], &[][..], 0, 0, 0), + (&[][..], &[][..], 0, 4, 4), + (&[][..], &[][..], usize::MAX, 8, 0), + (&[0.0][..], &[][..], 1, 1, 0), + (&[0.0][..], &[0.0][..], 0, 1, 0), + (&[0.0][..], &[0.0, 0.0][..], 1, 1, 0), + ] { + assert!(matches!( + MeterInput::strided(left, right, frames, stride, lane), + Err(MeterObservationError::LaneLength) + )); + } + let config = MeterConfig { + period_frames: NonZeroU32::new(2).unwrap(), + peak_hold_frames: 0, + peak_decay_db_per_second: 0.0, + queue_capacity: NonZeroUsize::new(4).unwrap(), + reset_generation: 1, + }; + let prepare = || { + MeterAccumulator::prepare(MeterHandle(NonZeroU64::new(1).unwrap()), config, 48_000).unwrap() + }; + let mut a = prepare(); + let mut b = prepare(); + a.accumulator.observe(&[0.5], &[-0.25], 3).unwrap(); + b.accumulator.observe(&[0.5], &[-0.25], 3).unwrap(); + assert_eq!( + a.accumulator.observe(&[1.0], &[], u64::MAX), + Err(MeterObservationError::LaneLength) + ); + let input = MeterInput::strided(&[0.0; 8], &[1.0; 8], 1, 8, 7).unwrap(); + assert_eq!( + a.accumulator.observe_input(input, u64::MAX), + Err(MeterObservationError::SampleTimeOverflow) + ); + for meter in [&mut a, &mut b] { + meter.accumulator.observe(&[0.125], &[-0.5], 4).unwrap(); + } + assert_snapshot_bits(a.consumer.try_pop().unwrap(), b.consumer.try_pop().unwrap()); + a.accumulator + .observe_input(MeterInput::strided(&[], &[], 0, 8, 7).unwrap(), u64::MAX) + .unwrap(); + b.accumulator.observe(&[], &[], u64::MAX).unwrap(); + for meter in [&mut a, &mut b] { + meter + .accumulator + .observe(&[0.0; 2], &[-0.0; 2], 20) + .unwrap(); + } + assert_snapshot_bits(a.consumer.try_pop().unwrap(), b.consumer.try_pop().unwrap()); +} diff --git a/crates/graph/src/lib.rs b/crates/graph/src/lib.rs index 8347329a3..c22678e62 100644 --- a/crates/graph/src/lib.rs +++ b/crates/graph/src/lib.rs @@ -20,10 +20,11 @@ pub use runtime::{ pub use runtime::{ TestOnlyFailedBufferCapture, TestOnlySelectedSplitFader, TestOnlySplitPairTableWitness, test_only_arm_failed_buffer_capture, test_only_completion_disabled, - test_only_failed_buffer_capture, test_only_reset_selected_split_fader, - test_only_reset_split_pair_table_witness, test_only_resident_input_counts, - test_only_resident_input_reset, test_only_selected_split_fader, - test_only_set_completion_disabled, test_only_split_pair_table_witness, + test_only_failed_buffer_capture, test_only_meter_input_counts, test_only_meter_input_reset, + test_only_reset_selected_split_fader, test_only_reset_split_pair_table_witness, + test_only_resident_input_counts, test_only_resident_input_reset, + test_only_selected_split_fader, test_only_set_completion_disabled, + test_only_split_pair_table_witness, }; use core::cell::Cell; @@ -1690,9 +1691,24 @@ pub struct GraphObservationBlock<'a> { pub right: &'a [f32], pub first_sample: u64, } +/// Immutable final bank output, offered at the same post-node observation point. +#[derive(Clone, Copy)] +pub struct GraphResidentObservationBlock<'a> { + pub lane: rack::ResidentOutputLane<'a>, + pub first_sample: u64, +} /// A bounded observer invoked after its node has completed. pub trait GraphRuntimeObserver: Send { fn observe(&mut self, block: GraphObservationBlock<'_>) -> Result<(), RenderError>; + + /// `None` declines without mutation and requests the ordinary planar observation. + /// Both `Some` results accept once; an accepted error propagates without retry. + fn observe_resident( + &mut self, + _block: GraphResidentObservationBlock<'_>, + ) -> Option> { + None + } } /// One immutable prepared observer binding, ordered by its stable meter handle. pub struct GraphNodeObserverBinding { diff --git a/crates/graph/src/runtime.rs b/crates/graph/src/runtime.rs index fb1fbdbb8..21d231373 100644 --- a/crates/graph/src/runtime.rs +++ b/crates/graph/src/runtime.rs @@ -49,6 +49,20 @@ pub(crate) const ARENA_BASE: u32 = 1; thread_local! { static TEST_ONLY_RESIDENT_DISABLED: std::cell::Cell = const { std::cell::Cell::new(false) }; static TEST_ONLY_RESIDENT_COUNTS: std::cell::Cell<[u64; 2]> = const { std::cell::Cell::new([0; 2]) }; + static TEST_ONLY_METER_RESIDENT_DISABLED: std::cell::Cell = const { std::cell::Cell::new(false) }; + static TEST_ONLY_METER_INPUT_COUNTS: std::cell::Cell<[u64; 3]> = const { std::cell::Cell::new([0; 3]) }; +} + +#[cfg(any(test, feature = "test-support"))] +pub fn test_only_meter_input_reset(disabled: bool) { + TEST_ONLY_METER_RESIDENT_DISABLED.with(|value| value.set(disabled)); + TEST_ONLY_METER_INPUT_COUNTS.with(|value| value.set([0; 3])); +} + +/// Actual `[planar acquisitions, resident offers, resident accepts]`, including accepted errors. +#[cfg(any(test, feature = "test-support"))] +pub fn test_only_meter_input_counts() -> [u64; 3] { + TEST_ONLY_METER_INPUT_COUNTS.with(std::cell::Cell::get) } #[cfg(any(test, feature = "test-support"))] @@ -1418,7 +1432,8 @@ impl Runtime { } } - /// Runs the observers of unit `index`, in member order and then by handle. + /// Runs observers in their existing member/direct/alias binding order. + /// GraphExecutor calls this only after this unit successfully executes in this block. pub(crate) fn observe_unit( &mut self, index: usize, @@ -1426,10 +1441,44 @@ impl Runtime { ) -> Result<(), RenderError> { let Self { lease, units, .. } = self; match &mut units[index] { - RuntimeUnit::Op(op) => observe(op, lease, first_sample), - RuntimeUnit::Bank { members, .. } => { - for member in members.iter_mut() { - observe(member, lease, first_sample)?; + RuntimeUnit::Op(op) => observe(op, lease, first_sample, None), + RuntimeUnit::Bank { + members, + lanes, + chain, + fold, + .. + } => { + let population = *lanes; + let width = chain.width().lanes() as usize; + // execute scatters exactly the final slot's output members. Observed direct + // and alias outputs cannot be redirected by scatter_target; extra readers + // and sends still consume that unchanged scatter. Folded forms decline. + let eligible = population > 0 + && population <= width + && !members.is_empty() + && members.len().is_multiple_of(population) + && chain.active().len() == width + && chain + .active() + .iter() + .enumerate() + .all(|(lane, active)| *active == (lane < population)) + && fold.is_empty() + && chain.fold_lanes().is_empty() + && chain.aux_lanes().is_empty(); + let frames = u32::try_from(lease.frames()).ok(); + let final_start = members.len().checked_sub(population); + let chain: &BankChain = chain; + for (index, member) in members.iter_mut().enumerate() { + let resident = if eligible { + final_start + .and_then(|start| index.checked_sub(start)) + .and_then(|lane| chain.final_output_lane(frames?, lane)) + } else { + None + }; + observe(member, lease, first_sample, resident)?; } Ok(()) } @@ -1674,12 +1723,47 @@ fn execute_op( Ok(()) } -fn observe(op: &mut RuntimeOp, lease: &ArenaLease, first_sample: u64) -> Result<(), RenderError> { - if op.observers.is_empty() { - return Ok(()); - } - let (left, right) = lease.read_stereo(op.output); +fn observe( + op: &mut RuntimeOp, + lease: &ArenaLease, + first_sample: u64, + resident: Option>, +) -> Result<(), RenderError> { + #[cfg(any(test, feature = "test-support"))] + let resident = + resident.filter(|_| !TEST_ONLY_METER_RESIDENT_DISABLED.with(std::cell::Cell::get)); + let mut planar = None; for observer in op.observers.iter_mut() { + if let Some(lane) = resident { + #[cfg(any(test, feature = "test-support"))] + TEST_ONLY_METER_INPUT_COUNTS.with(|value| { + let mut counts = value.get(); + counts[1] += 1; + value.set(counts); + }); + if let Some(result) = observer + .observer + .observe_resident(crate::GraphResidentObservationBlock { lane, first_sample }) + { + #[cfg(any(test, feature = "test-support"))] + TEST_ONLY_METER_INPUT_COUNTS.with(|value| { + let mut counts = value.get(); + counts[2] += 1; + value.set(counts); + }); + result?; + continue; + } + } + let (left, right) = *planar.get_or_insert_with(|| { + #[cfg(any(test, feature = "test-support"))] + TEST_ONLY_METER_INPUT_COUNTS.with(|value| { + let mut counts = value.get(); + counts[0] += 1; + value.set(counts); + }); + lease.read_stereo(op.output) + }); observer.observer.observe(GraphObservationBlock { left, right, @@ -3931,6 +4015,196 @@ mod tests { atomic::{AtomicUsize, Ordering}, }; + #[test] + fn resident_meter_dispatch_preserves_binding_order_lazy_fallback_and_accepted_errors() { + struct Observer { + handle: u64, + accept: Option, + trace: Arc>>, + } + impl crate::GraphRuntimeObserver for Observer { + fn observe(&mut self, block: GraphObservationBlock<'_>) -> Result<(), RenderError> { + self.trace.lock().unwrap().push((self.handle, false)); + assert_eq!(block.left, &[7.0; 3]); + assert_eq!(block.right, &[-9.0; 3]); + Ok(()) + } + fn observe_resident( + &mut self, + block: crate::GraphResidentObservationBlock<'_>, + ) -> Option> { + let fail = self.accept?; + self.trace.lock().unwrap().push((self.handle, true)); + assert_eq!(block.first_sample, 71); + for frame in 0..3 { + let at = frame * block.lane.width().lanes() as usize + block.lane.lane(); + assert_eq!(block.lane.left()[at], 7.0); + assert_eq!(block.lane.right()[at], -9.0); + } + Some(if fail { + Err(RenderError::InvalidEnvelope) + } else { + Ok(()) + }) + } + } + let mut lease = stereo_lease(3, 1); + lease.write(0, 1).fill(7.0); + lease.write(1, 1).fill(-9.0); + let mut chain = BankChain::new( + AoSoaScratch::new(effect_contract::BankWidth::Four, 5).unwrap(), + Box::new([true, false, false, false]), + vec![], + ) + .unwrap(); + chain + .run( + &mut ArenaMembers { + lease: &mut lease, + inputs: &[1], + outputs: &[1], + fold: &[], + master: 0, + }, + 3, + 71, + ) + .unwrap(); + let view = chain.final_output_lane(3, 0).unwrap(); + for accepts in [ + vec![], + vec![Some(false), Some(false)], + vec![None, None], + vec![Some(false), None, Some(false), None], + vec![Some(true), None, Some(false)], + vec![None, Some(true), Some(false)], + ] { + let trace = Arc::new(std::sync::Mutex::new(Vec::new())); + let observers = accepts + .iter() + .enumerate() + .map(|(handle, accept)| { + GraphNodeObserverBinding::new( + GraphNodeId::Output { + output_id: crate::StableGraphId::parse("out").unwrap(), + }, + handle as u64, + Box::new(Observer { + handle: handle as u64, + accept: *accept, + trace: Arc::clone(&trace), + }), + ) + }) + .collect(); + let mut op = RuntimeOp { + inputs: Box::new([]), + staged: Box::new([]), + sidechain: None, + output: 1, + kind: NodeKind::BankMember, + split_pair: None, + observers, + }; + test_only_meter_input_reset(false); + let result = observe(&mut op, &lease, 71, Some(view)); + let take = accepts + .iter() + .position(|a| *a == Some(true)) + .map_or(accepts.len(), |i| i + 1); + assert_eq!(result.is_err(), accepts.contains(&Some(true))); + let seen = &accepts[..take]; + assert_eq!( + *trace.lock().unwrap(), + seen.iter() + .enumerate() + .map(|(i, a)| (i as u64, a.is_some())) + .collect::>() + ); + assert_eq!( + test_only_meter_input_counts(), + [ + u64::from(seen.contains(&None)), + take as u64, + seen.iter().filter(|a| a.is_some()).count() as u64, + ] + ); + } + test_only_meter_input_reset(false); + } + + #[test] + fn resident_meter_entry_has_one_final_output_dispatch_and_admission_control() { + fn valid(source: &str) -> bool { + let production = source.split("\n#[cfg(test)]\nmod tests {").next().unwrap(); + let observation = production + .split("pub(crate) fn observe_unit(") + .nth(1) + .unwrap() + .split(concat!("// REALTIME_POLICY_", "END")) + .next() + .unwrap(); + // Effect-state publication has an unrelated method with the same name. + // Count calls only inside the meter-observer dispatcher, whose body ends + // at the existing realtime-region boundary. + let Some(dispatcher) = production + .split_once("\nfn observe(\n") + .and_then(|(_, body)| body.split_once(concat!("// REALTIME_POLICY_", "END"))) + .map(|(body, _)| body) + else { + return false; + }; + dispatcher.matches(".observe_resident(").count() == 1 + && production.matches(".final_output_lane(").count() == 1 + && [ + "let Self { lease, units, .. } = self;", + "RuntimeUnit::Op(op) => observe(op, lease, first_sample, None)", + "let eligible = population > 0", + "population <= width", + "!members.is_empty()", + "members.len().is_multiple_of(population)", + "chain.active().len() == width", + "*active == (lane < population)", + "fold.is_empty()", + "chain.fold_lanes().is_empty()", + "chain.aux_lanes().is_empty()", + "u32::try_from(lease.frames()).ok()", + "members.len().checked_sub(population)", + "let chain: &BankChain = chain;", + "let resident = if eligible", + "index.checked_sub(start)", + "chain.final_output_lane(frames?, lane)", + "observe(member, lease, first_sample, resident)?;", + ] + .iter() + .all(|term| observation.contains(term)) + } + let source = include_str!("runtime.rs"); + assert!(valid(source)); + for (from, to) in [ + ( + ".observe_resident(crate::GraphResidentObservationBlock", + ".observe_other(crate::GraphResidentObservationBlock", + ), + ( + "let mut planar = None;", + "let mut planar = None; observer.observe_resident(block);", + ), + ("let resident = if eligible", "let resident = if true"), + ("index.checked_sub(start)", "Some(index)"), + ( + "chain.final_output_lane(frames?, lane)", + "chain.final_output_lane(1, lane)", + ), + ( + "observe(member, lease, first_sample, resident)?;", + "observe(member, lease, first_sample, resident).ok();", + ), + ] { + assert!(!valid(&source.replacen(from, to, 1)), "control: {from}"); + } + } + #[test] fn rt9_resident_entry_has_one_guarded_production_caller_and_control() { fn valid(runtime: &str, graph: &str, rack: &str) -> bool { diff --git a/crates/rack/src/lib.rs b/crates/rack/src/lib.rs index 0a88dbf56..b90244518 100644 --- a/crates/rack/src/lib.rs +++ b/crates/rack/src/lib.rs @@ -419,6 +419,28 @@ pub trait BankStage: Send { ChannelSymmetryWitness::DECLINED } + /// Publish one fixed-width witness bank for this block's valid, non-identity lanes. + /// + /// The default preserves existing `lane_symmetry` overrides: its calls are made through + /// the concrete implementation of this default, after one chain-to-stage virtual call. + /// A processor boxed inside that implementation may still forward its own virtual queries. + /// Lanes outside `width` or absent from `active_lanes` remain declined and are never queried. + // REALTIME_POLICY_BEGIN + fn lane_symmetry_bank( + &self, + width: BankWidth, + active_lanes: u8, + ) -> [ChannelSymmetryWitness; 8] { + let mut witnesses = [ChannelSymmetryWitness::DECLINED; 8]; + for (lane, witness) in witnesses[..width.lanes() as usize].iter_mut().enumerate() { + if active_lanes & (1u8 << lane) != 0 { + *witness = self.lane_symmetry(lane); + } + } + witnesses + } + // REALTIME_POLICY_END + /// Drain this stage's live-console queues, before any lane of the block is dispatched. /// /// # Why the drain is a separate call and not the first paragraph of [`process`](Self::process) @@ -1406,6 +1428,43 @@ pub trait BankMembers { } } +/// An immutable active lane of the final left/right resident bank output. +/// +/// The borrowed words contain `frames * width.lanes()` entries per plane; a lane's +/// sample at frame `f` is at `f * width.lanes() + lane`. Only graph's observation +/// point after successful execution can establish current-block and node identity. +#[derive(Clone, Copy)] +pub struct ResidentOutputLane<'a> { + left: &'a [f32], + right: &'a [f32], + frames: u32, + width: BankWidth, + lane: usize, +} + +impl<'a> ResidentOutputLane<'a> { + #[must_use] + pub fn left(&self) -> &'a [f32] { + self.left + } + #[must_use] + pub fn right(&self) -> &'a [f32] { + self.right + } + #[must_use] + pub const fn frames(&self) -> u32 { + self.frames + } + #[must_use] + pub const fn width(&self) -> BankWidth { + self.width + } + #[must_use] + pub const fn lane(&self) -> usize { + self.lane + } +} + /// One bank chain: a resident L/R AoSoA block plus its ordered slots. /// /// Exactly one gather and one scatter per [`run`](BankChain::run), whatever the slot count @@ -1691,28 +1750,10 @@ impl BankChain { /// This cohort lane's channel-symmetry witness: the conjunction over every slot of the chain. /// - /// # Why this is a pull and not a stored aggregate - /// - /// The terms themselves are event-maintained -- a slot's live terms move only when a record is - /// drained, and its designed term only when the plan is rebuilt -- so the aggregate is a - /// conjunction of at most `slots` already-computed values. That is the cost the collapse - /// design priced as "an AND over eight lane witnesses, nanoseconds", and it is why nothing - /// here is cached: a cache would add a per-block invalidation check to buy back an `and` over - /// four bools. - /// - /// What *is* cached is one level down, and it has to be: `lane_channel_symmetry` is a walk over - /// every designed word a kernel reads, and [`BankChain::run`] pulls this once per lane per slot - /// per block. `EffectBankStage::designed` and its console twin take that comparison once, at - /// bind, where it is fixed for the life of the bound bank; the live terms stay live, because - /// they are the ones a drain moves. So the per-block cost of the whole witness is an `and` over - /// `slots * lanes` cached flags plus one stored byte per console-driven lane. - /// - /// The **input** bank is the third holder of that cache and the only one whose words move - /// within a plan, because #210 phase 3 made its trim and polarity words live. It keeps the - /// same shape anyway -- the builtins crate's `InputStage::symmetry` -- maintained by the five - /// writers of a compared word rather than fixed at bind. Until #235 it alone re-derived its - /// walk on every pull, which is what made this paragraph a description of two thirds of the - /// tree rather than of all of it. + /// Stages maintain designed and live terms at preparation or event-drain boundaries. This + /// single-lane evidence helper pulls only the requested lane. Render and the bank-wide + /// helpers publish all lanes once per active stage through `lane_symmetry_bank` instead. + /// The aggregate is local to the decision, so a later block always sees its completed drains. /// /// An inactive lane declines: it renders no track, so there is nothing to collapse. #[must_use] @@ -1731,6 +1772,42 @@ impl BankChain { witness } + // REALTIME_POLICY_BEGIN + /// One publication per active stage; slot identities contribute no terms. + fn lane_symmetry_bank(&self) -> [ChannelSymmetryWitness; 8] { + let mut witnesses = [ChannelSymmetryWitness::DECLINED; 8]; + for (witness, active) in witnesses.iter_mut().zip(self.active.iter()) { + if *active { + *witness = ChannelSymmetryWitness::SYMMETRIC; + } + } + for slot in &self.slots { + if slot.has_active_lanes() { + let stage = slot + .stage + .lane_symmetry_bank(self.scratch.width, slot.active_lanes); + for lane in 0..self.lanes { + if slot.lane_active(lane) { + witnesses[lane] = witnesses[lane].and(stage[lane]); + } + } + } + } + witnesses + } + + fn all_active_witnesses_hold( + &self, + witnesses: &[ChannelSymmetryWitness; 8], + terms: u8, + ) -> bool { + witnesses + .iter() + .zip(self.active.iter()) + .all(|(witness, active)| !active || witness.holds(terms)) + } + // REALTIME_POLICY_END + /// One flag per **active** lane, in lane order: does that lane's whole witness hold? /// /// The localisable form of [`symmetry_counters`](Self::symmetry_counters), and the form the @@ -1740,9 +1817,10 @@ impl BankChain { /// member list does. #[must_use] pub fn active_lane_eligibility(&self) -> Vec { + let witnesses = self.lane_symmetry_bank(); (0..self.lanes) .filter(|lane| self.active[*lane]) - .map(|lane| self.lane_symmetry(lane).eligible()) + .map(|lane| witnesses[lane].eligible()) .collect() } @@ -1754,9 +1832,10 @@ impl BankChain { /// planner's problem, not the chain's. #[must_use] pub fn all_lanes_symmetric(&self) -> bool { - (0..self.lanes) - .filter(|lane| self.active[*lane]) - .all(|lane| self.lane_symmetry(lane).eligible()) + self.all_active_witnesses_hold( + &self.lane_symmetry_bank(), + ChannelSymmetryWitness::SYMMETRIC.terms(), + ) } /// Whether a **dual** block rendered under this cohort's witness leaves every active lane's two @@ -1766,25 +1845,23 @@ impl BankChain { /// `UNBYPASSED` term: see [`ChannelSymmetryWitness::AGREEING`] for why a bypass window is the /// one way to lose the witness without moving the two channels apart. /// - /// Short-circuits, like its sibling, and [`run`](Self::run) reaches it only when the answer can - /// change something -- see the guard chain there, which is what keeps this off the steady-state - /// path of every session that is not in the middle of a bypass. - fn all_lanes_preserve_agreement(&self) -> bool { - (0..self.lanes) - .filter(|lane| self.active[*lane]) - .all(|lane| self.lane_symmetry(lane).preserves_channel_agreement()) + /// Reuses the eligibility publication when armed. The forced-off path publishes only if + /// this maintenance step is reached, after any disengage has restored channel state. + fn all_lanes_preserve_agreement(&self, witnesses: &[ChannelSymmetryWitness; 8]) -> bool { + self.all_active_witnesses_hold(witnesses, ChannelSymmetryWitness::AGREEING) } /// `[eligible active lanes, active lanes]` for this chain. Evidence and gates only. #[must_use] pub fn symmetry_counters(&self) -> [u64; 2] { + let witnesses = self.lane_symmetry_bank(); let mut counters = [0_u64; 2]; - for lane in 0..self.lanes { + for (lane, witness) in witnesses.iter().enumerate().take(self.lanes) { if !self.active[lane] { continue; } counters[1] += 1; - if self.lane_symmetry(lane).eligible() { + if witness.eligible() { counters[0] += 1; } } @@ -1838,6 +1915,34 @@ impl BankChain { } // REALTIME_POLICY_BEGIN + /// Borrow the actual final scratch planes for one active lane. + /// + /// This validates dimensions only. The caller must establish successful execution + /// in the current block and an identical final-output member mapping before offering + /// it to an observer. In particular, a previous collapse does not prove freshness, + /// and a dual suffix can change the right plane after the collapse seam's copy. + #[must_use] + pub fn final_output_lane(&self, frames: u32, lane: usize) -> Option> { + let width = self.scratch.width; + let lanes = width.lanes() as usize; + if frames == 0 + || frames > self.scratch.quantum + || self.lanes != lanes + || self.active.len() != lanes + || !self.active.get(lane).copied().unwrap_or(false) + { + return None; + } + let words = usize::try_from(frames).ok()?.checked_mul(lanes)?; + Some(ResidentOutputLane { + left: self.scratch.left.get(..words)?, + right: self.scratch.right.get(..words)?, + frames, + width, + lane, + }) + } + /// Gather every active lane, run every non-identity slot over the resident block, scatter every /// active lane back. No allocation, no shape `Result`, one transpose round-trip. pub fn run( @@ -1894,45 +1999,16 @@ impl BankChain { slot.stage.begin_block(first_sample)?; } } - // The dispatch. One `bool` per block per chain, decided before a sample is gathered, from - // the event-maintained witness the slots already carry: `all_lanes_symmetric` is an `and` - // over `slots * lanes` cached flags plus, for a console slot, one stored byte per lane. - // - // A command lands at a block boundary, so the eligibility this reads is the eligibility - // that holds for every sample of this block -- which is what makes a per-block mode legal - // at all. - // - // M3 adds one term to the M2 dispatch and no work to it: `collapse_channels_agree` is the - // premise the witness does not supply, maintained at the bottom of this section. - // - // That sentence was **false as shipped** and is true again (#235). M3 hoisted the witness - // out of the M2 conjunction's short-circuit into an unconditional per-block pull, which - // reached an input-bank walk nothing cached: +39-42% on the dispatch-dominated rows and - // +4.5% on the forced-off arm, on chains that could never collapse at all. Both halves - // are repaired here -- `armed &&` below restores the short-circuit, and - // `InputStage::symmetry` gives the input bank the caching the first paragraph above always - // claimed for it -- and the second half also retires ~2 us the *eligible* arm had been - // paying since M2, which no short-circuit can reach because that arm's walk is the one the - // dispatch genuinely needs. + // Publish after every drain. The armed decision combines each stage's lane bank once, + // then reuses it if declining eligibility requires agreement maintenance below. Stages + // outside rack may still forward nested processor queries; only this boundary is bulk. let armed = self.collapse_prefix > 0 && self.collapse_source && !self.collapse_forced_off; - // `armed &&` is M2's short-circuit, restored (#235). The walk runs only where its answer - // can change something, and the `false` it substitutes elsewhere is not an approximation: - // - // * the recovery-window proof and the collapse decision below are both - // `armed && witness && ..`, so an unarmed chain's witness cannot reach either; - // * the invariant's maintenance step is `.. && self.can_collapse() - // && self.collapse_channels_agree && !witness && !self.all_lanes_preserve_agreement()`. - // An unarmed chain that can collapse at all is the forced-off arm, and there the - // substituted `false` opens the `!witness` clause and hands the question to - // `all_lanes_preserve_agreement`. That is the *same verdict*, because `AGREEING` is a - // subset of `ALL`: eligible implies preserving, so `!eligible && !preserving` and - // `!preserving` clear the flag on exactly the same blocks. The arm pays one walk either - // way, and the invariant is maintained on it exactly as M3 wrote it. - // - // So a chain with no collapsible prefix and a stereo-source chain -- every session M2 left - // alone -- take no walk at all, which is what the guard chain below claims and what the - // hoist had stopped being true. - let witness = armed && self.all_lanes_symmetric(); + // Keep the #235 short-circuit: a chain that cannot collapse publishes nothing. Forced-off + // chains publish lazily at the maintenance step, after any disengage, in the original order. + let witnesses = armed.then(|| self.lane_symmetry_bank()); + let witness = witnesses.as_ref().is_some_and(|bank| { + self.all_active_witnesses_hold(bank, ChannelSymmetryWitness::SYMMETRIC.terms()) + }); // The way back for a chain the invariant has declined. Asked at most once per block per // prefix slot, and only inside a *recovery window* -- the chain is otherwise ready to // collapse and this is the only thing refusing it -- so a session that never disagrees @@ -1969,13 +2045,9 @@ impl BankChain { // it is why the M3 dispatch adds no work at all to the sessions M2 left alone; // * a chain that has **already lost** agreement is skipped: only a proof brings it back, // and the proof is above; - // * a block whose witness was **eligible** is skipped, because eligible implies preserving - // -- `AGREEING` is a subset of `ALL` -- so the second walk would be asking a question the - // first already answered. Since #235 restored M2's short-circuit this clause is the one - // the *armed* arm takes; the forced-off arm reaches here with `witness == false` because - // the eligible walk was never taken, and answers the same question once through - // `all_lanes_preserve_agreement` instead. Same verdict, same one walk -- see the - // short-circuit's own note above. + // * a block whose witness was **eligible** is skipped, because eligible implies preserving. + // A declining armed decision reuses its publication. A forced-off decision publishes here + // once, only when agreement can still be lost, preserving the original short-circuit. // // What is left is the case the walk is for: a collapsible chain rendering dual under a // witness that is not eligible, with agreement still to lose. That is a bypass window, or @@ -1992,7 +2064,9 @@ impl BankChain { && self.can_collapse() && self.collapse_channels_agree && !witness - && !self.all_lanes_preserve_agreement() + && !self.all_lanes_preserve_agreement( + &witnesses.unwrap_or_else(|| self.lane_symmetry_bank()), + ) { self.collapse_channels_agree = false; } @@ -2618,6 +2692,86 @@ impl BankChain { mod tests { use super::*; + #[test] + fn resident_output_lane_checks_shape_and_preserves_final_planes_after_collapse() { + for width in [BankWidth::Four, BankWidth::Eight] { + let lanes = width.lanes() as usize; + for population in [1, lanes - 1, lanes] { + let active: Vec<_> = (0..lanes).map(|lane| lane < population).collect(); + for suffix in [false, true] { + let mut slots = + vec![slot(active.clone(), Box::new(Scale::new([0.5; 2], true)))]; + if suffix { + slots.push(slot( + active.clone(), + Box::new(Matrix { + coefficients: [0.75, 0.125, -0.25, 0.5], + }), + )); + } + let mut chain = BankChain::new( + AoSoaScratch::new(width, 17).unwrap(), + active.clone().into_boxed_slice(), + slots, + ) + .unwrap(); + chain.arm_mono_collapse(true); + // Engage, forced decline, and recovery all expose the final right plane. + for (block, forced) in [false, true, false].into_iter().enumerate() { + chain.force_mono_collapse_off(forced); + chain.scratch.left.fill(f32::from_bits(0x7fc0_0714)); + chain.scratch.right.fill(f32::from_bits(0xffc0_0714)); + let frames = 9; + let mut planes = Planes { + left: (0..lanes) + .map(|lane| vec![(lane + 1) as f32; frames as usize]) + .collect(), + right: (0..lanes) + .map(|lane| vec![(lane + 1) as f32; frames as usize]) + .collect(), + }; + chain.run(&mut planes, frames, block as u64 * 9).unwrap(); + assert_eq!(chain.is_collapsed(), !forced); + for lane in 0..lanes { + let view = chain.final_output_lane(frames, lane); + if lane >= population { + assert!(view.is_none()); + continue; + } + let view = view.unwrap(); + assert_eq!( + (view.frames(), view.width(), view.lane()), + (frames, width, lane) + ); + assert_eq!(view.left().len(), frames as usize * lanes); + assert_eq!(view.right().len(), frames as usize * lanes); + for frame in 0..frames as usize { + assert_eq!( + view.left()[frame * lanes + lane].to_bits(), + planes.left[lane][frame].to_bits() + ); + assert_eq!( + view.right()[frame * lanes + lane].to_bits(), + planes.right[lane][frame].to_bits() + ); + } + if suffix { + assert_ne!(view.left()[lane], view.right()[lane]); + } + } + assert!(chain.final_output_lane(0, 0).is_none()); + assert!(chain.final_output_lane(18, 0).is_none()); + assert!(chain.final_output_lane(frames, lanes).is_none()); + } + chain.scratch.right = Box::new([]); + assert!(chain.final_output_lane(1, 0).is_none()); + chain.active = Box::new([]); + assert!(chain.final_output_lane(1, 0).is_none()); + } + } + } + } + #[test] fn rt9_resident_copy_matches_scalar_gather_and_preserves_poisoned_words() { for width in [BankWidth::Four, BankWidth::Eight] { @@ -3777,7 +3931,10 @@ mod tests { ) ); let (queries, lane_inspections) = prepared_activity_observation(); - assert_eq!(queries, 8, "ordinary, prefix-mono and seam-dual guards"); + assert_eq!( + queries, 10, + "ordinary, prefix-mono and seam-dual guards plus two stage publications" + ); assert_eq!( lane_inspections, 0, "prepared queries do not scan lane masks" @@ -5261,6 +5418,309 @@ mod tests { } } + #[derive(Clone, Copy, Default)] + struct WitnessCalls { + bulk: [usize; 3], + direct: [usize; 3], + lanes: [[usize; 8]; 3], + begun: u8, + } + + thread_local! { + static WITNESS_CALLS: std::cell::Cell = + std::cell::Cell::new(WitnessCalls::default()); + } + + fn record_witness_call(update: impl FnOnce(&mut WitnessCalls)) { + WITNESS_CALLS.with(|calls| { + let mut value = calls.get(); + update(&mut value); + calls.set(value); + }); + } + + fn reset_witness_calls() { + WITNESS_CALLS.with(|calls| calls.set(WitnessCalls::default())); + } + + /// Models an external implementation that only overrides the pre-existing lane method. + struct DefaultWitnessStage { + id: usize, + witnesses: [ChannelSymmetryWitness; 8], + } + + impl BankStage for DefaultWitnessStage { + fn process(&mut self, _block: BankBlock<'_>) -> Result<(), RenderError> { + Ok(()) + } + + fn lane_symmetry(&self, lane: usize) -> ChannelSymmetryWitness { + record_witness_call(|calls| calls.lanes[self.id][lane] += 1); + self.witnesses[lane] + } + } + + /// The two outer entry points count separately, so restoring the old lane/slot walk fails + /// even if it returns the same bits. The concrete inner default also records every lane read. + struct WitnessStage { + inner: DefaultWitnessStage, + width: BankWidth, + active_lanes: u8, + require_drains: u8, + drain_change: Option, + } + + impl BankStage for WitnessStage { + fn process(&mut self, _block: BankBlock<'_>) -> Result<(), RenderError> { + Ok(()) + } + + fn supports_mono_collapse(&self) -> bool { + true + } + + fn begin_block(&mut self, first_sample: u64) -> Result<(), RenderError> { + record_witness_call(|calls| calls.begun |= 1 << self.inner.id); + if first_sample == 8 + && let Some(witness) = self.drain_change + { + self.inner.witnesses[0] = witness; + } + Ok(()) + } + + fn lane_symmetry(&self, lane: usize) -> ChannelSymmetryWitness { + record_witness_call(|calls| calls.direct[self.inner.id] += 1); + self.inner.lane_symmetry(lane) + } + + fn lane_symmetry_bank( + &self, + width: BankWidth, + active_lanes: u8, + ) -> [ChannelSymmetryWitness; 8] { + assert_eq!(width, self.width); + assert_eq!(active_lanes, self.active_lanes); + record_witness_call(|calls| { + assert_eq!(calls.begun & self.require_drains, self.require_drains); + calls.bulk[self.inner.id] += 1; + }); + self.inner.lane_symmetry_bank(width, active_lanes) + } + } + + fn witness_slot( + id: usize, + width: BankWidth, + mask: u8, + witnesses: [ChannelSymmetryWitness; 8], + require_drains: u8, + drain_change: Option, + ) -> BankSlot { + slot( + (0..width.lanes()) + .map(|lane| mask & (1 << lane) != 0) + .collect(), + Box::new(WitnessStage { + inner: DefaultWitnessStage { id, witnesses }, + width, + active_lanes: mask, + require_drains, + drain_change, + }), + ) + } + + #[test] + fn stage_witness_bank_default_preserves_overrides_width_and_identity_bounds() { + let values = core::array::from_fn(|lane| ChannelSymmetryWitness::from_terms(lane as u8)); + let classified: Box = Box::new(DefaultWitnessStage { + id: 0, + witnesses: values, + }); + let unclassified: Box = Box::new(PassThrough); + for width in [BankWidth::Four, BankWidth::Eight] { + for mask in [0, 0x55, 0xff] { + reset_witness_calls(); + let bank = classified.lane_symmetry_bank(width, mask); + let queries = WITNESS_CALLS.with(|calls| calls.get().lanes[0]); + for lane in 0..8 { + let active = lane < width.lanes() as usize && mask & (1 << lane) != 0; + assert_eq!(queries[lane], usize::from(active)); + assert_eq!( + bank[lane], + if active { + values[lane] + } else { + ChannelSymmetryWitness::DECLINED + } + ); + } + assert_eq!( + unclassified.lane_symmetry_bank(width, mask), + [ChannelSymmetryWitness::DECLINED; 8] + ); + } + } + } + + #[test] + fn stage_witness_bank_aggregates_exact_terms_for_full_partial_and_identity_lanes() { + let symmetric = ChannelSymmetryWitness::SYMMETRIC; + for width in [BankWidth::Four, BankWidth::Eight] { + let lanes = width.lanes() as usize; + let valid = if lanes == 4 { 0x0f } else { 0xff }; + for active_mask in [valid, valid & 0x55] { + let active: Vec = (0..lanes) + .map(|lane| active_mask & (1 << lane) != 0) + .collect(); + let masks = [active_mask & 0x33, active_mask & 0x66, 0]; + let first = core::array::from_fn(|lane| { + if lane == 0 { + symmetric + } else { + ChannelSymmetryWitness::symmetric_except(ChannelSymmetryWitness::UNBYPASSED) + } + }); + let second = core::array::from_fn(|lane| { + if lane == 2 { + ChannelSymmetryWitness::DECLINED + } else { + ChannelSymmetryWitness::symmetric_except(ChannelSymmetryWitness::LIVE) + } + }); + let chain = BankChain::new( + AoSoaScratch::new(width, 8).expect("scratch"), + active.clone().into_boxed_slice(), + vec![ + witness_slot(0, width, masks[0], first, 0, None), + witness_slot(1, width, masks[1], second, 0, None), + witness_slot( + 2, + width, + masks[2], + [ChannelSymmetryWitness::DECLINED; 8], + 0, + None, + ), + ], + ) + .expect("chain"); + let expected: [ChannelSymmetryWitness; 8] = core::array::from_fn(|lane| { + if lane >= lanes || !active[lane] { + return ChannelSymmetryWitness::DECLINED; + } + let a = if masks[0] & (1 << lane) != 0 { + first[lane] + } else { + symmetric + }; + let b = if masks[1] & (1 << lane) != 0 { + second[lane] + } else { + symmetric + }; + a.and(b) + }); + reset_witness_calls(); + assert_eq!(chain.lane_symmetry_bank(), expected); + let calls = WITNESS_CALLS.with(std::cell::Cell::get); + assert_eq!(calls.bulk, [1, 1, 0]); + assert_eq!(calls.direct, [0; 3]); + for (stage, mask) in masks.iter().enumerate() { + for lane in 0..8 { + assert_eq!( + calls.lanes[stage][lane], + usize::from(mask & (1 << lane) != 0) + ); + } + } + let eligible: Vec = (0..lanes) + .filter(|lane| active[*lane]) + .map(|lane| expected[lane].eligible()) + .collect(); + assert_eq!(chain.active_lane_eligibility(), eligible); + assert_eq!( + chain.all_lanes_symmetric(), + eligible.iter().all(|value| *value) + ); + assert_eq!( + chain.symmetry_counters(), + [ + eligible.iter().filter(|value| **value).count() as u64, + eligible.len() as u64 + ] + ); + for (lane, witness) in expected.iter().enumerate() { + assert_eq!(chain.lane_symmetry(lane), *witness); + } + assert_eq!( + chain.lane_symmetry(usize::MAX), + ChannelSymmetryWitness::DECLINED + ); + } + } + } + + #[test] + fn stage_witness_bank_entire_armed_decision_publishes_once_even_when_declining() { + let symmetric = ChannelSymmetryWitness::SYMMETRIC; + for width in [BankWidth::Four, BankWidth::Eight] { + let lanes = width.lanes() as usize; + let valid = if lanes == 4 { 0x0f } else { 0xff }; + for mask in [valid, valid & 0x55] { + for changed in [ + symmetric, + ChannelSymmetryWitness::symmetric_except(ChannelSymmetryWitness::UNBYPASSED), + ChannelSymmetryWitness::symmetric_except(ChannelSymmetryWitness::LIVE), + ] { + let active = (0..lanes).map(|lane| mask & (1 << lane) != 0).collect(); + let mut chain = BankChain::new( + AoSoaScratch::new(width, 8).expect("scratch"), + active, + vec![ + witness_slot(0, width, mask, [symmetric; 8], 0b11, None), + witness_slot(1, width, mask, [symmetric; 8], 0b11, Some(changed)), + ], + ) + .expect("chain"); + chain.arm_mono_collapse(true); + let mut planes = identical_planes(lanes, 8); + for sample in [0, 8] { + reset_witness_calls(); + chain.run(&mut planes, 8, sample).expect("render"); + let calls = WITNESS_CALLS.with(std::cell::Cell::get); + assert_eq!( + calls.bulk, + [1, 1, 0], + "whole decision, including preservation" + ); + assert_eq!(calls.direct, [0; 3], "no outer per-lane virtual calls"); + for stage in 0..2 { + for lane in 0..8 { + assert_eq!( + calls.lanes[stage][lane], + usize::from(mask & (1 << lane) != 0) + ); + } + } + let current = if sample == 0 { symmetric } else { changed }; + assert_eq!(chain.is_collapsed(), current.eligible()); + assert_eq!( + chain.collapse_channels_agree(), + current.preserves_channel_agreement() + ); + } + assert_eq!(chain.collapses(), 1 + u64::from(changed.eligible())); + assert_eq!( + chain.collapse_transitions(), + [u64::from(!changed.eligible()), 0, 0] + ); + } + } + } + } + /// A chain nobody armed never collapses, whatever its witness says. /// /// The `SOURCE` term is decided on the control plane, keyed by track id, and a chain sees only diff --git a/hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md b/hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md index 004b76034..57e40cece 100644 --- a/hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md +++ b/hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md @@ -1,7 +1,7 @@ # Browser deployment matrix -This matrix is generated from the pinned Playwright 1.62.1 headless Linux qualification run over candidate `5a66a1fa41e57437cfc76ee27d30a59fce0dbbcb` and the single shipped simd128 AudioWorklet artifact `6d3a6144109bbc4578dd661bdb84a13490fc94c5191b72608409ca40b44581f8`. The version shown is the lowest version qualified by this run; older versions are unqualified, not implicitly supported. +This matrix is generated from the pinned Playwright 1.62.1 headless Linux qualification run over candidate `cbc3c4f7917d5704396fa904b68e0c5c021da610` and the single shipped simd128 AudioWorklet artifact `ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b`. The version shown is the lowest version qualified by this run; older versions are unqualified, not implicitly supported. | Browser engine | Qualified version floor | Attestation outcome | SIMD gate | AudioWorklet boot | Native corpus digest | Live console (#137) | Observation (#143) | 100 ms main-thread stall | | --- | --- | --- | --- | --- | --- | --- | --- | --- | diff --git a/hosts/host-web/qualification/results.json b/hosts/host-web/qualification/results.json index 82217056e..dd6f41d9f 100644 --- a/hosts/host-web/qualification/results.json +++ b/hosts/host-web/qualification/results.json @@ -1,7 +1,7 @@ { "schema": "miso.web.qualification.matrix.v1", - "candidateCommit": "5a66a1fa41e57437cfc76ee27d30a59fce0dbbcb", - "wasmSha256": "6d3a6144109bbc4578dd661bdb84a13490fc94c5191b72608409ca40b44581f8", + "candidateCommit": "cbc3c4f7917d5704396fa904b68e0c5c021da610", + "wasmSha256": "ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b", "playwrightVersion": "1.62.1", "platform": "linux-headless", "artifact": "single shipped simd128 AudioWorklet artifact", diff --git a/hosts/host-web/web/miso-engine-v1-audio-worklet-artifact.sha256 b/hosts/host-web/web/miso-engine-v1-audio-worklet-artifact.sha256 index 287206932..a192dbde5 100644 --- a/hosts/host-web/web/miso-engine-v1-audio-worklet-artifact.sha256 +++ b/hosts/host-web/web/miso-engine-v1-audio-worklet-artifact.sha256 @@ -1 +1 @@ -6d3a6144109bbc4578dd661bdb84a13490fc94c5191b72608409ca40b44581f8 +ea8f843b254bfc96c277f22d4946db0c33555a281113e221d17f8ec6067a468b