"Compilat" (German "Kompilat") was a coined noun for the product of the
bootstrap compilation — neither English nor a natural fit. The runtime
artifact already has a code identifier for exactly this thing: the
`FlatGraph` struct (harness.rs). Replace the coinage with that identifier:
- prose mentions -> "flat graph" (mirrors the type, reads plainly)
- definitional anchors -> `FlatGraph` (C11, C23, the running-graph line)
- `render_compilat` -> `render_flat_graph` (historical render symbol;
keeps the `render_blueprint` / `render_flat_graph`
source-vs-product pairing)
- `intra-compilat` -> `intra-graph`
- `from_compilat` (test) -> `from_flat`
"compilation" / "re-compilation" / "bootstrap-as-compilation" (the process,
ordinary English) are deliberately left untouched. Behaviour-preserving:
only comments, design ledger, specs/plans, one test-local variable and its
assert messages change. Full workspace test suite green; clippy clean.
27 KiB
Param-set injection — Implementation Plan
Parent spec:
docs/specs/0016-param-set-injection.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill 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— addLeafFactory. - Modify:
crates/aura-core/src/scalar.rs— addas_i64/as_f64. - Modify:
crates/aura-core/src/lib.rs— re-exportLeafFactory. - Modify:
crates/aura-std/src/{sma,exposure,lincomb,sub,add,sim_broker,recorder}.rs— each gainsfn factory(...)+ a factory↔schema params test. - Modify:
crates/aura-engine/src/blueprint.rs—Leaf(LeafFactory),From<LeafFactory>,collect_params,compile_with_params,bootstrap_with_params,CompileErrorvariants, remove vestigialschemamethods + their test, re-express fixtures + tests. - Modify:
crates/aura-cli/src/graph.rs—render_blueprintusesLeafFactory::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
LeafFactorytonode.rs
After the ParamSpec struct (ends node.rs:58) and before the NodeSchema doc,
add (the use at node.rs:11 already imports Scalar):
/// 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<ParamSpec>,
build: Box<dyn Fn(&[Scalar]) -> Box<dyn Node>>,
}
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<ParamSpec>,
build: impl Fn(&[Scalar]) -> Box<dyn Node> + '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<dyn Node> {
(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:
/// The `i64` payload, or `None` if this scalar is not an `I64`.
pub fn as_i64(self) -> Option<i64> {
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<f64> {
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.rsandscalar.rs
In node.rs tests (reuse the Bare node already defined in that module, node.rs
test mod), add:
#[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::<ParamSpec>::new());
}
In scalar.rs tests, add:
#[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 <Node> block
(beside new). Add LeafFactory to each file's use aura_core::{...} line.
- Step 1:
Sma::factory(sma.rs)
/// 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)
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.
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)
// 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.
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).
pub fn factory(
kinds: Vec<ScalarKind>,
firing: Firing,
tx: Sender<(Timestamp, Vec<Scalar>)>,
) -> 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:
#[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<N>
(blueprint.rs:33-37):
pub enum BlueprintNode {
Leaf(LeafFactory),
Composite(Composite),
}
impl From<LeafFactory> 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
schemamethods
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_paramsreadsfactory.params()
In collect_params (blueprint.rs:217-240), the Leaf arm (220-229):
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
CompileErrorvariants
In enum CompileError (blueprint.rs:120-130) add:
/// 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:
fn lower_items(
items: Vec<BlueprintNode>,
params: &[Scalar],
cursor: &mut usize,
flat_nodes: &mut Vec<Box<dyn Node>>,
flat_edges: &mut Vec<Edge>,
) -> Result<Vec<ItemLowering>, 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:
fn inline_composite(
c: Composite,
params: &[Scalar],
cursor: &mut usize,
flat_nodes: &mut Vec<Box<dyn Node>>,
flat_edges: &mut Vec<Edge>,
) -> Result<ItemLowering, CompileError> {
- 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):
/// 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<Box<dyn Node>>, Vec<SourceSpec>, Vec<Edge>), CompileError> {
let expected = self.param_space().len();
if params.len() != expected {
return Err(CompileError::ParamArity { expected, got: params.len() });
}
let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
let mut flat_edges: Vec<Edge> = 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<SourceSpec> = Vec::with_capacity(self.sources.len());
for src in &self.sources {
let mut targets: Vec<Target> = 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<Box<dyn Node>>, Vec<SourceSpec>, Vec<Edge>), CompileError> {
self.compile_with_params(&[])
}
/// Compile under an injected vector, then hand the flat graph to the
/// unchanged `Harness::bootstrap`.
pub fn bootstrap_with_params(self, params: Vec<Scalar>) -> Result<Harness, CompileError> {
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<Harness, CompileError> {
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(); thekmoves 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 twoSma::factory()leaves; callers movefast/slowinto their vector.- A test-local node
Leaf(Box::new(X))(sites440,535,576,618,648) → an inline factory:BlueprintNode::Leaf(LeafFactory::new("X", vec![], |_| Box::new(X::new())))(orX::new().into()if that test node is given afactory(); inline is simpler for one-off test nodes). bp.bootstrap()→bp.bootstrap_with_params(vec![..])with the vector matching the leaves' declared params inparam_space()order;bp.compile()→bp.compile_with_params(&[..]). For a paramless fixture,bootstrap_with_params( vec![])/compile_with_params(&[])(or the thinbootstrap()/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 viabp.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]); the hand-wired side staysSma::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): callbp.compile_with_params(&[..])with the matching vector instead ofbp.compile(); theflat_nodes.iter().flat_map(|n| n.schema().params)projection and the kind-by-slot assertions are unchanged (built nodes still carryschema().params). For the single-level case the vector is[Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]; for the nestedstrategy → { 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; theparam_space()assertions are unchanged. -
Step 9: New injection tests
Add to the test module:
#[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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>(), eq2.try_iter().collect::<Vec<_>>());
}
(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_blueprintreadsLeafFactory::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():
BlueprintNode::Leaf(factory) => {
let id = labels.len();
labels.push(factory.label());
item_display.push(ItemDisplay::Leaf(id));
}
and inside the composite loop:
BlueprintNode::Leaf(factory) => {
let id = labels.len();
labels.push(factory.label());
interior_ids.push(id);
}
(render_flat_graph / 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 twoSma::factory()leaves; dropfast/slowfrom the builder (they move to the injected vector). Keepnamefor the composite. -
build_sample(fast, slow)(main.rs:136-161): the four.into()lifts becomeExposure::factory().into(),SimBroker::factory(0.0001).into(),Recorder::factory(...).into()per their constructors; the composite isBlueprintNode::Composite(sma_cross(name)).build_sampleno longer bakesfast/slow. -
sample_blueprint(main.rs:164-166) and thebp.compile()sites (main.rs:180,221,272): supply the point vector. The sample'sparam_space()is[length:I64, length:I64, scale:F64], so its vector isvec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)]. Usecompile_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_flat_graph 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.