Files
Aura/docs/plans/0019-name-composite-boundary.md
T
Brummel c9115e3e0f plan: 0019 name composite boundary
Four-task plan executing spec 0019, sequenced like cycle 0018 (type-shape
change rippling engine -> CLI):

- T1 engine type migration, behaviour-preserving: Role/ParamAlias types,
  Composite struct/new/accessors, inline_composite role-walk, lib.rs
  re-export, all engine test Composite::new sites. Gate per-crate
  (cargo test -p aura-engine, NOT --workspace; aura-cli compiles in T3).
  The new params field is dormant -> param_space byte-identical, C23
  anchor goldens green.
- T2 param aliasing RED-first: collect_params alias relabel + inline_composite
  alias range-validation (-> BadInteriorIndex, reusing the variant), with
  4 new unit tests (relabel, out-of-range, unaliased regression, partial).
- T3 CLI: render_definition [in:name]/[param:name] markers; MACD author
  site (named role + fast/slow/signal aliases, the worked example);
  sma_cross named role only; blueprint_view_golden re-capture;
  compiled_view_golden stays byte-identical (C23 guard).
- T4 fieldtests sweep (separate workspace root -> explicit cargo build
  gate, guarding the #42 latent-drift recurrence) + full --workspace
  triple (-D warnings).

Aliasing demonstrated on the CLI MACD site only (the spec's evidence);
every other composite site gets the forced role-name + empty params, so
the param_space C23 anchor goldens stay byte-identical.

refs #41
2026-06-08 02:08:57 +02:00

28 KiB

Name the Composite Boundary — Implementation Plan

Parent spec: docs/specs/0019-name-composite-boundary.md

For agentic workers: REQUIRED SUB-SKILL: use the implement skill to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Make composite input roles and params named projections (Role { name, targets }, ParamAlias { name, node, slot }) — the same shape #40 gave outputs — surfacing the full named boundary signature in the blueprint render, with the param-space sweep surface unchanged (pure naming overlay, C23).

Architecture: A type-shape change that ripples engine → CLI, sequenced like cycle 0018. Task 1 migrates the engine types behaviour-preserving (the new params field is dormant; gate is per-crate cargo test -p aura-engine, NOT --workspaceaura-cli will not compile until Task 3, which is expected). Task 2 wires param aliasing into param_space() RED-first. Task 3 does the CLI render + author sites. Task 4 sweeps the out-of-CI fixtures (separate workspace root) and runs the full --workspace triple. Aliasing is demonstrated on the CLI MACD site only (the spec's worked example); every other composite site gets the forced role-name + params: vec![], so the param-space C23 anchor goldens stay byte-identical.

Tech Stack: crates/aura-engine/src/blueprint.rs (types, collect_params, inline_composite), crates/aura-engine/src/lib.rs (re-export), crates/aura-cli/src/graph.rs (render_definition), crates/aura-cli/src/main.rs (author sites + render goldens), fieldtests/milestone-construction-layer/mc_*.rs.


Files this plan creates or modifies:

  • Modify: crates/aura-engine/src/blueprint.rs — new Role/ParamAlias types; Composite struct + new + accessors; collect_params aliasing; inline_composite role-walk + alias validation; test-site migration + 4 new unit tests.
  • Modify: crates/aura-engine/src/lib.rs:38 — re-export Role, ParamAlias.
  • Modify: crates/aura-cli/src/graph.rs:105-131render_definition named roles + param markers.
  • Modify: crates/aura-cli/src/main.rsmacd (:186) + sma_cross (:121) author sites; test sites (:382, :391, :398); blueprint_view_golden (:469) re-capture; compiled_view_golden (:510) must stay byte-identical.
  • Modify: fieldtests/milestone-construction-layer/mc_1..mc_4*.rs — role literals + params arg + input_roles() reads.

Task 1: Engine type migration (behaviour-preserving)

Files:

  • Modify: crates/aura-engine/src/blueprint.rs:43-93 (types, struct, new, accessors), :309,342-357 (inline_composite), :525-1152 (test Composite::new sites)

  • Modify: crates/aura-engine/src/lib.rs:38

  • Step 1: Add Role and ParamAlias struct defs

Insert both, immediately before the Composite struct (currently blueprint.rs:49), after the BlueprintNode impl that ends at :42:

/// One named input role: role `r` (by position) fans the source value into
/// `targets`. The `name` is a non-load-bearing render symbol (C23); identity is
/// the role index, which survives lowering — the name does not.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Role {
    pub name: String,
    pub targets: Vec<Target>,
}

/// A composite-level alias relabelling one interior leaf param slot's surface
/// name in `param_space()`. `node` is the interior item index, `slot` the param
/// slot within that leaf. Pure legibility: the alias relabels in place and never
/// reorders, adds, or removes a slot (C23 — identity stays the slot).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamAlias {
    pub name: String,
    pub node: usize,
    pub slot: usize,
}
  • Step 2: Change the Composite struct fields

blueprint.rs:49-55. Replace:

pub struct Composite {
    name: String,
    nodes: Vec<BlueprintNode>,
    edges: Vec<Edge>,
    input_roles: Vec<Vec<Target>>,
    output: Vec<OutField>,
}

with:

pub struct Composite {
    name: String,
    nodes: Vec<BlueprintNode>,
    edges: Vec<Edge>,
    input_roles: Vec<Role>,
    params: Vec<ParamAlias>,
    output: Vec<OutField>,
}
  • Step 3: Change Composite::new signature + body

blueprint.rs:62-70. Replace:

    pub fn new(
        name: impl Into<String>,
        nodes: Vec<BlueprintNode>,
        edges: Vec<Edge>,
        input_roles: Vec<Vec<Target>>,
        output: Vec<OutField>,
    ) -> Self {
        Self { name: name.into(), nodes, edges, input_roles, output }
    }

with (new params arg between input_roles and output):

    pub fn new(
        name: impl Into<String>,
        nodes: Vec<BlueprintNode>,
        edges: Vec<Edge>,
        input_roles: Vec<Role>,
        params: Vec<ParamAlias>,
        output: Vec<OutField>,
    ) -> Self {
        Self { name: name.into(), nodes, edges, input_roles, params, output }
    }
  • Step 4: Update input_roles() accessor return type + add params()

blueprint.rs:84-87. Replace:

    /// The input roles: role `r` fans into `input_roles()[r]` interior targets.
    pub fn input_roles(&self) -> &[Vec<Target>] {
        &self.input_roles
    }

with:

    /// The input roles: role `r` fans into `input_roles()[r].targets` interior
    /// targets, under the boundary name `input_roles()[r].name` (C23 — name is a
    /// render symbol, identity is the role index).
    pub fn input_roles(&self) -> &[Role] {
        &self.input_roles
    }
    /// The param aliases: each relabels one interior leaf param slot's surface
    /// name in `param_space()` (pure naming overlay; identity stays the slot, C23).
    pub fn params(&self) -> &[ParamAlias] {
        &self.params
    }
  • Step 5: Re-export the new types from lib.rs

crates/aura-engine/src/lib.rs:38. Replace:

pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField};

