From 3fb05e93db5d2a22f2f5b7bbbb3ed0621e3126c9 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 01:25:26 +0000 Subject: [PATCH 01/16] Scope bounded current implementation of #162 --- ...borrow-session-during-graph-compilation.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md diff --git a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md new file mode 100644 index 00000000..47f635d9 --- /dev/null +++ b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md @@ -0,0 +1,57 @@ +# Graph compiler: remove the per-compile `CompiledSession` clone (#99 F5 successor) + +## Approved current scope — 2026-09-11 + +Remove the full CompiledSession clone in GraphCompiler::compile_with_builtin_tails by confining fallible preparation to a helper borrowing effects and returning owned build results. Ownership is consumed only after the final fallible checks; every failure returns the original caller-owned effects and builtins. Current canonical format is JSON and the function is roughly620lines, not the historical TOML/500line description. + +Own crates/graph-compiler/src/compile.rs and directly relevant graph-compiler tests only. Preserve diagnostic ordering, all early/late failures, schedule, banking, PDC, arithmetic, graph identity and canonical bytes. No owned-string front-half redesign or other audit expansion. If a smaller lexical borrowing solution safely removes the clone without the larger helper extraction, prefer it and record why. + +Gates: representative success and early/late failure ownership tests using distinguishable prepared state; exact diagnostic order and graph/audio parity; direct proof the full-session clone was removed (no replacing it with another equivalent copy). Focused compiler tests and strict Clippy. Root owns any changed shipped artifact qualification, with exact PR/main CI before closure. No speculative speedup or broad allocation campaign. + +Astra XHIGH scoping approved; user authorized execution. Astra LOW implements, Astra XHIGH independently verifies. Five attempts maximum; each coherent pass gets one adversarial verdict. Root checkpoints exact paths and pushes promptly when focused checks pass, before more implementation. At most two active issues: #220 and #162; #221 queued behind delivered #220. Isolated worktrees; no overlapping edits. Root/lane B owns artifact qualification and pinning. Preserve histories and failed evidence; never weaken gates or commit compiler-IR captures. + +Record actual argv/environment/source/exits/logs externally; pause green for root checkpoint. Stop on first unexpected failure and report for bounded correction. Required exact-head PR and main qualification plus upstream GitHub synchronization precede closure. Remove clean delivered worktrees after preserving evidence/history. Historical model names below are superseded by user routing. + +## Historical issue body + +`GraphCompiler::compile_with_builtin_tails` clones the whole `CompiledSession` — canonical TOML included — once per compile, purely to satisfy the borrow checker. #99 is closed, so the debt it was recorded against no longer tracks it. This issue carries it forward. + +## Where + +`crates/miso-engine-graph-compiler/src/compile.rs`, in `compile_with_builtin_tails`: + +```rust +let session = effects.session.clone(); +let model = session.normalized_model(); +``` + +## Why it is there + +`model` borrows the session, and the transactional failure path must hand `effects` back **by value** from inside the loops that read `model`. Every early `return Err(failure(effects, diagnostics))` therefore needs the borrow to be over, which the clone buys. + +## Why it was not removed under #99 + +Removing it means restructuring a ~500-line function so every early `failure(effects, ..)` happens after the borrow ends, and the failure path is a frozen API contract. It was deliberately left as a bounded successor rather than rushed inside the #99 window. The comment at the site says so explicitly (`NOT YET REMOVED (#99 F5, deliberately)`). + +## Successor shape (as specified in the #99 F5 comment) + +Extract a + +```text +build(&effects) -> Result> +``` + +that returns **owned** outputs, with `failure(effects, ..)` called only on its `Err`. That confines the borrow of `effects` to `build`, so the clone is no longer needed to end it. + +## Scope note + +The dominant F5 cost — the canonical dump, its SHA-256 and the Graphviz string on every compile — is already gone; `GraphCompiler::evidence` produces those on demand. What remains is this one `CompiledSession` clone per compile. + +## Constraints on the fix + +- Compile-path surgery: the transactional failure contract (every caller-owned input handed back by value on any failure) must be preserved exactly. +- Bit-identity: class-A digests and the wasm gate legs must be unchanged by the restructure. + +## Provenance + +Filed from the `allow-hygiene` window-cleanup pass (job 4, item 4), which was scoped to file-or-fix this debt and explicitly not to perform the restructure. From ec0dab19e76c00fc09551d0eebac7b9bb09b545b Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 01:28:55 +0000 Subject: [PATCH 02/16] Borrow compiled session through graph preparation (#162) --- ...borrow-session-during-graph-compilation.md | 13 ++ crates/graph-compiler/src/compile.rs | 16 +- crates/graph-compiler/src/lib.rs | 157 ++++++++++++++++++ 3 files changed, 174 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md index 47f635d9..4f02f55b 100644 --- a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md +++ b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md @@ -55,3 +55,16 @@ The dominant F5 cost — the canonical dump, its SHA-256 and the Graphviz string ## Provenance Filed from the `allow-hygiene` window-cleanup pass (job 4, item 4), which was scoped to file-or-fix this debt and explicitly not to perform the restructure. + +## Astra LOW attempt 1 focused checkpoint + +The smallest lexical borrowing solution compiled: borrow effects.session directly +and pass that reference to the index helper. No large extraction or equivalent +copy is needed. The focused test preserves original session allocation, distinct +processor ownership and builtins allocations across early zero-cap and late +node-cap failures, then retries successfully with identical canonical graph +and nonzero four-block PCM. Formatting and this focused test passed. Actual +argv/env/source/exits/logs: `/tmp/engine-162-attempt1`. Incidental Cargo lock +ordering was preserved externally and restored; no dependency change. +Root checkpoints now; full compiler tests, strict Clippy and independent review +remain. No allocation-count or performance claim beyond removing this clone. diff --git a/crates/graph-compiler/src/compile.rs b/crates/graph-compiler/src/compile.rs index b4c1ae1d..b4cdd2ee 100644 --- a/crates/graph-compiler/src/compile.rs +++ b/crates/graph-compiler/src/compile.rs @@ -157,17 +157,9 @@ impl GraphCompiler { if !caps.all_nonzero() { diagnostics.push(diag("graph.resource.limit", "$.graph_compile_caps")); } - // NOT YET REMOVED (#99 F5, deliberately; tracked by #162): this clones the whole - // `CompiledSession`, canonical JSON included, purely to satisfy the borrow checker -- - // `model` borrows the session, and the transactional failure path must hand `effects` - // back **by value** from inside the loops that read `model`. Removing it means - // restructuring a 500-line function so every early `failure(effects, ..)` happens after - // the borrow ends, and the failure path is a frozen API contract. Left as a bounded - // successor rather than rushed: the shape is a - // `build(&effects) -> Result>` that returns owned outputs, - // with `failure(effects, ..)` called only on its `Err`. The dominant F5 cost -- the - // canonical dump, its SHA and the Graphviz string on every compile -- is gone. - let session = effects.session.clone(); + // Borrow only the session field: rejecting branches end the borrow before returning + // all of `effects`, while success consumes the disjoint entries field after validation. + let session = &effects.session; let model = session.normalized_model(); if model.outputs.len() != 1 { diagnostics.push(diag("graph.output.cardinality", "$.outputs")); @@ -445,7 +437,7 @@ impl GraphCompiler { // native effect and -- when this is the `compile_with_builtins` path -- from each track's // prepared input section. `GraphCompiler::compile` has no input sections in its plan at // all, so having one fewer contributor there is the honest answer rather than a gap. - let mut pool_classes = SessionPoolClasses::from_session(&session); + let mut pool_classes = SessionPoolClasses::from_session(session); for entry in &effects.entries { let mut witness = ChannelSymmetryWitness::SYMMETRIC; witness.set( diff --git a/crates/graph-compiler/src/lib.rs b/crates/graph-compiler/src/lib.rs index c0daccc7..7d5b4f5a 100644 --- a/crates/graph-compiler/src/lib.rs +++ b/crates/graph-compiler/src/lib.rs @@ -3162,6 +3162,163 @@ mod tests { .expect("effects") } + #[test] + fn borrowed_session_returns_original_inputs_on_early_and_late_rejection() { + let prepare = || { + let effects = cross_index_effect_fixture(); + let builtins = prepare_session_builtins( + &effects.session, + &[], + BuiltinCompileCaps { + maximum_total_state_bytes: u64::MAX, + maximum_total_retained_payload_bytes: u64::MAX, + maximum_total_meter_items: u64::MAX, + maximum_total_meter_bytes: u64::MAX, + maximum_single_allocation_bytes: u64::MAX, + maximum_meter_streams: u64::MAX, + maximum_period_frames: u32::MAX, + maximum_peak_hold_frames: u32::MAX, + maximum_smoothing_samples: u32::MAX, + }, + ) + .expect("builtins"); + (effects, builtins) + }; + let compile = |effects, builtins, caps| { + GraphCompiler::compile_with_builtins(GraphBuiltinsCompileRequest { + plan_id: 162, + effects, + builtins, + caps, + dispatch: Backend::Scalar, + }) + }; + let (baseline_effects, baseline_builtins) = prepare(); + let baseline = compile(baseline_effects, baseline_builtins, integration_caps()) + .unwrap_or_else(|failure| panic!("baseline: {:?}", failure.diagnostics)); + let (mut effects, mut builtins) = prepare(); + let session_tracks = effects.session.normalized_model().tracks.as_ptr(); + let processors = effects + .entries + .iter() + .map(|entry| &*entry.processor as *const dyn PreparedNativeEffect as *const ()) + .collect::>(); + let builtin_tracks = builtins + .tails() + .map(|(track, _)| track.as_ptr()) + .collect::>(); + for maximum_nodes in [0, 1] { + let mut constrained = integration_caps(); + constrained.maximum_nodes = maximum_nodes; + let failure = match compile(effects, builtins, constrained) { + Ok(_) => panic!("zero cap rejects early; nonzero node cap rejects after planning"), + Err(failure) => failure, + }; + assert_eq!( + failure + .diagnostics + .diagnostics() + .iter() + .map(|diagnostic| (diagnostic.code, diagnostic.path.as_str())) + .collect::>(), + vec![("graph.resource.limit", "$.graph_compile_caps")] + ); + assert_eq!( + failure.effects.session.normalized_model().tracks.as_ptr(), + session_tracks + ); + assert_eq!( + failure + .effects + .entries + .iter() + .map(|entry| &*entry.processor as *const dyn PreparedNativeEffect as *const ()) + .collect::>(), + processors + ); + assert_eq!( + failure + .builtins + .tails() + .map(|(track, _)| track.as_ptr()) + .collect::>(), + builtin_tracks + ); + assert!( + failure + .builtins + .validate_for_session(&failure.effects.session) + .0 + .is_empty() + ); + effects = failure.effects; + builtins = failure.builtins; + } + let recovered = compile(effects, builtins, integration_caps()) + .unwrap_or_else(|failure| panic!("retry: {:?}", failure.diagnostics)); + assert_eq!( + GraphCompiler::evidence(baseline.graph(), baseline.report()).canonical_bytes, + GraphCompiler::evidence(recovered.graph(), recovered.report()).canonical_bytes + ); + let render = |artifact: PreparedGraphBuiltinsArtifact| { + let envelope = artifact.envelope(); + let frames = envelope.quantum.0 as usize; + let nodes = artifact + .external_binding_nodes() + .map(|node| { + let processor: Box = match node { + GraphNodeId::TrackStage { + track_id, + stage: TrackStage::Input, + } => Box::new(AsymmetricTrackImpulseBinding { + left: if track_id.as_str() == "cross0" { + 0.25 + } else { + 0.5 + }, + right: -0.125, + }), + _ => Box::new(IdentityBinding), + }; + GraphNodeBinding::new(node.clone(), processor) + }) + .collect(); + let mut bound = artifact + .into_bound(GraphRuntimeBindings { + envelope, + nodes, + observers: Vec::new(), + }) + .unwrap_or_else(|failure| panic!("bind: {}", failure.code)); + (0..4) + .map(|block| { + let mut pcm = vec![0.0; frames * 2]; + bound + .plan + .render( + RenderIo { + input: None, + output: PlanarBufferMut::try_new(&mut pcm, 2, frames, frames) + .expect("output"), + }, + RenderTime { + absolute_sample: block * frames as u64, + }, + ) + .expect("render"); + pcm + }) + .collect::>() + }; + let expected = render(baseline); + assert!(expected.iter().flatten().any(|sample| *sample != 0.0)); + assert_pcm_bits_equal( + &expected, + &render(recovered), + "original effects and builtins survive rejected compiles", + ); + } + fn cross_index_metadata_latency(track: &str, rack: EffectRack, effect: &str) -> u64 { match (track, rack, effect) { ("cross0", EffectRack::Simd1, "chain0") => 11, From d4906302381953efe8ce85dd3e0a9658c0b6f193 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 01:35:02 +0000 Subject: [PATCH 03/16] Avoid large Result forwarding closure in ownership test (#162) --- ...borrow-session-during-graph-compilation.md | 12 ++++++ crates/graph-compiler/src/lib.rs | 37 ++++++++++++------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md index 4f02f55b..371a2ca8 100644 --- a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md +++ b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md @@ -68,3 +68,15 @@ argv/env/source/exits/logs: `/tmp/engine-162-attempt1`. Incidental Cargo lock ordering was preserved externally and restored; no dependency change. Root checkpoints now; full compiler tests, strict Clippy and independent review remain. No allocation-count or performance claim beyond removing this clone. + +## Attempt 1 review and bounded attempt 2 + +Independent debug and release-unwind suites each passed86 tests. Strict Clippy +then rejected the new test closure's large Result error type; the frozen +ownership-returning API itself is unchanged. Receipt: +`/tmp/issue162-xhigh-qemy_v30/clippy.json`. Root authorized only replacing that +forwarding closure with direct compiler calls. Astra LOW did so; formatting, +locked strict all-target/all-feature Clippy and focused ownership test passed. +Evidence: `/tmp/engine-162-attempt2/manifest.json`. No production/API/dependency +change beyond original borrowing patch. Independent follow-up verdict pending; +prepared external final-plan-cap ownership control remains unrun. diff --git a/crates/graph-compiler/src/lib.rs b/crates/graph-compiler/src/lib.rs index 7d5b4f5a..1d3637ab 100644 --- a/crates/graph-compiler/src/lib.rs +++ b/crates/graph-compiler/src/lib.rs @@ -3184,18 +3184,15 @@ mod tests { .expect("builtins"); (effects, builtins) }; - let compile = |effects, builtins, caps| { - GraphCompiler::compile_with_builtins(GraphBuiltinsCompileRequest { - plan_id: 162, - effects, - builtins, - caps, - dispatch: Backend::Scalar, - }) - }; let (baseline_effects, baseline_builtins) = prepare(); - let baseline = compile(baseline_effects, baseline_builtins, integration_caps()) - .unwrap_or_else(|failure| panic!("baseline: {:?}", failure.diagnostics)); + let baseline = GraphCompiler::compile_with_builtins(GraphBuiltinsCompileRequest { + plan_id: 162, + effects: baseline_effects, + builtins: baseline_builtins, + caps: integration_caps(), + dispatch: Backend::Scalar, + }) + .unwrap_or_else(|failure| panic!("baseline: {:?}", failure.diagnostics)); let (mut effects, mut builtins) = prepare(); let session_tracks = effects.session.normalized_model().tracks.as_ptr(); let processors = effects @@ -3210,7 +3207,13 @@ mod tests { for maximum_nodes in [0, 1] { let mut constrained = integration_caps(); constrained.maximum_nodes = maximum_nodes; - let failure = match compile(effects, builtins, constrained) { + let failure = match GraphCompiler::compile_with_builtins(GraphBuiltinsCompileRequest { + plan_id: 162, + effects, + builtins, + caps: constrained, + dispatch: Backend::Scalar, + }) { Ok(_) => panic!("zero cap rejects early; nonzero node cap rejects after planning"), Err(failure) => failure, }; @@ -3254,8 +3257,14 @@ mod tests { effects = failure.effects; builtins = failure.builtins; } - let recovered = compile(effects, builtins, integration_caps()) - .unwrap_or_else(|failure| panic!("retry: {:?}", failure.diagnostics)); + let recovered = GraphCompiler::compile_with_builtins(GraphBuiltinsCompileRequest { + plan_id: 162, + effects, + builtins, + caps: integration_caps(), + dispatch: Backend::Scalar, + }) + .unwrap_or_else(|failure| panic!("retry: {:?}", failure.diagnostics)); assert_eq!( GraphCompiler::evidence(baseline.graph(), baseline.report()).canonical_bytes, GraphCompiler::evidence(recovered.graph(), recovered.report()).canonical_bytes From 49f83155cc236fa0deb1d77aaa6ad7b3064db935 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 01:41:08 +0000 Subject: [PATCH 04/16] Record compiler borrowing PASS and frozen delivery checkpoint (#162) --- .../162-borrow-session-during-graph-compilation.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md index 371a2ca8..adb28751 100644 --- a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md +++ b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md @@ -80,3 +80,16 @@ locked strict all-target/all-feature Clippy and focused ownership test passed. Evidence: `/tmp/engine-162-attempt2/manifest.json`. No production/API/dependency change beyond original borrowing patch. Independent follow-up verdict pending; prepared external final-plan-cap ownership control remains unrun. + +## Independent Astra XHIGH attempt 2 source PASS + +Reviewed d4906302381953efe8ce85dd3e0a9658c0b6f193: production borrowing patch +unchanged, direct test calls fix Clippy without lint suppression or API changes. +Retained86 debug and86 release-unwind test passes. Independent final-plan-cap +control (maximum_plan_bytes=1) reaches the final check after bank preparation, +returns original session/effect/builtin ownership with exact diagnostic, then +retries with equal canonical graph and four nonzero PCM blocks. Separate copy +and target used. Evidence: `/tmp/issue162-xhigh-qemy_v30/attempt2-review.md` and +final-cap-attempt2 receipts. Root defers artifact qualification for a coherent +joint delivery with #221 after #220's required delivery, while source remains +frozen. Required artifact/PR/main gates and synchronized closure remain pending. From 2e87f8b2d51e4e79f3590375932ec10653f0d11d Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 01:56:06 +0000 Subject: [PATCH 05/16] docs: scope ownership-safe route-fold bind rejection --- .../221-reject-invalid-route-fold-binding.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md new file mode 100644 index 00000000..a2ed3714 --- /dev/null +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -0,0 +1,45 @@ +# apply_route_fold's debug_assert arms must be hard bind errors + +## Approved bounded scope — 2026-09-11 + +Prerequisite: #220 delivered with independent differential coverage and required +PR/main CI. Astra XHIGH scoped the following ownership-safe correction. + +Return hard bind errors for invalid route-fold installation before ownership +transfer. GraphBindFailure/GraphSourceBindFailure must return every original +caller-owned input; simply making build_sequential fallible after consuming +RuntimeParts is forbidden. Use a borrowed preflight before bind_optional_source_set +moves observers/bindings. Reuse the same run/fold planning logic through a narrow +borrowed metadata view; avoid a second independent routing algorithm. Validate +fold run mappings, bank-unit ownership, exact prefix masks against first bank +width/active lanes, and emitted plain-singleton master mapping. Include missing +mappings currently silently skipped. The consuming executor must use the same private validated planning result/proof. +All recoverable route-fold installation errors return before any observer, +binding or source ownership transfer, pairing-factory consumption, route +retirement or fold arming. Application of that validated result is infallible; +it must not recompute an unchecked fold plan or replace assertions with panics. + +Do not add generic post-consumption recovery, change route-fold admission or PCM, +or weaken any transactional bind promise. If the smallest implementation cannot +reuse existing planning without broad architecture expansion, pause and report. +Own crates/graph/src/lib.rs, runtime.rs and directly relevant graph tests; +console-workload helper/callsites replace GraphIdentity with GraphNodeBinding::identity +as already requested by this issue. Root owns artifact qualification/pinning. + +Gates: bounded fault injection for each former assertion/missing mapping through +preflight/public bind; exact typed rejection and original owners/source set returned, +then successful retry. Preserve #220 decisions, fold counts and bitwise PCM in +debug/release; strict focused Clippy; zero-input identity poison-buffer control. +No new performance framework or compiler-IR captures. Coordinate shared artifact +qualification with accepted #162 compiler borrowing if both remain frozen. + +Astra LOW implements; Astra XHIGH independently reviews. Five attempts maximum; +root checkpoints/pushes exact paths promptly on focused green, before more work. +At most two active issues (#221 and #162 once #220 closes). Isolated worktrees. +Record actual commands/env/source/exits/logs externally; stop unexpected failure. +Required reviewed-head PR and main qualification precede upstream issue closure +and clean delivered worktree removal. Historical model names below are superseded. + +## Historical issue body + +Required follow-up F2 from strip Job 3's verification: the `debug_assert!(false)` arms in `apply_route_fold` are commented as inert, but by the time any arm runs the route ops are already retired from unit emission — a reachable instance in RELEASE would be silent wrong audio (stale route buffers / vanished contribution), not a safe decline. Each arm was verified unreachable today (routes are singleton plain units; membership⇒Bank; active masks are planner prefixes), and the code runs at bind on the control plane — so the fix is cheap: make them hard bind errors. Also (F6, same area): console-workload's `GraphIdentity` do-nothing Bound processor should become `GraphNodeBinding::identity` per the new zero-input contract. \ No newline at end of file From 9aeffc2615467e367457b5e4ef71c9eeb3708ada Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 01:59:54 +0000 Subject: [PATCH 06/16] docs: bound rack construction proof for route-fold errors --- .../221-reject-invalid-route-fold-binding.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index a2ed3714..07c8be0e 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -40,6 +40,25 @@ Record actual commands/env/source/exits/logs externally; stop unexpected failure Required reviewed-head PR and main qualification precede upstream issue closure and clean delivered worktree removal. Historical model names below are superseded. +## Bounded rack API amendment — Astra XHIGH, 2026-09-11 + +Inspection before implementation found no existing infallible fold-installation +seam. Extend exact-path ownership to crates/rack/src/lib.rs and focused rack tests. +Use an opaque prepared fold configuration that validates width, its owned/copied +active mask, and requested fold mask during borrowed graph preflight. A narrow +constructor variant consumes that configuration as the sole source of the new +chain's active/fold masks: no second caller-supplied active mask and no transferable +setter that can apply a proof to an arbitrary chain. Delegate ordinary scratch/slot +shape checks to existing BankChain::new, then install the validated fold through a +private infallible helper shared with arm_fold. Preserve existing arm_fold behavior. +Graph carries each configuration in the same validated run plan into chain_for / +build_chain, paired with the first run bank's exact width/active metadata through +pairing/fallback. All fold-mask errors precede ownership consumption. The existing +bank-shape constructor Result/expect is unchanged; no new post-consumption fold +error or panic is permitted. Gates include width mismatch, inactive armed lane, +all-false disarm, valid partial/full masks, and proof/configuration misuse prevention. +This is a scope correction before source edits, not a failed implementation attempt. + ## Historical issue body Required follow-up F2 from strip Job 3's verification: the `debug_assert!(false)` arms in `apply_route_fold` are commented as inert, but by the time any arm runs the route ops are already retired from unit emission — a reachable instance in RELEASE would be silent wrong audio (stale route buffers / vanished contribution), not a safe decline. Each arm was verified unreachable today (routes are singleton plain units; membership⇒Bank; active masks are planner prefixes), and the code runs at bind on the control plane — so the fix is cheap: make them hard bind errors. Also (F6, same area): console-workload's `GraphIdentity` do-nothing Bound processor should become `GraphNodeBinding::identity` per the new zero-input contract. \ No newline at end of file From 648c2b523f2009c76a37b2d6ce710937a0359766 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:00:12 +0000 Subject: [PATCH 07/16] docs: specify fold construction equivalence controls --- .../ISSUE_SPECS/221-reject-invalid-route-fold-binding.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 07c8be0e..6f4771c8 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -57,6 +57,12 @@ pairing/fallback. All fold-mask errors precede ownership consumption. The existi bank-shape constructor Result/expect is unchanged; no new post-consumption fold error or panic is permitted. Gates include width mismatch, inactive armed lane, all-false disarm, valid partial/full masks, and proof/configuration misuse prevention. +Compare old/new constructor paths at widths 4/8 for identical masks and bitwise +PCM, including prefix subsets with additional active lanes left unfolded, active +mask holes, all-false/disarm, and partial-bank staging allocation before render. +Proof supplies the actual active mask, not a comparison copy. Cover pairing +success/fallback. No unsafe or unchecked public setter, mask truncation/intersection +fallback, or render-path change. This is a scope correction before source edits, not a failed implementation attempt. ## Historical issue body From 2029b7cd52c8671cab24fe30532637e4c9316011 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:02:20 +0000 Subject: [PATCH 08/16] rack: prepare owned fold masks before chain construction --- .../221-reject-invalid-route-fold-binding.md | 11 +- crates/rack/src/lib.rs | 143 +++++++++++++++++- 2 files changed, 151 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 6f4771c8..4a5cc57c 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -67,4 +67,13 @@ This is a scope correction before source edits, not a failed implementation atte ## Historical issue body -Required follow-up F2 from strip Job 3's verification: the `debug_assert!(false)` arms in `apply_route_fold` are commented as inert, but by the time any arm runs the route ops are already retired from unit emission — a reachable instance in RELEASE would be silent wrong audio (stale route buffers / vanished contribution), not a safe decline. Each arm was verified unreachable today (routes are singleton plain units; membership⇒Bank; active masks are planner prefixes), and the code runs at bind on the control plane — so the fix is cheap: make them hard bind errors. Also (F6, same area): console-workload's `GraphIdentity` do-nothing Bound processor should become `GraphNodeBinding::identity` per the new zero-input contract. \ No newline at end of file +Required follow-up F2 from strip Job 3's verification: the `debug_assert!(false)` arms in `apply_route_fold` are commented as inert, but by the time any arm runs the route ops are already retired from unit emission — a reachable instance in RELEASE would be silent wrong audio (stale route buffers / vanished contribution), not a safe decline. Each arm was verified unreachable today (routes are singleton plain units; membership⇒Bank; active masks are planner prefixes), and the code runs at bind on the control plane — so the fix is cheap: make them hard bind errors. Also (F6, same area): console-workload's `GraphIdentity` do-nothing Bound processor should become `GraphNodeBinding::identity` per the new zero-input contract. +## Attempt 1 checkpoint: rack construction + +Astra LOW added opaque owned active/fold configuration and a consuming constructor; +arm_fold shares its private installation helper. Formatting and six focused rack +fold tests passed (cargo test --locked -p rack fold --lib). Evidence with actual +argv/env/source/log/exits: /tmp/issue221-attempt1-rack. Graph is unchanged; borrowed +preflight, public ownership controls and full old/new PCM controls remain pending. +Root checkpoints this coherent tranche before further implementation; no PASS +verdict or delivery is claimed. diff --git a/crates/rack/src/lib.rs b/crates/rack/src/lib.rs index b9024451..98c5360b 100644 --- a/crates/rack/src/lib.rs +++ b/crates/rack/src/lib.rs @@ -1597,6 +1597,39 @@ pub struct BankChain { transitions: [u64; 3], } +/// Owned chain masks validated before graph bindings transfer their processors. The constructor +/// consumes this configuration as its only active/fold source; it cannot arm an existing chain. +pub struct PreparedFoldConfiguration { + width: BankWidth, + active: Box<[bool]>, + fold: Box<[bool]>, +} + +impl PreparedFoldConfiguration { + pub fn new( + width: BankWidth, + active: Box<[bool]>, + fold: Box<[bool]>, + ) -> Result { + let lanes = width.lanes() as usize; + if active.len() != lanes + || !active.iter().any(|lane| *lane) + || fold.len() != lanes + || fold + .iter() + .zip(active.iter()) + .any(|(fold, active)| *fold && !*active) + { + return Err(RackError::Shape); + } + Ok(Self { + width, + active, + fold, + }) + } +} + impl BankChain { /// Validates the whole shape once, off the render thread: `active` and every slot mask have /// exactly `lanes` entries, a slot may only be active on an active lane, and at least one lane @@ -1668,6 +1701,21 @@ impl BankChain { }) } + /// Construct from masks validated before processor ownership transfer. Ordinary scratch and + /// slot checks remain those of `new`; installing the configuration introduces no fold error. + pub fn new_with_prepared_fold( + scratch: AoSoaScratch, + configuration: PreparedFoldConfiguration, + slots: Vec, + ) -> Result { + if scratch.width() != configuration.width { + return Err(RackError::WidthMismatch); + } + let mut chain = Self::new(scratch, configuration.active, slots)?; + chain.install_fold(configuration.fold); + Ok(chain) + } + /// How many leading slots a collapsed block of this chain would run one-plane, or `0`. /// /// Decided once, at bind, from the slots' own declarations. Four clauses, and every one of them @@ -1896,16 +1944,20 @@ impl BankChain { { return Err(RackError::Shape); } + self.install_fold(lanes); + Ok(()) + } + + fn install_fold(&mut self, lanes: Box<[bool]>) { if !lanes.iter().any(|lane| *lane) { self.fold = Box::default(); - return Ok(()); + return; } if self.staging_left.len() != self.scratch.left.len() { self.staging_left = vec![0.0; self.scratch.left.len()].into_boxed_slice(); self.staging_right = vec![0.0; self.scratch.right.len()].into_boxed_slice(); } self.fold = lanes; - Ok(()) } /// Lanes whose scatter this chain folds; empty when it folds none. Evidence only. @@ -2692,6 +2744,93 @@ impl BankChain { mod tests { use super::*; + #[test] + fn prepared_fold_configuration_preserves_masks_and_constructor_checks() { + for width in [BankWidth::Four, BankWidth::Eight] { + let lanes = width.lanes() as usize; + assert!(matches!( + PreparedFoldConfiguration::new( + width, + vec![true; lanes - 1].into_boxed_slice(), + vec![false; lanes].into_boxed_slice() + ), + Err(RackError::Shape) + )); + assert!(matches!( + PreparedFoldConfiguration::new( + width, + vec![true; lanes].into_boxed_slice(), + vec![false; lanes - 1].into_boxed_slice() + ), + Err(RackError::Shape) + )); + assert!(matches!( + PreparedFoldConfiguration::new( + width, + vec![false; lanes].into_boxed_slice(), + vec![false; lanes].into_boxed_slice() + ), + Err(RackError::Shape) + )); + let mut active = vec![true; lanes]; + active[lanes - 1] = false; + assert!(matches!( + PreparedFoldConfiguration::new( + width, + active.clone().into_boxed_slice(), + vec![true; lanes].into_boxed_slice() + ), + Err(RackError::Shape) + )); + for fold in [vec![false; lanes], active.clone()] { + let configuration = PreparedFoldConfiguration::new( + width, + active.clone().into_boxed_slice(), + fold.clone().into_boxed_slice(), + ) + .expect("configuration"); + let chain = BankChain::new_with_prepared_fold( + AoSoaScratch::new(width, 16).expect("scratch"), + configuration, + Vec::new(), + ) + .expect("chain"); + assert_eq!(&*chain.active, active); + assert_eq!( + chain.fold_lanes(), + if fold.iter().any(|lane| *lane) { + &fold[..] + } else { + &[] + } + ); + if fold.iter().any(|lane| *lane) { + assert_eq!(chain.staging_left.len(), chain.scratch.left.len()); + assert_eq!(chain.staging_right.len(), chain.scratch.right.len()); + } + } + let configuration = PreparedFoldConfiguration::new( + width, + active.clone().into_boxed_slice(), + active.into_boxed_slice(), + ) + .expect("configuration"); + let other = if width == BankWidth::Four { + BankWidth::Eight + } else { + BankWidth::Four + }; + assert!(matches!( + BankChain::new_with_prepared_fold( + AoSoaScratch::new(other, 16).expect("scratch"), + configuration, + Vec::new() + ), + Err(RackError::WidthMismatch) + )); + } + } + #[test] fn resident_output_lane_checks_shape_and_preserves_final_planes_after_collapse() { for width in [BankWidth::Four, BankWidth::Eight] { From a01dc5489b5ec5d27b2467cbc43f9b7583ee101c Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:07:36 +0000 Subject: [PATCH 09/16] graph: validate route-fold installation before ownership transfer --- .../221-reject-invalid-route-fold-binding.md | 11 + crates/graph/src/lib.rs | 18 +- crates/graph/src/runtime.rs | 446 +++++++++++++----- 3 files changed, 346 insertions(+), 129 deletions(-) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 4a5cc57c..a485df4c 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -77,3 +77,14 @@ argv/env/source/log/exits: /tmp/issue221-attempt1-rack. Graph is unchanged; borr preflight, public ownership controls and full old/new PCM controls remain pending. Root checkpoints this coherent tranche before further implementation; no PASS verdict or delivery is claimed. + +## Attempt 1 checkpoint: borrowed graph planning + +Borrowed preflight precedes ownership transfer and hands the same owned schedule, +mappings and fold configurations into consuming construction. The old +post-consumption apply_route_fold step is removed. Formatting, focused route +controls, the seeded4000-graph corpus and RT1 direct-bank bit-exact/allocation +control passed. An initial unused test-wrapper warning was corrected; final +format/route rebuild passed without it. Receipts: /tmp/issue221-attempt1-graph-plan. +Public ownership/fault matrix, rack PCM/pairing controls, console identity and +strict Clippy remain before independent attempt review. diff --git a/crates/graph/src/lib.rs b/crates/graph/src/lib.rs index c22678e6..5f11fa3f 100644 --- a/crates/graph/src/lib.rs +++ b/crates/graph/src/lib.rs @@ -1377,6 +1377,11 @@ impl PreparedGraphPlan { let Some(program) = self.lowered() else { return Err((self, bindings, source_set, "graph.scheduler.layout")); }; + let planning = + match runtime::preflight_sequential(&self, &program, &bindings, source_set.as_ref()) { + Ok(planning) => planning, + Err(code) => return Err((self, bindings, source_set, code)), + }; let envelope = self.envelope; let plan_id = self.plan_id; let mut plan = self; @@ -1385,8 +1390,14 @@ impl PreparedGraphPlan { observers.append(&mut bindings.observers); observers }; - let executor = - GraphExecutor::new(plan, &program, bindings.nodes, observers, source_set.take()); + let executor = GraphExecutor::new( + plan, + &program, + bindings.nodes, + observers, + source_set.take(), + planning, + ); Ok(PreparedRenderPlan::prepare_with_executor( PrepareRenderPlan { plan_id, @@ -1794,6 +1805,7 @@ impl GraphExecutor { bindings: Vec, observers: Vec, source_set: Option, + planning: runtime::SequentialPlan, ) -> Self { let frames = plan.envelope.quantum.0 as usize; let source_inputs: BTreeSet<_> = source_set @@ -1838,7 +1850,7 @@ impl GraphExecutor { plan.track_delays, frames, ); - let runtime = runtime::build_sequential(program, &plan.spec, parts, frames); + let runtime = runtime::build_sequential(program, &plan.spec, parts, frames, planning); Self { runtime, output, diff --git a/crates/graph/src/runtime.rs b/crates/graph/src/runtime.rs index 72ed7ce2..823283c2 100644 --- a/crates/graph/src/runtime.rs +++ b/crates/graph/src/runtime.rs @@ -1859,10 +1859,20 @@ pub(crate) fn trailing_active_mask(members: usize, width: BankWidth) -> Box<[boo /// slot only when *every* lane of the group runs it (`banks::bind_rack_banks`, #96 F7). The rack /// crate has supported multi-slot chains since it was written and unit-tests three of them; until /// issue #181 nothing in the graph layer ever handed it more than one. +#[cfg(feature = "test-support")] fn bank_chain( scratch: AoSoaScratch, active: Box<[bool]>, slots: Vec>, +) -> BankChain { + bank_chain_with_fold(scratch, active, slots, None) +} + +fn bank_chain_with_fold( + scratch: AoSoaScratch, + active: Box<[bool]>, + slots: Vec>, + fold: Option, ) -> BankChain { let slot_count = slots.len(); let mut prepared_slots = Vec::with_capacity(slot_count); @@ -1879,7 +1889,13 @@ fn bank_chain( ); #[cfg(feature = "test-support")] BANK_CHAIN_CAPACITIES.with(|value| value.set([prepared_slots.capacity(), slot_count])); - BankChain::new(scratch, active, prepared_slots).expect("validated bank shape") + match fold { + Some(configuration) => { + BankChain::new_with_prepared_fold(scratch, configuration, prepared_slots) + } + None => BankChain::new(scratch, active, prepared_slots), + } + .expect("validated bank shape") } #[cfg(feature = "test-support")] @@ -2215,7 +2231,17 @@ impl RuntimeParts { /// The scratch and the lane mask come from the run's first slot; every slot of a cohort /// covers the same lanes by construction, and `BankChain::new` re-checks it rather than /// trusting it. + #[cfg(test)] fn chain_for(&mut self, run: &[Membership], members: usize) -> BankChain { + self.chain_for_with_fold(run, members, None) + } + + fn chain_for_with_fold( + &mut self, + run: &[Membership], + members: usize, + fold: Option, + ) -> BankChain { // Every slot arrives with its own `AoSoaScratch`, because a bound bank is prepared // without knowing whether it will end up sharing a chain. One chain has one resident // block, so the run keeps the first slot's scratch and drops the rest here, on the @@ -2318,10 +2344,11 @@ impl RuntimeParts { observed.maximum_runtime_slots = observed.maximum_runtime_slots.max(stages.len()); facts.set(observed); }); - bank_chain( + bank_chain_with_fold( scratch.expect("a unit has at least one slot"), active.expect("a unit has at least one slot"), stages, + fold, ) } @@ -2515,12 +2542,179 @@ pub(crate) fn units_of( units } +/// Prepared before caller-owned processors, observers, banks or sources move. Emission consumes +/// this exact schedule and its owned fold configurations; it never replans route retirement. +pub(crate) struct SequentialPlan { + run_units: Vec<(Vec, Vec)>, + fold: Option, + unit_of_run: Vec>, + op_slot: Vec>, + installations: Vec>, +} + +struct FoldInstallation { + configuration: rack::PreparedFoldConfiguration, + lanes: Box<[FoldLane]>, + master: u32, +} + +pub(crate) fn preflight_sequential( + plan: &crate::PreparedGraphPlan, + program: &ExecutionProgram, + bindings: &crate::GraphRuntimeBindings, + sources: Option<&crate::GraphPreparedSourceSet>, +) -> Result { + let metadata = BorrowedPlanningMetadata { + plan, + bindings, + sources, + membership: bank_membership(&plan.spec, &plan.banks, &plan.builtin_banks), + }; + let grouped = units_of(program, metadata.membership()); + let runs = cohort_runs(program, &plan.spec, &metadata, &grouped); + let run_units: Vec<(Vec, Vec)> = runs + .iter() + .map(|run| { + ( + run.iter().filter_map(|index| grouped[*index].0).collect(), + run.iter() + .flat_map(|index| grouped[*index].1.iter().copied()) + .collect(), + ) + }) + .collect(); + let fold = route_fold(program, &plan.spec, &metadata, &run_units); + validate_fold_installation(plan, program, run_units, fold) +} + +fn validate_fold_installation( + plan: &crate::PreparedGraphPlan, + program: &ExecutionProgram, + run_units: Vec<(Vec, Vec)>, + fold: Option, +) -> Result { + let retired = fold.as_ref().map(|fold| &fold.retired); + let mut unit_of_run = vec![None; run_units.len()]; + let mut op_slot = vec![None; program.ops.len()]; + let mut emitted = 0; + for (run, (membership, ops)) in run_units.iter().enumerate() { + if ops.is_empty() { + return Err("graph.route_fold.mapping"); + } + if ops + .iter() + .any(|op| retired.is_some_and(|retired| retired.contains(op))) + { + if !membership.is_empty() || ops.len() != 1 { + return Err("graph.route_fold.bank"); + } + continue; + } + unit_of_run[run] = Some(emitted); + for (member, op) in ops.iter().enumerate() { + let entry = op_slot.get_mut(*op).ok_or("graph.route_fold.mapping")?; + if entry.replace((emitted, member)).is_some() { + return Err("graph.route_fold.mapping"); + } + } + emitted += 1; + } + let mut installations: Vec> = + (0..run_units.len()).map(|_| None).collect(); + if let Some(fold) = &fold { + let master_slot = op_slot + .get(fold.master_op) + .copied() + .flatten() + .ok_or("graph.route_fold.master")?; + let master_run = unit_of_run + .iter() + .position(|unit| *unit == Some(master_slot.0)) + .ok_or("graph.route_fold.master")?; + let (membership, ops) = &run_units[master_run]; + if !membership.is_empty() || ops.as_slice() != [fold.master_op] || master_slot.1 != 0 { + return Err("graph.route_fold.master"); + } + let mut owned = std::collections::BTreeSet::new(); + for (run, lanes) in &fold.runs { + let (membership, ops) = run_units.get(*run).ok_or("graph.route_fold.mapping")?; + if unit_of_run.get(*run).copied().flatten().is_none() { + return Err("graph.route_fold.mapping"); + } + if membership.is_empty() + || lanes.is_empty() + || ops.len() != membership.len() * lanes.len() + { + return Err("graph.route_fold.bank"); + } + let mut first_shape = None; + for bank in membership { + let (key, width, active, members) = match bank { + Membership::Effect(index) => { + let bank = plan.banks.get(*index).ok_or("graph.route_fold.bank")?; + ( + (false, *index), + bank.scratch.width(), + bank.active_mask.clone(), + bank.members.len(), + ) + } + Membership::Builtin(index) => { + let bank = plan + .builtin_banks + .get(*index) + .ok_or("graph.route_fold.bank")?; + ( + (true, *index), + bank.scratch.width(), + trailing_active_mask(bank.members.len(), bank.scratch.width()), + bank.members.len(), + ) + } + }; + if !owned.insert(key) || members != lanes.len() { + return Err("graph.route_fold.bank"); + } + if first_shape.is_none() { + first_shape = Some((width, active)); + } + } + let (width, active) = first_shape.ok_or("graph.route_fold.bank")?; + if lanes.len() > width.lanes() as usize { + return Err("graph.route_fold.mask"); + } + let mask = trailing_active_mask(lanes.len(), width); + let configuration = rack::PreparedFoldConfiguration::new(width, active, mask) + .map_err(|_| "graph.route_fold.mask")?; + let slot = installations + .get_mut(*run) + .ok_or("graph.route_fold.mapping")?; + if slot.is_some() { + return Err("graph.route_fold.mapping"); + } + *slot = Some(FoldInstallation { + configuration, + lanes: lanes.clone().into_boxed_slice(), + master: fold.master.0 + ARENA_BASE, + }); + } + } + Ok(SequentialPlan { + run_units, + fold, + unit_of_run, + op_slot, + installations, + }) +} + /// Builds the sequential executor's runtime: one coloured arena, producers read in place. pub(crate) fn build_sequential( program: &ExecutionProgram, spec: &GraphSpec, parts: RuntimeParts, frames: usize, + planning: SequentialPlan, ) -> Runtime { #[cfg(any(test, feature = "test-support"))] test_only_reset_selected_split_fader(); @@ -2529,29 +2723,18 @@ pub(crate) fn build_sequential( // arena buffer `b + ARENA_BASE`. let arena = |buffer: u32| buffer + ARENA_BASE; let taps = taps_by_op(program, spec); - let grouped = units_of(program, &parts.membership.clone()); let delays = program .delays .iter() .map(|line| CompensationDelay::new(line.samples as usize)) .collect(); - // Issue #181: consecutive slots of one cohort chain become one unit with one chain, so the - // pair pays one planar/AoSoA round-trip per block where it used to pay two. - let runs = cohort_runs(program, spec, &parts, &grouped); - let run_units: Vec<(Vec, Vec)> = runs - .iter() - .map(|run| { - ( - run.iter().filter_map(|index| grouped[*index].0).collect(), - run.iter() - .flat_map(|index| grouped[*index].1.iter().copied()) - .collect(), - ) - }) - .collect(); - // Issue #218: decided here, before `build_op` consumes `parts.observers` and before any op is - // built, because it decides which ops are built at all. - let fold = route_fold(program, spec, &parts, &run_units); + let SequentialPlan { + run_units, + fold, + unit_of_run, + op_slot, + installations, + } = planning; let folded_runs: std::collections::BTreeSet = fold .as_ref() .map(|fold| fold.runs.iter().map(|(run, _)| *run).collect()) @@ -2765,24 +2948,19 @@ pub(crate) fn build_sequential( } } } - // Where each op's `RuntimeOp` ended up, so a redirect can neutralise the consumer's reduction. - let mut op_slot: Vec> = vec![None; program.ops.len()]; - // Run unit -> the unit index it was emitted at, for the chains the fold arms. - let mut unit_of_run: Vec> = vec![None; run_units.len()]; let mut units = Vec::with_capacity(run_units.len()); // The bind-time half of the collapse-eligibility query, one row per emitted unit. Built here // rather than by a later walk because this is the only place the unit's ops and the spec's // node ids are both in hand: `RuntimeOp` deliberately carries no node id, and reconstructing // one from the arena buffers afterwards would be a second opinion about which lane is which. let mut identity: Vec = Vec::with_capacity(run_units.len()); - for (run, (membership, ops)) in run_units.iter().enumerate() { + for ((membership, ops), installation) in run_units.iter().zip(installations) { // A retired route op is absorbed by its cohort's epilogue: no unit, no dispatch, no // reduction, no `mix2x2_block` pass of its own. if ops.iter().all(|index| retired.contains(index)) { continue; } let membership = membership.clone(); - unit_of_run[run] = Some(units.len()); { // `ops` is slot major with `lanes` ops per slot (`units_of` sorts by the member's // position within its bank), so slot `s`'s lane `l` is `ops[s * lanes + l]` and lane @@ -2805,9 +2983,6 @@ pub(crate) fn build_sequential( .collect(), }); } - for (member, index) in ops.iter().enumerate() { - op_slot[*index] = Some((units.len(), member)); - } let members: Vec = ops .iter() .map(|index| { @@ -2827,6 +3002,9 @@ pub(crate) fn build_sequential( } } } + if fold.as_ref().is_some_and(|fold| fold.master_op == *index) { + inputs = vec![arena(op.output.0)]; + } let sidechain = op.sidechain.map(|side| match side.delay { None => arena(side.buffer.0), Some(delay) => { @@ -2851,10 +3029,12 @@ pub(crate) fn build_sequential( ) }) .collect(); - units.push(finish_unit(&mut parts, &membership, members)); + units.push(finish_unit(&mut parts, &membership, members, installation)); } apply_scatter_redirects(program, &redirects, &op_slot, &mut units); - let folds = apply_route_fold(fold.as_ref(), &unit_of_run, &op_slot, arena, &mut units); + let folds = fold.as_ref().map_or(0, |fold| { + fold.runs.iter().map(|(_, lanes)| lanes.len() as u64).sum() + }); arm_resident_inputs( program, &first_producer, @@ -2964,71 +3144,6 @@ fn resident_units_match(before: &RuntimeUnit, after: &RuntimeUnit) -> bool { /// Arm every admitted chain's epilogue and neutralise the reduction it performed. /// -/// Three edits, and they are each other's counterparts. The chain is told which lanes to hand to -/// `fold_plane` and given the master buffer and the per-lane constants; the retired route ops were -/// never built into units at all; and the master op's inputs become its own output, which is the -/// shape `reduce_plane` already treats as "nothing to copy", so the op still runs -- with whatever -/// kind it has, a host binding included -- over the sum its cohorts' epilogues already wrote. -/// -/// Returns the number of lanes armed, which is the only honest way to state that the fold fired: -/// like the scatter redirect it optimises by *not doing* something, so there is no output -/// difference to observe and no timing difference a gate may rest on. -fn apply_route_fold( - fold: Option<&RouteFold>, - unit_of_run: &[Option], - op_slot: &[Option<(usize, usize)>], - arena: impl Fn(u32) -> u32, - units: &mut [RuntimeUnit], -) -> u64 { - let Some(fold) = fold else { - return 0; - }; - let master = arena(fold.master.0); - let mut armed = 0_u64; - for (run, lanes) in &fold.runs { - let Some(unit) = unit_of_run[*run] else { - continue; - }; - let RuntimeUnit::Bank { - chain, - fold: slot, - master: destination, - .. - } = &mut units[unit] - else { - // Unreachable by construction: `route_fold` only ever names a banked run unit. - debug_assert!(false, "only a bank chain carries a folded epilogue"); - continue; - }; - let width = chain.width().lanes() as usize; - let mut mask = vec![false; width].into_boxed_slice(); - for lane in 0..lanes.len().min(width) { - mask[lane] = true; - } - if chain.arm_fold(mask).is_err() { - // Unreachable by construction: the mask is the chain's own rendered lanes, which are - // exactly its active ones. Left inert rather than half-armed. - debug_assert!(false, "a chain's rendered lanes are its active lanes"); - continue; - } - armed += lanes.len() as u64; - *destination = master; - *slot = lanes.clone().into_boxed_slice(); - } - if armed == 0 { - return 0; - } - if let Some((unit, _)) = op_slot[fold.master_op] { - match &mut units[unit] { - RuntimeUnit::Op(op) => op.inputs = vec![master].into_boxed_slice(), - // Unreachable by construction: `route_fold` declines a banked master outright, because - // such a master's reduction is its chain's gather. - RuntimeUnit::Bank { .. } => debug_assert!(false, "a banked master never folds"), - } - } - armed -} - /// One chain's scatter redirect: `(run, lane, consumer op)` for every lane whose scatter may land /// in its consumer's buffer instead of the last slot's own (issue #202 rec 3). type ScatterRedirect = (usize, usize, usize); @@ -3382,15 +3497,93 @@ const fn folded_route(transform: &RouteTransform) -> [f32; 4] { /// restates the same cascade as a query, in the same precedence order, and returns `None` for /// every arm that is not a plain route. Keep its exclusions coupled to new `node_kind` arms. /// Bindings, banks, sources and effects take precedence regardless of the node's session name. -fn plain_route_gains(parts: &RuntimeParts, node: &GraphNodeId, index: u32) -> Option<[f32; 4]> { - if parts.source_inputs.contains(node) - || parts.membership.contains_key(&index) - || matches!(parts.bindings.get(node), Some(Some(_))) - || parts.effects.contains_key(node) +trait PlanningMetadata { + fn membership(&self) -> &BankMembership; + fn has_source(&self, node: &GraphNodeId) -> bool; + fn has_binding(&self, node: &GraphNodeId) -> bool; + fn has_effect(&self, node: &GraphNodeId) -> bool; + fn has_observer(&self, node: &GraphNodeId) -> bool; + fn route(&self, node: &GraphNodeId) -> Option<&RouteTransform>; +} + +impl PlanningMetadata for RuntimeParts { + fn membership(&self) -> &BankMembership { + &self.membership + } + fn has_source(&self, node: &GraphNodeId) -> bool { + self.source_inputs.contains(node) + } + fn has_binding(&self, node: &GraphNodeId) -> bool { + matches!(self.bindings.get(node), Some(Some(_))) + } + fn has_effect(&self, node: &GraphNodeId) -> bool { + self.effects.contains_key(node) + } + fn has_observer(&self, node: &GraphNodeId) -> bool { + self.observers.contains_key(node) + } + fn route(&self, node: &GraphNodeId) -> Option<&RouteTransform> { + self.routes.get(node) + } +} + +struct BorrowedPlanningMetadata<'a> { + plan: &'a crate::PreparedGraphPlan, + bindings: &'a crate::GraphRuntimeBindings, + sources: Option<&'a crate::GraphPreparedSourceSet>, + membership: BankMembership, +} + +impl PlanningMetadata for BorrowedPlanningMetadata<'_> { + fn membership(&self) -> &BankMembership { + &self.membership + } + fn has_source(&self, node: &GraphNodeId) -> bool { + self.sources + .is_some_and(|set| set.claims().iter().any(|claim| &claim.node == node)) + } + fn has_binding(&self, node: &GraphNodeId) -> bool { + self.bindings + .nodes + .iter() + .any(|binding| &binding.node == node && binding.processor.is_some()) + } + fn has_effect(&self, node: &GraphNodeId) -> bool { + self.plan + .effects + .iter() + .any(|effect| matches!(node, GraphNodeId::Effect(id) if *id == effect.id)) + } + fn has_observer(&self, node: &GraphNodeId) -> bool { + self.plan + .observers + .iter() + .chain(self.bindings.observers.iter()) + .any(|observer| &observer.node == node) + } + fn route(&self, node: &GraphNodeId) -> Option<&RouteTransform> { + self.plan + .routes + .iter() + .rev() + .find(|route| &route.node == node) + .map(|route| &route.transform) + } +} + +fn plain_route_gains( + parts: &impl PlanningMetadata, + node: &GraphNodeId, + index: u32, +) -> Option<[f32; 4]> { + if parts.has_source(node) + || parts.membership().contains_key(&index) + || parts.has_binding(node) + || parts.has_effect(node) { return None; } - parts.routes.get(node).map(folded_route) + parts.route(node).map(folded_route) } /// Whether anything can *see* the buffer op `index` writes other than by reading it as an input. @@ -3402,22 +3595,18 @@ fn plain_route_gains(parts: &RuntimeParts, node: &GraphNodeId, index: u32) -> Op fn observed( program: &ExecutionProgram, spec: &GraphSpec, - parts: &RuntimeParts, + parts: &impl PlanningMetadata, index: usize, ) -> bool { let node = &spec.nodes[program.ops[index].node as usize].id; - if parts.observers.contains_key(node) { + if parts.has_observer(node) { return true; } program .taps .iter() .filter(|tap| tap.after_op as usize == index) - .any(|tap| { - parts - .observers - .contains_key(&spec.nodes[tap.node as usize].id) - }) + .any(|tap| parts.has_observer(&spec.nodes[tap.node as usize].id)) } /// One chain's folded epilogue: which run unit it is, and one entry per rendered lane. @@ -3468,7 +3657,7 @@ fn input_producers(program: &ExecutionProgram, target: usize) -> Vec], first_producer: &[Option], producer: usize, @@ -3573,7 +3762,7 @@ fn foldable_lane( fn route_fold( program: &ExecutionProgram, spec: &GraphSpec, - parts: &RuntimeParts, + parts: &impl PlanningMetadata, run_units: &[(Vec, Vec)], ) -> Option { let (readers, first_producer) = op_dataflow(program); @@ -3630,10 +3819,8 @@ fn route_fold( .inputs_of(master) .iter() .any(|input| input.delay.is_some()) - || parts.membership.contains_key(&master.node) - || parts - .source_inputs - .contains(&spec.nodes[master.node as usize].id) + || parts.membership().contains_key(&master.node) + || parts.has_source(&spec.nodes[master.node as usize].id) { return None; } @@ -3761,18 +3948,29 @@ fn finish_unit( parts: &mut RuntimeParts, run: &[Membership], mut members: Vec, + installation: Option, ) -> RuntimeUnit { if run.is_empty() { return RuntimeUnit::Op(members.pop().expect("one op per plain unit")); } let lanes = members.len() / run.len(); - let chain = parts.chain_for(run, lanes); + let (configuration, fold, master) = installation.map_or_else( + || (None, Box::default(), 0), + |installation| { + ( + Some(installation.configuration), + installation.lanes, + installation.master, + ) + }, + ); + let chain = parts.chain_for_with_fold(run, lanes, configuration); RuntimeUnit::Bank { members: members.into_boxed_slice(), lanes, chain, - fold: Box::default(), - master: 0, + fold, + master, } } @@ -3855,7 +4053,7 @@ fn op_dataflow(program: &ExecutionProgram) -> (Vec>, Vec], first_producer: &[Option], earlier: &[usize], @@ -3881,18 +4079,14 @@ fn chains_into( return false; } let node = &spec.nodes[producer.node as usize].id; - if parts.observers.contains_key(node) { + if parts.has_observer(node) { return false; } if program .taps .iter() .filter(|tap| tap.after_op as usize == *before) - .any(|tap| { - parts - .observers - .contains_key(&spec.nodes[tap.node as usize].id) - }) + .any(|tap| parts.has_observer(&spec.nodes[tap.node as usize].id)) { return false; } @@ -3939,7 +4133,7 @@ fn chains_into( fn cohort_runs( program: &ExecutionProgram, spec: &GraphSpec, - parts: &RuntimeParts, + parts: &impl PlanningMetadata, units: &[PlannedUnit], ) -> Vec> { let (readers, first_producer) = op_dataflow(program); From bf135562050c9728e71d9ab6954e9e11c234585c Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:11:32 +0000 Subject: [PATCH 10/16] graph: prove route-fold rejection returns original bind owners --- .../221-reject-invalid-route-fold-binding.md | 10 + crates/graph/src/lib.rs | 241 ++++++++++++++++++ crates/graph/src/runtime.rs | 78 ++++++ 3 files changed, 329 insertions(+) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index a485df4c..f25654c3 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -88,3 +88,13 @@ control passed. An initial unused test-wrapper warning was corrected; final format/route rebuild passed without it. Receipts: /tmp/issue221-attempt1-graph-plan. Public ownership/fault matrix, rack PCM/pairing controls, console identity and strict Clippy remain before independent attempt review. + +## Attempt 1 checkpoint: public recovery matrix + +Eleven one-shot raw installation faults run through both public bind families +(22 rejection/retry paths). The focused test checks exact returned graph/bank/ +bound/observer owner addresses, source driver identity, no source work/drop +before rejection, then successful same-owner retry with four folded lanes and +known three-block PCM. Added folded-op mapping consistency rejection. Formatting +and focused recovery test passed; receipts /tmp/issue221-attempt1-recovery. +Rack PCM/pairing, console identity and strict gates remain before final review. diff --git a/crates/graph/src/lib.rs b/crates/graph/src/lib.rs index 5f11fa3f..28c1a90a 100644 --- a/crates/graph/src/lib.rs +++ b/crates/graph/src/lib.rs @@ -2831,6 +2831,247 @@ mod tests { ) } + /// One admitted four-lane fold, retaining both plan-owned and caller-owned observers on + /// inputs. Observing the last bank slot would decline the fold and make fault tests vacuous. + fn route_fold_recovery_plan() -> (PreparedGraphPlan, GraphRuntimeBindings, Arc) { + let (mut plan, mut bindings, observations) = four_track_builtin_plan(221, true, false); + let output = plan.sequential_schedule.pop().expect("output"); + let routes: Vec<_> = (0..4) + .map(|lane| GraphNodeId::Route { + route_id: StableGraphId::parse(&format!("route{lane}")).expect("route"), + }) + .collect(); + let mut destinations = Vec::new(); + for edge in &mut plan.spec.edges { + if let GraphEdgeId::RouteSource { route_id } = &edge.id { + let route = GraphNodeId::Route { + route_id: route_id.clone(), + }; + edge.destination.node = route.clone(); + destinations.push(GraphEdge { + id: GraphEdgeId::RouteDestination { + route_id: route_id.clone(), + }, + source: GraphPortId { + node: route, + kind: GraphPortKind::MainOutput, + effect_port: None, + }, + destination: GraphPortId { + node: output.clone(), + kind: GraphPortKind::MainInput, + effect_port: None, + }, + path: "$".into(), + }); + } + } + plan.spec.edges.extend(destinations); + plan.spec.edges.sort_by(|a, b| a.id.cmp(&b.id)); + plan.spec + .nodes + .extend(routes.iter().cloned().map(|id| GraphNode { + id, + latency: LatencySamples(0), + tail: TailSamples::Finite(0), + })); + plan.spec.nodes.sort_by(|a, b| a.id.cmp(&b.id)); + plan.sequential_schedule.extend(routes.iter().cloned()); + plan.sequential_schedule.push(output.clone()); + plan.dependency_levels.pop(); + plan.dependency_levels.push(DependencyLevel { + level: 2, + nodes: routes.clone(), + }); + plan.dependency_levels.push(DependencyLevel { + level: 3, + nodes: vec![output], + }); + plan.routes = routes + .into_iter() + .map(|node| PreparedRoute { + node, + transform: RouteTransform { + gain: 1.0, + ll: 1.0, + lr: 0.0, + rl: 0.0, + rr: 1.0, + }, + }) + .collect(); + for observer in &mut bindings.observers { + if let GraphNodeId::TrackStage { stage, .. } = &mut observer.node { + *stage = TrackStage::Input; + } + } + plan.observers.extend(bindings.observers.drain(..2)); + (plan, bindings, observations) + } + + struct RecoverySource { + sample: u64, + begins: Arc, + copies: Arc, + drops: Arc, + } + impl Drop for RecoverySource { + fn drop(&mut self) { + self.drops.fetch_add(1, Ordering::SeqCst); + } + } + impl GraphPreparedSourceSetDriver for RecoverySource { + fn claim_count(&self) -> usize { + 1 + } + fn begin_block(&mut self, first_sample: u64, _frames: u32) -> Result<(), RenderError> { + self.sample = first_sample; + self.begins.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + fn copy_track_input( + &mut self, + claim: usize, + left: &mut [f32], + right: &mut [f32], + ) -> Result<(), RenderError> { + assert_eq!(claim, 0); + self.copies.fetch_add(1, Ordering::SeqCst); + left.fill((self.sample + 1) as f32); + right.fill(-((self.sample + 1) as f32)); + Ok(()) + } + } + + /// Raw installation faults pass through the real public bind boundary. All metadata and + /// original owners are returned, with the one-shot fault removed for a successful retry. + #[test] + fn route_fold_preflight_returns_original_owners_and_sources_for_retry() { + use runtime::FoldFault; + let faults = [ + (FoldFault::MissingRun, "graph.route_fold.mapping"), + (FoldFault::MissingUnit, "graph.route_fold.mapping"), + (FoldFault::WrongUnit, "graph.route_fold.mapping"), + (FoldFault::UnbankedRun, "graph.route_fold.bank"), + (FoldFault::MissingBank, "graph.route_fold.bank"), + (FoldFault::OversizedMask, "graph.route_fold.mask"), + (FoldFault::InactiveLane, "graph.route_fold.mask"), + (FoldFault::ActiveWidth, "graph.route_fold.mask"), + (FoldFault::MissingMaster, "graph.route_fold.master"), + (FoldFault::BankedMaster, "graph.route_fold.master"), + (FoldFault::WrongMaster, "graph.route_fold.master"), + ]; + let addresses = |plan: &PreparedGraphPlan, bindings: &GraphRuntimeBindings| { + let mut owners = vec![ + plan.spec.nodes.as_ptr() as usize, + plan.routes.as_ptr() as usize, + ]; + owners.extend( + plan.builtin_banks + .iter() + .map(|bank| core::ptr::from_ref(&*bank.processor).cast::<()>() as usize), + ); + owners.extend( + bindings + .nodes + .iter() + .filter_map(|binding| binding.processor.as_ref()) + .map(|processor| core::ptr::from_ref(&**processor).cast::<()>() as usize), + ); + owners.extend( + plan.observers + .iter() + .chain(bindings.observers.iter()) + .map(|binding| core::ptr::from_ref(&*binding.observer).cast::<()>() as usize), + ); + owners + }; + for with_source in [false, true] { + for (fault, expected) in faults { + let (plan, mut bindings, observations) = route_fold_recovery_plan(); + let begins = Arc::new(AtomicU64::new(0)); + let copies = Arc::new(AtomicU64::new(0)); + let drops = Arc::new(AtomicU64::new(0)); + let source = with_source.then(|| { + let input = bindings.nodes.remove(0).node; + GraphPreparedSourceSet::new( + plan.envelope, + vec![GraphSourceInputClaim { node: input }], + GraphSourceSetResourceReport { + pcm_payload_already_charged_bytes: 0, + overhead_bytes: 0, + total_engine_owned_bytes: 0, + largest_allocation_bytes: 0, + }, + Box::new(RecoverySource { + sample: 0, + begins: Arc::clone(&begins), + copies: Arc::clone(&copies), + drops: Arc::clone(&drops), + }), + ) + }); + let before = addresses(&plan, &bindings); + runtime::inject_fold_fault(fault); + let (returned, bindings, source) = match source { + Some(source) => { + let address = core::ptr::from_ref(&*source.driver).cast::<()>() as usize; + let failure = match plan.bind_with_source_set(bindings, source) { + Ok(_) => panic!("accepted {fault:?}"), + Err(failure) => failure, + }; + assert_eq!(failure.code, expected, "{fault:?}"); + assert_eq!( + core::ptr::from_ref(&*failure.source_set.driver).cast::<()>() as usize, + address + ); + (*failure.plan, failure.bindings, Some(failure.source_set)) + } + None => { + let failure = match plan.bind(bindings) { + Ok(_) => panic!("accepted {fault:?}"), + Err(failure) => failure, + }; + assert_eq!(failure.code, expected, "{fault:?}"); + (*failure.plan, failure.bindings, None) + } + }; + assert_eq!( + addresses(&returned, &bindings), + before, + "{fault:?}: original owners" + ); + assert_eq!(observations.load(Ordering::SeqCst), 0); + assert_eq!(begins.load(Ordering::SeqCst), 0); + assert_eq!(copies.load(Ordering::SeqCst), 0); + assert_eq!(drops.load(Ordering::SeqCst), 0); + let retry = match source { + Some(source) => returned + .bind_with_source_set(bindings, source) + .unwrap_or_else(|failure| panic!("retry: {}", failure.code)), + None => returned + .bind(bindings) + .unwrap_or_else(|failure| panic!("retry: {}", failure.code)), + }; + assert_eq!(retry.bank_route_folds(), 4); + assert_eq!( + render_three_blocks(retry).0.map(f32::to_bits), + [10.0_f32, -15.0, 20.0, -30.0, 30.0, -45.0].map(f32::to_bits) + ); + assert_eq!(observations.load(Ordering::SeqCst), 12); + assert_eq!( + begins.load(Ordering::SeqCst), + if with_source { 3 } else { 0 } + ); + assert_eq!( + copies.load(Ordering::SeqCst), + if with_source { 3 } else { 0 } + ); + assert_eq!(drops.load(Ordering::SeqCst), u64::from(with_source)); + } + } + } + fn render_three_blocks(mut plan: PreparedRenderPlan) -> ([f32; 6], [u64; 2]) { let mut pcm = [0.0_f32; 6]; for block in 0..3 { diff --git a/crates/graph/src/runtime.rs b/crates/graph/src/runtime.rs index 823283c2..4a063491 100644 --- a/crates/graph/src/runtime.rs +++ b/crates/graph/src/runtime.rs @@ -2542,6 +2542,34 @@ pub(crate) fn units_of( units } +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +pub(crate) enum FoldFault { + MissingRun, + MissingUnit, + WrongUnit, + UnbankedRun, + MissingBank, + OversizedMask, + InactiveLane, + ActiveWidth, + MissingMaster, + BankedMaster, + WrongMaster, +} + +#[cfg(test)] +thread_local! { + static FOLD_FAULT: std::cell::Cell> = const { std::cell::Cell::new(None) }; +} + +#[cfg(test)] +pub(crate) fn inject_fold_fault(fault: FoldFault) { + FOLD_FAULT.with(|slot| { + assert!(slot.replace(Some(fault)).is_none()); + }); +} + /// Prepared before caller-owned processors, observers, banks or sources move. Emission consumes /// this exact schedule and its owned fold configurations; it never replans route retirement. pub(crate) struct SequentialPlan { @@ -2619,6 +2647,37 @@ fn validate_fold_installation( } emitted += 1; } + #[cfg(test)] + let fault = FOLD_FAULT.with(std::cell::Cell::take); + #[cfg(test)] + let (run_units, fold) = { + let mut run_units = run_units; + let mut fold = fold; + if let Some(fault) = fault { + let fold = fold + .as_mut() + .expect("fault fixture must reach an admitted fold"); + let run = fold.runs[0].0; + match fault { + FoldFault::MissingRun => fold.runs[0].0 = run_units.len(), + FoldFault::MissingUnit => unit_of_run[run] = None, + FoldFault::WrongUnit => unit_of_run[run] = Some(usize::MAX), + FoldFault::UnbankedRun => run_units[run].0.clear(), + FoldFault::MissingBank => run_units[run].0[0] = Membership::Builtin(usize::MAX), + FoldFault::MissingMaster => op_slot[fold.master_op] = None, + FoldFault::BankedMaster => { + let master = unit_of_run + .iter() + .position(|unit| *unit == op_slot[fold.master_op].map(|slot| slot.0)) + .expect("master run"); + run_units[master].0.push(Membership::Builtin(0)); + } + FoldFault::WrongMaster => op_slot[fold.master_op] = op_slot[run_units[run].1[0]], + FoldFault::OversizedMask | FoldFault::InactiveLane | FoldFault::ActiveWidth => {} + } + } + (run_units, fold) + }; let mut installations: Vec> = (0..run_units.len()).map(|_| None).collect(); if let Some(fold) = &fold { @@ -2641,6 +2700,11 @@ fn validate_fold_installation( if unit_of_run.get(*run).copied().flatten().is_none() { return Err("graph.route_fold.mapping"); } + if ops.iter().enumerate().any(|(member, op)| { + op_slot.get(*op).copied().flatten() != unit_of_run[*run].map(|unit| (unit, member)) + }) { + return Err("graph.route_fold.mapping"); + } if membership.is_empty() || lanes.is_empty() || ops.len() != membership.len() * lanes.len() @@ -2684,6 +2748,20 @@ fn validate_fold_installation( return Err("graph.route_fold.mask"); } let mask = trailing_active_mask(lanes.len(), width); + #[cfg(test)] + let (active, mask) = { + let mut active = active.into_vec(); + let mut mask = mask.into_vec(); + match fault { + Some(FoldFault::OversizedMask) => mask.push(true), + Some(FoldFault::InactiveLane) => active[0] = false, + Some(FoldFault::ActiveWidth) => { + active.pop(); + } + _ => {} + } + (active.into_boxed_slice(), mask.into_boxed_slice()) + }; let configuration = rack::PreparedFoldConfiguration::new(width, active, mask) .map_err(|_| "graph.route_fold.mask")?; let slot = installations From d1cb3653133c77b1ddb8764ab0b3168f3ead87b2 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:15:02 +0000 Subject: [PATCH 11/16] graph: preserve fold qualification and console counter failure --- .../221-reject-invalid-route-fold-binding.md | 26 ++ crates/graph/src/runtime.rs | 236 +++++++++++------- crates/rack/src/lib.rs | 107 ++++++++ tools/console-workload/src/lib.rs | 34 +-- 4 files changed, 284 insertions(+), 119 deletions(-) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index f25654c3..1abd5434 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -98,3 +98,29 @@ before rejection, then successful same-owner retry with four folded lanes and known three-block PCM. Added folded-op mapping consistency rejection. Formatting and focused recovery test passed; receipts /tmp/issue221-attempt1-recovery. Rack PCM/pairing, console identity and strict gates remain before final review. + +## Attempt 1 implementation evidence — console identity gate pause + +Rack prepared-fold constructor/PCM tests (2), graph accepted/declined pairing test +(1), and the existing poisoned zero-input Identity/Bound control (1) passed. +`cargo test --locked -p console-workload --test chain_shape` exited 101: 20 passed, +`the_half_mono_cohort_banks_like_a_uniform_one` failed at line 472 because the mono +symmetry census was `(64, [65, 129])`, previously expected `(64, [64, 129])`. +The helper now binds the master with the requested `GraphNodeBinding::identity`; +its identity witness appears to explain the additional eligible unit. This is a +hypothesis pending independent review, not a changed acceptance gate. Source edits +stopped at this failure; strict Clippy was not launched by the sequential runner. +Exact source/head/argv/environment/logs/exits are preserved externally in +`/tmp/issue221-attempt1-pcm`. No benchmark or timing claim is made. + +## Attempt 1 preserved gate failure + +Rack old/new constructor PCM controls, pairing success/fallback and existing +poisoned identity-buffer control passed. The required console helper now uses +GraphNodeBinding::identity for non-source nodes. Console chain_shape compiled +and passed20 tests, but the_half_mono_cohort_banks_like_a_uniform_one failed: +mono eligible counters were [65,129], expected [64,129] at line472. Execution +stopped before Clippy; no retry or correction was made. Preserve this useful +compiling checkpoint and /tmp/issue221-attempt1-pcm logs/argv/env/exits. Astra +XHIGH is reviewing whether the additional master identity explains the counter +and what bounded correction is warranted; attempt PASS is not claimed. diff --git a/crates/graph/src/runtime.rs b/crates/graph/src/runtime.rs index 4a063491..e98f6654 100644 --- a/crates/graph/src/runtime.rs +++ b/crates/graph/src/runtime.rs @@ -2231,11 +2231,6 @@ impl RuntimeParts { /// The scratch and the lane mask come from the run's first slot; every slot of a cohort /// covers the same lanes by construction, and `BankChain::new` re-checks it rather than /// trusting it. - #[cfg(test)] - fn chain_for(&mut self, run: &[Membership], members: usize) -> BankChain { - self.chain_for_with_fold(run, members, None) - } - fn chain_for_with_fold( &mut self, run: &[Membership], @@ -5011,7 +5006,7 @@ mod tests { assert_eq!(runtime.buffer(ARENA_BASE).1, &[-0.25, -0.25]); } - struct DecliningPairOwner(Arc); + struct DecliningPairOwner(Arc, bool); fn decline_pair( left: crate::BuiltinProcessor, right: crate::BuiltinProcessor, @@ -5026,7 +5021,7 @@ mod tests { self } fn pair_factory(&self) -> Option { - Some(decline_pair) + Some(if self.1 { accept_pair } else { decline_pair }) } fn process( &mut self, @@ -5042,6 +5037,31 @@ mod tests { Ok(()) } } + struct AcceptedPair(crate::BuiltinProcessor, crate::BuiltinProcessor); + fn accept_pair( + left: crate::BuiltinProcessor, + right: crate::BuiltinProcessor, + ) -> Result { + Ok(Box::new(AcceptedPair(left, right))) + } + impl GraphPreparedBuiltinBankProcessor for AcceptedPair { + fn as_any(&self) -> &dyn Any { + self + } + fn into_any(self: Box) -> Box { + self + } + fn process( + &mut self, + left: &mut [f32], + right: &mut [f32], + frames: u32, + sample: u64, + ) -> Result<(), RenderError> { + self.0.process(left, right, frames, sample)?; + self.1.process(left, right, frames, sample) + } + } struct PlainPairOwner(Arc); impl GraphPreparedBuiltinBankProcessor for PlainPairOwner { fn as_any(&self) -> &dyn Any { @@ -5066,93 +5086,123 @@ mod tests { } #[test] - fn a_declined_first_pair_retains_the_first_slots_scratch() { - let track = crate::StableGraphId::parse("decline").expect("id"); - let fader = GraphNodeId::TrackStage { - track_id: track.clone(), - stage: TrackStage::PostFader, - }; - let matrix = GraphNodeId::TrackStage { - track_id: track, - stage: TrackStage::PostMatrix, - }; - let spec = GraphSpec { - nodes: vec![ - crate::GraphNode { - id: fader.clone(), - latency: effect_contract::LatencySamples(0), - tail: effect_contract::TailSamples::Finite(0), - }, - crate::GraphNode { - id: matrix.clone(), - latency: effect_contract::LatencySamples(0), - tail: effect_contract::TailSamples::Finite(0), - }, - ], - ports: Vec::new(), - edges: Vec::new(), - }; - let bank = |member, processor: Box| { - GraphPreparedBuiltinBank { - backend: lane::Backend::Simd4, - members: vec![member].into_boxed_slice(), - processor, - scratch: AoSoaScratch::new(effect_contract::BankWidth::Four, 8).expect("scratch"), + fn prepared_fold_retains_first_slots_scratch_through_pair_success_and_decline() { + for accepted in [false, true] { + for folded in [false, true] { + let track = crate::StableGraphId::parse("decline").expect("id"); + let fader = GraphNodeId::TrackStage { + track_id: track.clone(), + stage: TrackStage::PostFader, + }; + let matrix = GraphNodeId::TrackStage { + track_id: track, + stage: TrackStage::PostMatrix, + }; + let spec = GraphSpec { + nodes: vec![ + crate::GraphNode { + id: fader.clone(), + latency: effect_contract::LatencySamples(0), + tail: effect_contract::TailSamples::Finite(0), + }, + crate::GraphNode { + id: matrix.clone(), + latency: effect_contract::LatencySamples(0), + tail: effect_contract::TailSamples::Finite(0), + }, + ], + ports: Vec::new(), + edges: Vec::new(), + }; + let bank = |member, processor: Box| { + GraphPreparedBuiltinBank { + backend: lane::Backend::Simd4, + members: vec![member].into_boxed_slice(), + processor, + scratch: AoSoaScratch::new(effect_contract::BankWidth::Four, 8) + .expect("scratch"), + } + }; + let first_calls = Arc::new(AtomicUsize::new(0)); + let second_calls = Arc::new(AtomicUsize::new(0)); + let mut parts = RuntimeParts::new( + &spec, + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + vec![ + bank( + fader, + Box::new(DecliningPairOwner(Arc::clone(&first_calls), accepted)), + ), + bank(matrix, Box::new(PlainPairOwner(Arc::clone(&second_calls)))), + ], + Vec::new(), + Vec::new(), + Default::default(), + Vec::new(), + 8, + ); + let configuration = folded.then(|| { + rack::PreparedFoldConfiguration::new( + BankWidth::Four, + vec![true, false, false, false].into_boxed_slice(), + vec![true, false, false, false].into_boxed_slice(), + ) + .expect("borrowed shape configuration") + }); + let mut chain = parts.chain_for_with_fold( + &[Membership::Builtin(0), Membership::Builtin(1)], + 1, + configuration, + ); + assert_eq!(chain.width(), BankWidth::Four); + assert_eq!( + chain.fold_lanes(), + if folded { + &[true, false, false, false][..] + } else { + &[] + } + ); + assert!( + parts.builtin_banks.iter().all(Option::is_none), + "both original owners moved once" + ); + const FRAMES: usize = 2; + let mut lease = stereo_lease(FRAMES, 3); + lease.write_stereo(1).0.copy_from_slice(&[1.0, 2.0]); + lease.write_stereo(1).1.copy_from_slice(&[-1.0, -2.0]); + let fold = [FoldLane { + coefficients: [1.0, 0.0, 0.0, 1.0], + store: true, + }]; + let mut members = ArenaMembers { + lease: &mut lease, + inputs: &[1], + outputs: &[2], + fold: if folded { &fold } else { &[] }, + master: 2, + }; + chain + .run(&mut members, FRAMES as u32, 0) + .expect("declined chain render"); + assert_eq!( + first_calls.load(Ordering::Relaxed), + 1, + "first returned owner executes" + ); + assert_eq!( + second_calls.load(Ordering::Relaxed), + 1, + "second returned owner executes" + ); + assert_eq!(members.lease.read_stereo(2).0, &[4.0, 6.0]); + assert_eq!(members.lease.read_stereo(2).1, &[0.0, -2.0]); } - }; - let first_calls = Arc::new(AtomicUsize::new(0)); - let second_calls = Arc::new(AtomicUsize::new(0)); - let mut parts = RuntimeParts::new( - &spec, - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - vec![ - bank( - fader, - Box::new(DecliningPairOwner(Arc::clone(&first_calls))), - ), - bank(matrix, Box::new(PlainPairOwner(Arc::clone(&second_calls)))), - ], - Vec::new(), - Vec::new(), - Default::default(), - Vec::new(), - 8, - ); - let mut chain = parts.chain_for(&[Membership::Builtin(0), Membership::Builtin(1)], 1); - assert!( - parts.builtin_banks.iter().all(Option::is_none), - "both original owners moved once" - ); - const FRAMES: usize = 2; - let mut lease = stereo_lease(FRAMES, 3); - lease.write_stereo(1).0.copy_from_slice(&[1.0, 2.0]); - lease.write_stereo(1).1.copy_from_slice(&[-1.0, -2.0]); - let mut members = ArenaMembers { - lease: &mut lease, - inputs: &[1], - outputs: &[2], - fold: &[], - master: 0, - }; - chain - .run(&mut members, FRAMES as u32, 0) - .expect("declined chain render"); - assert_eq!( - first_calls.load(Ordering::Relaxed), - 1, - "first returned owner executes" - ); - assert_eq!( - second_calls.load(Ordering::Relaxed), - 1, - "second returned owner executes" - ); - assert_eq!(members.lease.read_stereo(2).0, &[4.0, 6.0]); - assert_eq!(members.lease.read_stereo(2).1, &[0.0, -2.0]); + } } /// The node's cached witness and the line's own answer are the same fact (#210 phase 2). diff --git a/crates/rack/src/lib.rs b/crates/rack/src/lib.rs index 98c5360b..22e89c3e 100644 --- a/crates/rack/src/lib.rs +++ b/crates/rack/src/lib.rs @@ -4598,6 +4598,113 @@ mod tests { } } + /// The new constructor and the existing arming path render identical words for both bank + /// widths, including active holes, prefix subsets, full/partial staging and disarmed masks. + #[test] + fn prepared_fold_constructor_preserves_pcm_and_disarm() { + const FRAMES: u32 = 13; + for width in [BankWidth::Four, BankWidth::Eight] { + let lanes = width.lanes() as usize; + for shape in 0..3 { + let mut active = vec![true; lanes]; + if shape == 1 { + active[lanes - 1] = false; + } + if shape == 2 { + active[1] = false; + } + for subset in 0..3 { + let fold: Vec = active + .iter() + .enumerate() + .map(|(lane, active)| { + *active + && match subset { + 0 => false, + 1 => lane < 2, + _ => true, + } + }) + .collect(); + let scratch = || AoSoaScratch::new(width, FRAMES).expect("scratch"); + let slots = || vec![slot(active.clone(), Box::new(ScaleByLane))]; + let mut old = + BankChain::new(scratch(), active.clone().into_boxed_slice(), slots()) + .expect("old"); + old.arm_fold(fold.clone().into_boxed_slice()) + .expect("old arm"); + let configuration = PreparedFoldConfiguration::new( + width, + active.clone().into_boxed_slice(), + fold.into_boxed_slice(), + ) + .expect("configuration"); + let mut new = + BankChain::new_with_prepared_fold(scratch(), configuration, slots()) + .expect("new"); + assert_eq!(old.active, new.active); + assert_eq!(old.fold_lanes(), new.fold_lanes()); + assert_eq!(old.staging_left.len(), new.staging_left.len()); + let provider = || PlanesWithFold { + planes: Planes { + left: (0..lanes) + .map(|lane| { + (0..FRAMES) + .map(|frame| (lane as f32 + 0.125) * (frame as f32 - 2.0)) + .collect() + }) + .collect(), + right: (0..lanes) + .map(|lane| { + (0..FRAMES) + .map(|frame| (lane as f32 - 0.75) * (frame as f32 + 1.0)) + .collect() + }) + .collect(), + }, + gains: (0..lanes).map(|lane| lane as f32 + 0.5).collect(), + bus_left: vec![0.0; FRAMES as usize], + bus_right: vec![0.0; FRAMES as usize], + taken: Vec::new(), + trace: Vec::new(), + cohorts: Vec::new(), + }; + for disarmed in [false, true] { + if disarmed { + old.arm_fold(vec![false; lanes].into_boxed_slice()) + .expect("disarm old"); + new.arm_fold(vec![false; lanes].into_boxed_slice()) + .expect("disarm new"); + } + let mut expected = provider(); + let mut actual = provider(); + old.run(&mut expected, FRAMES, 0).expect("old run"); + new.run(&mut actual, FRAMES, 0).expect("new run"); + let words = |provider: &PlanesWithFold| { + provider + .planes + .left + .iter() + .chain(&provider.planes.right) + .flat_map(|plane| plane.iter()) + .chain(&provider.bus_left) + .chain(&provider.bus_right) + .map(|word| word.to_bits()) + .collect::>() + }; + assert_eq!( + words(&expected), + words(&actual), + "{width:?}/{shape}/{subset}/{disarmed}" + ); + assert_eq!(expected.trace, actual.trace); + assert_eq!(expected.taken, actual.taken); + } + } + } + } + } + /// The fold epilogue: absent by default, and when armed it *replaces* the lane's plane write /// with the same words handed over. /// diff --git a/tools/console-workload/src/lib.rs b/tools/console-workload/src/lib.rs index 08b09b1e..47ef8a65 100644 --- a/tools/console-workload/src/lib.rs +++ b/tools/console-workload/src/lib.rs @@ -881,12 +881,7 @@ impl SessionRuntime { let nodes = graph .required_bindings .iter() - .map(|node| { - GraphNodeBinding::new( - node.clone(), - source_binding(node, silent, &source, &mappings), - ) - }) + .map(|node| source_binding(node, silent, &source, &mappings)) .collect(); let plan = graph .bind(GraphRuntimeBindings { @@ -912,12 +907,7 @@ impl SessionRuntime { let envelope = artifact.envelope(); let nodes = artifact .external_binding_nodes() - .map(|node| { - GraphNodeBinding::new( - node.clone(), - source_binding(node, silent, &source, &mappings), - ) - }) + .map(|node| source_binding(node, silent, &source, &mappings)) .collect(); // `observers` stays empty on purpose: it is the *external* observer slot. A meter // observer is compiler-owned and is appended to this vector by the sealed builtins @@ -1489,17 +1479,6 @@ impl GraphRuntimeProcessor for FrozenGraphSource { } } -struct GraphIdentity; - -impl GraphRuntimeProcessor for GraphIdentity { - fn process( - &mut self, - _block: GraphBindingBlock<'_>, - ) -> Result<(), engine::realtime::RenderError> { - Ok(()) - } -} - /// Every track's declared `(left_source_channel, right_source_channel)`, in model order. /// /// Read from the compiled model rather than assumed, because it is the field the mono fixture @@ -1524,7 +1503,7 @@ fn source_binding( silent: bool, source: &SourceSignal, mappings: &[(usize, usize)], -) -> Box { +) -> GraphNodeBinding { if let GraphNodeId::TrackStage { track_id, stage: TrackStage::Input, @@ -1542,8 +1521,11 @@ fn source_binding( .get(track) .copied() .unwrap_or_else(|| panic!("the model must declare a source mapping for track {track}")); - Box::new(FrozenGraphSource::from_block(&block, mapping)) + GraphNodeBinding::new( + node.clone(), + Box::new(FrozenGraphSource::from_block(&block, mapping)), + ) } else { - Box::new(GraphIdentity) + GraphNodeBinding::identity(node.clone()) } } From ed8609612fb54e1b154e4a0ee739a99062f19eca Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:16:39 +0000 Subject: [PATCH 12/16] docs: record fold review failure and bounded census correction --- .../221-reject-invalid-route-fold-binding.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 1abd5434..0a4e5a0e 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -124,3 +124,17 @@ stopped before Clippy; no retry or correction was made. Preserve this useful compiling checkpoint and /tmp/issue221-attempt1-pcm logs/argv/env/exits. Astra XHIGH is reviewing whether the additional master identity explains the counter and what bounded correction is warranted; attempt PASS is not claimed. + +## Astra XHIGH attempt 1 FAIL; bounded attempt 2 + +At d1cb3653 required chain_shape exited101; Clippy is unrun. Review confirms +the requested master identity changes DECLINED to SYMMETRIC, adding one eligible +census lane without changing unit/track/collapse counts. Correct only +tools/console-workload/tests/chain_shape.rs: mono (64,[65,129]), half-mono +(32,[129,193]), and adjacent derivation. Preserve structural64/32/0, folds, +shape, PCM and cohort assertions. Add a discriminating last/master row assertion: +nonbanked, one lane, lane_tracks [""], upstream_of_seam_stages0 and lane_eligible +[true], identifying the extra lane as the vacuous master witness. No production +correction authorized. Reuse rack PCM/pairing/poison passes; run corrected +chain_shape followed by outstanding strict gates, then independent review. +This is attempt2 of the user-authorized five-attempt maximum. From 13c974cf2d47bc180df8039a91879a6b3834dae1 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:20:09 +0000 Subject: [PATCH 13/16] test: identify the identity masters symmetry census contribution --- .../221-reject-invalid-route-fold-binding.md | 11 ++++++++++ tools/console-workload/tests/chain_shape.rs | 21 +++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 0a4e5a0e..01be3d8b 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -138,3 +138,14 @@ nonbanked, one lane, lane_tracks [""], upstream_of_seam_stages0 and lane_eligibl correction authorized. Reuse rack PCM/pairing/poison passes; run corrected chain_shape followed by outstanding strict gates, then independent review. This is attempt2 of the user-authorized five-attempt maximum. + +## Attempt 2 focused green checkpoint + +Astra LOW corrected only chain_shape.rs census/derivation and added the master +identity discriminant. Initial E0277 from comparing Box with str is preserved +in /tmp/issue221-attempt2; XHIGH classified its exact type-correct assertion fix +as ordinary compiler feedback within unfinished attempt2, with no gate change. +Fresh corrected receipts show21/21 console tests, strict locked all-target/ +all-feature Clippy for graph/rack/console-workload, and formatting passed. No +lint warnings. Production unchanged since the reviewed attempt1 source. +Independent final review and representative debug/release verification remain. diff --git a/tools/console-workload/tests/chain_shape.rs b/tools/console-workload/tests/chain_shape.rs index ff05a0bf..df668ed6 100644 --- a/tools/console-workload/tests/chain_shape.rs +++ b/tools/console-workload/tests/chain_shape.rs @@ -464,6 +464,18 @@ fn the_half_mono_cohort_banks_like_a_uniform_one() { // session, which is what makes this a mixed cohort rather than a relabelled one. let counted = |workload| { let runtime = SessionRuntime::build(workload, PlanConfig::BASELINE); + let rows = runtime.unit_eligibility(); + let master = rows.last().expect("master row"); + assert!(!master.banked, "master is a plain unit"); + assert_eq!(master.lanes(), 1); + assert_eq!(master.lane_tracks.len(), 1); + assert_eq!(master.lane_tracks[0].as_ref(), ""); + assert_eq!(master.upstream_of_seam_stages, 0); + assert_eq!( + master.lane_eligible.as_ref(), + &[true], + "the identity master supplies the extra vacuous witness" + ); ( runtime.structural_mono_tracks(), runtime.symmetry_counters(), @@ -471,7 +483,7 @@ fn the_half_mono_cohort_banks_like_a_uniform_one() { }; assert_eq!( counted(Workload::SixtyFourTrackConsoleMono), - (64, [64, 129]), + (64, [65, 129]), "every track of the mono fixture is collapse-eligible" ); // The half-mono row's own census, derived rather than pinned -- and it is the standing worked @@ -479,20 +491,21 @@ fn the_half_mono_cohort_banks_like_a_uniform_one() { // // The uniform mono row counts 129 "lanes": 64 bank-chain lanes, 64 source-input ops and the // master, its 64 route ops having been absorbed by the #218 fold and never built as units at - // all. The half-mono row's fold declines (see + // all. Its identity master contributes one eligible, vacuous witness in addition to the 64 + // eligible bank-chain lanes; it adds no collapse-eligible track. The half-mono row's fold declines (see // `every_standing_workload_folds_one_route_per_track` for the association-order derivation), // so its 64 route ops *are* dispatched: 193 lanes, not 129. And a `Route` reports // `ChannelSymmetryWitness::SYMMETRIC` -- it is not per-track upstream work, so nothing about // it can make two channels disagree -- which adds 64 to the eligible half as well. // - // So the pair moves from `[64, 129]` to `[128, 193]` on a row where **not one track's + // So the pair moves from `[65, 129]` to `[129, 193]` on a row where **not one track's // symmetry changed**: 32 of its tracks are collapse-eligible before and after, which is what // `structural_mono_tracks` still reports and what the per-cohort rows below actually measure. // Two censuses are comparable only when the plans' unit inventories are; this is that caveat // with numbers on it. assert_eq!( counted(Workload::SixtyFourTrackConsoleHalfMono), - (32, [64 + 64, 129 + 64]), + (32, [65 + 64, 129 + 64]), "half the tracks of the half-mono row read two source channels; the census additionally \ carries the 64 route ops this row's declined fold left dispatched" ); From 5e45c7a10a3713d06bdef2e7b875ad16b4afa91d Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:26:40 +0000 Subject: [PATCH 14/16] docs: record reviewed fold and compiler joint source checkpoint --- ...62-borrow-session-during-graph-compilation.md | 8 ++++++++ .../221-reject-invalid-route-fold-binding.md | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md index adb28751..b88d2633 100644 --- a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md +++ b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md @@ -93,3 +93,11 @@ and target used. Evidence: `/tmp/issue162-xhigh-qemy_v30/attempt2-review.md` and final-cap-attempt2 receipts. Root defers artifact qualification for a coherent joint delivery with #221 after #220's required delivery, while source remains frozen. Required artifact/PR/main gates and synchronized closure remain pending. + +## Joint delivery checkpoint — 2026-09-11 + +#220 prerequisite is delivered/closed at mainc476f5b4 with PR/main gates PASS. +#221 received independent attempt2 SOURCE PASS at13c974cf and is now combined +with this frozen49f83155 history for one root-owned artifact qualification. +Compiler source/evidence is unchanged; artifact, required PR/main qualification +and upstream closure remain pending. Active slots are #221/#162. diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 01be3d8b..9bb6a29b 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -149,3 +149,19 @@ Fresh corrected receipts show21/21 console tests, strict locked all-target/ all-feature Clippy for graph/rack/console-workload, and formatting passed. No lint warnings. Production unchanged since the reviewed attempt1 source. Independent final review and representative debug/release verification remain. + +## Independent Astra XHIGH attempt 2 SOURCE PASS + +Reviewed13c974cf2d47bc180df8039a91879a6b3834dae1. Full graph/rack all-feature +debug124PASS; release-unwind fold16PASS (including pair success/fallback and +all22 public recovery paths), corpus1PASS, RT1 allocation/PCM1PASS, console21PASS. +Strict Clippy, console debug21 and formatting receipts reused. No production +correction after attempt1; master identity assertion discriminates the intended +vacuous witness. Evidence: /tmp/issue221-xhigh-mp0nkqhi/review.md and adjacent +actual argv/env/source/exit/log records. Fixture directly covers stateful builtin, +bindings, both observer sets and sources; effect-bank/control ownership follows +from the reviewed wholly borrowed boundary. + +Root integrates separately accepted #162 checkpoint49f83155 by history-preserving +merge for one shared artifact qualification. Artifact and required PR/main gates +remain pending; no issue closure or performance claim yet. From 06f0ab6b3fbddde93f9db4bc24650625e33f65c3 Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:27:47 +0000 Subject: [PATCH 15/16] web: checkpoint provisional joint fold and compiler artifact pin --- hosts/host-web/web/miso-engine-v1-audio-worklet-artifact.sha256 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 664080dd..f701a632 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 @@ -80abeec2688be94807bf4086861639fa63e4111df1d1978e37b1288ff2151de4 +a24d0cae46f097336c02268b946f92fbf4419f05b6e05d20a1476f19c76d7e60 From d6f70c18c8a800e3bdd89b1b864d5af0bbb65b6e Mon Sep 17 00:00:00 2001 From: BL Date: Fri, 11 Sep 2026 02:32:51 +0000 Subject: [PATCH 16/16] web: record qualified joint fold and compiler artifact --- ...-borrow-session-during-graph-compilation.md | 18 ++++++++++++++++++ .../221-reject-invalid-route-fold-binding.md | 18 ++++++++++++++++++ hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md | 2 +- hosts/host-web/qualification/results.json | 4 ++-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md index b88d2633..73ceb817 100644 --- a/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md +++ b/.github/ISSUE_SPECS/162-borrow-session-during-graph-compilation.md @@ -101,3 +101,21 @@ frozen. Required artifact/PR/main gates and synchronized closure remain pending. with this frozen49f83155 history for one root-owned artifact qualification. Compiler source/evidence is unchanged; artifact, required PR/main qualification and upstream closure remain pending. Active slots are #221/#162. + +## Shared artifact qualification — 2026-09-11 + +Astra XHIGH combined-source PASS at5e45c7a10a3713d06bdef2e7b875ad16b4afa91d: +merge29e68c26 preserves reviewed221/162 trees/history, and the combined compiler +ownership/retry/canonical/PCM control passed with unchanged lock. Evidence: +/tmp/issue221-xhigh-mp0nkqhi/combined-review.md and adjacent source/command records. + +Root probe and ordinary build reproduced Wasm SHA-256 +a24d0cae46f097336c02268b946f92fbf4419f05b6e05d20a1476f19c76d7e60 +(provisional pin06f0ab6b); all five companion artifact files remain byte-identical +to qualified80abeec2. Static artifact, expected-resource parity, hermetic host, +SDK types/headless/package, Chromium/Firefox/WebKit matrix with mutations, and +generated-matrix check all passed. Matrix candidate is frozen source5e45c7a1. +Actual argv/env/head/exit/stdout/stderr, six-file comparison and artifact bytes: +/tmp/issue162-221-artifact-pc2gtj6w. Only expected matrix candidate/digest changes +are committed; no generated compiler captures or performance claim. +Independent artifact/exact-head review and required PR/main CI remain pending. diff --git a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md index 9bb6a29b..e5dfd61a 100644 --- a/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md +++ b/.github/ISSUE_SPECS/221-reject-invalid-route-fold-binding.md @@ -165,3 +165,21 @@ from the reviewed wholly borrowed boundary. Root integrates separately accepted #162 checkpoint49f83155 by history-preserving merge for one shared artifact qualification. Artifact and required PR/main gates remain pending; no issue closure or performance claim yet. + +## Shared artifact qualification — 2026-09-11 + +Astra XHIGH combined-source PASS at5e45c7a10a3713d06bdef2e7b875ad16b4afa91d: +merge29e68c26 preserves reviewed221/162 trees/history, and the combined compiler +ownership/retry/canonical/PCM control passed with unchanged lock. Evidence: +/tmp/issue221-xhigh-mp0nkqhi/combined-review.md and adjacent source/command records. + +Root probe and ordinary build reproduced Wasm SHA-256 +a24d0cae46f097336c02268b946f92fbf4419f05b6e05d20a1476f19c76d7e60 +(provisional pin06f0ab6b); all five companion artifact files remain byte-identical +to qualified80abeec2. Static artifact, expected-resource parity, hermetic host, +SDK types/headless/package, Chromium/Firefox/WebKit matrix with mutations, and +generated-matrix check all passed. Matrix candidate is frozen source5e45c7a1. +Actual argv/env/head/exit/stdout/stderr, six-file comparison and artifact bytes: +/tmp/issue162-221-artifact-pc2gtj6w. Only expected matrix candidate/digest changes +are committed; no generated compiler captures or performance claim. +Independent artifact/exact-head review and required PR/main CI remain pending. diff --git a/hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md b/hosts/host-web/BROWSER_DEPLOYMENT_MATRIX.md index b6751324..4b2ff3a0 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 `ee779e2f7234380545fa8933b5450d117419aeb1` and the single shipped simd128 AudioWorklet artifact `80abeec2688be94807bf4086861639fa63e4111df1d1978e37b1288ff2151de4`. 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 `5e45c7a10a3713d06bdef2e7b875ad16b4afa91d` and the single shipped simd128 AudioWorklet artifact `a24d0cae46f097336c02268b946f92fbf4419f05b6e05d20a1476f19c76d7e60`. 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 b29f7836..33126b7a 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": "ee779e2f7234380545fa8933b5450d117419aeb1", - "wasmSha256": "80abeec2688be94807bf4086861639fa63e4111df1d1978e37b1288ff2151de4", + "candidateCommit": "5e45c7a10a3713d06bdef2e7b875ad16b4afa91d", + "wasmSha256": "a24d0cae46f097336c02268b946f92fbf4419f05b6e05d20a1476f19c76d7e60", "playwrightVersion": "1.62.1", "platform": "linux-headless", "artifact": "single shipped simd128 AudioWorklet artifact",