refactor(aura-cli): author the sample blueprints via GraphBuilder

Adopt the name-based GraphBuilder (cycle 0039) in the CLI sample blueprints,
which previously hand-wired raw positional Edge/Role/OutField via
Composite::new. sma_cross, macd, signals, sample_blueprint_with_sinks, and
macd_strategy_blueprint now read as typed-handle authoring: nodes are added
for NodeHandles, ports/fields are wired by name (series/lhs/rhs/value,
exposure/price, term[i], col[0], the macd histogram/signal/macd outputs), and
build() lowers to the same index-wired Composite. This is what #64 was for —
the builder was previously exercised only in its own tests; the showcase
sample now dogfoods it.

Behaviour-preserving: node order, instance names (.named), bound params
(blend.weights[2]), and wiring are byte-identical, so param_space, the graph
render, the single run, and the sweep are unchanged — guarded green by the
existing aura-cli sample/sweep/param_space/render tests and the engine suite
(full workspace 0 failed; clippy --all-targets -D warnings clean).

sample_harness is deliberately left as a direct FlatGraph construction (baked
Sma::new(2) nodes, the single-run path) — it builds the compilat directly, not
a value-empty Composite, so it is not a GraphBuilder target.
This commit is contained in:
2026-06-14 03:28:32 +02:00
parent 7865030d33
commit 50105c1957
+74 -129
View File
@@ -10,8 +10,8 @@ mod render;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{ use aura_engine::{
f64_field, summarize, BlueprintNode, Composite, Edge, FlatGraph, Harness, f64_field, summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness,
OutField, Role, RunManifest, RunReport, SourceSpec, SweepFamily, Target, RunManifest, RunReport, SourceSpec, SweepFamily, Target,
}; };
use aura_registry::{rank_by, Registry}; use aura_registry::{rank_by, Registry};
use aura_std::{Ema, Exposure, LinComb, Recorder, SimBroker, Sma, Sub}; use aura_std::{Ema, Exposure, LinComb, Recorder, SimBroker, Sma, Sub};
@@ -151,24 +151,16 @@ fn run_sample() -> RunReport {
/// `blueprint.rs`'s test helper is the dedup tracked in #14). Value-empty: the SMA /// `blueprint.rs`'s test helper is the dedup tracked in #14). Value-empty: the SMA
/// lengths are injected at compile, not baked here. /// lengths are injected at compile, not baked here.
fn sma_cross(name: &str) -> Composite { fn sma_cross(name: &str) -> Composite {
Composite::new( let mut g = GraphBuilder::new(name);
name, let fast = g.add(Sma::builder().named("fast")); // fast SMA leg
vec![ let slow = g.add(Sma::builder().named("slow")); // slow SMA leg
Sma::builder().named("fast").into(), // fast SMA leg let sub = g.add(Sub::builder());
Sma::builder().named("slow").into(), // slow SMA leg let price = g.input_role("price");
Sub::builder().into(), g.feed(price, [fast.in_("series"), slow.in_("series")]);
], g.connect(fast.out("value"), sub.in_("lhs"));
vec![ g.connect(slow.out("value"), sub.in_("rhs"));
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, g.expose(sub.out("value"), "cross");
Edge { from: 1, to: 2, slot: 1, from_field: 0 }, g.build().expect("sample sma_cross wiring resolves")
],
vec![Role {
name: "price".into(),
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
source: None,
}],
vec![OutField { node: 2, field: 0, name: "cross".into() }],
)
} }
/// The blended signal: a trend leg (SMA-cross) and a momentum leg (MACD), combined /// The blended signal: a trend leg (SMA-cross) and a momentum leg (MACD), combined
@@ -176,35 +168,25 @@ fn sma_cross(name: &str) -> Composite {
/// momentum}); the blend is a multi-param node living inside it, with one weight /// momentum}); the blend is a multi-param node living inside it, with one weight
/// bound as a structural constant (so it drops out of the sweepable surface). /// bound as a structural constant (so it drops out of the sweepable surface).
fn signals(name: &str) -> Composite { fn signals(name: &str) -> Composite {
Composite::new( let mut g = GraphBuilder::new(name);
name, let trend = g.add(sma_cross("trend")); // trend leg (one f64 "cross")
vec![ let momentum = g.add(macd("momentum")); // momentum leg (3 outputs)
BlueprintNode::Composite(sma_cross("trend")), // 0 trend leg (one f64 "cross") // blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal].
BlueprintNode::Composite(macd("momentum")), // 1 momentum leg (3 outputs) // weights[2] is bound (a fixed signal-line weight) → removed from param_space;
// 2 blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal]. // weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.*
// weights[2] is bound (a fixed signal-line weight) → removed from param_space; // (and renders the `blend:` prefix).
// weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.* let blend = g.add(
// (and renders the `blend:` prefix). LinComb::builder(3)
LinComb::builder(3) .named("blend")
.named("blend") .bind("weights[2]", Scalar::F64(0.5)),
.bind("weights[2]", Scalar::F64(0.5)) );
.into(), let price = g.input_role("price");
], g.feed(price, [trend.in_("price"), momentum.in_("price")]);
vec![ g.connect(trend.out("cross"), blend.in_("term[0]")); // trend.cross → blend.term[0]
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // trend.cross → blend.term[0] g.connect(momentum.out("histogram"), blend.in_("term[1]")); // momentum.histogram → blend.term[1]
Edge { from: 1, to: 2, slot: 1, from_field: 2 }, // momentum.histogram → blend.term[1] g.connect(momentum.out("signal"), blend.in_("term[2]")); // momentum.signal → blend.term[2]
Edge { from: 1, to: 2, slot: 2, from_field: 1 }, // momentum.signal → blend.term[2] g.expose(blend.out("value"), "signal");
], g.build().expect("sample signals wiring resolves")
vec![Role {
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 }, // price → trend role 0
Target { node: 1, slot: 0 }, // price → momentum role 0
],
source: None,
}],
vec![OutField { node: 2, field: 0, name: "signal".into() }],
)
} }
/// The sample signal-quality blueprint (value-empty) **with its two recording /// The sample signal-quality blueprint (value-empty) **with its two recording
@@ -222,31 +204,19 @@ fn sample_blueprint_with_sinks() -> (
) { ) {
let (tx_eq, rx_eq) = mpsc::channel(); let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel();
let bp = Composite::new( let mut g = GraphBuilder::new("sample");
"sample", let sig = g.add(signals("signals"));
vec![ let exposure = g.add(Exposure::builder());
BlueprintNode::Composite(signals("signals")), let broker = g.add(SimBroker::builder(0.0001));
Exposure::builder().into(), let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
SimBroker::builder(0.0001).into(), let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(), let price = g.source_role("price", ScalarKind::F64);
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(), g.feed(price, [sig.in_("price"), broker.in_("price")]);
], g.connect(sig.out("signal"), exposure.in_("signal")); // blended signal -> Exposure
vec![ g.connect(exposure.out("exposure"), broker.in_("exposure")); // exposure -> broker slot 0
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // spread -> Exposure g.connect(broker.out("equity"), eq.in_("col[0]")); // equity -> sink
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker slot 0 g.connect(exposure.out("exposure"), ex.in_("col[0]")); // exposure -> sink
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity -> sink let bp = g.build().expect("sample blueprint wiring resolves");
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure -> sink
],
vec![Role {
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 }, // price -> sma_cross role 0
Target { node: 2, slot: 1 }, // price -> SimBroker price slot
],
source: Some(ScalarKind::F64),
}],
vec![], // output: the root ends in sinks, no re-export
);
(bp, rx_eq, rx_ex) (bp, rx_eq, rx_ex)
} }
@@ -394,36 +364,23 @@ fn load_runs_or_exit() -> Vec<RunReport> {
/// histogram). Three `length` knobs (fast, slow, signal) are injected at compile /// histogram). Three `length` knobs (fast, slow, signal) are injected at compile
/// in node order; value-empty here. /// in node order; value-empty here.
fn macd(name: &str) -> Composite { fn macd(name: &str) -> Composite {
Composite::new( let mut g = GraphBuilder::new(name);
name, let fast = g.add(Ema::builder().named("fast")); // fast EMA
vec![ let slow = g.add(Ema::builder().named("slow")); // slow EMA
Ema::builder().named("fast").into(), // 0 fast EMA let line = g.add(Sub::builder()); // MACD line = fast slow
Ema::builder().named("slow").into(), // 1 slow EMA let signal = g.add(Ema::builder().named("signal")); // signal EMA of the MACD line
Sub::builder().into(), // 2 MACD line = fast slow let hist = g.add(Sub::builder()); // histogram = MACD line signal
Ema::builder().named("signal").into(), // 3 signal EMA of the MACD line let price = g.input_role("price");
Sub::builder().into(), // 4 histogram = MACD line signal g.feed(price, [fast.in_("series"), slow.in_("series")]);
], g.connect(fast.out("value"), line.in_("lhs")); // fast → line
vec![ g.connect(slow.out("value"), line.in_("rhs")); // slow → line
Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast → line[0] g.connect(line.out("value"), signal.in_("series")); // line → signal EMA
Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // slow → line[1] g.connect(line.out("value"), hist.in_("lhs")); // line → histogram
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // line → signal EMA g.connect(signal.out("value"), hist.in_("rhs")); // signal → histogram
Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // line → histogram[0] g.expose(line.out("value"), "macd"); // the MACD line
Edge { from: 3, to: 4, slot: 1, from_field: 0 }, // signal → histogram[1] g.expose(signal.out("value"), "signal"); // the signal line
], g.expose(hist.out("value"), "histogram"); // the histogram
vec![Role { g.build().expect("sample macd wiring resolves")
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 }, // price → fast EMA
Target { node: 1, slot: 0 }, // price → slow EMA
],
source: None,
}],
vec![
OutField { node: 2, field: 0, name: "macd".into() }, // the MACD line
OutField { node: 3, field: 0, name: "signal".into() }, // the signal line
OutField { node: 4, field: 0, name: "histogram".into() }, // the histogram
],
)
} }
/// The MACD strategy blueprint (value-empty): the `macd` histogram → `Exposure` → /// The MACD strategy blueprint (value-empty): the `macd` histogram → `Exposure` →
@@ -433,31 +390,19 @@ fn macd_strategy_blueprint(
tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>, tx_eq: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>, tx_ex: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
) -> Composite { ) -> Composite {
Composite::new( let mut g = GraphBuilder::new("macd_strategy");
"macd_strategy", let macd_node = g.add(macd("macd"));
vec![ let exposure = g.add(Exposure::builder());
BlueprintNode::Composite(macd("macd")), let broker = g.add(SimBroker::builder(0.0001));
Exposure::builder().into(), let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq));
SimBroker::builder(0.0001).into(), let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex));
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(), let price = g.source_role("price", ScalarKind::F64);
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(), g.feed(price, [macd_node.in_("price"), broker.in_("price")]);
], g.connect(macd_node.out("histogram"), exposure.in_("signal")); // histogram → Exposure
vec![ g.connect(exposure.out("exposure"), broker.in_("exposure")); // exposure → broker slot 0
Edge { from: 0, to: 1, slot: 0, from_field: 2 }, // histogram → Exposure g.connect(broker.out("equity"), eq.in_("col[0]")); // equity → sink
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure → broker slot 0 g.connect(exposure.out("exposure"), ex.in_("col[0]")); // exposure → sink
Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity → sink g.build().expect("macd_strategy wiring resolves")
Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure → sink
],
vec![Role {
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 }, // price → macd role 0
Target { node: 2, slot: 1 }, // price → SimBroker price slot
],
source: Some(ScalarKind::F64),
}],
vec![], // output: the root ends in sinks, no re-export
)
} }
/// The MACD strategy blueprint as a param-space fixture (receivers dropped, since /// The MACD strategy blueprint as a param-space fixture (receivers dropped, since