with:

pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField, ParamAlias, Role};
  • Step 6: Migrate inline_composite — destructure + role-walk

blueprint.rs:309. Replace:

    let Composite { name: _, nodes, edges, input_roles, output } = c;

with (params do NOT lower — bound _ here in Task 1; Task 2 wires alias validation):

    let Composite { name: _, nodes, edges, input_roles, params: _, output } = c;

Then blueprint.rs:345 — the inner role-target loop. Replace:

        for t in role {

with (role is now &Role, not &Vec<Target>):

        for t in &role.targets {

(The accumulator let mut roles: Vec<Vec<Target>> at :342, the for (r, role) in input_roles.iter().enumerate() at :343, and ItemLowering::Composite { ..., roles } at :256/:359 are the internal flat-roles type — names already dropped — and do NOT change.)

  • Step 7: Migrate every engine-test Composite::new call site

Composite::new gained an arg and input_roles changed element type, so all #[cfg(test)] construction sites break. Apply this exact transform at each — (a) wrap each bare role vec vec![Target { .. }, ..] as Role { name: "price".into(), targets: vec![Target { .. }, .. ] }, and (b) insert vec![] (empty params) as the new arg immediately before the output (final) arg. No aliases on any engine test site — that keeps the param-space goldens byte-identical (these sites are the C23 anchors).

Worked example — the sma_cross() test helper at blueprint.rs:847-858. Replace:

        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() }],
        )

