"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.
18 KiB
Name the Composite Boundary — Design Spec
Date: 2026-06-08 Status: Draft — awaiting user spec review Authors: orchestrator + Claude
Goal
Make all three composite-boundary edge-kinds — input roles, params, and
outputs — a uniform named projection of interior handles. Cycle 0018 (#40)
did this for outputs (Vec<OutField { node, field, name }>). This cycle does the
same for the other two:
- Input role naming.
input_roles: Vec<Vec<Target>>(bare, positional, rendered[in:0],[in:1]) becomesVec<Role { name, targets }>, rendered[in:price]. - Param aliasing. Today
param_space()aggregates interior leaf params under their factory names, so the three EMAlengthknobs of a MACD composite all surface asmacd.length— distinguishable only by slot. A composite gains aparams: Vec<ParamAlias { name, node, slot }>projection that relabels a named interior param slot's surface name (fast,slow,signal).
This is pure legibility, not a capability (the load-bearing decision — see
Architecture). Inputs and params already function (wireable by role index,
sweepable by slot); this names slots that already work. The payoff is the full
boundary signature visible in one render — conceptually, for MACD, named inputs
(price), named params (fast, slow, signal), and named outputs (macd,
signal, histogram). The authoritative output format is the marker DAG of
render_definition (see "Render — the full named signature" below), not a
one-line signature string.
The implementation commit closes #41.
Architecture
The one load-bearing decision: naming overlay, not curation
When a composite aliases a param slot, does it change the sweep surface
(param_space()) — selectively exposing/hiding interior slots — or is it a
pure naming overlay?
Resolved → pure naming overlay. Every interior param slot remains in
param_space() and stays sweepable exactly as today; an alias only relabels one
slot's surface name from the factory name to the alias. Nothing is hidden, the
surface arity is unchanged, the alias is cosmetic. This is forced by two sources:
- The issue's own framing: "Pure legibility, NOT a capability: params and inputs already work; this renames slots that already function." A selective/curation reading (the composite curates which knobs are public) is a capability the issue explicitly disclaims.
- C23 (the design ledger): a boundary name is a non-load-bearing debug symbol; identity is positional and survives lowering, the name does not. An alias that changed what is sweepable would make the name load-bearing — a C23 violation.
Consequence, made explicit so the implementation cannot drift: a param's identity
in param_space() stays its slot (position), never its name. Aliases relabel
in place; they never reorder, add, or remove a slot. The existing invariant test
param_space_mirrors_compiled_flat_node_param_order (slot order mirrors the flat
flat graph) therefore stays green unchanged — it is the C23 anchor for this cycle.
Invariant-neutrality
- C23 honoured. Names are non-load-bearing debug symbols dropped at lowering;
identity is positional everywhere (param slot, role index, output field index).
The flat graph is byte-identical —
compiled_view_goldenis the regression guard, exactly as #40 honoured it. The role/param/output names never reach aBox<dyn Node>. - C8 / C7 / C4 untouched. This is authoring-surface legibility only: no change to the runtime record shape, the four base scalars, or one-record-per-cycle.
param_space()surface unchanged in size, order, kind, and sweepability — only thenamestring of an aliased slot changes.
Orthogonal to #36
Issue #36 ("param declared twice: factory().params() and schema().params must
stay in lockstep") touches the leaf param-declaration surface. #41 is purely
additive at the composite level (an alias overlay consulted during
aggregation). They are orthogonal: this cycle neither resolves nor depends on #36,
and the spec deliberately does not entangle them.
Concrete code shapes
Worked author example (the acceptance evidence) — MACD
The MACD composite (crates/aura-cli/src/main.rs) is the worked example: three
interior EMAs each carry a length param, so today its boundary reads by position
only. Below is the author site before → after.
Before (today — one bare role, params anonymous, outputs already named in #40):
fn macd(name: &str) -> Composite {
Composite::new(
name,
vec![
Ema::factory().into(), // 0 fast EMA
Ema::factory().into(), // 1 slow EMA
Sub::factory().into(), // 2 MACD line = fast − slow
Ema::factory().into(), // 3 signal EMA of the MACD line
Sub::factory().into(), // 4 histogram = MACD line − signal
],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
Edge { from: 2, to: 4, slot: 0, from_field: 0 },
Edge { from: 3, to: 4, slot: 1, from_field: 0 },
],
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() },
OutField { node: 3, field: 0, name: "signal".into() },
OutField { node: 4, field: 0, name: "histogram".into() },
],
)
}
macd_blueprint().param_space() today surfaces (the three EMA length knobs,
indistinguishable by name):
["macd.length", "macd.length", "macd.length"]
After (this cycle — named role, aliased params; outputs unchanged):
fn macd(name: &str) -> Composite {
Composite::new(
name,
vec![
Ema::factory().into(), // 0 fast EMA
Ema::factory().into(), // 1 slow EMA
Sub::factory().into(), // 2 MACD line = fast − slow
Ema::factory().into(), // 3 signal EMA of the MACD line
Sub::factory().into(), // 4 histogram = MACD line − signal
],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
Edge { from: 2, to: 4, slot: 0, from_field: 0 },
Edge { from: 3, to: 4, slot: 1, from_field: 0 },
],
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() },
OutField { node: 3, field: 0, name: "signal".into() },
OutField { node: 4, field: 0, name: "histogram".into() },
],
)
}
macd_blueprint().param_space() now surfaces (same slots, same order, same kinds —
only the names change):
["macd.fast", "macd.slow", "macd.signal"]
The injected point vector (sample_point / the MACD point) is unchanged — slot
order is identical; only the labels improved. This is the empirical proof of the
naming-overlay decision: the sweep surface is byte-for-byte the same shape, the
author just reads it by name now.
Render — the full named signature
aura graph --macd renders the composite definition. Input roles render
[in:<name>] (was [in:<index>]); outputs already render [out:<name>] (#40);
aliased params render [param:<name>] markers wired into the leaf they configure
(direction: marker → node, like [in:]). Marker labels stay short (one word) to
avoid the ascii-dag wide-sibling-label garble. The MACD definition gains:
[in:price] (was [in:0])
[param:fast] → EMA(0)
[param:slow] → EMA(1)
[param:signal] → EMA(3)
[out:macd] [out:signal] [out:histogram] (unchanged, #40)
The compiled view (aura graph --macd --compiled) stays name-free — the
boundary dissolves at inline, markers gone (C23). compiled_view_golden is
byte-identical.
Secondary — engine struct / signature shapes (before → after)
Role (new) and ParamAlias (new) in crates/aura-engine/src/blueprint.rs,
each #[derive(Clone, Debug, PartialEq, Eq)] (mirroring Target / OutField):
/// 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.
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).
pub struct ParamAlias {
pub name: String,
pub node: usize,
pub slot: usize,
}
Composite gains a params field; input_roles changes element type:
// before
pub struct Composite {
name: String,
nodes: Vec<BlueprintNode>,
edges: Vec<Edge>,
input_roles: Vec<Vec<Target>>,
output: Vec<OutField>,
}
// after
pub struct Composite {
name: String,
nodes: Vec<BlueprintNode>,
edges: Vec<Edge>,
input_roles: Vec<Role>,
params: Vec<ParamAlias>,
output: Vec<OutField>,
}
Composite::new takes input_roles: Vec<Role> and a new params: Vec<ParamAlias>
argument (positioned between input_roles and output, matching the
inputs → params → outputs signature order). New accessors:
pub fn input_roles(&self) -> &[Role] { &self.input_roles }
pub fn params(&self) -> &[ParamAlias] { &self.params } // new
// output() unchanged
collect_params (the param_space() walk) threads the directly-containing
composite's alias slice and consults it when naming a leaf param:
// before: fn collect_params(items: &[BlueprintNode], prefix: &str, out: &mut Vec<ParamSpec>)
// after: aliases is the alias list of the composite directly containing `items`
// (empty &[] at the top level).
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);
}
}
}
}
Blueprint::param_space seeds the top level with no aliases:
collect_params(&self.nodes, "", &[], &mut out).
inline_composite (the compile-path role walk, ~blueprint.rs:342) iterates
Roles instead of bare target vecs — for (r, role) in input_roles.iter().enumerate()
becomes a walk over role.targets; the role's name is inert at compile (dropped,
C23). The destructure let Composite { name: _, nodes, edges, input_roles, output } = c;
gains params: _ (params do not lower — they are a read-side projection only).
render_definition (crates/aura-cli/src/graph.rs): the input-role loop reads
role.name for the marker label and role.targets for the edges; a new loop emits
[param:<name>] markers for c.params(), each wired marker → alias.node.
Components
| Component | File | Change |
|---|---|---|
Role struct |
crates/aura-engine/src/blueprint.rs |
new; re-exported from lib.rs |
ParamAlias struct |
crates/aura-engine/src/blueprint.rs |
new; re-exported from lib.rs |
Composite |
crates/aura-engine/src/blueprint.rs |
input_roles: Vec<Role>; new params: Vec<ParamAlias>; new arg; params() accessor |
collect_params |
crates/aura-engine/src/blueprint.rs |
thread aliases: &[ParamAlias]; relabel matched slot |
inline_composite |
crates/aura-engine/src/blueprint.rs |
walk role.targets; destructure params: _ |
render_definition |
crates/aura-cli/src/graph.rs |
[in:<name>]; [param:<name>] markers |
| MACD + sample author sites | crates/aura-cli/src/main.rs |
Role { … }, ParamAlias { … } |
| Construction-layer fixtures | fieldtests/milestone-construction-layer/mc_*.rs |
Role { … } sweep (single unnamed role → Role { name, targets }) |
Data flow
- Author writes a
CompositewithVec<Role>,Vec<ParamAlias>,Vec<OutField>. param_space()walks the tree; for each leaf param slot it picks the alias name (if the directly-containing composite aliases that(node, slot)) or the factory name, path-qualified by compositename()s as today. Slot order is unchanged — aliases relabel, never reorder.compile_with_paramslowers exactly as today: roles fan in bytargets, params bind slot-by-slot to the injected vector, names dropped (C23). An out-of-range alias(node, slot)is rejected here (see Error handling).- Render surfaces
names as markers in the blueprint view; the compiled view is name-free.
Error handling
- Alias references a missing/non-leaf interior node or a slot beyond the leaf's
param count. (A composite interior item has no params, so any slot on it is
out of range.) Rejected at
compile_with_params(the fallible, tree-walking path that already range-checks edges and outputs) asCompileError::BadInteriorIndex— reusing the existing variant, no new variant (the same widening posture #40 took for outputs).param_space()stays infallible: an alias that matches no emitted(node, slot)is simply inert there (no slot is relabelled), and the structural defect surfaces at compile. This keepsparam_space()deterministic and slot-stable regardless of alias validity. - Role with a bad target — unchanged: already
BadInteriorIndex/RoleKindMismatch { role }at compile. - Duplicate aliases for the same
(node, slot). First match wins (find); benign and not worth a dedicated error — the surface is still a valid relabel.
Testing strategy
Engine unit tests (crates/aura-engine/src/blueprint.rs):
- Alias relabels in place. A composite with two same-type leaves + two
aliases →
param_space()names are the aliases (e.g.["c.shortLen", "c.longLen"]), not duplicate factory names. - Unaliased params unchanged (regression). A composite with
params: vec![]→param_space()identical to today's path-qualified factory names. - Slot order invariant (C23 anchor).
param_space_mirrors_compiled_flat_node_param_orderstays green unchanged — aliases relabel, never reorder; the slot still mirrors the flat graph. (No new test; the existing one is the guard.) - Out-of-range alias →
BadInteriorIndexatcompile_with_params(aliasnode/slotpast the interior leaf's params). - Partial aliasing. A composite aliasing one of two slots → the aliased slot takes the alias name, the other keeps its factory name; order intact.
CLI render test (crates/aura-cli):
- Named role + param markers render.
render_definitionof the MACD (or a fixture) composite contains[in:price],[param:fast],[param:slow],[param:signal]and the existing[out:*]; asserts no[in:0]. compiled_view_goldenbyte-identical (C23 regression — names dropped at lowering). If it drifts, that is a bug, not a golden to re-capture.
Author-site / build:
cargo build/test/clippy --workspace(-D warnings) all green; the MACD run (aura run --macd) stays deterministic (same metrics as 0018 — params only got names, the injected vector is unchanged).
Acceptance criteria
- A trader authoring a multi-line indicator reads
macd.fast/macd.slow/macd.signaland[in:price]instead of three indistinguishablemacd.lengthand[in:0]— the worked MACD before → after is the evidence the project's intended audience naturally reaches for this. param_space()surface is unchanged in size, order, kind, and sweepability; only names change (the naming-overlay decision, empirically shown by the unchanged point vector).- C23 preserved:
compiled_view_goldenbyte-identical; no role/param/output name reaches aBox<dyn Node>. cargo build/test/clippy --workspace(-D warnings) green;aura run --macddeterministic and unchanged in metrics.- Closes #41.