# Param-set injection — Implementation Plan > **Parent spec:** `docs/specs/0016-param-set-injection.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run > this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Make a blueprint value-empty — a leaf is a `LeafFactory` recipe (`params → sized node`) — and bind a positional `Scalar` vector at bootstrap via a new build-then-wire compile path, with kind + arity checks. **Architecture:** `LeafFactory { name, params, build }` lands in aura-core; the 7 aura-std nodes expose `factory()`; aura-engine's `BlueprintNode::Leaf` becomes a factory, `compile_with_params`/`bootstrap_with_params` build each leaf from its kind-checked param slice while lowering (the existing structural inline/edge/source rewrite is unchanged), and the vestigial pre-build `schema` methods are removed; aura-cli's blueprint render reads the param-generic `LeafFactory::label()` and the sample/goldens are re-expressed against the vector. **Tech Stack:** Rust workspace — aura-core (Node/Scalar contract), aura-std (nodes), aura-engine (blueprint/compile/harness), aura-cli (run/graph faces). **Sequencing (compile gates):** The `Leaf(LeafFactory)` change breaks every blueprint-leaf author site until repaired, so tasks are gated crate-by-crate: Task 1 `cargo build -p aura-core`, Task 2 `-p aura-std`, Task 3 `cargo build -p aura-engine --all-targets`, Task 4 `cargo build -p aura-cli --all-targets` then `cargo test --workspace`. Tasks 1–2 are purely additive (no breakage); Task 3 is the breaking change and repairs every aura-engine site (incl. its own tests) in one task so its compile gate is satisfiable; Task 4 repairs aura-cli + runs the workspace gate. --- ## Files this plan creates or modifies - Modify: `crates/aura-core/src/node.rs` — add `LeafFactory`. - Modify: `crates/aura-core/src/scalar.rs` — add `as_i64`/`as_f64`. - Modify: `crates/aura-core/src/lib.rs` — re-export `LeafFactory`. - Modify: `crates/aura-std/src/{sma,exposure,lincomb,sub,add,sim_broker,recorder}.rs` — each gains `fn factory(...)` + a factory↔schema params test. - Modify: `crates/aura-engine/src/blueprint.rs` — `Leaf(LeafFactory)`, `From`, `collect_params`, `compile_with_params`, `bootstrap_with_params`, `CompileError` variants, remove vestigial `schema` methods + their test, re-express fixtures + tests. - Modify: `crates/aura-cli/src/graph.rs` — `render_blueprint` uses `LeafFactory::label()`. - Modify: `crates/aura-cli/src/main.rs` — sample blueprint → factories + vector; param-form call sites; re-capture blueprint-view goldens; move the swap to the compiled view. --- ## Task 1: aura-core — `LeafFactory` + `Scalar` accessors **Files:** - Modify: `crates/aura-core/src/node.rs` - Modify: `crates/aura-core/src/scalar.rs` - Modify: `crates/aura-core/src/lib.rs` - [ ] **Step 1: Add `LeafFactory` to `node.rs`** After the `ParamSpec` struct (ends `node.rs:58`) and before the `NodeSchema` doc, add (the `use` at `node.rs:11` already imports `Scalar`): ```rust /// A param-generic blueprint leaf (C19): a node's declared tunable params plus a /// closure that builds a sized instance through the node's own constructor (the /// single sizing/validation gate). A blueprint holds these recipes, never built /// instances, so it stays value-empty until a param-set is injected (C19/C23). pub struct LeafFactory { name: &'static str, params: Vec, build: Box Box>, } impl LeafFactory { /// `name` is the param-generic render label (the node type, e.g. `"SMA"`); /// `params` the declared knobs; `build` constructs a sized node from a /// kind-checked param slice. pub fn new( name: &'static str, params: Vec, build: impl Fn(&[Scalar]) -> Box + 'static, ) -> Self { Self { name, params, build: Box::new(build) } } /// The declared tunable params (read by `Blueprint::param_space`, pre-build). pub fn params(&self) -> &[ParamSpec] { &self.params } /// Build a sized node from its param slice (the slice is kind-checked by the /// caller before this runs). pub fn build(&self, params: &[Scalar]) -> Box { (self.build)(params) } /// The param-generic render label for the blueprint view (C22 "structure /// before"): the node type plus its tunable param *names* — no values, a /// value-empty recipe has none — e.g. `SMA(length)`, `LinComb(weights[0], /// weights[1])`, or bare `SimBroker` when paramless. pub fn label(&self) -> String { if self.params.is_empty() { self.name.to_string() } else { let knobs: Vec<&str> = self.params.iter().map(|p| p.name.as_str()).collect(); format!("{}({})", self.name, knobs.join(", ")) } } } ``` - [ ] **Step 2: Add value accessors to `scalar.rs`** Inside the existing `impl Scalar` block (after `kind`, `scalar.rs:30-37`), add: ```rust /// The `i64` payload, or `None` if this scalar is not an `I64`. pub fn as_i64(self) -> Option { if let Scalar::I64(v) = self { Some(v) } else { None } } /// The `f64` payload, or `None` if this scalar is not an `F64`. pub fn as_f64(self) -> Option { if let Scalar::F64(v) = self { Some(v) } else { None } } ``` - [ ] **Step 3: Re-export `LeafFactory`** In `crates/aura-core/src/lib.rs:42`, add `LeafFactory` to the `pub use node::{...}` list (keep alphabetical): `pub use node::{FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, ParamSpec};` - [ ] **Step 4: Tests in `node.rs` and `scalar.rs`** In `node.rs` tests (reuse the `Bare` node already defined in that module, `node.rs` test mod), add: ```rust #[test] fn leaf_factory_label_is_param_generic() { let with = LeafFactory::new( "SMA", vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], |_| Box::new(Bare), ); assert_eq!(with.label(), "SMA(length)"); let none = LeafFactory::new("Sub", vec![], |_| Box::new(Bare)); assert_eq!(none.label(), "Sub"); } #[test] fn leaf_factory_build_runs_the_closure() { let f = LeafFactory::new("Bare", vec![], |_| Box::new(Bare)); assert_eq!(f.build(&[]).schema().params, Vec::::new()); } ``` In `scalar.rs` tests, add: ```rust #[test] fn scalar_value_accessors_are_kind_exact() { assert_eq!(Scalar::I64(3).as_i64(), Some(3)); assert_eq!(Scalar::I64(3).as_f64(), None); assert_eq!(Scalar::F64(0.5).as_f64(), Some(0.5)); assert_eq!(Scalar::F64(0.5).as_i64(), None); } ``` (If `scalar.rs` has no `#[cfg(test)] mod tests`, add one with `use super::*;`.) - [ ] **Step 5: Gate** Run: `cargo test -p aura-core` Expected: PASS, including `leaf_factory_label_is_param_generic`, `leaf_factory_build_runs_the_closure`, `scalar_value_accessors_are_kind_exact`. --- ## Task 2: aura-std — `factory()` on the 7 nodes **Files:** Modify each of `crates/aura-std/src/{sma,exposure,lincomb,sub,add,sim_broker,recorder}.rs`. Each `factory()` is an inherent method in the node's existing `impl ` block (beside `new`). Add `LeafFactory` to each file's `use aura_core::{...}` line. - [ ] **Step 1: `Sma::factory` (`sma.rs`)** ```rust /// The param-generic recipe for a blueprint leaf: declares `length` and builds /// through `Sma::new` (the single sizing/validation gate; the slice is /// kind-checked before `build` runs, so the typed read is total). pub fn factory() -> LeafFactory { LeafFactory::new( "SMA", vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }], |p| Box::new(Sma::new(p[0].as_i64().expect("length slot is I64") as usize)), ) } ``` - [ ] **Step 2: `Exposure::factory` (`exposure.rs`)** ```rust pub fn factory() -> LeafFactory { LeafFactory::new( "Exposure", vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }], |p| Box::new(Exposure::new(p[0].as_f64().expect("scale slot is F64"))), ) } ``` - [ ] **Step 3: `LinComb::factory(arity)` (`lincomb.rs`)** The arity is topology (fixed per blueprint, C19), taken as a factory arg; only the weight *values* are injected. ```rust pub fn factory(arity: usize) -> LeafFactory { let params = (0..arity) .map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 }) .collect(); LeafFactory::new( "LinComb", params, |p| Box::new(LinComb::new( p.iter().map(|s| s.as_f64().expect("weight slot is F64")).collect(), )), ) } ``` - [ ] **Step 4: paramless `Sub`/`Add::factory` (`sub.rs`, `add.rs`)** ```rust // sub.rs pub fn factory() -> LeafFactory { LeafFactory::new("Sub", vec![], |_| Box::new(Sub::new())) } // add.rs pub fn factory() -> LeafFactory { LeafFactory::new("Add", vec![], |_| Box::new(Add::new())) } ``` - [ ] **Step 5: `SimBroker::factory(pip_size)` (`sim_broker.rs`)** `pip_size` is metadata (C10/C15), not a tunable param — captured by the closure. ```rust pub fn factory(pip_size: f64) -> LeafFactory { LeafFactory::new("SimBroker", vec![], move |_| Box::new(SimBroker::new(pip_size))) } ``` - [ ] **Step 6: `Recorder::factory(kinds, firing, tx)` (`recorder.rs`)** The channel + kinds + firing are non-param construction args — captured; `tx` is cloned per build (`mpsc::Sender: Clone`). ```rust pub fn factory( kinds: Vec, firing: Firing, tx: Sender<(Timestamp, Vec)>, ) -> LeafFactory { LeafFactory::new("Recorder", vec![], move |_| { Box::new(Recorder::new(&kinds, firing, tx.clone())) }) } ``` - [ ] **Step 7: factory↔schema params agreement test (one per node)** Add to each node's `#[cfg(test)] mod tests` a test asserting `factory().params()` equals the built node's `schema().params`. Example for `sma.rs`: ```rust #[test] fn factory_params_match_built_node_schema() { let f = Sma::factory(); let built = f.build(&[Scalar::I64(3)]); assert_eq!(f.params(), built.schema().params.as_slice()); } ``` Mirror it per node with a valid sample slice: `Exposure` `&[Scalar::F64(0.5)]`; `LinComb::factory(2)` `&[Scalar::F64(1.0), Scalar::F64(-1.0)]`; `Sub`/`Add` `&[]`; `SimBroker::factory(0.0001)` `&[]`; `Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx)` `&[]` (make a throwaway `mpsc::channel()` for `tx`). - [ ] **Step 8: Gate** Run: `cargo test -p aura-std` Expected: PASS, including the 7 `factory_params_match_built_node_schema` tests. --- ## Task 3: aura-engine — value-empty leaf, build-then-wire compile, errors **Files:** Modify `crates/aura-engine/src/blueprint.rs`. - [ ] **Step 1: `BlueprintNode::Leaf` + the lift** Change the enum (`blueprint.rs:27-30`) and replace the generic `From` (`blueprint.rs:33-37`): ```rust pub enum BlueprintNode { Leaf(LeafFactory), Composite(Composite), } impl From for BlueprintNode { fn from(factory: LeafFactory) -> Self { BlueprintNode::Leaf(factory) } } ``` Add `LeafFactory` and `Scalar` to the `use aura_core::{...}` at `blueprint.rs:14`. - [ ] **Step 2: Remove the vestigial pre-build `schema` methods** Delete the `impl BlueprintNode { fn schema(&self) -> NodeSchema {...} }` block (`blueprint.rs:39-48`) and `Composite::schema` (`blueprint.rs:103-114`). Both have no live caller — `compile` resolves every interface on the built flat nodes. Delete the unit test `composite_schema_derives_role_and_output_kinds` (`blueprint.rs:435`). - [ ] **Step 3: `collect_params` reads `factory.params()`** In `collect_params` (`blueprint.rs:217-240`), the `Leaf` arm (`220-229`): ```rust BlueprintNode::Leaf(factory) => { for p in factory.params() { let name = if prefix.is_empty() { p.name.clone() } else { format!("{prefix}.{}", p.name) }; out.push(ParamSpec { name, kind: p.kind }); } } ``` (`param_space` at `blueprint.rs:166-170` is otherwise unchanged.) - [ ] **Step 4: Two new `CompileError` variants** In `enum CompileError` (`blueprint.rs:120-130`) add: ```rust /// An injected param value's scalar kind does not match the slot's declared /// kind. `slot` is the flat param-space index. ParamKindMismatch { slot: usize, expected: ScalarKind, got: ScalarKind }, /// The injected vector's length does not equal the sum of declared params. ParamArity { expected: usize, got: usize }, ``` `ScalarKind` is already imported (`blueprint.rs:14`). - [ ] **Step 5: Thread params+cursor through `lower_items` / `inline_composite`** `lower_items` (`blueprint.rs:255-274`) gains `params: &[Scalar]` and `cursor: &mut usize`, and its `Leaf` arm builds (kind-checking) instead of moving a node: ```rust fn lower_items( items: Vec, params: &[Scalar], cursor: &mut usize, flat_nodes: &mut Vec>, flat_edges: &mut Vec, ) -> Result, CompileError> { let mut lowerings = Vec::with_capacity(items.len()); for item in items { match item { BlueprintNode::Leaf(factory) => { let n = factory.params().len(); let slice = ¶ms[*cursor..*cursor + n]; // in range: arity checked up front for (i, spec) in factory.params().iter().enumerate() { let got = slice[i].kind(); if got != spec.kind { return Err(CompileError::ParamKindMismatch { slot: *cursor + i, expected: spec.kind, got, }); } } let index = flat_nodes.len(); flat_nodes.push(factory.build(slice)); *cursor += n; lowerings.push(ItemLowering::Leaf { index }); } BlueprintNode::Composite(c) => { lowerings.push(inline_composite(c, params, cursor, flat_nodes, flat_edges)?); } } } Ok(lowerings) } ``` `inline_composite` (`blueprint.rs:278-339`) gains the same `params: &[Scalar]` + `cursor: &mut usize` params and forwards them on its recursive `lower_items` call (`blueprint.rs:295`): `let interior = lower_items(nodes, params, cursor, flat_nodes, flat_edges)?;`. Its signature line becomes: ```rust fn inline_composite( c: Composite, params: &[Scalar], cursor: &mut usize, flat_nodes: &mut Vec>, flat_edges: &mut Vec, ) -> Result { ``` - [ ] **Step 6: `compile_with_params` + `bootstrap_with_params` + thin no-param wrappers** Replace `compile` (`blueprint.rs:179-204`) and `bootstrap` (`207-210`) with the param-driven path plus no-param wrappers. The arity is checked up front via `param_space().len()` so the per-leaf slices never overrun (only kind can fail): ```rust /// Compile the value-empty recipe under an injected param vector: build each /// leaf from its kind-checked slice while lowering, then rewrite edges/sources /// exactly as before (structure is param-invariant, C19/C23). The vector is /// total and positional — one value per `param_space()` slot, in slot order. #[allow(clippy::type_complexity)] pub fn compile_with_params( self, params: &[Scalar], ) -> Result<(Vec>, Vec, Vec), CompileError> { let expected = self.param_space().len(); if params.len() != expected { return Err(CompileError::ParamArity { expected, got: params.len() }); } let mut flat_nodes: Vec> = Vec::new(); let mut flat_edges: Vec = Vec::new(); let mut cursor = 0usize; let lowerings = lower_items(self.nodes, params, &mut cursor, &mut flat_nodes, &mut flat_edges)?; for e in &self.edges { for fe in rewrite_edge(e, &lowerings, &flat_nodes)? { flat_edges.push(fe); } } let mut flat_sources: Vec = Vec::with_capacity(self.sources.len()); for src in &self.sources { let mut targets: Vec = Vec::new(); for t in &src.targets { targets.extend(resolve_target(t, &lowerings)?); } flat_sources.push(SourceSpec { kind: src.kind, targets }); } Ok((flat_nodes, flat_sources, flat_edges)) } /// No-param compile (a blueprint that declares no params); errors `ParamArity` /// if any param is declared. #[allow(clippy::type_complexity)] pub fn compile(self) -> Result<(Vec>, Vec, Vec), CompileError> { self.compile_with_params(&[]) } /// Compile under an injected vector, then hand the flat compilat to the /// unchanged `Harness::bootstrap`. pub fn bootstrap_with_params(self, params: Vec) -> Result { let (nodes, sources, edges) = self.compile_with_params(¶ms)?; Harness::bootstrap(nodes, sources, edges).map_err(CompileError::Bootstrap) } /// No-param bootstrap (paramless blueprint). pub fn bootstrap(self) -> Result { self.bootstrap_with_params(vec![]) } ``` (Keep the existing `#[allow(clippy::type_complexity)]` + comment that sat above `compile`.) - [ ] **Step 7: Re-express the fixtures (mechanical)** Every blueprint-leaf author site in the test module changes by the rules below; the hand-wired `hand_wired_sma_cross_harness` (which builds nodes directly into `Harness::bootstrap`, not via `BlueprintNode`) is **unchanged**. - `Sma::new(k).into()` → `Sma::factory().into()`; the `k` moves into the caller's bootstrap/compile vector (`Scalar::I64(k)`). - `Exposure::new(s).into()` → `Exposure::factory().into()`; `s` → `Scalar::F64(s)`. - `SimBroker::new(p).into()` → `SimBroker::factory(p).into()` (pip captured, no vector slot). - `Recorder::new(&[K..], f, tx).into()` → `Recorder::factory(vec![K..], f, tx).into()`. - `LinComb::new(w).into()` → `LinComb::factory(w.len()).into()`; the weights → `w.iter().map(|x| Scalar::F64(*x))` in the caller's vector. - `sma_cross(fast, slow)` builder (`blueprint.rs:721-732`) → `sma_cross()` taking no args, building two `Sma::factory()` leaves; callers move `fast`/`slow` into their vector. - A test-local node `Leaf(Box::new(X))` (sites `440`, `535`, `576`, `618`, `648`) → an inline factory: `BlueprintNode::Leaf(LeafFactory::new("X", vec![], |_| Box::new(X::new())))` (or `X::new().into()` if that test node is given a `factory()`; inline is simpler for one-off test nodes). - `bp.bootstrap()` → `bp.bootstrap_with_params(vec![..])` with the vector matching the leaves' declared params in `param_space()` order; `bp.compile()` → `bp.compile_with_params(&[..])`. For a paramless fixture, `bootstrap_with_params( vec![])` / `compile_with_params(&[])` (or the thin `bootstrap()`/`compile()`). `composite_sma_cross_harness` (`blueprint.rs:736-766`) becomes value-empty: `Composite(sma_cross())`, `Exposure::factory().into()`, `SimBroker::factory(0.0001) .into()`, `Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx_eq).into()`, etc. Its declared `param_space()` is `[length:I64, length:I64, scale:F64]`, so its point vector is `vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]`. - [ ] **Step 8: Re-express the load-bearing tests** - `composite_sma_cross_runs_bit_identical_to_hand_wired` (`blueprint.rs:768-792`): build the composite via `bp.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])`; the hand-wired side stays `Sma::new(2)`, `Sma::new(4)`, `Exposure::new(0.5)`. Assertions unchanged (traces bit-identical). - `param_space_mirrors_compiled_flat_node_param_order` (`802-834`) and `..._under_nesting` (`886-936`): call `bp.compile_with_params(&[..])` with the matching vector instead of `bp.compile()`; the `flat_nodes.iter().flat_map(|n| n.schema().params)` projection and the kind-by-slot assertions are unchanged (built nodes still carry `schema().params`). For the single-level case the vector is `[Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]`; for the nested `strategy → { fast_slow → [Sma, Sma, Sub], LinComb }` case it is `[Scalar::I64(2), Scalar::I64(4), Scalar::F64(1.0), Scalar::F64(-1.0)]`. - `param_space_is_flat_path_qualified_and_slot_disambiguated` (`837`), `top_level_leaf_params_are_unqualified` (`876`), `param_space_is_deterministic` (`885`), `param_space_empty_for_paramless_and_empty_blueprints` (`896`): rebuild their blueprints with factory leaves; the `param_space()` assertions are unchanged. - [ ] **Step 9: New injection tests** Add to the test module: ```rust #[test] fn injecting_a_different_vector_changes_the_run() { let prices = synthetic_prices(); let (bp, eq, _ex) = composite_sma_cross_harness(); let mut a = bp.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]) .expect("compiles"); a.run(vec![prices.clone()]); let a_eq = eq.try_iter().collect::>(); let (bp2, eq2, _ex2) = composite_sma_cross_harness(); let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(5), Scalar::I64(20), Scalar::F64(1.0)]) .expect("compiles"); b.run(vec![prices]); let b_eq = eq2.try_iter().collect::>(); assert!(!a_eq.is_empty() && !b_eq.is_empty(), "both traces populated"); assert_ne!(a_eq, b_eq, "a different vector must yield a different run"); } #[test] fn wrong_kind_is_a_param_kind_mismatch() { let (bp, _eq, _ex) = composite_sma_cross_harness(); // slot 0 is I64 (an SMA length); inject F64 there let err = bp.bootstrap_with_params(vec![Scalar::F64(2.0), Scalar::I64(4), Scalar::F64(0.5)]) .unwrap_err(); assert!(matches!(err, CompileError::ParamKindMismatch { slot: 0, .. })); } #[test] fn wrong_arity_is_a_param_arity_error() { let (short, _e1, _x1) = composite_sma_cross_harness(); assert!(matches!( short.bootstrap_with_params(vec![Scalar::I64(2)]).unwrap_err(), CompileError::ParamArity { expected: 3, got: 1 } )); let (long, _e2, _x2) = composite_sma_cross_harness(); assert!(matches!( long.bootstrap_with_params( vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5), Scalar::F64(0.0)] ).unwrap_err(), CompileError::ParamArity { expected: 3, got: 4 } )); } #[test] fn same_vector_bootstraps_identically() { let prices = synthetic_prices(); let (bp, eq, _ex) = composite_sma_cross_harness(); let mut a = bp.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)]) .expect("compiles"); a.run(vec![prices.clone()]); let (bp2, eq2, _ex2) = composite_sma_cross_harness(); let mut b = bp2.bootstrap_with_params(vec![Scalar::I64(3), Scalar::I64(9), Scalar::F64(0.7)]) .expect("compiles"); b.run(vec![prices]); assert_eq!(eq.try_iter().collect::>(), eq2.try_iter().collect::>()); } ``` (If `composite_sma_cross_harness` returns a fresh blueprint+receivers per call, each test calls it anew as shown; keep its existing return signature.) - [ ] **Step 10: Gate** Run: `cargo build -p aura-engine --all-targets` Expected: 0 errors (every fixture site repaired). Run: `cargo test -p aura-engine` Expected: PASS — bit-identity, both mirror tests, the four new injection tests, and all `param_space*` tests green. --- ## Task 4: aura-cli — param-generic render + sample + goldens **Files:** Modify `crates/aura-cli/src/graph.rs`, `crates/aura-cli/src/main.rs`. - [ ] **Step 1: `render_blueprint` reads `LeafFactory::label()` (`graph.rs`)** The two leaf arms (`graph.rs:60-63` top-level, `:69-72` composite-interior) push `node.label()`. The leaf is now a `LeafFactory`; push `factory.label()`: ```rust BlueprintNode::Leaf(factory) => { let id = labels.len(); labels.push(factory.label()); item_display.push(ItemDisplay::Leaf(id)); } ``` and inside the composite loop: ```rust BlueprintNode::Leaf(factory) => { let id = labels.len(); labels.push(factory.label()); interior_ids.push(id); } ``` (`render_compilat` / the compiled-view renderer operates on built flat nodes via `Node::label()` and is unchanged.) - [ ] **Step 2: Sample blueprint → factories (`main.rs`)** - `sma_cross(name, fast, slow)` (`main.rs:120-131`): build two `Sma::factory()` leaves; drop `fast`/`slow` from the builder (they move to the injected vector). Keep `name` for the composite. - `build_sample(fast, slow)` (`main.rs:136-161`): the four `.into()` lifts become `Exposure::factory().into()`, `SimBroker::factory(0.0001).into()`, `Recorder::factory(...).into()` per their constructors; the composite is `BlueprintNode::Composite(sma_cross(name))`. `build_sample` no longer bakes `fast`/`slow`. - `sample_blueprint` (`main.rs:164-166`) and the `bp.compile()` sites (`main.rs:180,221,272`): supply the point vector. The sample's `param_space()` is `[length:I64, length:I64, scale:F64]`, so its vector is `vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]`. Use `compile_with_params(&[..])` / `bootstrap_with_params(vec![..])` at those sites. - [ ] **Step 3: Move the swap to the compiled view (`main.rs`)** `sample_blueprint_swapped` (`main.rs:201-203`) + `swapped_sma_inputs_render_differently` (`main.rs:230`): the blueprint view is now param-generic and identical for both orderings, so the swap is not observable there. Re-express the swap as a different injected vector (`vec![Scalar::I64(4), Scalar::I64(2), Scalar::F64(0.5)]`) and assert the **compiled** view differs (`render_compilat` of the compiled flat nodes shows `SMA(4)`/`SMA(2)` swapped), not the blueprint view. Rename the test to its new premise (e.g. `swapped_param_vector_changes_the_compiled_render`). - [ ] **Step 4: Re-capture the blueprint-view goldens (`main.rs`)** `blueprint_view_shows_cluster_and_param_labels` (`main.rs:206`), `blueprint_view_golden` (`:238`): the blueprint-view labels become param-generic — `SMA(length)` (both SMAs identical), `Exposure(scale)`, `SimBroker`. Update the pinned ASCII strings to the param-generic form. The compiled-view goldens `compiled_view_dissolves_the_composite_boundary` (`:219`) and `compiled_view_golden` (`:270`) stay valued (`SMA(2)`, `SMA(4)`, `Exposure(0.5)`, `SimBroker(0.0001)`) — do not change them. To get the exact new blueprint-view bytes, run the rendering in a scratch assertion or `cargo run -- graph` (blueprint view) after Steps 1-2 compile, and paste the produced ASCII verbatim into the golden. Do not hand-guess box-drawing columns. - [ ] **Step 5: Gate** Run: `cargo build -p aura-cli --all-targets` Expected: 0 errors. Run: `cargo test --workspace` Expected: PASS — all crates green, including the re-captured goldens and the re-premised swap test. Run: `cargo clippy --workspace --all-targets -- -D warnings` Expected: clean.