Files
Aura/fieldtests/milestone-construction-layer/mc_3_nested_composite.rs
T
Brummel 241bb626e6 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
2026-06-08 01:23:47 +02:00

103 lines
4.0 KiB
Rust

// Milestone fieldtest — axis (c): nest a composite INSIDE another composite,
// build a Blueprint, compile() + bootstrap() + run(). The milestone promises
// composites "nest arbitrarily" and that the compiled/flat view handles
// nesting (#26 notes the CLUSTERED blueprint render has an `unimplemented!` on
// nested composites — but that path is only reachable through `aura graph` on
// the built-in sample, which a downstream consumer cannot point at their own
// graph; see the spec finding).
//
// Here we verify the run-path promise from the consumer side: a composite that
// CONTAINS a composite lowers (inlines, recursively) to a flat runnable
// instance that produces a populated trace.
//
// Public interface only.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, SourceSpec, Target};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
/// Inner composite: the SMA(2)/SMA(4) cross (one price role -> spread output).
fn inner_cross() -> Composite {
Composite::new(
"sma_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![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
}
/// Outer composite: wraps inner_cross then maps it through Exposure(0.5).
/// Interior: 0 = inner_cross (Composite), 1 = Exposure(0.5).
/// One price role fans into the inner composite's role 0.
/// Output: Exposure's output field.
fn strategy() -> Composite {
Composite::new(
"strategy",
vec![
BlueprintNode::Composite(inner_cross()),
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
vec![OutField { node: 1, field: 0, name: "out".into() }],
)
}
fn main() {
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
// Top-level: 0 = strategy (nested Composite), 1 = SimBroker, 2 = Recorder.
let blueprint = Blueprint::new(
vec![
BlueprintNode::Composite(strategy()),
SimBroker::factory(1e-4).into(),
Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx).into(),
],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![
Target { node: 0, slot: 0 }, // strategy price role
Target { node: 1, slot: 1 }, // broker price leg
],
}],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // strategy -> broker.exposure
Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // broker -> Recorder
],
);
// 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
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
.expect("nested-composite blueprint should bootstrap");
let prices: Vec<f64> = vec![
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
.collect();
harness.run(vec![stream]);
drop(harness);
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
println!("nested-composite recorded rows: {}", rows.len());
assert!(
!rows.is_empty(),
"a composite-in-composite must inline to a flat runnable instance"
);
println!("OK: composite-inside-composite inlined, bootstrapped, ran, recorded.");
}