feat(aura-engine): name the composite boundary — input roles + param aliases

Brings the other two composite-boundary edge-kinds to the named-projection
shape #40 gave outputs. input_roles changes from a bare Vec<Vec<Target>>
to Vec<Role { name, targets }> (rendered [in:<name>]); a composite gains
params: Vec<ParamAlias { name, node, slot }> that relabels an interior
leaf param slot's surface name in param_space() (rendered [param:<name>]).

Param aliasing is a PURE NAMING OVERLAY, not curation (the load-bearing
decision, per spec 0019): every interior param slot stays in param_space()
and sweepable; the alias only relabels in place, never reorders or hides.
Proven empirically — the MACD run is byte-identical (total_pips
0.1637945563898923, 3 sign flips), only the param labels improved:
param_space() now surfaces [macd.fast, macd.slow, macd.signal] instead of
three indistinguishable macd.length, and the manifest reads ema_fast/
ema_slow/ema_signal.

C23 honoured: role/param/output names are non-load-bearing debug symbols
dropped at lowering; identity is positional (role index, param slot,
output field). compiled_view_golden is byte-identical (verified: the
golden region is untouched in the diff). An out-of-range alias (missing/
non-leaf node or slot past the leaf's param count) is rejected at
compile_with_params as BadInteriorIndex, mirroring the output range-check
(no new variant). Orthogonal to #36 — purely additive at the composite
level.

Aliasing is demonstrated on the CLI MACD site only (the spec's worked
example + a new E2E test macd_param_space_surfaces_the_three_named_aliases);
sma_cross, the engine test fixtures, and the construction-layer fieldtests
get the forced role-name + empty params, so the param_space C23 anchor
goldens (param_space_mirrors_compiled_flat_node_param_order + siblings)
stay byte-identical.

Verification (orchestrator-run, not trusted from the agent report):
cargo build/test/clippy --workspace -D warnings all green (engine 66,
cli 12); the separate-workspace construction-layer fieldtest crate builds
(guards the #42 latent-drift recurrence); compiled_view_golden + MACD
determinism unchanged.

Two faithful repairs to the plan's literal test/code bodies, no semantic
change: the out-of-range test uses .err()/Some(BadInteriorIndex) (the Ok
arm Vec<Box<dyn Node>> is not Debug, so .unwrap_err() would not compile),
and the inline_composite destructure binds params as `param_aliases` to
avoid shadowing the injected `params: &[Scalar]` arg.

closes #41
This commit is contained in:
2026-06-08 02:21:30 +02:00
parent c9115e3e0f
commit 41cbb5506f
8 changed files with 323 additions and 66 deletions
+11 -5
View File
@@ -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:<name>]` marker per
/// nested composites as opaque `[name]`, plus an `[in:<name>]` entry marker per
/// input role (wired to its interior targets), a `[param:<name>]` marker per param
/// alias (wired to the leaf it relabels), and an `[out:<name>]` marker per
/// re-exported output field (wired from its producer). Prefixed `"<name>:\n"`.
fn render_definition(c: &Composite) -> String {
let mut labels: Vec<String> = 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));
+52 -10
View File
@@ -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<String> =
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]
+224 -35
View File
@@ -41,6 +41,26 @@ impl From<LeafFactory> 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<Target>,
}
/// 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<BlueprintNode>,
edges: Vec<Edge>,
input_roles: Vec<Vec<Target>>,
input_roles: Vec<Role>,
params: Vec<ParamAlias>,
output: Vec<OutField>,
}
@@ -63,10 +84,11 @@ impl Composite {
name: impl Into<String>,
nodes: Vec<BlueprintNode>,
edges: Vec<Edge>,
input_roles: Vec<Vec<Target>>,
input_roles: Vec<Role>,
params: Vec<ParamAlias>,
output: Vec<OutField>,
) -> 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<Target>] {
/// 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<ParamSpec> {
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<ParamSpec>) {
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<ParamSpec>,
) {
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<ParamSpec
} else {
format!("{prefix}.{}", c.name())
};
collect_params(c.nodes(), &child, out);
collect_params(c.nodes(), &child, c.params(), out);
}
}
}
@@ -306,9 +350,22 @@ fn inline_composite(
) -> Result<ItemLowering, CompileError> {
// `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 &param_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<Target>> = Vec::with_capacity(input_roles.len());
for (r, role) in input_roles.iter().enumerate() {
let mut flat_targets: Vec<Target> = 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<String> = 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<dyn Node>,
// 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<String> = 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<String> = 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![]);
+1 -1
View File
@@ -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