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
This commit is contained in:
@@ -0,0 +1,740 @@
|
||||
# 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 `--workspace`
|
||||
— `aura-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-131` — `render_definition` named roles + param markers.
|
||||
- Modify: `crates/aura-cli/src/main.rs` — `macd` (: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`:
|
||||
|
||||
```rust
|
||||
/// 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:
|
||||
|
||||
```rust
|
||||
pub struct Composite {
|
||||
name: String,
|
||||
nodes: Vec<BlueprintNode>,
|
||||
edges: Vec<Edge>,
|
||||
input_roles: Vec<Vec<Target>>,
|
||||
output: Vec<OutField>,
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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`):
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
/// The input roles: role `r` fans into `input_roles()[r]` interior targets.
|
||||
pub fn input_roles(&self) -> &[Vec<Target>] {
|
||||
&self.input_roles
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
/// 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:
|
||||
|
||||
```rust
|
||||
pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField};
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField, ParamAlias, Role};
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Migrate `inline_composite` — destructure + role-walk**
|
||||
|
||||
`blueprint.rs:309`. Replace:
|
||||
|
||||
```rust
|
||||
let Composite { name: _, nodes, edges, input_roles, output } = c;
|
||||
```
|
||||
|
||||
with (params do NOT lower — bound `_` here in Task 1; Task 2 wires alias validation):
|
||||
|
||||
```rust
|
||||
let Composite { name: _, nodes, edges, input_roles, params: _, output } = c;
|
||||
```
|
||||
|
||||
Then `blueprint.rs:345` — the inner role-target loop. Replace:
|
||||
|
||||
```rust
|
||||
for t in role {
|
||||
```
|
||||
|
||||
with (`role` is now `&Role`, not `&Vec<Target>`):
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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`):
|
||||
|
||||
```rust
|
||||
#[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:
|
||||
|
||||
```rust
|
||||
pub fn param_space(&self) -> Vec<ParamSpec> {
|
||||
let mut out = Vec::new();
|
||||
collect_params(&self.nodes, "", &mut out);
|
||||
out
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
#[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):
|
||||
|
||||
```rust
|
||||
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`):
|
||||
|
||||
```rust
|
||||
// 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 ¶ms {
|
||||
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:
|
||||
|
||||
```rust
|
||||
#[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:
|
||||
|
||||
```rust
|
||||
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:]`):
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
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):
|
||||
|
||||
```rust
|
||||
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-569` — `macd_blueprint_renders_a_nested_composite_definition`. After the
|
||||
existing assertions, add (the rendered MACD definition must now carry the named
|
||||
inputs/params):
|
||||
|
||||
```rust
|
||||
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>>`.)
|
||||
Reference in New Issue
Block a user