with:

        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![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() }],
        )

Apply the identical transform at every other engine-test Composite::new (the role-literal line is named for each): :525 (role lit :532), :571 (:576-577, two roles), :622 (:626), :656 (:661-662), :669 (:674-675), :707 (:711), :722 (:726), :737 (:741), :753 (:757), :938 (:943-944, two roles), :1086 (:1093), :1097 (:1101), :1137 (:1144), :1148 (:1152). A site with two roles wraps each as its own Role { name, targets } — give the second role a distinct name (e.g. "price2" / a name matching its semantics); the role name is never asserted by these tests (they assert param_space() names or run behaviour), so any non-empty name is safe. The Step-8 compiler gate enumerates any site missed.

  • Step 8: Build + test the engine crate (behaviour-preserving gate)

Run: cargo test -p aura-engine Expected: PASS — compiles and all existing tests green. The new params field is dormant (not yet consulted), the role-walk is equivalent, no aliases anywhere, so param_space() output is byte-identical: the anchors param_space_mirrors_compiled_flat_node_param_order, param_space_mirrors_compiled_flat_node_param_order_under_nesting, and param_space_is_flat_path_qualified_and_slot_disambiguated stay green unchanged. (aura-cli is NOT built here — -p aura-engine is per-crate; --workspace is deferred to Task 4, exactly as cycle 0018 sequenced. An unused-arg warning on the params: _ / dormant field is acceptable at this gate — Task 2 consumes them, and the -D warnings clippy gate is Task 4.)


Task 2: Param aliasing in param_space() (RED-first)

Files:

  • Modify: crates/aura-engine/src/blueprint.rs:151 (param_space call), :223-246 (collect_params), :309-310 (inline_composite validation)

  • Test: crates/aura-engine/src/blueprint.rs #[cfg(test)] — 4 new tests

  • Step 1: Write the RED test — alias relabels in place

Add to the #[cfg(test)] module (near the other param_space tests, ~:1083):

    #[test]
    fn param_alias_relabels_param_space_name_in_place() {
        // two Sma leaves (each one `length` param) under a composite that aliases
        // slot 0 of node 0 -> "shortLen" and slot 0 of node 1 -> "longLen".
        let c = Composite::new(
            "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![Role {
                name: "price".into(),
                targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
            }],
            vec![
                ParamAlias { name: "shortLen".into(), node: 0, slot: 0 },
                ParamAlias { name: "longLen".into(), node: 1, slot: 0 },
            ],
            vec![OutField { node: 2, field: 0, name: "out".into() }],
        );
        let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
        let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
        // aliased in place: names are the aliases, NOT two duplicate "cross.length".
        assert_eq!(names, vec!["cross.shortLen".to_string(), "cross.longLen".to_string()]);
    }
  • Step 2: Run the RED test

Run: cargo test -p aura-engine param_alias_relabels_param_space_name_in_place Expected: FAIL — collect_params ignores params, so names are ["cross.length", "cross.length"] (assertion left/right mismatch).

  • Step 3: Thread aliases through collect_params + seed param_space

blueprint.rs:149-153 — the param_space body. Replace:

    pub fn param_space(&self) -> Vec<ParamSpec> {
        let mut out = Vec::new();
        collect_params(&self.nodes, "", &mut out);
        out
    }

with:

    pub fn param_space(&self) -> Vec<ParamSpec> {
        let mut out = Vec::new();
        collect_params(&self.nodes, "", &[], &mut out);
        out
    }

Then blueprint.rs:223-246 — replace the whole collect_params fn with:

