# Blueprint → flat graph: composite inlining — Implementation Plan > **Parent spec:** `docs/specs/0012-blueprint-compile-composites.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run > this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Add a `Blueprint` / `Composite` construction layer to `aura-engine` that compiles a named graph-as-data — inlining composites by raw-index lowering — into the flat `(nodes, sources, edges)` the unchanged `Harness::bootstrap` consumes, and prove an SMA-cross composite runs bit-identically to today's hand-wired graph (C1). **Architecture:** A new module `crates/aura-engine/src/blueprint.rs` sits *above* `Harness::bootstrap`. `Blueprint::compile()` recursively inlines every `Composite` (append interior nodes at an offset, rewrite interior edges, fan input roles out, resolve the one output port), producing the same flat graph a hand-wiring would. The run loop, `bootstrap`'s signature, and `Edge`/`Target`/`SourceSpec`/`Node` are untouched; bootstrap's existing kind- and Kahn-cycle-check validate the lowered flat graph. Optimisation passes (C23) are explicitly out of scope. **Tech Stack:** Rust, `aura-engine` (depends on `aura-core`; `aura-std` is a dev-dependency reachable from tests). No new external dependencies (C16). --- **Files this plan creates or modifies:** - Create: `crates/aura-engine/src/blueprint.rs` — the construction layer: `OutPort`, `BlueprintNode` (+ `From` lift), `Composite` (`new` + derived `schema`), `Blueprint` (`new` + `compile` + `bootstrap`), `CompileError`, the recursive inliner, and an inline `#[cfg(test)] mod tests`. - Modify: `crates/aura-engine/src/lib.rs:34-38` — declare `mod blueprint;` and re-export the public construction types. - Test: `crates/aura-engine/src/blueprint.rs` (inline `#[cfg(test)] mod tests`) — schema derivation, inliner happy-path + nested + error paths, and the headline bit-identical demonstrator `composite_sma_cross_runs_bit_identical_to_hand_wired`. Reference shapes (read-only, must NOT change): `crates/aura-engine/src/harness.rs` (`Edge`/`Target`/`SourceSpec` `:29-52`, `BootstrapError` `:56-65`, `Harness::bootstrap` `:114-200`, run loop `:208-283`), `crates/aura-core/src/node.rs` (`Node`/`NodeSchema`/`InputSpec`/`FieldSpec`/`Firing`), `crates/aura-cli/src/main.rs:42-78` (the `sample_harness` wiring the demonstrator reproduces). --- ### Task 1: Construction-layer types + derived schema **Files:** - Create: `crates/aura-engine/src/blueprint.rs` - Modify: `crates/aura-engine/src/lib.rs:34-38` - Test: `crates/aura-engine/src/blueprint.rs` (inline `mod tests`) - [ ] **Step 1: Create `blueprint.rs` with the types, the `From` lift, and derived schema** Create `crates/aura-engine/src/blueprint.rs` with exactly this content: ```rust //! The construction layer (C9/C19/C23): a named, param-generic graph-as-data //! (`Blueprint`) that **compiles** to the flat, type-erased instance the run loop //! already runs (the *flat graph*). The unit of reuse is the [`Composite`]: a //! nestable sub-graph fragment exposing one output port (C8) and named input //! roles, which `compile` **inlines** into the flat `(nodes, sources, edges)` the //! unchanged [`crate::Harness::bootstrap`] consumes. //! //! The flat graph is wired by raw index, **not by name** (C23): a composite's //! boundary dissolves at compile time; field/role names, where kept, are //! non-load-bearing debug symbols (as `FieldSpec.name` already is). This module //! adds no optimisation pass (CSE/DCE, sweep-invariant hoisting are deferred, //! C23) and no external dependency (C16). use aura_core::{Node, NodeSchema, ScalarKind}; use crate::harness::{BootstrapError, Edge, Harness, SourceSpec, Target}; /// Which interior `(node, output-field)` is a composite's single output port (C8). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct OutPort { pub node: usize, pub field: usize, } /// A blueprint item: a leaf node or a nested composite. Both present a declared /// interface (typed inputs + one output) to the enclosing graph. pub enum BlueprintNode { Leaf(Box), Composite(Composite), } /// Ergonomic lift: any concrete `Node` becomes a `Leaf` blueprint item. impl From for BlueprintNode { fn from(node: N) -> Self { BlueprintNode::Leaf(Box::new(node)) } } impl BlueprintNode { /// The declared interface this item presents to the enclosing graph: a leaf's /// own `Node::schema`, or a composite's derived [`Composite::schema`]. fn schema(&self) -> NodeSchema { match self { BlueprintNode::Leaf(node) => node.schema(), BlueprintNode::Composite(c) => c.schema(), } } } /// A reusable sub-graph fragment compiled away by inlining (C9/C23). It is **not** /// a [`Node`]: it is never `eval`'d. It holds interior items (local indices), /// interior edges (local indices), input roles (role `r` fans into the interior /// targets `input_roles[r]`), and the one exposed output port. pub struct Composite { nodes: Vec, edges: Vec, input_roles: Vec>, output: OutPort, } impl Composite { /// Build a composite from its interior items, interior edges (local indices), /// input roles, and output port. pub fn new( nodes: Vec, edges: Vec, input_roles: Vec>, output: OutPort, ) -> Self { Self { nodes, edges, input_roles, output } } /// The derived interface the enclosing graph wires against: input role `r`'s /// spec is taken from its first interior target's slot; the output field is the /// interior output port's field. This is a *derivation*, not a `Node` impl, and /// it assumes well-formed indices — `compile` is the validator that rejects a /// malformed composite with a typed [`CompileError`]. pub fn schema(&self) -> NodeSchema { let inputs = self .input_roles .iter() .map(|role| { let first = role[0]; self.nodes[first.node].schema().inputs[first.slot] }) .collect(); let out_field = self.nodes[self.output.node].schema().output[self.output.field]; NodeSchema { inputs, output: vec![out_field] } } } /// The root graph-as-data, before compilation: blueprint items + sources + edges, /// all addressing blueprint-level indices. pub struct Blueprint { nodes: Vec, sources: Vec, edges: Vec, } impl Blueprint { /// Build a blueprint from its items, sources, and edges (blueprint-level /// indices; a target/edge endpoint may name a composite). pub fn new(nodes: Vec, sources: Vec, edges: Vec) -> Self { Self { nodes, sources, edges } } } #[cfg(test)] mod tests { use super::*; use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Scalar}; /// A 2-input f64 node, one f64 output. Test-local fixture (C9: examples for the /// engine's own tests, no speculative `aura-std` surface). struct Join2 { out: [Scalar; 1], } impl Node for Join2 { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![ InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, ], output: vec![FieldSpec { name: "v", kind: ScalarKind::F64 }], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let a = ctx.f64_in(0); let b = ctx.f64_in(1); if a.is_empty() || b.is_empty() { return None; } self.out[0] = Scalar::F64(a[0] + b[0]); Some(&self.out) } } #[test] fn composite_schema_derives_role_and_output_kinds() { // one interior node (Join2: 2 f64 inputs, 1 f64 output); two roles, each // feeding one interior slot; output port = the Join2 output field 0. let c = Composite::new( vec![BlueprintNode::Leaf(Box::new(Join2 { out: [Scalar::F64(0.0)] }))], vec![], vec![ vec![Target { node: 0, slot: 0 }], vec![Target { node: 0, slot: 1 }], ], OutPort { node: 0, field: 0 }, ); let schema = c.schema(); assert_eq!(schema.inputs.len(), 2); assert_eq!(schema.inputs[0].kind, ScalarKind::F64); assert_eq!(schema.inputs[1].kind, ScalarKind::F64); assert_eq!(schema.output, vec![FieldSpec { name: "v", kind: ScalarKind::F64 }]); } } ``` - [ ] **Step 2: Declare the module and re-export the public types in `lib.rs`** In `crates/aura-engine/src/lib.rs`, replace the module declarations and re-export block (`:34-38`): ```rust mod harness; mod report; ``` becomes: ```rust mod blueprint; mod harness; mod report; ``` and add a re-export line after the existing `pub use harness::{...};` (`:37`) so the block reads: ```rust pub use blueprint::{Blueprint, BlueprintNode, Composite, OutPort}; pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target}; pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport}; ``` - [ ] **Step 3: Run the schema test to verify it passes** Run: `cargo test -p aura-engine blueprint::tests::composite_schema_derives_role_and_output_kinds` Expected: PASS (`test result: ok. 1 passed`). - [ ] **Step 4: Verify the workspace still compiles and existing tests stay green** Run: `cargo test -p aura-engine` Expected: PASS — the existing harness/report tests still pass and the one new schema test passes; `0 failed`. --- ### Task 2: The recursive inliner — `compile()` + `bootstrap()` **Files:** - Modify: `crates/aura-engine/src/blueprint.rs` (add `CompileError`, the inliner, and `impl Blueprint { compile, bootstrap }`) - Modify: `crates/aura-engine/src/lib.rs` (add `CompileError` to the re-export) - Test: `crates/aura-engine/src/blueprint.rs` (inline `mod tests`) - [ ] **Step 1: Write the failing inliner tests** Append these test fixtures and tests inside the existing `#[cfg(test)] mod tests` in `crates/aura-engine/src/blueprint.rs` (after the `Join2` fixture and the schema test): ```rust /// A 1-input f64 node, one f64 output. Test-local fixture. struct Pass1 { out: [Scalar; 1], } impl Node for Pass1 { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }], output: vec![FieldSpec { name: "v", kind: ScalarKind::F64 }], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let w = ctx.f64_in(0); if w.is_empty() { return None; } self.out[0] = Scalar::F64(w[0]); Some(&self.out) } } /// A pure consumer with one f64 input and no output (sink role, C8). struct SinkF64; impl Node for SinkF64 { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }], output: vec![], } } fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> { None } } /// A pure consumer with one i64 input and no output. Used to provoke a role / /// edge kind mismatch (its slot is i64 where an f64 is fanned in). struct SinkI64; impl Node for SinkI64 { fn schema(&self) -> NodeSchema { NodeSchema { inputs: vec![InputSpec { kind: ScalarKind::I64, lookback: 1, firing: Firing::Any }], output: vec![], } } fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Scalar]> { None } } fn pass1() -> BlueprintNode { BlueprintNode::Leaf(Box::new(Pass1 { out: [Scalar::F64(0.0)] })) } fn join2() -> BlueprintNode { BlueprintNode::Leaf(Box::new(Join2 { out: [Scalar::F64(0.0)] })) } /// A composite: two Pass1 leaves feeding a Join2, role 0 fanning the source /// into BOTH Pass1 slots, output = the Join2 field 0. The generic analogue of /// the SMA-cross shape. fn fan_composite() -> Composite { Composite::new( vec![pass1(), pass1(), join2()], 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 }]], OutPort { node: 2, field: 0 }, ) } #[test] fn single_composite_inlines_with_offset_fan_and_output() { // composite as item 0; a source into its role 0; an edge out of it to a sink. let bp = Blueprint::new( vec![BlueprintNode::Composite(fan_composite()), BlueprintNode::Leaf(Box::new(SinkF64))], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ); let (nodes, sources, edges) = bp.compile().expect("valid composite"); // 3 interior nodes (Pass1, Pass1, Join2) at flat 0..2, then SinkF64 at 3 assert_eq!(nodes.len(), 4); // interior edges rewritten at offset 0, then the output edge resolves the // composite's OutPort (interior node 2, field 0) to the sink (flat node 3) assert_eq!( edges, vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, ] ); // the source target into role 0 fanned into BOTH Pass1 slots assert_eq!(sources.len(), 1); assert_eq!( sources[0].targets, vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }] ); } #[test] fn nested_composite_inlines() { // outer composite wraps the inner fan_composite as its only interior item, // re-exposing the inner's role 0 (outer role 0 -> inner role 0) and the // inner's output. A source into the outer role 0 must fan to BOTH inner // Pass1 slots; the inner Join2 lands at flat index 2. let inner = fan_composite(); let outer = Composite::new( vec![BlueprintNode::Composite(inner)], vec![], vec![vec![Target { node: 0, slot: 0 }]], OutPort { node: 0, field: 0 }, ); let bp = Blueprint::new( vec![BlueprintNode::Composite(outer), BlueprintNode::Leaf(Box::new(SinkF64))], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ); let (nodes, sources, edges) = bp.compile().expect("valid nested composite"); assert_eq!(nodes.len(), 4); // Pass1, Pass1, Join2, SinkF64 assert_eq!( sources[0].targets, vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }] ); // inner interior edges + the output edge from the inner Join2 (flat 2) to sink assert_eq!( edges, vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, ] ); } #[test] fn bad_interior_index_rejected() { // interior edge references interior node 9, which does not exist let c = Composite::new( vec![pass1()], vec![Edge { from: 0, to: 9, slot: 0, from_field: 0 }], vec![vec![Target { node: 0, slot: 0 }]], OutPort { node: 0, field: 0 }, ); let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); assert_eq!(bp.compile().unwrap_err(), CompileError::BadInteriorIndex); } #[test] fn role_kind_mismatch_rejected() { // role 0 fans into a Pass1 f64 slot AND a SinkI64 i64 slot -> mismatch let c = Composite::new( vec![pass1(), BlueprintNode::Leaf(Box::new(SinkI64))], vec![], vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]], OutPort { node: 0, field: 0 }, ); let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); assert_eq!(bp.compile().unwrap_err(), CompileError::RoleKindMismatch { role: 0 }); } #[test] fn output_port_out_of_range_rejected() { // output names field 5 of a node whose output has one field let c = Composite::new( vec![pass1()], vec![], vec![vec![Target { node: 0, slot: 0 }]], OutPort { node: 0, field: 5 }, ); let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]); assert_eq!(bp.compile().unwrap_err(), CompileError::OutputPortOutOfRange); } #[test] fn bootstrap_error_is_wrapped() { // a top-level kind mismatch: a Pass1 f64 output wired into a SinkI64 i64 // input. compile() lowers it faithfully; bootstrap's kind-check rejects it. let bp = Blueprint::new( vec![pass1(), BlueprintNode::Leaf(Box::new(SinkI64))], vec![], vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], ); match bp.bootstrap().unwrap_err() { CompileError::Bootstrap(BootstrapError::KindMismatch { producer, consumer }) => { assert_eq!(producer, ScalarKind::F64); assert_eq!(consumer, ScalarKind::I64); } other => panic!("expected Bootstrap(KindMismatch), got {other:?}"), } } ``` - [ ] **Step 2: Run the inliner tests to verify they fail** Run: `cargo test -p aura-engine blueprint::tests` Expected: FAIL — compile error `no method named \`compile\` found` / `no method named \`bootstrap\`` / `cannot find type \`CompileError\`` (the inliner does not exist yet). - [ ] **Step 3: Add `CompileError`, the lowering helpers, and `impl Blueprint`** In `crates/aura-engine/src/blueprint.rs`, add the `CompileError` enum immediately after the `Composite` impl block (before `pub struct Blueprint`): ```rust /// A construction-phase fault, caught before the flat graph reaches /// `Harness::bootstrap`. #[derive(Debug, PartialEq, Eq)] pub enum CompileError { /// An interior edge, role target, or output index is out of range. BadInteriorIndex, /// Input role `role` fans into interior slots of differing scalar kinds. RoleKindMismatch { role: usize }, /// The output port names a missing interior node or output field. OutputPortOutOfRange, /// The lowered flat graph failed `Harness::bootstrap`'s checks (kind /// mismatch, bad index, or directed cycle). Bootstrap(BootstrapError), } ``` Then add the `compile` and `bootstrap` methods inside `impl Blueprint` (after `new`): ```rust /// Lower to the flat graph: inline every composite (recursive), offset /// interior indices, rewrite edges, and fan input roles out. The run loop and /// `bootstrap`'s data model are unchanged; the lowered flat graph is wired by raw /// index (C23). // The flat triple is exactly `Harness::bootstrap`'s argument list; naming it // would be a speculative type alias this cycle (same call as the CLI's sample). #[allow(clippy::type_complexity)] pub fn compile(self) -> Result<(Vec>, Vec, Vec), CompileError> { let mut flat_nodes: Vec> = Vec::new(); let mut flat_edges: Vec = Vec::new(); // lower every top-level item (recursively inlining composites) let lowerings = lower_items(self.nodes, &mut flat_nodes, &mut flat_edges)?; // rewrite top-level edges through the lowerings (fan-out into composites) for e in &self.edges { for fe in rewrite_edge(e, &lowerings, &flat_nodes)? { flat_edges.push(fe); } } // rewrite sources: each target into a composite fans into its role targets let mut flat_sources: Vec = Vec::with_capacity(self.sources.len()); for src in &self.sources { let mut targets: Vec = Vec::new(); for t in &src.targets { targets.extend(resolve_target(t, &lowerings)?); } flat_sources.push(SourceSpec { kind: src.kind, targets }); } Ok((flat_nodes, flat_sources, flat_edges)) } /// Compile, then hand the flat graph to the unchanged `Harness::bootstrap`. pub fn bootstrap(self) -> Result { let (nodes, sources, edges) = self.compile()?; Harness::bootstrap(nodes, sources, edges).map_err(CompileError::Bootstrap) } ``` Finally add the free lowering helpers and the `ItemLowering` enum at the end of the file (after the `impl Blueprint` block, before `#[cfg(test)] mod tests`): ```rust /// How one blueprint item resolved into the flat graph. Edges and source /// targets to/from an item are resolved through this. enum ItemLowering { /// A leaf lowered to exactly one flat node at this index. Leaf { index: usize }, /// A composite lowered to its interior: its single output port is this flat /// `(node, field)`, and input role `r` fans into `roles[r]` (flat targets). Composite { output: (usize, usize), roles: Vec> }, } /// Lower a list of blueprint items into the flat node array, appending interior /// nodes and (for composites) their interior edges. Returns one `ItemLowering` per /// input item, in order. fn lower_items( items: Vec, flat_nodes: &mut Vec>, flat_edges: &mut Vec, ) -> Result, CompileError> { let mut lowerings = Vec::with_capacity(items.len()); for item in items { match item { BlueprintNode::Leaf(node) => { let index = flat_nodes.len(); flat_nodes.push(node); lowerings.push(ItemLowering::Leaf { index }); } BlueprintNode::Composite(c) => { lowerings.push(inline_composite(c, flat_nodes, flat_edges)?); } } } Ok(lowerings) } /// Inline one composite: recursively lower its interior items, rewrite its interior /// edges, then resolve its output port and per-role flat targets. fn inline_composite( c: Composite, flat_nodes: &mut Vec>, flat_edges: &mut Vec, ) -> Result { let Composite { nodes, edges, input_roles, output } = c; let item_count = nodes.len(); // the output port must name an in-range interior item (field range checked // once the item's lowering is known) if output.node >= item_count { return Err(CompileError::OutputPortOutOfRange); } // recursively lower interior items, then rewrite interior edges through them let interior = lower_items(nodes, flat_nodes, flat_edges)?; for e in &edges { for fe in rewrite_edge(e, &interior, flat_nodes)? { flat_edges.push(fe); } } // resolve the output port to a flat (node, field) let out = match &interior[output.node] { ItemLowering::Leaf { index } => { if output.field >= flat_nodes[*index].schema().output.len() { return Err(CompileError::OutputPortOutOfRange); } (*index, output.field) } ItemLowering::Composite { output: nested, .. } => { // a nested composite exposes exactly one output field if output.field != 0 { return Err(CompileError::OutputPortOutOfRange); } *nested } }; // resolve each input role to flat targets (a target into a nested composite // fans further) and kind-check every role let mut roles: Vec> = Vec::with_capacity(input_roles.len()); for (r, role) in input_roles.iter().enumerate() { let mut flat_targets: Vec = Vec::new(); for t in role { flat_targets.extend(resolve_target(t, &interior)?); } if let Some((first, rest)) = flat_targets.split_first() { let k0 = slot_kind(*first, flat_nodes)?; for ft in rest { if slot_kind(*ft, flat_nodes)? != k0 { return Err(CompileError::RoleKindMismatch { role: r }); } } } roles.push(flat_targets); } Ok(ItemLowering::Composite { output: out, roles }) } /// Rewrite one blueprint-level edge into flat edges. The `from` endpoint resolves /// to a single flat producer `(node, field)`; the `to` endpoint may fan out (a /// composite input role fans into several interior targets). fn rewrite_edge( e: &Edge, lowerings: &[ItemLowering], flat_nodes: &[Box], ) -> Result, CompileError> { if e.from >= lowerings.len() { return Err(CompileError::BadInteriorIndex); } let (from_node, from_field) = match &lowerings[e.from] { ItemLowering::Leaf { index } => { if e.from_field >= flat_nodes[*index].schema().output.len() { return Err(CompileError::BadInteriorIndex); } (*index, e.from_field) } ItemLowering::Composite { output, .. } => { // a composite exposes one output field; reading any other is malformed if e.from_field != 0 { return Err(CompileError::BadInteriorIndex); } *output } }; let targets = resolve_target(&Target { node: e.to, slot: e.slot }, lowerings)?; Ok(targets .into_iter() .map(|t| Edge { from: from_node, to: t.node, slot: t.slot, from_field }) .collect()) } /// Resolve a blueprint-level target `(node, slot)` into flat target(s). A target /// into a leaf is itself (remapped index); a target into a composite fans into /// that composite's input-role flat targets. fn resolve_target(t: &Target, lowerings: &[ItemLowering]) -> Result, CompileError> { if t.node >= lowerings.len() { return Err(CompileError::BadInteriorIndex); } match &lowerings[t.node] { ItemLowering::Leaf { index } => Ok(vec![Target { node: *index, slot: t.slot }]), ItemLowering::Composite { roles, .. } => { let role = roles.get(t.slot).ok_or(CompileError::BadInteriorIndex)?; Ok(role.clone()) } } } /// The declared scalar kind of a flat node's input slot (for role kind-checking). fn slot_kind(t: Target, flat_nodes: &[Box]) -> Result { flat_nodes[t.node] .schema() .inputs .get(t.slot) .map(|spec| spec.kind) .ok_or(CompileError::BadInteriorIndex) } ``` - [ ] **Step 4: Add `CompileError` to the `lib.rs` re-export** In `crates/aura-engine/src/lib.rs`, extend the blueprint re-export line so it reads: ```rust pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutPort}; ``` - [ ] **Step 5: Run the inliner tests to verify they pass** Run: `cargo test -p aura-engine blueprint::tests` Expected: PASS — `single_composite_inlines_with_offset_fan_and_output`, `nested_composite_inlines`, `bad_interior_index_rejected`, `role_kind_mismatch_rejected`, `output_port_out_of_range_rejected`, `bootstrap_error_is_wrapped`, and `composite_schema_derives_role_and_output_kinds` all pass; `0 failed`. --- ### Task 3: Headline acceptance — composite ≡ hand-wired, bit-for-bit (C1) **Files:** - Test: `crates/aura-engine/src/blueprint.rs` (inline `mod tests`) - [ ] **Step 1: Write the failing bit-identity demonstrator + fixtures** Append to the existing `#[cfg(test)] mod tests` in `crates/aura-engine/src/blueprint.rs`. Extend the test-module imports — change the existing `use` lines at the top of `mod tests` to also bring in the timestamp type, the std channel, and the `aura-std` nodes: ```rust use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Scalar, Timestamp}; use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; use std::sync::mpsc; ``` (The first line replaces the Task-1 `use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Scalar};` line; the two new lines are added below it.) Then append the fixtures and the headline test: ```rust /// The built-in synthetic price stream (a local copy of the CLI sample's /// stream): rises through t=4 then reverses, so the trace is non-degenerate. fn synthetic_prices() -> Vec<(Timestamp, Scalar)> { [ (1_i64, 1.0000_f64), (2, 1.0010), (3, 1.0030), (4, 1.0060), (5, 1.0040), (6, 1.0010), (7, 0.9990), ] .iter() .map(|&(t, p)| (Timestamp(t), Scalar::F64(p))) .collect() } /// Today's flat, hand-wired SMA-cross signal-quality harness (the /// `sample_harness` wiring from `aura-cli`), with two recording sinks. #[allow(clippy::type_complexity)] fn hand_wired_sma_cross_harness() -> ( Harness, mpsc::Receiver<(Timestamp, Vec)>, mpsc::Receiver<(Timestamp, Vec)>, ) { let (tx_eq, rx_eq) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel(); let h = Harness::bootstrap( vec![ Box::new(Sma::new(2)), Box::new(Sma::new(4)), Box::new(Sub::new()), Box::new(Exposure::new(0.5)), Box::new(SimBroker::new(0.0001)), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex)), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }, Target { node: 4, slot: 1 }, ], }], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, Edge { from: 3, to: 4, slot: 0, from_field: 0 }, Edge { from: 4, to: 5, slot: 0, from_field: 0 }, Edge { from: 3, to: 6, slot: 0, from_field: 0 }, ], ) .expect("valid hand-wired DAG"); (h, rx_eq, rx_ex) } /// The SMA-cross signal as a reusable composite: one input role (price), one /// output (the fast-minus-slow spread). Interior wired with raw local indices. fn sma_cross(fast: usize, slow: usize) -> Composite { Composite::new( vec![Sma::new(fast).into(), Sma::new(slow).into(), Sub::new().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 }]], OutPort { node: 2, field: 0 }, ) } /// The same signal-quality harness authored as a composite blueprint. #[allow(clippy::type_complexity)] fn composite_sma_cross_harness() -> ( Blueprint, mpsc::Receiver<(Timestamp, Vec)>, mpsc::Receiver<(Timestamp, Vec)>, ) { let (tx_eq, rx_eq) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel(); let bp = Blueprint::new( vec![ BlueprintNode::Composite(sma_cross(2, 4)), Exposure::new(0.5).into(), SimBroker::new(0.0001).into(), Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq).into(), Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex).into(), ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, // price -> sma_cross role 0 Target { node: 2, slot: 1 }, // price -> SimBroker price slot ], }], vec![ Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // composite out -> Exposure Edge { from: 1, to: 2, slot: 0, from_field: 0 }, // exposure -> broker slot 0 Edge { from: 2, to: 3, slot: 0, from_field: 0 }, // equity -> sink Edge { from: 1, to: 4, slot: 0, from_field: 0 }, // exposure -> sink ], ); (bp, rx_eq, rx_ex) } #[test] fn composite_sma_cross_runs_bit_identical_to_hand_wired() { let prices = synthetic_prices(); // (a) today's flat, hand-wired graph let (mut flat, flat_eq, flat_ex) = hand_wired_sma_cross_harness(); flat.run(vec![prices.clone()]); // (b) the same graph authored as a composite blueprint, compiled let (bp, comp_eq, comp_ex) = composite_sma_cross_harness(); let mut composed = bp.bootstrap().expect("composite blueprint compiles"); composed.run(vec![prices]); let flat_eq_v = flat_eq.try_iter().collect::>(); let flat_ex_v = flat_ex.try_iter().collect::>(); let comp_eq_v = comp_eq.try_iter().collect::>(); let comp_ex_v = comp_ex.try_iter().collect::>(); // both recording sinks captured the same equity + exposure traces, bit-for-bit assert_eq!(flat_eq_v, comp_eq_v, "equity traces differ"); assert_eq!(flat_ex_v, comp_ex_v, "exposure traces differ"); // and the trace is populated (non-degenerate), so the equality is meaningful assert!(!comp_eq_v.is_empty(), "equity trace must be populated"); assert!(!comp_ex_v.is_empty(), "exposure trace must be populated"); } ``` - [ ] **Step 2: Run the headline test to verify it passes** Run: `cargo test -p aura-engine blueprint::tests::composite_sma_cross_runs_bit_identical_to_hand_wired` Expected: PASS (`test result: ok. 1 passed`). This is an acceptance test over the inliner built in Task 2 — the RED/GREEN boundary for the bit-identity property is between Task 2 (no `bootstrap()`) and Task 3. To confirm the assertion is load-bearing (not vacuously green on an empty trace), the test asserts the drained traces are non-empty; if it ever reports `0 passed; 0 filtered`, the test name in the filter is wrong — fall back to Step 3's unfiltered run. - [ ] **Step 3: Run the full engine test suite to verify everything passes** Run: `cargo test -p aura-engine` Expected: PASS — `composite_sma_cross_runs_bit_identical_to_hand_wired` passes alongside all Task-1/Task-2 tests and the pre-existing harness/report tests; `0 failed`. - [ ] **Step 4: Verify the workspace builds clean and lint is green** Run: `cargo clippy --workspace --all-targets -- -D warnings` Expected: PASS — no warnings. (Confirms no dead-code / unused-import regressions from the new module and that the demonstrator does not perturb the rest of the workspace.)