"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.
23 KiB
Composite multi-output record — Implementation Plan
Parent spec:
docs/specs/0018-composite-multi-output-record.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill 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,Compositestruct/new/output(),ItemLowering::Composite.output,inline_composite,rewrite_edge, all#[cfg(test)]OutPortliterals; new capability tests. - Modify:
crates/aura-engine/src/lib.rs:38— re-exportOutPort→OutField. - Modify:
crates/aura-cli/src/graph.rs:105–129—render_definitionK[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_crosssingle-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→OutFieldand addname(blueprint.rs:18–23)
Replace:
/// 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:
/// 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.outputto a record (blueprint.rs:43–49)
In struct Composite, change the field:
input_roles: Vec<Vec<Target>>,
output: Vec<OutField>,
(was output: OutPort, at :48.)
- Step 3: Update
Composite::newandoutput()(blueprint.rs:56–85)
In Composite::new, change the param type:
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):
/// 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:
/// 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_compositebuild the output record, caps kept (blueprint.rs:292–355)
Remove the single pre-check at :306–308:
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):
// 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:
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.rsre-export (lib.rs:38)
pub use blueprint::{Blueprint, BlueprintNode, CompileError, Composite, OutField};
(was … Composite, OutPort};.)
- Step 8: Migrate all existing
OutPorttest 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:
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:
#[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_edgecap (blueprint.rs composite arm)
Replace the composite arm (the Step-6 Task-1 body) with the range-checked index:
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:
#[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:
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
#[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:
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:
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_crossto a single-field record (main.rs:130)
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):
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:
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:
vec![OutField { node: 0, field: 0, name: "out".into() }], // :391 inner
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]:
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→OutFieldacross 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
OutPortremains 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 --workspacegreen.cargo test --workspacegreen (incl. the four new engine capability tests and the re-capturedblueprint_view_golden).cargo clippy --workspace --all-targets -- -D warningsgreen.rg "OutPort" --type rustreturns nothing (type fully retired).compiled_view_goldenunchanged (flat graph byte-identical — C23 / acceptance criterion 6).- The MACD PoC re-exports
macd/signal/histogram; the run still trades the histogram (viafrom_field: 2).
The implementation commit closes #40.