fn collect_params(
    items: &[BlueprintNode],
    prefix: &str,
    aliases: &[ParamAlias],
    out: &mut Vec<ParamSpec>,
) {
    for (i, item) in items.iter().enumerate() {
        match item {
            BlueprintNode::Leaf(factory) => {
                for (s, p) in factory.params().iter().enumerate() {
                    // an alias for this exact (node, slot) relabels in place;
                    // otherwise the factory param name, as today.
                    let local = aliases
                        .iter()
                        .find(|a| a.node == i && a.slot == s)
                        .map(|a| a.name.as_str())
                        .unwrap_or(p.name.as_str());
                    let name = if prefix.is_empty() {
                        local.to_string()
                    } else {
                        format!("{prefix}.{local}")
                    };
                    out.push(ParamSpec { name, kind: p.kind });
                }
            }
            BlueprintNode::Composite(c) => {
                let child = if prefix.is_empty() {
                    c.name().to_string()
                } else {
                    format!("{prefix}.{}", c.name())
                };
                collect_params(c.nodes(), &child, c.params(), out);
            }
        }
    }
}
  • Step 4: Run the test to verify GREEN

Run: cargo test -p aura-engine param_alias_relabels_param_space_name_in_place Expected: PASS.

  • Step 5: Write the RED test — out-of-range alias rejected at compile

Add to the #[cfg(test)] module:

    #[test]
    fn out_of_range_param_alias_rejected() {
        // alias names node 9 (no such interior item) -> caught at compile.
        let c = Composite::new(
            "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![Role {
                name: "price".into(),
                targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
            }],
            vec![ParamAlias { name: "bogus".into(), node: 9, slot: 0 }],
            vec![OutField { node: 2, field: 0, name: "out".into() }],
        );
        let bp = Blueprint::new(
            vec![BlueprintNode::Composite(c)],
            vec![SourceSpec { kind: ScalarKind::F64, targets: vec![] }],
            vec![],
        );
        // two Sma leaves => two i64 length slots; supply a matching vector so the
        // ONLY error is the bad alias, not arity.
        let err = bp.compile_with_params(&[Scalar::I64(2), Scalar::I64(4)]).unwrap_err();
        assert_eq!(err, CompileError::BadInteriorIndex);
    }
  • Step 6: Run the RED test

Run: cargo test -p aura-engine out_of_range_param_alias_rejected Expected: FAIL — no alias validation yet (compile succeeds or errors with a different variant), so unwrap_err()/assert_eq! mismatches.

  • Step 7: Add alias range-validation in inline_composite

blueprint.rs:309 — bind params (was params: _ from Task 1):

    let Composite { name: _, nodes, edges, input_roles, params, output } = c;

Then immediately after let item_count = nodes.len(); (:310), insert the validation loop (before nodes is moved into lower_items at :313):

    // an alias must name a real interior leaf param slot (C23 — names are cosmetic
    // but a dangling handle is an author error). Mirrors the output range-check.
    for a in &params {
        let ok = a.node < item_count
            && matches!(&nodes[a.node], BlueprintNode::Leaf(f) if a.slot < f.params().len());
        if !ok {
            return Err(CompileError::BadInteriorIndex);
        }
    }
  • Step 8: Run the test to verify GREEN

Run: cargo test -p aura-engine out_of_range_param_alias_rejected Expected: PASS.

  • Step 9: Add the regression + partial-aliasing tests

Add both to the #[cfg(test)] module:

    #[test]
    fn unaliased_params_keep_factory_names() {
        // no aliases => param_space identical to the pre-#41 path-qualified names.
        let c = Composite::new(
            "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![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() }],
        );
        let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
        let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
        assert_eq!(names, vec!["cross.length".to_string(), "cross.length".to_string()]);
    }

    #[test]
    fn partial_aliasing_relabels_only_the_named_slot() {
        // alias node 0 only; node 1 keeps its factory name; order intact.
        let c = Composite::new(
            "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![Role {
                name: "price".into(),
                targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
            }],
            vec![ParamAlias { name: "shortLen".into(), node: 0, slot: 0 }],
            vec![OutField { node: 2, field: 0, name: "out".into() }],
        );
        let bp = Blueprint::new(vec![BlueprintNode::Composite(c)], vec![], vec![]);
        let names: Vec<String> = bp.param_space().into_iter().map(|p| p.name).collect();
        assert_eq!(names, vec!["cross.shortLen".to_string(), "cross.length".to_string()]);
    }
  • Step 10: Run the full engine suite (anchors stay green)

Run: cargo test -p aura-engine Expected: PASS — the four new tests green; the C23 anchors (param_space_mirrors_compiled_flat_node_param_order and the _under_nesting + _disambiguated siblings) still green unchanged (no aliases on those fixtures, so their name goldens are byte-identical).


