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:
@@ -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));
|
||||
|
||||
@@ -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![
|
||||
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]
|
||||
|
||||
@@ -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 ¶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<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![]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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![
|
||||
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() }],
|
||||
);
|
||||
|
||||
@@ -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<dyn Node>` 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() }],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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() }],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user