41cbb5506f
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
99 lines
4.1 KiB
Rust
99 lines
4.1 KiB
Rust
// Milestone fieldtest — axis (b): deliberately mis-wire a graph (swap the
|
|
// fast/slow SMA legs of the cross) and confirm the wiring reads back
|
|
// differently — the milestone's headline promise that a render visibly
|
|
// surfaces a mis-wiring.
|
|
//
|
|
// The headline render is the `aura graph` CLI (exercised separately, in the
|
|
// accompanying .sh-free flow: see the spec). HERE we test the property the
|
|
// render RELIES ON from the consumer side: that the labels + graph-as-data of a
|
|
// correctly-wired composite differ observably from a swapped one. If a swap
|
|
// produced an identical graph-as-data, no renderer could ever surface it.
|
|
//
|
|
// 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, 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
|
|
// labels off a built graph.
|
|
use aura_std::{Sma, Sub};
|
|
|
|
/// Build an SMA-cross composite. `swap` flips which SMA feeds Sub's slot 0.
|
|
/// Correct: fast(SMA2) -> minuend(slot0), slow(SMA4) -> subtrahend(slot1).
|
|
/// Swapped: slow -> slot0, fast -> slot1 (the classic fast/slow mis-wire).
|
|
fn cross(swap: bool) -> Composite {
|
|
let (e_fast, e_slow) = if swap {
|
|
// slow(node 1) into slot0, fast(node 0) into slot1
|
|
(
|
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 0, to: 2, slot: 1, from_field: 0 },
|
|
)
|
|
} else {
|
|
(
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
)
|
|
};
|
|
Composite::new(
|
|
"sma_cross",
|
|
vec![
|
|
Sma::factory().into(),
|
|
Sma::factory().into(),
|
|
Sub::factory().into(),
|
|
],
|
|
vec![e_fast, e_slow],
|
|
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() }],
|
|
)
|
|
}
|
|
|
|
fn node_labels(c: &Composite) -> Vec<String> {
|
|
c.nodes()
|
|
.iter()
|
|
.map(|n| match n {
|
|
BlueprintNode::Leaf(node) => node.label(),
|
|
BlueprintNode::Composite(inner) => format!("composite:{}", inner.name()),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn main() {
|
|
let correct = cross(false);
|
|
let swapped = cross(true);
|
|
|
|
// The blueprint-view label is the bare value-empty type (SMA), not SMA(2)/
|
|
// SMA(4): post cycle-0016 the lengths are injected params, so two same-type
|
|
// leaves share a blueprint label and are disambiguated by slot/edge-table
|
|
// (the compiled view labels them valued via Node::label, separately).
|
|
println!("correct labels: {:?}", node_labels(&correct));
|
|
println!("swapped labels: {:?}", node_labels(&swapped));
|
|
|
|
// The mis-wire lives in the EDGE table; the graph-as-data must reflect it.
|
|
let ce: Vec<Edge> = correct.edges().to_vec();
|
|
let se: Vec<Edge> = swapped.edges().to_vec();
|
|
println!("correct interior edges: {ce:?}");
|
|
println!("swapped interior edges: {se:?}");
|
|
|
|
// Headline property: the two graphs-as-data differ. A renderer reading the
|
|
// edge table + labels can therefore make the swap visible.
|
|
assert_ne!(
|
|
ce, se,
|
|
"a fast/slow swap MUST change the graph-as-data, else no render could surface it"
|
|
);
|
|
|
|
// The blueprint-view labels carry the value-empty node type (both SMAs read
|
|
// as "SMA"); the swap's observability rests on the edge table above, not on a
|
|
// per-leg label. The labels still type-distinguish SMA from Sub.
|
|
let labels = node_labels(&correct);
|
|
assert!(
|
|
labels.iter().filter(|l| l.contains("SMA")).count() == 2 && labels.iter().any(|l| l.contains("Sub")),
|
|
"labels must type-distinguish the two SMAs from the Sub: {labels:?}"
|
|
);
|
|
println!("OK: a fast/slow swap is observable in graph-as-data; labels are value-empty types.");
|
|
}
|