Task 3: CLI render + author sites

Files:

  • Modify: crates/aura-cli/src/graph.rs:105-131

  • Modify: crates/aura-cli/src/main.rs:121-132 (sma_cross), :186-213 (macd), :382, :391-404, :469-507 (blueprint golden), :561-569 (macd render test)

  • Step 1: Render named roles + param markers in render_definition

graph.rs:117-128. Replace the input-role loop + (keep) output loop:

    for (role, targets) in c.input_roles().iter().enumerate() {
        let in_id = labels.len();
        labels.push(format!("in:{role}"));
        for t in targets {
            edges.push((in_id, t.node));
        }
    }
    for of in c.output() {
        let out_id = labels.len();
        labels.push(format!("out:{}", of.name));
        edges.push((of.node, out_id));
    }

with (roles read .name/.targets; new [param:<name>] marker loop wired marker → configured leaf, like [in:]):

    for role in c.input_roles() {
        let in_id = labels.len();
        labels.push(format!("in:{}", role.name));
        for t in &role.targets {
            edges.push((in_id, t.node));
        }
    }
    for a in c.params() {
        let p_id = labels.len();
        labels.push(format!("param:{}", a.name));
        edges.push((p_id, a.node));
    }
    for of in c.output() {
        let out_id = labels.len();
        labels.push(format!("out:{}", of.name));
        edges.push((of.node, out_id));
    }

Also update the doc comment at graph.rs:101-104 to mention the [in:<name>] and [param:<name>] markers (replace [in:k] with [in:<name>]; add the param marker clause). Cosmetic; keep it accurate.

  • Step 2: MACD author site — named role + aliases (the worked example)

main.rs:203-211 — the role + output args of macd(...). Replace:

        vec![vec![
            Target { node: 0, slot: 0 }, // price → fast EMA
            Target { node: 1, slot: 0 }, // price → slow EMA
        ]],
        vec![
            OutField { node: 2, field: 0, name: "macd".into() },      // the MACD line
            OutField { node: 3, field: 0, name: "signal".into() },    // the signal line
            OutField { node: 4, field: 0, name: "histogram".into() }, // the histogram
        ],

with:

        vec![Role {
            name: "price".into(),
            targets: vec![
                Target { node: 0, slot: 0 }, // price → fast EMA
                Target { node: 1, slot: 0 }, // price → slow EMA
            ],
        }],
        vec![
            ParamAlias { name: "fast".into(), node: 0, slot: 0 },   // fast EMA length
            ParamAlias { name: "slow".into(), node: 1, slot: 0 },   // slow EMA length
            ParamAlias { name: "signal".into(), node: 3, slot: 0 }, // signal EMA length
        ],
        vec![
            OutField { node: 2, field: 0, name: "macd".into() },      // the MACD line
            OutField { node: 3, field: 0, name: "signal".into() },    // the signal line
            OutField { node: 4, field: 0, name: "histogram".into() }, // the histogram
        ],

Ensure ParamAlias and Role are imported in main.rs (extend the existing use aura_engine::{... OutField ...} line to include ParamAlias, Role).

  • Step 3: sma_cross author site — named role, no aliases

main.rs:129-130. Replace:

        vec![vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]],
        vec![OutField { node: 2, field: 0, name: "cross".into() }],

with (forced role-rename only; sma_cross stays unaliased — keeps the sample minimal):

        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: "cross".into() }],
  • Step 4: Migrate the two Composite::new sites in the render test

main.rs:391-397 and :398-404 (inside nested_composite_renders_without_panic). Apply the Task-1 transform to each: wrap the role literal (:395, :402) as Role { name: "price".into(), targets: vec![..] }, and insert vec![] before the output arg. No aliases.

  • Step 5: Update the render needle for the renamed role

main.rs:382 — the needle array in blueprint_view_defines_each_composite_once. Replace the "[in:0]" element with "[in:price]" (the sma_cross role is now named price). Keep "[out:cross]" and the type needles unchanged.

  • Step 6: Assert the MACD definition render shows the named signature

