diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index cfe400d..bc5f68f 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -872,6 +872,69 @@ mod tests { assert_eq!(space[2].kind, ScalarKind::F64); } + /// E2E (issue #34): the C23/#31 mirror invariant *under composite nesting*. + /// `param_space()` (via `collect_params`) duplicates `lower_items`' depth- + /// first traversal rather than sharing it, so the two orders must stay in + /// lockstep. The single-level mirror test + /// (`param_space_mirrors_compiled_flat_node_param_order`) never compiles a + /// composite whose interior holds *another* composite; the nested + /// `param_space` order test never compiles. This closes that gap: it + /// compiles a `strategy → { fast_slow → [Sma, Sma, Sub], LinComb }` nest and + /// asserts the aggregated space lines up, kind-by-slot, with the compiled + /// flat-node param order — so a future inliner reorder that desynced the two + /// projections *only under nesting* would fail here, not slip through. + #[test] + fn param_space_mirrors_compiled_flat_node_param_order_under_nesting() { + use aura_std::{LinComb, Sma, Sub}; + // inner composite "fast_slow": two SMAs + a Sub (same nest as the + // isolated path-qualification test above) + let fast_slow = Composite::new( + "fast_slow", + vec![Sma::new(2).into(), Sma::new(4).into(), Sub::new().into()], + vec![ + Edge { from: 0, to: 2, slot: 0, from_field: 0 }, + Edge { from: 1, to: 2, slot: 1, from_field: 0 }, + ], + vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]], + OutPort { node: 2, field: 0 }, + ); + // outer composite "strategy": the inner composite + a LinComb([1,-1]) + let strategy = Composite::new( + "strategy", + vec![BlueprintNode::Composite(fast_slow), LinComb::new(vec![1.0, -1.0]).into()], + vec![], + vec![vec![Target { node: 0, slot: 0 }]], + OutPort { node: 0, field: 0 }, + ); + let bp = Blueprint::new(vec![BlueprintNode::Composite(strategy)], vec![], vec![]); + + // the aggregated, path-qualified projection (borrows; take it first since + // compile() consumes self — same ordering as the single-level mirror test) + let space = bp.param_space(); + + // the same blueprint, compiled to its flat node array; each flat node's + // own declared params, concatenated in flat-node order + let (flat_nodes, _sources, _edges) = bp.compile().expect("nested composite compiles"); + let from_compilat: Vec = + flat_nodes.iter().flat_map(|n| n.schema().params).collect(); + + // same count, same per-slot kind, same order — the nested projection + // mirrors the compilation (names differ: param_space path-qualifies, the + // raw node does not, so compare on the load-bearing axis, kind-by-slot) + assert_eq!(space.len(), from_compilat.len(), "param count must match the compilat"); + assert_eq!( + space.iter().map(|p| p.kind).collect::>(), + from_compilat.iter().map(|p| p.kind).collect::>(), + "per-slot param kinds must line up with the compiled flat-node order, under nesting", + ); + // pin the concrete shape: two Sma lengths (I64), Sub none, two LinComb + // weights (F64) + assert_eq!( + space.iter().map(|p| p.kind).collect::>(), + [ScalarKind::I64, ScalarKind::I64, ScalarKind::F64, ScalarKind::F64], + ); + } + #[test] fn top_level_leaf_params_are_unqualified() { use aura_std::Sma;