# 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<N: Node>` 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<dyn Node>),
    Composite(Composite),
}

/// Ergonomic lift: any concrete `Node` becomes a `Leaf` blueprint item.
impl<N: Node + 'static> From<N> 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<BlueprintNode>,
    edges: Vec<Edge>,
    input_roles: Vec<Vec<Target>>,
    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<BlueprintNode>,
        edges: Vec<Edge>,
        input_roles: Vec<Vec<Target>>,
        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<BlueprintNode>,
    sources: Vec<SourceSpec>,
    edges: Vec<Edge>,
}

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<BlueprintNode>, sources: Vec<SourceSpec>, edges: Vec<Edge>) -> 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<Box<dyn Node>>, Vec<SourceSpec>, Vec<Edge>), CompileError> {
        let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
        let mut flat_edges: Vec<Edge> = 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<SourceSpec> = Vec::with_capacity(self.sources.len());
        for src in &self.sources {
            let mut targets: Vec<Target> = 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<Harness, CompileError> {
        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<Vec<Target>> },
}

/// 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<BlueprintNode>,
    flat_nodes: &mut Vec<Box<dyn Node>>,
    flat_edges: &mut Vec<Edge>,
) -> Result<Vec<ItemLowering>, 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<Box<dyn Node>>,
    flat_edges: &mut Vec<Edge>,
) -> Result<ItemLowering, CompileError> {
    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<Target>> = Vec::with_capacity(input_roles.len());
    for (r, role) in input_roles.iter().enumerate() {
        let mut flat_targets: Vec<Target> = 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<dyn Node>],
) -> Result<Vec<Edge>, 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<Vec<Target>, 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<dyn Node>]) -> Result<ScalarKind, CompileError> {
    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<Scalar>)>,
        mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
    ) {
        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<Scalar>)>,
        mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
    ) {
        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::<Vec<_>>();
        let flat_ex_v = flat_ex.try_iter().collect::<Vec<_>>();
        let comp_eq_v = comp_eq.try_iter().collect::<Vec<_>>();
        let comp_ex_v = comp_ex.try_iter().collect::<Vec<_>>();

        // 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.)