main.rs:561-569macd_blueprint_renders_a_nested_composite_definition. After the existing assertions, add (the rendered MACD definition must now carry the named inputs/params):

    assert!(rendered.contains("[in:price]"), "named MACD input role: {rendered}");
    assert!(rendered.contains("[param:fast]"), "aliased fast length: {rendered}");
    assert!(rendered.contains("[param:slow]"), "aliased slow length: {rendered}");
    assert!(rendered.contains("[param:signal]"), "aliased signal length: {rendered}");

(Use the same binding name the test already gives the rendered string; if it is not rendered, match the local name at that site.)

  • Step 7: Re-capture the blueprint_view_golden

Run: cargo test -p aura-cli blueprint_view_golden Expected: FAIL — the golden at main.rs:469-507 still pins [in:0]; the render now emits [in:price] (and the sample sma_cross block re-layouts). Read the assertion failure's "actual" rendered block and paste it wholesale into the golden string literal (do not hand-edit field-by-field). Re-run: PASS.

  • Step 8: Confirm compiled_view_golden is byte-identical (C23 guard)

Run: cargo test -p aura-cli compiled_view_golden Expected: PASS with NO edit to the golden (main.rs:510-535). The boundary names never reach the compilat (C23); if this golden drifts, STOP — that is a bug, not a golden to re-capture.

  • Step 9: Confirm MACD run determinism is unchanged

Run: cargo test -p aura-cli run_macd_compiles_from_nested_composite_and_is_deterministic Expected: PASS — params only gained names; the injected point vector and the run are unchanged.

  • Step 10: Build + test the CLI crate

Run: cargo test -p aura-cli Expected: PASS — aura-cli compiles against the migrated engine, all render/run tests green.


Task 4: Fieldtests sweep + full workspace gate

Files:

  • Modify: fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs:31,44, mc_2_miswire_render.rs:38,46, mc_3_nested_composite.rs:23,34,44,51, mc_4_introspect_graph.rs:22,60,66,109,110

  • Step 1: Port the four fixture author sites + input_roles() reads

These are a separate workspace root (fieldtests/milestone-construction-layer/), not reached by cargo build --workspace. Apply the Task-1 transform to every Composite::new in mc_1..mc_4 — wrap each role literal as Role { name: "price".into(), targets: vec![..] } and insert vec![] before the output arg (no aliases). Sites: mc_1:31 (role lit :44), mc_2:38 (role lit :46), mc_3:23 (:34) and mc_3:44 (:51), mc_4:22.

Then fix the input_roles() read sites in mc_4_introspect_graph.rs, whose element type changed from Vec<Target> to Role:

  • :60, :66 — wherever the role is iterated/printed, read role.targets (and the role's .name is now available to print). Match the current access shape at each line and route it through .targets.
  • :109, :110:110 does input_roles()[0].len(); Role has no .len() → becomes input_roles()[0].targets.len(). Adjust :109 to whatever it reads on the role accordingly. Keep the fixture's introspection axis intact (do not gut the assertion — port it; the fixture demonstrates graph introspection via public accessors).

Add Role (and ParamAlias if referenced) to each fixture's use aura_engine::{...} imports as needed.

  • Step 2: Build the construction-layer fixture crate (separate-workspace gate)

Run: cd fieldtests/milestone-construction-layer && cargo build Expected: exit 0 — all four bins compile. (This gate exists because --workspace does not reach this tree; it is the guard against the 0018 latent-drift recurrence tracked in #42.)

  • Step 3: Full workspace triple (-D warnings)

Run: cargo build --workspace && cargo test --workspace && cargo clippy --workspace --all-targets -- -D warnings Expected: all PASS — engine + CLI compile, every test green, no clippy warnings (the Task-1 params/role threading is now fully consumed, so no dead-code/unused warnings remain).

  • Step 4: Confirm the named MACD param surface (acceptance evidence)

Run: cargo test -p aura-engine && cargo test -p aura-cli Expected: PASS. Then sanity-check the boundary type is fully migrated:

Run: git grep -n "Vec<Vec<Target>>" crates/ Expected: matches ONLY the internal flat-roles accumulator/ItemLowering in blueprint.rs (let mut roles: Vec<Vec<Target>> and the ItemLowering::Composite field) — NOT any Composite boundary field or Composite::new signature. (The boundary now uses Vec<Role>; the internal post-resolution flat type legitimately stays Vec<Vec<Target>>.)