From 50105c195750de7cc30cb5880903f0f10d2b48bf Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 14 Jun 2026 03:28:32 +0200 Subject: [PATCH] refactor(aura-cli): author the sample blueprints via GraphBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/aura-cli/src/main.rs | 203 +++++++++++++----------------------- 1 file changed, 74 insertions(+), 129 deletions(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index e3ecc39..098ef07 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -10,8 +10,8 @@ mod render; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{ - f64_field, summarize, BlueprintNode, Composite, Edge, FlatGraph, Harness, - OutField, Role, RunManifest, RunReport, SourceSpec, SweepFamily, Target, + f64_field, summarize, Composite, Edge, FlatGraph, GraphBuilder, Harness, + RunManifest, RunReport, SourceSpec, SweepFamily, Target, }; use aura_registry::{rank_by, Registry}; 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 /// lengths are injected at compile, not baked here. fn sma_cross(name: &str) -> Composite { - Composite::new( - name, - vec![ - Sma::builder().named("fast").into(), // fast SMA leg - Sma::builder().named("slow").into(), // slow SMA leg - Sub::builder().into(), - ], - vec![ - Edge { from: 0, to: 2, slot: 0, from_field: 0 }, - Edge { from: 1, to: 2, slot: 1, from_field: 0 }, - ], - 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() }], - ) + let mut g = GraphBuilder::new(name); + let fast = g.add(Sma::builder().named("fast")); // fast SMA leg + let slow = g.add(Sma::builder().named("slow")); // slow SMA leg + let sub = g.add(Sub::builder()); + let price = g.input_role("price"); + g.feed(price, [fast.in_("series"), slow.in_("series")]); + g.connect(fast.out("value"), sub.in_("lhs")); + g.connect(slow.out("value"), sub.in_("rhs")); + g.expose(sub.out("value"), "cross"); + g.build().expect("sample sma_cross wiring resolves") } /// 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 /// bound as a structural constant (so it drops out of the sweepable surface). fn signals(name: &str) -> Composite { - Composite::new( - name, - vec![ - BlueprintNode::Composite(sma_cross("trend")), // 0 trend leg (one f64 "cross") - BlueprintNode::Composite(macd("momentum")), // 1 momentum leg (3 outputs) - // 2 blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal]. - // weights[2] is bound (a fixed signal-line weight) → removed from param_space; - // weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.* - // (and renders the `blend:` prefix). - LinComb::builder(3) - .named("blend") - .bind("weights[2]", Scalar::F64(0.5)) - .into(), - ], - vec![ - Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // trend.cross → blend.term[0] - Edge { from: 1, to: 2, slot: 1, from_field: 2 }, // momentum.histogram → blend.term[1] - Edge { from: 1, to: 2, slot: 2, from_field: 1 }, // momentum.signal → blend.term[2] - ], - 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() }], - ) + let mut g = GraphBuilder::new(name); + let trend = g.add(sma_cross("trend")); // trend leg (one f64 "cross") + let momentum = g.add(macd("momentum")); // momentum leg (3 outputs) + // blend: Σ wᵢ·termᵢ over [trend.cross, momentum.histogram, momentum.signal]. + // weights[2] is bound (a fixed signal-line weight) → removed from param_space; + // weights[0]/[1] stay tunable. `.named("blend")` makes the path signals.blend.* + // (and renders the `blend:` prefix). + let blend = g.add( + LinComb::builder(3) + .named("blend") + .bind("weights[2]", Scalar::F64(0.5)), + ); + let price = g.input_role("price"); + g.feed(price, [trend.in_("price"), momentum.in_("price")]); + g.connect(trend.out("cross"), blend.in_("term[0]")); // trend.cross → blend.term[0] + g.connect(momentum.out("histogram"), blend.in_("term[1]")); // momentum.histogram → blend.term[1] + g.connect(momentum.out("signal"), blend.in_("term[2]")); // momentum.signal → blend.term[2] + g.expose(blend.out("value"), "signal"); + g.build().expect("sample signals wiring resolves") } /// 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_ex, rx_ex) = mpsc::channel(); - let bp = Composite::new( - "sample", - vec![ - BlueprintNode::Composite(signals("signals")), - Exposure::builder().into(), - SimBroker::builder(0.0001).into(), - Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(), - Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(), - ], - vec![ - Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // spread -> Exposure - Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker slot 0 - Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity -> sink - 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 - ); + let mut g = GraphBuilder::new("sample"); + let sig = g.add(signals("signals")); + let exposure = g.add(Exposure::builder()); + let broker = g.add(SimBroker::builder(0.0001)); + let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq)); + let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex)); + let price = g.source_role("price", ScalarKind::F64); + g.feed(price, [sig.in_("price"), broker.in_("price")]); + g.connect(sig.out("signal"), exposure.in_("signal")); // blended signal -> Exposure + g.connect(exposure.out("exposure"), broker.in_("exposure")); // exposure -> broker slot 0 + g.connect(broker.out("equity"), eq.in_("col[0]")); // equity -> sink + g.connect(exposure.out("exposure"), ex.in_("col[0]")); // exposure -> sink + let bp = g.build().expect("sample blueprint wiring resolves"); (bp, rx_eq, rx_ex) } @@ -394,36 +364,23 @@ fn load_runs_or_exit() -> Vec { /// histogram). Three `length` knobs (fast, slow, signal) are injected at compile /// in node order; value-empty here. fn macd(name: &str) -> Composite { - Composite::new( - name, - vec![ - Ema::builder().named("fast").into(), // 0 fast EMA - Ema::builder().named("slow").into(), // 1 slow EMA - Sub::builder().into(), // 2 MACD line = fast − slow - Ema::builder().named("signal").into(), // 3 signal EMA of the MACD line - Sub::builder().into(), // 4 histogram = MACD line − signal - ], - vec![ - Edge { from: 0, to: 2, slot: 0, from_field: 0 }, // fast → line[0] - Edge { from: 1, to: 2, slot: 1, from_field: 0 }, // slow → line[1] - Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // line → signal EMA - Edge { from: 2, to: 4, slot: 0, from_field: 0 }, // line → histogram[0] - Edge { from: 3, to: 4, slot: 1, from_field: 0 }, // signal → histogram[1] - ], - vec![Role { - 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 - ], - ) + let mut g = GraphBuilder::new(name); + let fast = g.add(Ema::builder().named("fast")); // fast EMA + let slow = g.add(Ema::builder().named("slow")); // slow EMA + let line = g.add(Sub::builder()); // MACD line = fast − slow + let signal = g.add(Ema::builder().named("signal")); // signal EMA of the MACD line + let hist = g.add(Sub::builder()); // histogram = MACD line − signal + let price = g.input_role("price"); + g.feed(price, [fast.in_("series"), slow.in_("series")]); + g.connect(fast.out("value"), line.in_("lhs")); // fast → line + g.connect(slow.out("value"), line.in_("rhs")); // slow → line + g.connect(line.out("value"), signal.in_("series")); // line → signal EMA + g.connect(line.out("value"), hist.in_("lhs")); // line → histogram + g.connect(signal.out("value"), hist.in_("rhs")); // signal → histogram + g.expose(line.out("value"), "macd"); // the MACD line + g.expose(signal.out("value"), "signal"); // the signal line + g.expose(hist.out("value"), "histogram"); // the histogram + g.build().expect("sample macd wiring resolves") } /// The MACD strategy blueprint (value-empty): the `macd` histogram → `Exposure` → @@ -433,31 +390,19 @@ fn macd_strategy_blueprint( tx_eq: mpsc::Sender<(Timestamp, Vec)>, tx_ex: mpsc::Sender<(Timestamp, Vec)>, ) -> Composite { - Composite::new( - "macd_strategy", - vec![ - BlueprintNode::Composite(macd("macd")), - Exposure::builder().into(), - SimBroker::builder(0.0001).into(), - Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq).into(), - Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex).into(), - ], - vec![ - Edge { from: 0, to: 1, slot: 0, from_field: 2 }, // histogram → Exposure - Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure → broker slot 0 - Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity → sink - 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 - ) + let mut g = GraphBuilder::new("macd_strategy"); + let macd_node = g.add(macd("macd")); + let exposure = g.add(Exposure::builder()); + let broker = g.add(SimBroker::builder(0.0001)); + let eq = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_eq)); + let ex = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_ex)); + let price = g.source_role("price", ScalarKind::F64); + g.feed(price, [macd_node.in_("price"), broker.in_("price")]); + g.connect(macd_node.out("histogram"), exposure.in_("signal")); // histogram → Exposure + g.connect(exposure.out("exposure"), broker.in_("exposure")); // exposure → broker slot 0 + g.connect(broker.out("equity"), eq.in_("col[0]")); // equity → sink + g.connect(exposure.out("exposure"), ex.in_("col[0]")); // exposure → sink + g.build().expect("macd_strategy wiring resolves") } /// The MACD strategy blueprint as a param-space fixture (receivers dropped, since