// 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, Role, 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![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 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, role) in c.input_roles().iter().enumerate() { println!("{prefix} role[{r}] {:?} fans into {:?}", role.name, role.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].name, "price", "the role carries its boundary name"); assert_eq!(c.input_roles()[0].targets.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"); } }