diff --git a/crates/aura-cli/src/graph.rs b/crates/aura-cli/src/graph.rs index c21dd83..b277efa 100644 --- a/crates/aura-cli/src/graph.rs +++ b/crates/aura-cli/src/graph.rs @@ -99,8 +99,9 @@ fn collect_distinct_composites(bp: &Blueprint) -> Vec<&Composite> { } /// Render one composite's interior as a flat graph: interior leaves as `[type]`, -/// nested composites as opaque `[name]`, plus an `[in:k]` entry marker per input -/// role (wired to its interior targets) and an `[out:]` marker per +/// nested composites as opaque `[name]`, plus an `[in:]` entry marker per +/// input role (wired to its interior targets), a `[param:]` marker per param +/// alias (wired to the leaf it relabels), and an `[out:]` marker per /// re-exported output field (wired from its producer). Prefixed `":\n"`. fn render_definition(c: &Composite) -> String { let mut labels: Vec = Vec::with_capacity(c.nodes().len()); @@ -114,13 +115,18 @@ fn render_definition(c: &Composite) -> String { for e in c.edges() { edges.push((e.from, e.to)); } - for (role, targets) in c.input_roles().iter().enumerate() { + for role in c.input_roles() { let in_id = labels.len(); - labels.push(format!("in:{role}")); - for t in targets { + labels.push(format!("in:{}", role.name)); + for t in &role.targets { edges.push((in_id, t.node)); } } + for a in c.params() { + let p_id = labels.len(); + labels.push(format!("param:{}", a.name)); + edges.push((p_id, a.node)); + } for of in c.output() { let out_id = labels.len(); labels.push(format!("out:{}", of.name)); diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 6cdd8c7..bba70fc 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -11,7 +11,7 @@ mod graph; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{ f64_field, summarize, Blueprint, BlueprintNode, Composite, Edge, Harness, OutField, - RunManifest, RunReport, SourceSpec, Target, + ParamAlias, Role, RunManifest, RunReport, SourceSpec, Target, }; use aura_std::{Ema, Exposure, Recorder, SimBroker, Sma, Sub}; use std::sync::mpsc::{self, Receiver}; @@ -126,7 +126,11 @@ fn sma_cross(name: &str) -> Composite { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "cross".into() }], ) } @@ -200,10 +204,18 @@ fn macd(name: &str) -> Composite { 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![vec![ - Target { node: 0, slot: 0 }, // price → fast EMA - Target { node: 1, slot: 0 }, // price → slow EMA - ]], + vec![Role { + name: "price".into(), + targets: vec![ + Target { node: 0, slot: 0 }, // price → fast EMA + Target { node: 1, slot: 0 }, // price → slow EMA + ], + }], + vec![ + ParamAlias { name: "fast".into(), node: 0, slot: 0 }, // fast EMA length + ParamAlias { name: "slow".into(), node: 1, slot: 0 }, // slow EMA length + ParamAlias { name: "signal".into(), node: 3, slot: 0 }, // signal EMA length + ], vec![ OutField { node: 2, field: 0, name: "macd".into() }, // the MACD line OutField { node: 3, field: 0, name: "signal".into() }, // the signal line @@ -379,7 +391,7 @@ mod tests { let out = graph::render_blueprint(&sample_blueprint()); // the sma_cross body is defined exactly once, with its interior + ports assert_eq!(out.matches("sma_cross:").count(), 1, "definition not rendered once:\n{out}"); - for needle in ["[SMA]", "[Sub]", "[in:0]", "[out:cross]"] { + for needle in ["[SMA]", "[Sub]", "[in:price]", "[out:cross]"] { assert!(out.contains(needle), "missing {needle} in definition:\n{out}"); } } @@ -392,14 +404,16 @@ mod tests { "inner", vec![Sma::factory().into()], vec![], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 0, name: "out".into() }], ); let outer = Composite::new( "outer", vec![BlueprintNode::Composite(inner), Sub::factory().into()], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 1, field: 0, name: "out".into() }], ); let bp = Blueprint::new( @@ -490,7 +504,7 @@ mod tests { where: sma_cross: - [in:0] + [in:price] ┌───└───┐ ↓ ↓ [SMA] [SMA] @@ -566,6 +580,34 @@ sma_cross: assert!(out.contains("[macd]"), "missing opaque macd node:\n{out}"); assert_eq!(out.matches("macd:").count(), 1, "macd defined once:\n{out}"); assert!(out.contains("[EMA]"), "macd interior must show EMA leaves:\n{out}"); + assert!(out.contains("[in:price]"), "named MACD input role: {out}"); + assert!(out.contains("[param:fast]"), "aliased fast length: {out}"); + assert!(out.contains("[param:slow]"), "aliased slow length: {out}"); + assert!(out.contains("[param:signal]"), "aliased signal length: {out}"); + } + + /// E2E acceptance (#41 / spec 0019, the worked example): the real MACD strategy + /// blueprint's swept param surface relabels the three otherwise-indistinguishable + /// EMA `length` slots to `macd.fast` / `macd.slow` / `macd.signal` — the named + /// composite boundary visible end-to-end through `param_space()`, with the slot + /// count and order unchanged (C23 — pure naming overlay, not curation: every + /// interior slot stays sweepable, the `scale` knob is unaffected). + #[test] + fn macd_param_space_surfaces_the_three_named_aliases() { + let names: Vec = + macd_blueprint().param_space().into_iter().map(|p| p.name).collect(); + // three aliased composite slots, in declared (fast, slow, signal) order, + // then the strategy-level Exposure `scale` (outside the composite, unaliased). + assert_eq!( + names, + vec![ + "macd.fast".to_string(), + "macd.slow".to_string(), + "macd.signal".to_string(), + "scale".to_string(), + ], + "MACD param surface must expose the three named EMA lengths + scale", + ); } #[test] diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index 7e39bb9..cd4c92e 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -41,6 +41,26 @@ impl From for BlueprintNode { } } +/// One named input role: role `r` (by position) fans the source value into +/// `targets`. The `name` is a non-load-bearing render symbol (C23); identity is +/// the role index, which survives lowering — the name does not. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Role { + pub name: String, + pub targets: Vec, +} + +/// A composite-level alias relabelling one interior leaf param slot's surface +/// name in `param_space()`. `node` is the interior item index, `slot` the param +/// slot within that leaf. Pure legibility: the alias relabels in place and never +/// reorders, adds, or removes a slot (C23 — identity stays the slot). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParamAlias { + pub name: String, + pub node: usize, + pub slot: usize, +} + /// A reusable sub-graph fragment compiled away by inlining (C9/C23). It is **not** /// a [`Node`]: it is never `eval`'d. It holds interior items (local indices), /// interior edges (local indices), input roles (role `r` fans into the interior @@ -50,7 +70,8 @@ pub struct Composite { name: String, nodes: Vec, edges: Vec, - input_roles: Vec>, + input_roles: Vec, + params: Vec, output: Vec, } @@ -63,10 +84,11 @@ impl Composite { name: impl Into, nodes: Vec, edges: Vec, - input_roles: Vec>, + input_roles: Vec, + params: Vec, output: Vec, ) -> Self { - Self { name: name.into(), nodes, edges, input_roles, output } + Self { name: name.into(), nodes, edges, input_roles, params, output } } /// The authored render name (cluster title, #13). Non-load-bearing. @@ -81,10 +103,17 @@ impl Composite { pub fn edges(&self) -> &[Edge] { &self.edges } - /// The input roles: role `r` fans into `input_roles()[r]` interior targets. - pub fn input_roles(&self) -> &[Vec] { + /// The input roles: role `r` fans into `input_roles()[r].targets` interior + /// targets, under the boundary name `input_roles()[r].name` (C23 — name is a + /// render symbol, identity is the role index). + pub fn input_roles(&self) -> &[Role] { &self.input_roles } + /// The param aliases: each relabels one interior leaf param slot's surface + /// name in `param_space()` (pure naming overlay; identity stays the slot, C23). + pub fn params(&self) -> &[ParamAlias] { + &self.params + } /// The exposed output record: each entry re-exports one interior /// `(node, output-field)` under a boundary name (C8 — one port, K columns). pub fn output(&self) -> &[OutField] { @@ -148,7 +177,7 @@ impl Blueprint { /// same-type siblings in one composite share a name — uniqueness is at the slot. pub fn param_space(&self) -> Vec { let mut out = Vec::new(); - collect_params(&self.nodes, "", &mut out); + collect_params(&self.nodes, "", &[], &mut out); out } @@ -218,17 +247,32 @@ impl Blueprint { /// Recursive read-only walk for `Blueprint::param_space`: a leaf contributes its /// declared params under the running path prefix; a composite pushes its `name()` -/// onto the path and recurses. Order mirrors `lower_items` (items in declared order, -/// composites depth-first) so a param's slot matches the later flat-node order. -fn collect_params(items: &[BlueprintNode], prefix: &str, out: &mut Vec) { - for item in items { +/// onto the path and recurses, passing its own param aliases down. A leaf param +/// slot matched by an `(node, slot)` alias is relabelled in place (C23 — pure +/// naming overlay; the slot stays, order is untouched). Order mirrors `lower_items` +/// (items in declared order, composites depth-first) so a param's slot matches the +/// later flat-node order. +fn collect_params( + items: &[BlueprintNode], + prefix: &str, + aliases: &[ParamAlias], + out: &mut Vec, +) { + for (i, item) in items.iter().enumerate() { match item { BlueprintNode::Leaf(factory) => { - for p in factory.params() { + for (s, p) in factory.params().iter().enumerate() { + // an alias for this exact (node, slot) relabels in place; + // otherwise the factory param name, as today. + let local = aliases + .iter() + .find(|a| a.node == i && a.slot == s) + .map(|a| a.name.as_str()) + .unwrap_or(p.name.as_str()); let name = if prefix.is_empty() { - p.name.clone() + local.to_string() } else { - format!("{prefix}.{}", p.name) + format!("{prefix}.{local}") }; out.push(ParamSpec { name, kind: p.kind }); } @@ -239,7 +283,7 @@ fn collect_params(items: &[BlueprintNode], prefix: &str, out: &mut Vec Result { // `name` is the non-load-bearing render symbol (#13); it dissolves at inline // (C23 — the boundary does not reach the compilat), so it is not destructured. - let Composite { name: _, nodes, edges, input_roles, output } = c; + // `params` here are the composite's ParamAlias overlay (renamed to avoid + // shadowing the injected scalar `params: &[Scalar]` arg consumed by + // `lower_items` below). + let Composite { name: _, nodes, edges, input_roles, params: param_aliases, output } = c; let item_count = nodes.len(); + // an alias must name a real interior leaf param slot (C23 — names are cosmetic + // but a dangling handle is an author error). Mirrors the output range-check. + for a in ¶m_aliases { + let ok = a.node < item_count + && matches!(&nodes[a.node], BlueprintNode::Leaf(f) if a.slot < f.params().len()); + if !ok { + return Err(CompileError::BadInteriorIndex); + } + } + // recursively lower interior items, then rewrite interior edges through them let interior = lower_items(nodes, params, cursor, flat_nodes, flat_edges)?; for e in &edges { @@ -342,7 +399,7 @@ fn inline_composite( let mut roles: Vec> = Vec::with_capacity(input_roles.len()); for (r, role) in input_roles.iter().enumerate() { let mut flat_targets: Vec = Vec::new(); - for t in role { + for t in &role.targets { flat_targets.extend(resolve_target(t, &interior)?); } if let Some((first, rest)) = flat_targets.split_first() { @@ -529,7 +586,11 @@ mod tests { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ) } @@ -573,9 +634,10 @@ mod tests { vec![pass1(), pass1()], vec![], vec![ - vec![Target { node: 0, slot: 0 }], - vec![Target { node: 1, slot: 0 }], + Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }, + Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }] }, ], + vec![], vec![ OutField { node: 0, field: 0, name: "a".into() }, OutField { node: 1, field: 0, name: "b".into() }, @@ -623,7 +685,8 @@ mod tests { "outer", vec![BlueprintNode::Composite(inner)], vec![], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 0, name: "out".into() }], ); let bp = Blueprint::new( @@ -658,9 +721,10 @@ mod tests { vec![pass1(), pass1()], vec![], vec![ - vec![Target { node: 0, slot: 0 }], - vec![Target { node: 1, slot: 0 }], + Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }, + Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }] }, ], + vec![], vec![ OutField { node: 0, field: 0, name: "a".into() }, OutField { node: 1, field: 0, name: "b".into() }, @@ -671,9 +735,10 @@ mod tests { vec![BlueprintNode::Composite(inner)], vec![], vec![ - vec![Target { node: 0, slot: 0 }], // outer role 0 -> inner role 0 - vec![Target { node: 0, slot: 1 }], // outer role 1 -> inner role 1 + Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }, // outer role 0 -> inner role 0 + Role { name: "price2".into(), targets: vec![Target { node: 0, slot: 1 }] }, // outer role 1 -> inner role 1 ], + vec![], vec![ OutField { node: 0, field: 0, name: "x".into() }, // inner field 0 OutField { node: 0, field: 1, name: "y".into() }, // inner field 1 (nested arm) @@ -708,7 +773,8 @@ mod tests { "c", vec![pass1()], vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 0, name: "out".into() }], ); let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); @@ -723,7 +789,11 @@ mod tests { "c", vec![pass1(), sink_i64()], vec![], - vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 0, field: 0, name: "out".into() }], ); let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); @@ -738,7 +808,8 @@ mod tests { "c", vec![pass1()], vec![], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 5, name: "out".into() }], ); let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); @@ -754,7 +825,8 @@ mod tests { "c", vec![pass1()], vec![], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 0, name: "a".into() }], ); let bp = Blueprint::new( @@ -852,7 +924,11 @@ mod tests { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ) } @@ -940,9 +1016,10 @@ mod tests { vec![Sma::factory().into(), Sma::factory().into()], vec![], vec![ - vec![Target { node: 0, slot: 0 }], - vec![Target { node: 1, slot: 0 }], + Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }, + Role { name: "price2".into(), targets: vec![Target { node: 1, slot: 0 }] }, ], + vec![], vec![ OutField { node: 0, field: 0, name: "a".into() }, OutField { node: 1, field: 0, name: "b".into() }, @@ -1079,6 +1156,108 @@ mod tests { ); } + #[test] + fn param_alias_relabels_param_space_name_in_place() { + // two Sma leaves (each one `length` param) under a composite that aliases + // slot 0 of node 0 -> "shortLen" and slot 0 of node 1 -> "longLen". + let c = Composite::new( + "cross", + vec![Sma::factory().into(), Sma::factory().into(), Sub::factory().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 }], + }], + vec![ + ParamAlias { name: "shortLen".into(), node: 0, slot: 0 }, + ParamAlias { name: "longLen".into(), node: 1, slot: 0 }, + ], + vec![OutField { node: 2, field: 0, name: "out".into() }], + ); + let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); + let names: Vec = bp.param_space().into_iter().map(|p| p.name).collect(); + // aliased in place: names are the aliases, NOT two duplicate "cross.length". + assert_eq!(names, vec!["cross.shortLen".to_string(), "cross.longLen".to_string()]); + } + + #[test] + fn out_of_range_param_alias_rejected() { + // alias names node 9 (no such interior item) -> caught at compile. + let c = Composite::new( + "cross", + vec![Sma::factory().into(), Sma::factory().into(), Sub::factory().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 }], + }], + vec![ParamAlias { name: "bogus".into(), node: 9, slot: 0 }], + vec![OutField { node: 2, field: 0, name: "out".into() }], + ); + let bp = Blueprint::new( + vec![BlueprintNode::Composite(c)], + vec![SourceSpec { kind: ScalarKind::F64, targets: vec![] }], + vec![], + ); + // two Sma leaves => two i64 length slots; supply a matching vector so the + // ONLY error is the bad alias, not arity. (The Ok arm holds Box, + // not Debug, so assert via the Err arm — as the other reject tests do.) + assert_eq!( + bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4)]).err(), + Some(CompileError::BadInteriorIndex), + ); + } + + #[test] + fn unaliased_params_keep_factory_names() { + // no aliases => param_space identical to the pre-#41 path-qualified names. + let c = Composite::new( + "cross", + vec![Sma::factory().into(), Sma::factory().into(), Sub::factory().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 }], + }], + vec![], + vec![OutField { node: 2, field: 0, name: "out".into() }], + ); + let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); + let names: Vec = bp.param_space().into_iter().map(|p| p.name).collect(); + assert_eq!(names, vec!["cross.length".to_string(), "cross.length".to_string()]); + } + + #[test] + fn partial_aliasing_relabels_only_the_named_slot() { + // alias node 0 only; node 1 keeps its factory name; order intact. + let c = Composite::new( + "cross", + vec![Sma::factory().into(), Sma::factory().into(), Sub::factory().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 }], + }], + vec![ParamAlias { name: "shortLen".into(), node: 0, slot: 0 }], + vec![OutField { node: 2, field: 0, name: "out".into() }], + ); + let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); + let names: Vec = bp.param_space().into_iter().map(|p| p.name).collect(); + assert_eq!(names, vec!["cross.shortLen".to_string(), "cross.length".to_string()]); + } + #[test] fn param_space_is_flat_path_qualified_and_slot_disambiguated() { use aura_std::{LinComb, Sma, Sub}; @@ -1090,7 +1269,11 @@ mod tests { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ); // outer composite "strategy": the inner composite + a LinComb([1,-1]) @@ -1098,7 +1281,8 @@ mod tests { "strategy", vec![BlueprintNode::Composite(fast_slow), LinComb::factory(2).into()], vec![], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 0, name: "out".into() }], ); let bp = Blueprint::new(vec![BlueprintNode::Composite(strategy)], vec![], vec![]); @@ -1141,7 +1325,11 @@ mod tests { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ); // outer composite "strategy": the inner composite + a LinComb([1,-1]) @@ -1149,7 +1337,8 @@ mod tests { "strategy", vec![BlueprintNode::Composite(fast_slow), LinComb::factory(2).into()], vec![], - vec![vec![Target { node: 0, slot: 0 }]], + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 0, field: 0, name: "out".into() }], ); let bp = Blueprint::new(vec![BlueprintNode::Composite(strategy)], vec![], vec![]); diff --git a/crates/aura-engine/src/lib.rs b/crates/aura-engine/src/lib.rs index b4ce7f3..fa234c2 100644 --- a/crates/aura-engine/src/lib.rs +++ b/crates/aura-engine/src/lib.rs @@ -35,7 +35,7 @@ mod blueprint; mod harness; mod report; -pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField}; +pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField, ParamAlias, Role}; pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target}; pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport}; // #29: re-export the core scalar vocabulary a Blueprint builder needs diff --git a/fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs b/fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs index 9bb1036..d239776 100644 --- a/fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs +++ b/fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs @@ -17,7 +17,7 @@ use std::sync::mpsc; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; -use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, SourceSpec, Target}; +use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, Role, SourceSpec, Target}; use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; fn main() { @@ -41,10 +41,15 @@ fn main() { Edge { from: 1, to: 2, slot: 1, from_field: 0 }, ], // input role 0 (price) fans into Sma(2).slot0 AND Sma(4).slot0 - vec![vec![ - Target { node: 0, slot: 0 }, - Target { node: 1, slot: 0 }, - ]], + vec![Role { + name: "price".into(), + targets: vec![ + Target { node: 0, slot: 0 }, + Target { node: 1, slot: 0 }, + ], + }], + // no param aliases + vec![], // single exposed output: Sub's output field 0 vec![OutField { node: 2, field: 0, name: "out".into() }], ); diff --git a/fieldtests/milestone-construction-layer/mc_2_miswire_render.rs b/fieldtests/milestone-construction-layer/mc_2_miswire_render.rs index 8a0a938..5a84865 100644 --- a/fieldtests/milestone-construction-layer/mc_2_miswire_render.rs +++ b/fieldtests/milestone-construction-layer/mc_2_miswire_render.rs @@ -12,7 +12,7 @@ // Public interface only: Composite / Blueprint accessors + Node::label() via // the boxed node; ledger C8-refinement (label is the disambiguating symbol). -use aura_engine::{BlueprintNode, Composite, Edge, OutField, Target}; +use aura_engine::{BlueprintNode, Composite, Edge, OutField, Role, Target}; // NB: `node.label()` on a `&Box` leaf dispatches via the trait object // without an explicit `use aura_core::Node` (the method is in scope through the // boxed trait object). A consumer does not need to import the Node trait to read @@ -43,7 +43,11 @@ fn cross(swap: bool) -> Composite { Sub::factory().into(), ], vec![e_fast, e_slow], - vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ) } diff --git a/fieldtests/milestone-construction-layer/mc_3_nested_composite.rs b/fieldtests/milestone-construction-layer/mc_3_nested_composite.rs index 4894c6d..f5da3bc 100644 --- a/fieldtests/milestone-construction-layer/mc_3_nested_composite.rs +++ b/fieldtests/milestone-construction-layer/mc_3_nested_composite.rs @@ -15,7 +15,7 @@ use std::sync::mpsc; use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; -use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, SourceSpec, Target}; +use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, Role, SourceSpec, Target}; use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; /// Inner composite: the SMA(2)/SMA(4) cross (one price role -> spread output). @@ -31,7 +31,11 @@ fn inner_cross() -> Composite { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ) } @@ -48,7 +52,9 @@ fn strategy() -> Composite { Exposure::factory().into(), ], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // cross -> Exposure - vec![vec![Target { node: 0, slot: 0 }]], // price -> inner role 0 + // price -> inner role 0 + vec![Role { name: "price".into(), targets: vec![Target { node: 0, slot: 0 }] }], + vec![], vec![OutField { node: 1, field: 0, name: "out".into() }], ) } diff --git a/fieldtests/milestone-construction-layer/mc_4_introspect_graph.rs b/fieldtests/milestone-construction-layer/mc_4_introspect_graph.rs index 4840e52..12c75e7 100644 --- a/fieldtests/milestone-construction-layer/mc_4_introspect_graph.rs +++ b/fieldtests/milestone-construction-layer/mc_4_introspect_graph.rs @@ -10,7 +10,7 @@ // Public interface only. use aura_core::ScalarKind; -use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, SourceSpec, Target}; +use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, Role, SourceSpec, Target}; // NB (fieldtest finding): `aura_engine::ScalarKind` does NOT resolve — the // construction surface (aura-engine) does not re-export the core scalar // vocabulary (ScalarKind / Scalar / Firing) that a graph-builder needs. A @@ -30,7 +30,11 @@ fn sma_cross() -> Composite { 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 }]], + vec![Role { + name: "price".into(), + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![], vec![OutField { node: 2, field: 0, name: "out".into() }], ) } @@ -63,8 +67,8 @@ fn describe_node(prefix: &str, n: &BlueprintNode) { for inner in c.nodes() { describe_node(&format!("{prefix} "), inner); } - for (r, targets) in c.input_roles().iter().enumerate() { - println!("{prefix} role[{r}] fans into {targets:?}"); + for (r, role) in c.input_roles().iter().enumerate() { + println!("{prefix} role[{r}] {:?} fans into {:?}", role.name, role.targets); } for e in c.edges() { println!("{prefix} interior edge {e:?}"); @@ -107,7 +111,8 @@ fn main() { assert_eq!(c.name(), "sma_cross"); assert_eq!(c.nodes().len(), 3, "composite interior should have 3 items"); assert_eq!(c.input_roles().len(), 1, "one price role"); - assert_eq!(c.input_roles()[0].len(), 2, "price fans into both SMAs"); + assert_eq!(c.input_roles()[0].name, "price", "the role carries its boundary name"); + assert_eq!(c.input_roles()[0].targets.len(), 2, "price fans into both SMAs"); let out = c.output(); assert_eq!(out.len(), 1, "the cross re-exports one output field"); assert_eq!((out[0].node, out[0].field), (2, 0), "Sub output is the port");