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));
|
||||
|
||||
+52
-10
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user