21c1621bd0
"Compilat" (German "Kompilat") was a coined noun for the product of the
bootstrap compilation — neither English nor a natural fit. The runtime
artifact already has a code identifier for exactly this thing: the
`FlatGraph` struct (harness.rs). Replace the coinage with that identifier:
- prose mentions -> "flat graph" (mirrors the type, reads plainly)
- definitional anchors -> `FlatGraph` (C11, C23, the running-graph line)
- `render_compilat` -> `render_flat_graph` (historical render symbol;
keeps the `render_blueprint` / `render_flat_graph`
source-vs-product pairing)
- `intra-compilat` -> `intra-graph`
- `from_compilat` (test) -> `from_flat`
"compilation" / "re-compilation" / "bootstrap-as-compilation" (the process,
ordinary English) are deliberately left untouched. Behaviour-preserving:
only comments, design ledger, specs/plans, one test-local variable and its
assert messages change. Full workspace test suite green; clippy clean.
624 lines
23 KiB
Markdown
624 lines
23 KiB
Markdown
# Composite multi-output record — Implementation Plan
|
||
|
||
> **Parent spec:** `docs/specs/0018-composite-multi-output-record.md`
|
||
>
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||
|
||
**Goal:** Make a composite's output a named, ordered, multi-field record
|
||
(`output: Vec<OutField { node, field, name }>`) so multi-line indicators
|
||
(MACD/Bollinger/Stochastic/Ichimoku) can be authored as a composition and
|
||
re-exported as a unit, selected downstream by `Edge::from_field`.
|
||
|
||
**Architecture:** The substrate is already multi-field everywhere except the
|
||
composite boundary (`NodeSchema.output: Vec<FieldSpec>`, `Edge::from_field`
|
||
selects a column, leaf multi-output already works). This cycle replaces the
|
||
single `OutPort` with a `Vec<OutField>` and lifts the three `field == 0` caps at
|
||
the boundary (`inline_composite` nested arm, `rewrite_edge` composite arm). Names
|
||
live at the blueprint boundary only and are dropped in the flat graph (C23); C8/C7/C4
|
||
are untouched — one port, one row, K columns.
|
||
|
||
**Tech Stack:** `crates/aura-engine/src/blueprint.rs` (type + compile logic),
|
||
`crates/aura-engine/src/lib.rs` (re-export), `crates/aura-cli/src/graph.rs`
|
||
(render), `crates/aura-cli/src/main.rs` (author sites + render tests),
|
||
`fieldtests/milestone-construction-layer/*.rs` (non-workspace stale-ref sweep).
|
||
|
||
**Files this plan creates or modifies:**
|
||
|
||
- Modify: `crates/aura-engine/src/blueprint.rs` — `OutPort`→`OutField`, `Composite`
|
||
struct/`new`/`output()`, `ItemLowering::Composite.output`, `inline_composite`,
|
||
`rewrite_edge`, all `#[cfg(test)]` `OutPort` literals; new capability tests.
|
||
- Modify: `crates/aura-engine/src/lib.rs:38` — re-export `OutPort`→`OutField`.
|
||
- Modify: `crates/aura-cli/src/graph.rs:105–129` — `render_definition` K `[out:<name>]` markers.
|
||
- Modify: `crates/aura-cli/src/main.rs` — import `:13`, `sma_cross` `:130`, `macd` `:206`,
|
||
nested-render-test literals `:391`/`:398`, needle-list test `:377`, `blueprint_view_golden`.
|
||
- Modify: `fieldtests/milestone-construction-layer/mc_1..mc_4*.rs` — `OutPort`→`OutField` (8 sites, non-workspace).
|
||
|
||
**Naming decisions (orchestrator, fixed for this plan):**
|
||
- `sma_cross` single-field output name → `"cross"` (renders as `[out:cross]` in the re-captured golden).
|
||
- Engine test-fixture single-field outputs → `"out"`. New multi-output test fields → `"a"`, `"b"` (and `"c"` where a third is needed).
|
||
- Fieldtest single-field outputs → `"out"`.
|
||
|
||
---
|
||
|
||
## Task 1: Engine — introduce `OutField`, widen the type (behaviour-preserving)
|
||
|
||
This task replaces `OutPort` with `OutField` and widens `Composite.output` /
|
||
`ItemLowering::Composite.output` to vectors, threading **every** call site in
|
||
`blueprint.rs` so the crate compiles and **all existing tests stay green**. The
|
||
three `field == 0` caps are **kept** here (single-field composites only re-export
|
||
field 0, so behaviour is preserved); Task 2 lifts them test-first.
|
||
|
||
**Files:**
|
||
- Modify: `crates/aura-engine/src/blueprint.rs`
|
||
- Modify: `crates/aura-engine/src/lib.rs:38`
|
||
|
||
- [ ] **Step 1: Rename `OutPort` → `OutField` and add `name` (blueprint.rs:18–23)**
|
||
|
||
Replace:
|
||
|
||
```rust
|
||
/// 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,
|
||
}
|
||
```
|
||
|
||
with:
|
||
|
||
```rust
|
||
/// One re-exported field of a composite's output record: an interior
|
||
/// `(node, output-field)` surfaced at the boundary under `name`. `name` is a
|
||
/// non-load-bearing render/debug symbol (C23) — like `FieldSpec.name` and
|
||
/// `Composite.name`, it does not reach the flat graph.
|
||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||
pub struct OutField {
|
||
pub node: usize,
|
||
pub field: usize,
|
||
pub name: String,
|
||
}
|
||
```
|
||
|
||
(`Copy` is dropped — `String` is not `Copy`. Callers that relied on `output()`
|
||
returning by Copy are updated in Step 3 and Step 6.)
|
||
|
||
- [ ] **Step 2: Widen `Composite.output` to a record (blueprint.rs:43–49)**
|
||
|
||
In `struct Composite`, change the field:
|
||
|
||
```rust
|
||
input_roles: Vec<Vec<Target>>,
|
||
output: Vec<OutField>,
|
||
```
|
||
|
||
(was `output: OutPort,` at `:48`.)
|
||
|
||
- [ ] **Step 3: Update `Composite::new` and `output()` (blueprint.rs:56–85)**
|
||
|
||
In `Composite::new`, change the param type:
|
||
|
||
```rust
|
||
input_roles: Vec<Vec<Target>>,
|
||
output: Vec<OutField>,
|
||
) -> Self {
|
||
Self { name: name.into(), nodes, edges, input_roles, output }
|
||
}
|
||
```
|
||
|
||
(was `output: OutPort,` at `:61`.) Change the accessor (`:83–85`):
|
||
|
||
```rust
|
||
/// The exposed output record: each entry re-exports one interior
|
||
/// `(node, output-field)` under a boundary name (C8 — one port, K columns).
|
||
pub fn output(&self) -> &[OutField] {
|
||
&self.output
|
||
}
|
||
```
|
||
|
||
(was `pub fn output(&self) -> OutPort { self.output }`.)
|
||
|
||
- [ ] **Step 4: Widen the lowering variant (blueprint.rs:243–249)**
|
||
|
||
In `enum ItemLowering`, change the `Composite` variant:
|
||
|
||
```rust
|
||
/// A composite lowered to its interior: its output record is these flat
|
||
/// `(node, field)` producers (one per re-exported field, declared order), and
|
||
/// input role `r` fans into `roles[r]` (flat targets). Names dropped (C23).
|
||
Composite { output: Vec<(usize, usize)>, roles: Vec<Vec<Target>> },
|
||
```
|
||
|
||
(was `output: (usize, usize)`.)
|
||
|
||
- [ ] **Step 5: Make `inline_composite` build the output record, caps kept (blueprint.rs:292–355)**
|
||
|
||
Remove the single pre-check at `:306–308`:
|
||
|
||
```rust
|
||
if output.node >= item_count {
|
||
return Err(CompileError::OutputPortOutOfRange);
|
||
}
|
||
```
|
||
|
||
Replace the single-output resolution block (`:318–333`, the `let out = match … ;`)
|
||
with a loop that builds the Vec. **The `!= 0` caps stay** (behaviour-preserving):
|
||
|
||
```rust
|
||
// resolve each re-exported field to a flat (node, field), in declared order
|
||
let mut out: Vec<(usize, usize)> = Vec::with_capacity(output.len());
|
||
for of in &output {
|
||
if of.node >= item_count {
|
||
return Err(CompileError::OutputPortOutOfRange);
|
||
}
|
||
let resolved = match &interior[of.node] {
|
||
ItemLowering::Leaf { index } => {
|
||
if of.field >= flat_nodes[*index].schema().output.len() {
|
||
return Err(CompileError::OutputPortOutOfRange);
|
||
}
|
||
(*index, of.field)
|
||
}
|
||
ItemLowering::Composite { output: nested, .. } => {
|
||
// cap kept in Task 1; Task 2 lifts this to nested.get(of.field)
|
||
if of.field != 0 {
|
||
return Err(CompileError::OutputPortOutOfRange);
|
||
}
|
||
nested[0]
|
||
}
|
||
};
|
||
out.push(resolved);
|
||
}
|
||
```
|
||
|
||
The terminal `Ok(ItemLowering::Composite { output: out, roles })` at `:354` is
|
||
unchanged (`out` is now a `Vec`).
|
||
|
||
- [ ] **Step 6: Thread `rewrite_edge`, cap kept (blueprint.rs:375–381)**
|
||
|
||
Replace the composite arm:
|
||
|
||
```rust
|
||
ItemLowering::Composite { output, .. } => {
|
||
// cap kept in Task 1; Task 2 lifts this to output.get(e.from_field)
|
||
if e.from_field != 0 {
|
||
return Err(CompileError::BadInteriorIndex);
|
||
}
|
||
output[0]
|
||
}
|
||
```
|
||
|
||
(was `*output` after the `!= 0` guard; `output` is now a `Vec`, so `output[0]`.)
|
||
|
||
- [ ] **Step 7: Update `lib.rs` re-export (lib.rs:38)**
|
||
|
||
```rust
|
||
pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField};
|
||
```
|
||
|
||
(was `… Composite, OutPort};`.)
|
||
|
||
- [ ] **Step 8: Migrate all existing `OutPort` test literals (blueprint.rs `#[cfg(test)]`)**
|
||
|
||
Every existing test composite is single-field. Rewrite each literal at
|
||
`:532, :578, :611, :626, :641, :736, :916, :924, :967, :975` from
|
||
`OutPort { node: N, field: F }` to a one-element record:
|
||
|
||
```rust
|
||
vec![OutField { node: N, field: F, name: "out".into() }]
|
||
```
|
||
|
||
preserving each site's existing `node`/`field` values. (The site at `:641` is
|
||
`OutPort { node: 0, field: 5 }` — the OHLCV-field re-export — becomes
|
||
`vec![OutField { node: 0, field: 5, name: "out".into() }]`.) The comment mention
|
||
at `:549` is prose; update its wording from "OutPort" to "OutField" if it names
|
||
the type, otherwise leave.
|
||
|
||
- [ ] **Step 9: Build and test the engine crate**
|
||
|
||
Run: `cargo test -p aura-engine`
|
||
Expected: PASS — the crate compiles and every existing test is green
|
||
(single-field records are behaviour-preserving; the caps still hold).
|
||
|
||
- [ ] **Step 10: Clippy the engine crate**
|
||
|
||
Run: `cargo clippy -p aura-engine --all-targets -- -D warnings`
|
||
Expected: PASS — no warnings (watch for a needless `Vec::with_capacity` or
|
||
`clone` lint; none expected).
|
||
|
||
---
|
||
|
||
## Task 2: Engine — lift the caps, multi-output capability (RED-first)
|
||
|
||
The type is in place (Task 1); now lift the two `field == 0` caps, each gated by
|
||
a failing test written first. Tests reuse the existing `#[cfg(test)]` helpers
|
||
`pass1()` (1-in/1-out f64 leaf), `sink_f64()` (f64 sink), `Blueprint::new`,
|
||
`SourceSpec`, and the `bp.compile()` / `bp.compile().err()` assertion idiom — no
|
||
new helper is introduced. Spec test 3 (out-of-range re-export) is already covered
|
||
by the migrated `output_port_out_of_range_rejected` (Task 1 Step 8), so it is not
|
||
re-added here.
|
||
|
||
**Files:**
|
||
- Modify: `crates/aura-engine/src/blueprint.rs` (the two caps + three new `#[cfg(test)]` tests)
|
||
|
||
- [ ] **Step 1: Write the multi-output happy-path test (RED)**
|
||
|
||
Add to the `#[cfg(test)]` module, next to `single_composite_inlines_with_offset_fan_and_output`:
|
||
|
||
```rust
|
||
#[test]
|
||
fn composite_reexports_two_fields_to_distinct_consumers() {
|
||
// composite: two independent Pass1 leaves; role 0 -> leaf 0, role 1 -> leaf 1;
|
||
// output record re-exports leaf 0 as "a", leaf 1 as "b".
|
||
let c = Composite::new(
|
||
"two_out",
|
||
vec![pass1(), pass1()],
|
||
vec![],
|
||
vec![
|
||
vec![Target { node: 0, slot: 0 }],
|
||
vec![Target { node: 1, slot: 0 }],
|
||
],
|
||
vec![
|
||
OutField { node: 0, field: 0, name: "a".into() },
|
||
OutField { node: 1, field: 0, name: "b".into() },
|
||
],
|
||
);
|
||
// composite is item 0; two sinks (items 1, 2) read its two output fields by
|
||
// from_field; one source fans into both roles.
|
||
let bp = Blueprint::new(
|
||
vec![BlueprintNode::Composite(c), sink_f64(), sink_f64()],
|
||
vec![SourceSpec {
|
||
kind: ScalarKind::F64,
|
||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
|
||
}],
|
||
vec![
|
||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // field "a" -> sink 1
|
||
Edge { from: 0, to: 2, slot: 0, from_field: 1 }, // field "b" -> sink 2
|
||
],
|
||
);
|
||
let (nodes, sources, edges) = bp.compile().expect("valid multi-output composite");
|
||
// flat layout: Pass1(0), Pass1(1), SinkF64(2), SinkF64(3)
|
||
assert_eq!(nodes.len(), 4);
|
||
// from_field 0 resolves to leaf 0, from_field 1 to leaf 1 — distinct producers
|
||
assert_eq!(
|
||
edges,
|
||
vec![
|
||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||
Edge { from: 1, to: 3, slot: 0, from_field: 0 },
|
||
]
|
||
);
|
||
// the source fanned into both interior leaves
|
||
assert_eq!(
|
||
sources[0].targets,
|
||
vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }]
|
||
);
|
||
}
|
||
```
|
||
|
||
Run: `cargo test -p aura-engine composite_reexports_two_fields_to_distinct_consumers`
|
||
Expected: FAIL — the consumer reading `from_field: 1` hits the `rewrite_edge` cap;
|
||
`compile` returns `Err(BadInteriorIndex)`, so `.expect(...)` panics.
|
||
|
||
- [ ] **Step 2: Lift the `rewrite_edge` cap (blueprint.rs composite arm)**
|
||
|
||
Replace the composite arm (the Step-6 Task-1 body) with the range-checked index:
|
||
|
||
```rust
|
||
ItemLowering::Composite { output, .. } => {
|
||
*output.get(e.from_field).ok_or(CompileError::BadInteriorIndex)?
|
||
}
|
||
```
|
||
|
||
Run: `cargo test -p aura-engine composite_reexports_two_fields_to_distinct_consumers`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 3: Write the nested multi-output test (RED)**
|
||
|
||
An **outer** composite re-exporting two fields of an **inner** multi-output
|
||
composite (exercises the nested arm). Add next to `nested_composite_inlines`:
|
||
|
||
```rust
|
||
#[test]
|
||
fn outer_reexports_two_fields_of_inner_composite() {
|
||
// inner re-exports two leaves as "a","b"; outer re-exposes both inner roles
|
||
// and re-exports inner field 0 and field 1 (the latter exercises the nested arm).
|
||
let inner = Composite::new(
|
||
"inner_two",
|
||
vec![pass1(), pass1()],
|
||
vec![],
|
||
vec![
|
||
vec![Target { node: 0, slot: 0 }],
|
||
vec![Target { node: 1, slot: 0 }],
|
||
],
|
||
vec![
|
||
OutField { node: 0, field: 0, name: "a".into() },
|
||
OutField { node: 1, field: 0, name: "b".into() },
|
||
],
|
||
);
|
||
let outer = Composite::new(
|
||
"outer_two",
|
||
vec![BlueprintNode::Composite(inner)],
|
||
vec![],
|
||
vec![
|
||
vec![Target { node: 0, slot: 0 }], // outer role 0 -> inner role 0
|
||
vec![Target { node: 0, slot: 1 }], // outer role 1 -> inner role 1
|
||
],
|
||
vec![
|
||
OutField { node: 0, field: 0, name: "x".into() }, // inner field 0
|
||
OutField { node: 0, field: 1, name: "y".into() }, // inner field 1 (nested arm)
|
||
],
|
||
);
|
||
let bp = Blueprint::new(
|
||
vec![BlueprintNode::Composite(outer), sink_f64(), sink_f64()],
|
||
vec![SourceSpec {
|
||
kind: ScalarKind::F64,
|
||
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
|
||
}],
|
||
vec![
|
||
Edge { from: 0, to: 1, slot: 0, from_field: 0 }, // outer field x -> sink 1
|
||
Edge { from: 0, to: 2, slot: 0, from_field: 1 }, // outer field y -> sink 2
|
||
],
|
||
);
|
||
let (nodes, _sources, edges) = bp.compile().expect("valid nested multi-output");
|
||
assert_eq!(nodes.len(), 4); // Pass1, Pass1, SinkF64, SinkF64
|
||
assert_eq!(
|
||
edges,
|
||
vec![
|
||
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
||
Edge { from: 1, to: 3, slot: 0, from_field: 0 },
|
||
]
|
||
);
|
||
}
|
||
```
|
||
|
||
Run: `cargo test -p aura-engine outer_reexports_two_fields_of_inner_composite`
|
||
Expected: FAIL — `inline_composite`'s nested arm caps `of.field != 0` on the
|
||
outer's `field: 1` re-export; `compile` returns `Err(OutputPortOutOfRange)`.
|
||
|
||
- [ ] **Step 4: Lift the nested arm cap (blueprint.rs inline_composite)**
|
||
|
||
Replace the nested arm in the Step-5 Task-1 loop:
|
||
|
||
```rust
|
||
ItemLowering::Composite { output: nested, .. } => {
|
||
*nested.get(of.field).ok_or(CompileError::OutputPortOutOfRange)?
|
||
}
|
||
```
|
||
|
||
(was the `if of.field != 0 { … } nested[0]` cap.)
|
||
|
||
Run: `cargo test -p aura-engine outer_reexports_two_fields_of_inner_composite`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Write the out-of-range consume guard test**
|
||
|
||
```rust
|
||
#[test]
|
||
fn consume_of_missing_output_field_is_rejected() {
|
||
// a single-field composite; a consumer reads from_field 1 (past the 1-field
|
||
// record) -> the rewrite_edge range-check rejects it.
|
||
let c = Composite::new(
|
||
"c",
|
||
vec![pass1()],
|
||
vec![],
|
||
vec![vec![Target { node: 0, slot: 0 }]],
|
||
vec![OutField { node: 0, field: 0, name: "a".into() }],
|
||
);
|
||
let bp = Blueprint::new(
|
||
vec![BlueprintNode::Composite(c), sink_f64()],
|
||
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
||
vec![Edge { from: 0, to: 1, slot: 0, from_field: 1 }], // only field 0 exists
|
||
);
|
||
assert_eq!(bp.compile().err(), Some(CompileError::BadInteriorIndex));
|
||
}
|
||
```
|
||
|
||
Run: `cargo test -p aura-engine consume_of_missing_output_field_is_rejected`
|
||
Expected: PASS — `output.get(1)` on a 1-field record is `None` → `BadInteriorIndex`.
|
||
(This is a guard, not a RED gate: it also errored under the Task-1 cap, but now via
|
||
the range-check that replaced it.)
|
||
|
||
- [ ] **Step 6: Full engine regression + clippy**
|
||
|
||
Run: `cargo test -p aura-engine`
|
||
Expected: PASS — all old tests (single-field, behaviour-preserving) plus the four
|
||
new capability tests are green.
|
||
|
||
Run: `cargo clippy -p aura-engine --all-targets -- -D warnings`
|
||
Expected: PASS.
|
||
|
||
---
|
||
|
||
## Task 3: CLI — multi-output render + author sites + golden re-capture
|
||
|
||
`aura-cli` depends on `aura-engine`; after Task 1 it no longer compiles (`OutPort`
|
||
removed from the import and four literals). This task threads every CLI site,
|
||
re-exports the MACD's three lines, renders K named output markers, and re-captures
|
||
the one drifting golden — one atomic compile-and-test unit.
|
||
|
||
**Files:**
|
||
- Modify: `crates/aura-cli/src/main.rs` (import `:13`, `sma_cross` `:130`, `macd` `:206`, nested-render-test literals `:391`/`:398`, needle test `:377`, `blueprint_view_golden`)
|
||
- Modify: `crates/aura-cli/src/graph.rs:124–126` (`render_definition`)
|
||
|
||
- [ ] **Step 1: Update the import (main.rs:13)**
|
||
|
||
Change `OutPort` to `OutField` in the `use` list:
|
||
|
||
```rust
|
||
f64_field, summarize, Blueprint, BlueprintNode, Composite, Edge, Harness, OutField,
|
||
```
|
||
|
||
- [ ] **Step 2: Render K named output markers (graph.rs:124–126)**
|
||
|
||
Replace the single `[out]` marker block:
|
||
|
||
```rust
|
||
for of in c.output() {
|
||
let out_id = labels.len();
|
||
labels.push(format!("out:{}", of.name));
|
||
edges.push((of.node, out_id));
|
||
}
|
||
```
|
||
|
||
(was the three lines pushing one `"out"` label wired from `c.output().node`.)
|
||
Update the function's doc-comment at `:101–104` to say "an `[out:<name>]` marker
|
||
per re-exported output field" (was "an `[out]` marker (wired from the output
|
||
port)").
|
||
|
||
- [ ] **Step 3: Migrate `sma_cross` to a single-field record (main.rs:130)**
|
||
|
||
```rust
|
||
vec![OutField { node: 2, field: 0, name: "cross".into() }],
|
||
```
|
||
|
||
(was `OutPort { node: 2, field: 0 }`.) Behaviour-preserving: one re-exported
|
||
field; renders as `[out:cross]`.
|
||
|
||
- [ ] **Step 4: Re-export all three MACD lines (main.rs:206)**
|
||
|
||
Replace the `macd` composite's output (was `OutPort { node: 4, field: 0 }, // the histogram`):
|
||
|
||
```rust
|
||
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
|
||
],
|
||
```
|
||
|
||
Update the `macd` doc-comment (`:178–184`): the composite now exposes the **three
|
||
MACD lines** as a record; the strategy trades the histogram by reading
|
||
`from_field: 2`.
|
||
|
||
**Then update the consumer edge** in `macd_strategy_blueprint` (`:233`). The
|
||
histogram is now field index **2** of the 3-field record (was the sole output at
|
||
field 0), so the edge feeding `Exposure` must select it:
|
||
|
||
```rust
|
||
Edge { from: 0, to: 1, slot: 0, from_field: 2 }, // histogram → Exposure
|
||
```
|
||
|
||
(was `from_field: 0`.) This keeps the strategy trading the **histogram** — leaving
|
||
it at `0` would silently feed the MACD line instead, changing the run and breaking
|
||
`run_macd_compiles_from_nested_composite_and_is_deterministic`. The other three
|
||
edges (`:234–236`) are unchanged.
|
||
|
||
- [ ] **Step 5: Migrate the nested-render-test literals (main.rs:391, :398)**
|
||
|
||
In `nested_composite_renders_without_panic`, both single-field composites:
|
||
|
||
```rust
|
||
vec![OutField { node: 0, field: 0, name: "out".into() }], // :391 inner
|
||
```
|
||
```rust
|
||
vec![OutField { node: 1, field: 0, name: "out".into() }], // :398 outer
|
||
```
|
||
|
||
- [ ] **Step 6: Build the CLI crate**
|
||
|
||
Run: `cargo build -p aura-cli`
|
||
Expected: PASS — every `OutPort` site threaded; crate compiles. (Golden/needle
|
||
tests may still be red — handled next.)
|
||
|
||
- [ ] **Step 7: Add the `[out:<name>]` needle assertion (main.rs:377)**
|
||
|
||
In `blueprint_view_defines_each_composite_once`, extend the needle list so the
|
||
multi-output marker is asserted. `sma_cross` is the rendered composite, so its
|
||
marker is `[out:cross]`:
|
||
|
||
```rust
|
||
for needle in ["[SMA]", "[Sub]", "[in:0]", "[out:cross]"] {
|
||
```
|
||
|
||
(was `… "[out]"`.)
|
||
|
||
Run: `cargo test -p aura-cli blueprint_view_defines_each_composite_once`
|
||
Expected: PASS — the render now emits `[out:cross]` for `sma_cross`.
|
||
|
||
- [ ] **Step 8: Re-capture the `blueprint_view_golden` (main.rs:464–502)**
|
||
|
||
The `[out]` → `[out:cross]` change drifts this golden. Follow the test's own
|
||
re-capture protocol (comment at `:466–468`): run the golden test, observe the
|
||
actual rendered string in the failure diff, and paste it verbatim into the golden
|
||
literal (`:497` region). Do **not** hand-edit the expected string field-by-field —
|
||
copy the actual output wholesale.
|
||
|
||
Run: `cargo test -p aura-cli blueprint_view_golden`
|
||
Expected: FAIL first (golden drift: `[out]` → `[out:cross]`), then PASS after the
|
||
literal is re-captured.
|
||
|
||
- [ ] **Step 9: Confirm the flat graph golden is byte-identical (main.rs:505–530)**
|
||
|
||
`compiled_view_golden` pins the flat graph render; per C23 names are dropped at
|
||
inline, so it must **not** change (acceptance criterion 6 — the regression guard).
|
||
|
||
Run: `cargo test -p aura-cli compiled_view_golden`
|
||
Expected: PASS with **no** edit to the golden literal. If it drifts, a name leaked
|
||
into the flat graph — a bug to fix, not a golden to re-capture.
|
||
|
||
- [ ] **Step 10: Full CLI test + clippy**
|
||
|
||
Run: `cargo test -p aura-cli`
|
||
Expected: PASS — including `run_macd_compiles_from_nested_composite_and_is_deterministic`
|
||
(the MACD run still produces the histogram, now via `from_field: 2`).
|
||
|
||
Run: `cargo clippy -p aura-cli --all-targets -- -D warnings`
|
||
Expected: PASS.
|
||
|
||
---
|
||
|
||
## Task 4: Non-workspace fieldtest sweep + full workspace gate
|
||
|
||
The construction-layer fieldtests are **not** workspace members, so
|
||
`cargo …--workspace` does not compile them — they hold stale `OutPort` references
|
||
to a now-deleted type. Sweep them for tree consistency (mechanical), then run the
|
||
full workspace gate.
|
||
|
||
**Files:**
|
||
- Modify: `fieldtests/milestone-construction-layer/mc_1_composite_build_run.rs:6,20,47`
|
||
- Modify: `fieldtests/milestone-construction-layer/mc_2_miswire_render.rs:15,47`
|
||
- Modify: `fieldtests/milestone-construction-layer/mc_3_nested_composite.rs:18,35,52`
|
||
- Modify: `fieldtests/milestone-construction-layer/mc_4_introspect_graph.rs:13,34`
|
||
|
||
- [ ] **Step 1: Sweep `OutPort` → `OutField` across the fieldtests**
|
||
|
||
In each file: update the `use … OutPort` import to `OutField`, and rewrite each
|
||
`OutPort { node: N, field: F }` literal to `vec![OutField { node: N, field: F, name: "out".into() }]`
|
||
**at the `Composite::new` output-arg position** (these are all single-field
|
||
composites). Update comment mentions of `OutPort` (e.g. mc_1 `:6`) to `OutField`.
|
||
|
||
- [ ] **Step 2: Verify no `OutPort` remains anywhere in the tree**
|
||
|
||
Run: `rg -n "OutPort" --type rust`
|
||
Expected: **no matches** — the type is fully retired (every reference is now
|
||
`OutField`).
|
||
|
||
- [ ] **Step 3: Full workspace build**
|
||
|
||
Run: `cargo build --workspace`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 4: Full workspace test**
|
||
|
||
Run: `cargo test --workspace`
|
||
Expected: PASS — every crate green; single-field composites behaviour-preserving,
|
||
multi-output capability covered by Task 2, render by Task 3.
|
||
|
||
- [ ] **Step 5: Full workspace clippy**
|
||
|
||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||
Expected: PASS — zero warnings.
|
||
|
||
---
|
||
|
||
## Acceptance gate (whole plan)
|
||
|
||
- [ ] `cargo build --workspace` green.
|
||
- [ ] `cargo test --workspace` green (incl. the four new engine capability tests and the re-captured `blueprint_view_golden`).
|
||
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` green.
|
||
- [ ] `rg "OutPort" --type rust` returns nothing (type fully retired).
|
||
- [ ] `compiled_view_golden` unchanged (flat graph byte-identical — C23 / acceptance criterion 6).
|
||
- [ ] The MACD PoC re-exports `macd`/`signal`/`histogram`; the run still trades the histogram (via `from_field: 2`).
|
||
|
||
The implementation commit closes #40.
|