fix(fieldtests): port construction-layer fixtures to current authoring API
The four standalone construction-layer fieldtest bins (mc_1..mc_4) no longer compiled: they are their own workspace root, so the engine's `cargo build --workspace` never touched them and two waves of API drift went latent. Two breakage classes, both confined to the fixture files (no engine change): - Stale cycle-0016 authoring. `BlueprintNode::from(<Node>::new(..))` relied on the `From<NodeType>` impls retired in the value-empty migration; only `From<LeafFactory>` survives. Ported to `<Node>::factory().into()` — nodes are value-empty blueprint items, params bound at compile. Knock-on in mc_4 `describe_node`: a blueprint leaf is now a `&LeafFactory` with no pre-build schema, so leaf introspection reports the render label + declared param count (inputs / outputs exist only on the compiled flat node, post-build). - Cycle-0018 (#40) OutPort -> OutField slice drift. `Composite::output()` now returns `&[OutField]`; the mc_4 read sites were still treating it as a single port. Ported to index the record; the walk assertion now also pins the arity (one re-exported field), which is the #40 shape. Faithful ports, not assertion-gutting: each fixture still exercises its axis (composite build+run / miswire render / nested composite / graph introspection), verified by running all four bins to their `OK:` line. One genuine semantic drift flagged in mc_2: the blueprint-view label is now the bare value-empty type ("SMA"), so two same-type leaves share a label and are disambiguated by the slot/edge table; the valued SMA(2)/SMA(4) label lives on the compiled view. The fixture asserts what the blueprint view actually exposes and keeps the load-bearing edge-table observability check untouched. closes #42
This commit is contained in:
@@ -26,12 +26,14 @@ fn main() {
|
|||||||
// 0: Sma(2) (fast) 1: Sma(4) (slow) 2: Sub (fast - slow)
|
// 0: Sma(2) (fast) 1: Sma(4) (slow) 2: Sub (fast - slow)
|
||||||
// One input role (the price) fans into BOTH SMAs' slot 0.
|
// One input role (the price) fans into BOTH SMAs' slot 0.
|
||||||
// The exposed output port is Sub's single output field.
|
// The exposed output port is Sub's single output field.
|
||||||
|
// Value-empty recipes (factories); the two SMA lengths are injected at
|
||||||
|
// bootstrap (param vector below), not baked here.
|
||||||
let sma_cross = Composite::new(
|
let sma_cross = Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::from(Sma::new(2)),
|
Sma::factory().into(),
|
||||||
BlueprintNode::from(Sma::new(4)),
|
Sma::factory().into(),
|
||||||
BlueprintNode::from(Sub::new()),
|
Sub::factory().into(),
|
||||||
],
|
],
|
||||||
// interior edges (local indices): fast -> Sub.slot0, slow -> Sub.slot1
|
// interior edges (local indices): fast -> Sub.slot0, slow -> Sub.slot1
|
||||||
vec![
|
vec![
|
||||||
@@ -55,9 +57,9 @@ fn main() {
|
|||||||
let blueprint = Blueprint::new(
|
let blueprint = Blueprint::new(
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::Composite(sma_cross),
|
BlueprintNode::Composite(sma_cross),
|
||||||
BlueprintNode::from(Exposure::new(0.5)),
|
Exposure::factory().into(),
|
||||||
BlueprintNode::from(SimBroker::new(1e-4)),
|
SimBroker::factory(1e-4).into(),
|
||||||
BlueprintNode::from(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx).into(),
|
||||||
],
|
],
|
||||||
// one f64 price source: feeds the composite (item 0, role 0) AND the
|
// one f64 price source: feeds the composite (item 0, role 0) AND the
|
||||||
// broker's price leg (item 2, slot 1).
|
// broker's price leg (item 2, slot 1).
|
||||||
@@ -78,8 +80,10 @@ fn main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- compile() + bootstrap() + run() -------------------------------------
|
// --- compile() + bootstrap() + run() -------------------------------------
|
||||||
|
// Param vector (depth-first item order, C12): sma_cross's two SMA lengths
|
||||||
|
// (I64), then Exposure's scale (F64); Sub/SimBroker/Recorder declare none.
|
||||||
let mut harness = blueprint
|
let mut harness = blueprint
|
||||||
.bootstrap()
|
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
||||||
.expect("composite-authored blueprint should bootstrap");
|
.expect("composite-authored blueprint should bootstrap");
|
||||||
|
|
||||||
// a synthetic upward price ramp then a dip, enough to warm SMA(4) and flip
|
// a synthetic upward price ramp then a dip, enough to warm SMA(4) and flip
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ fn cross(swap: bool) -> Composite {
|
|||||||
Composite::new(
|
Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::from(Sma::new(2)),
|
Sma::factory().into(),
|
||||||
BlueprintNode::from(Sma::new(4)),
|
Sma::factory().into(),
|
||||||
BlueprintNode::from(Sub::new()),
|
Sub::factory().into(),
|
||||||
],
|
],
|
||||||
vec![e_fast, e_slow],
|
vec![e_fast, e_slow],
|
||||||
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
|
vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
|
||||||
@@ -62,8 +62,10 @@ fn main() {
|
|||||||
let correct = cross(false);
|
let correct = cross(false);
|
||||||
let swapped = cross(true);
|
let swapped = cross(true);
|
||||||
|
|
||||||
// The per-node labels carry identifying params (SMA(2) vs SMA(4)) so two
|
// The blueprint-view label is the bare value-empty type (SMA), not SMA(2)/
|
||||||
// SMAs are distinguishable in a render.
|
// 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!("correct labels: {:?}", node_labels(&correct));
|
||||||
println!("swapped labels: {:?}", node_labels(&swapped));
|
println!("swapped labels: {:?}", node_labels(&swapped));
|
||||||
|
|
||||||
@@ -80,11 +82,13 @@ fn main() {
|
|||||||
"a fast/slow swap MUST change the graph-as-data, else no render could surface it"
|
"a fast/slow swap MUST change the graph-as-data, else no render could surface it"
|
||||||
);
|
);
|
||||||
|
|
||||||
// And the disambiguating labels are present (SMA(2)/SMA(4) distinguishable).
|
// 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);
|
let labels = node_labels(&correct);
|
||||||
assert!(
|
assert!(
|
||||||
labels.iter().any(|l| l.contains("SMA(2)")) && labels.iter().any(|l| l.contains("SMA(4)")),
|
labels.iter().filter(|l| l.contains("SMA")).count() == 2 && labels.iter().any(|l| l.contains("Sub")),
|
||||||
"labels must carry SMA params so a swapped leg reads differently: {labels:?}"
|
"labels must type-distinguish the two SMAs from the Sub: {labels:?}"
|
||||||
);
|
);
|
||||||
println!("OK: a fast/slow swap is observable in graph-as-data + labels.");
|
println!("OK: a fast/slow swap is observable in graph-as-data; labels are value-empty types.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ fn inner_cross() -> Composite {
|
|||||||
Composite::new(
|
Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::from(Sma::new(2)),
|
Sma::factory().into(),
|
||||||
BlueprintNode::from(Sma::new(4)),
|
Sma::factory().into(),
|
||||||
BlueprintNode::from(Sub::new()),
|
Sub::factory().into(),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
@@ -45,7 +45,7 @@ fn strategy() -> Composite {
|
|||||||
"strategy",
|
"strategy",
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::Composite(inner_cross()),
|
BlueprintNode::Composite(inner_cross()),
|
||||||
BlueprintNode::from(Exposure::new(0.5)),
|
Exposure::factory().into(),
|
||||||
],
|
],
|
||||||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // cross -> Exposure
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // cross -> Exposure
|
||||||
vec![vec![Target { node: 0, slot: 0 }]], // price -> inner role 0
|
vec![vec![Target { node: 0, slot: 0 }]], // price -> inner role 0
|
||||||
@@ -59,8 +59,8 @@ fn main() {
|
|||||||
let blueprint = Blueprint::new(
|
let blueprint = Blueprint::new(
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::Composite(strategy()),
|
BlueprintNode::Composite(strategy()),
|
||||||
BlueprintNode::from(SimBroker::new(1e-4)),
|
SimBroker::factory(1e-4).into(),
|
||||||
BlueprintNode::from(Recorder::new(&[ScalarKind::F64], Firing::Any, tx)),
|
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx).into(),
|
||||||
],
|
],
|
||||||
vec![SourceSpec {
|
vec![SourceSpec {
|
||||||
kind: ScalarKind::F64,
|
kind: ScalarKind::F64,
|
||||||
@@ -75,8 +75,10 @@ fn main() {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Param vector (depth-first, C12): strategy { inner_cross's two SMA lengths
|
||||||
|
// (I64), Exposure scale (F64) }; Sub/SimBroker/Recorder declare none.
|
||||||
let mut harness = blueprint
|
let mut harness = blueprint
|
||||||
.bootstrap()
|
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
||||||
.expect("nested-composite blueprint should bootstrap");
|
.expect("nested-composite blueprint should bootstrap");
|
||||||
|
|
||||||
let prices: Vec<f64> = vec![
|
let prices: Vec<f64> = vec![
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ fn sma_cross() -> Composite {
|
|||||||
Composite::new(
|
Composite::new(
|
||||||
"sma_cross",
|
"sma_cross",
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::from(aura_std::Sma::new(2)),
|
aura_std::Sma::factory().into(),
|
||||||
BlueprintNode::from(aura_std::Sma::new(4)),
|
aura_std::Sma::factory().into(),
|
||||||
BlueprintNode::from(aura_std::Sub::new()),
|
aura_std::Sub::factory().into(),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||||||
@@ -37,23 +37,28 @@ fn sma_cross() -> Composite {
|
|||||||
|
|
||||||
fn describe_node(prefix: &str, n: &BlueprintNode) {
|
fn describe_node(prefix: &str, n: &BlueprintNode) {
|
||||||
match n {
|
match n {
|
||||||
BlueprintNode::Leaf(node) => {
|
BlueprintNode::Leaf(factory) => {
|
||||||
let sch = node.schema();
|
// A blueprint leaf is a value-empty LeafFactory (post cycle-0016): no
|
||||||
|
// built node, so no input/output schema yet — introspect its declared
|
||||||
|
// render label + tunable param count (inputs/outputs exist only after
|
||||||
|
// build, on the compiled flat node).
|
||||||
println!(
|
println!(
|
||||||
"{prefix}LEAF {:<14} inputs={} output_fields={}",
|
"{prefix}LEAF {:<14} params={}",
|
||||||
node.label(),
|
factory.label(),
|
||||||
sch.inputs.len(),
|
factory.params().len()
|
||||||
sch.output.len()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
BlueprintNode::Composite(c) => {
|
BlueprintNode::Composite(c) => {
|
||||||
|
// output() is now a record (&[OutField]); render the whole field list
|
||||||
|
// (one entry today, but the type is the multi-field shape of #40).
|
||||||
|
let out_fields: Vec<(usize, usize)> =
|
||||||
|
c.output().iter().map(|f| (f.node, f.field)).collect();
|
||||||
println!(
|
println!(
|
||||||
"{prefix}COMPOSITE name={:?} interior_items={} roles={} out=({},{})",
|
"{prefix}COMPOSITE name={:?} interior_items={} roles={} out={:?}",
|
||||||
c.name(),
|
c.name(),
|
||||||
c.nodes().len(),
|
c.nodes().len(),
|
||||||
c.input_roles().len(),
|
c.input_roles().len(),
|
||||||
c.output().node,
|
out_fields,
|
||||||
c.output().field,
|
|
||||||
);
|
);
|
||||||
for inner in c.nodes() {
|
for inner in c.nodes() {
|
||||||
describe_node(&format!("{prefix} "), inner);
|
describe_node(&format!("{prefix} "), inner);
|
||||||
@@ -72,7 +77,7 @@ fn main() {
|
|||||||
let bp = Blueprint::new(
|
let bp = Blueprint::new(
|
||||||
vec![
|
vec![
|
||||||
BlueprintNode::Composite(sma_cross()),
|
BlueprintNode::Composite(sma_cross()),
|
||||||
BlueprintNode::from(aura_std::Exposure::new(0.5)),
|
aura_std::Exposure::factory().into(),
|
||||||
],
|
],
|
||||||
vec![SourceSpec {
|
vec![SourceSpec {
|
||||||
kind: ScalarKind::F64,
|
kind: ScalarKind::F64,
|
||||||
@@ -104,7 +109,8 @@ fn main() {
|
|||||||
assert_eq!(c.input_roles().len(), 1, "one price role");
|
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].len(), 2, "price fans into both SMAs");
|
||||||
let out = c.output();
|
let out = c.output();
|
||||||
assert_eq!((out.node, out.field), (2, 0), "Sub output is the port");
|
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");
|
||||||
println!("OK: walked the composite interior via public accessors only.");
|
println!("OK: walked the composite interior via public accessors only.");
|
||||||
} else {
|
} else {
|
||||||
panic!("expected composite as first item");
|
panic!("expected composite as first item");
|
||||||
|
|||||||
Reference in New Issue
Block a user