Files
Aura/fieldtests/milestone-construction-layer/mc_4_introspect_graph.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

119 lines
4.7 KiB
Rust

// Milestone fieldtest — axis (d): introspect a built Blueprint via the
// read-only graph-as-data accessors — can a consumer walk the graph as data
// WITHOUT reading engine internals?
//
// Accessors under test (all public, from rustdoc):
// Blueprint::nodes() / sources() / edges()
// Composite::name() / nodes() / edges() / input_roles() / output()
// BlueprintNode::{Leaf, Composite}; Node::label() on a boxed leaf
//
// Public interface only.
use aura_core::ScalarKind;
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, OutField, 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
// downstream author building a Blueprint must reach into aura_core directly.
// Recorded as friction in the spec. `node.label()` below dispatches on the
// boxed leaf via the Node trait object without an explicit `use` of Node.
fn sma_cross() -> Composite {
Composite::new(
"sma_cross",
vec![
aura_std::Sma::factory().into(),
aura_std::Sma::factory().into(),
aura_std::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() }],
)
}
fn describe_node(prefix: &str, n: &BlueprintNode) {
match n {
BlueprintNode::Leaf(factory) => {
// 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!(
"{prefix}LEAF {:<14} params={}",
factory.label(),
factory.params().len()
);
}
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!(
"{prefix}COMPOSITE name={:?} interior_items={} roles={} out={:?}",
c.name(),
c.nodes().len(),
c.input_roles().len(),
out_fields,
);
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 e in c.edges() {
println!("{prefix} interior edge {e:?}");
}
}
}
}
fn main() {
let bp = Blueprint::new(
vec![
BlueprintNode::Composite(sma_cross()),
aura_std::Exposure::factory().into(),
],
vec![SourceSpec {
kind: ScalarKind::F64,
targets: vec![Target { node: 0, slot: 0 }],
}],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
);
println!("== Blueprint graph-as-data walk ==");
println!("sources: {}", bp.sources().len());
for (i, s) in bp.sources().iter().enumerate() {
println!(" source[{i}] kind={:?} -> {:?}", s.kind, s.targets);
}
println!("top-level items: {}", bp.nodes().len());
for n in bp.nodes() {
describe_node(" ", n);
}
println!("top-level edges: {}", bp.edges().len());
for e in bp.edges() {
println!(" edge {e:?}");
}
// The walk must reach the interior of the composite purely through the
// public accessors — no engine internals.
let first = &bp.nodes()[0];
if let BlueprintNode::Composite(c) = first {
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");
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");
println!("OK: walked the composite interior via public accessors only.");
} else {
panic!("expected composite as first item");
}
}