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.
313 lines
15 KiB
Markdown
313 lines
15 KiB
Markdown
# Composite Output Re-exports as Producer Bindings — Design Spec
|
|
|
|
**Date:** 2026-06-08
|
|
**Status:** Draft — awaiting user spec review
|
|
**Authors:** orchestrator + Claude
|
|
|
|
## Goal
|
|
|
|
In the blueprint view's `where:` section, render a composite's output re-exports
|
|
as **bindings on their producing node's label** (`name := <producer-label>`)
|
|
instead of as standalone terminal nodes wired from the producer. This refines the
|
|
output-port rendering settled in 0018/0019; everything else in the render lineage
|
|
(0017 definitions, 0020 signature, 0021 fan-in identifiers) is untouched.
|
|
|
|
Today `render_definition` materializes each `OutField` as a fresh node labelled
|
|
`of.name`, plus an edge `of.node → that node`. But an `OutField` re-exports the
|
|
value of an **existing** interior node — it is never a new node. The current form
|
|
has three defects:
|
|
|
|
1. **It draws false terminals.** In MACD the macd line (a `Sub`) feeds *both* the
|
|
signal EMA and the histogram `Sub` internally, yet renders as a dead-end leaf
|
|
`[macd]`. macd and signal are interior junctions, not endpoints.
|
|
2. **It inflates the node count by the output arity.** MACD's 5 compute nodes
|
|
render as 8. Node count is the graph's first-order structural signal; showing
|
|
8 where the truth is 5 corrupts it, and the inflation compounds through
|
|
composite nesting.
|
|
3. **It is redundant.** The output names already appear in the signature line
|
|
`-> (macd, signal, histogram)`, so each leaf stub repeats information already
|
|
shown.
|
|
|
|
All three MACD outputs (macd, signal, histogram) are duplicates of existing
|
|
interior nodes — histogram included. There is no case where an `OutField` warrants
|
|
a real extra node, so the fix is uniform: fold the name into the producer.
|
|
|
|
This is **pure render-layer legibility**, not a capability. `OutField.name` is a
|
|
non-load-bearing render/debug symbol (C23) that never reaches the flat graph; the
|
|
engine, blueprint data model, and bootstrap are untouched. The implementation
|
|
commit closes #46.
|
|
|
|
## Architecture
|
|
|
|
### The settled decision: output names are bindings, inputs stay nodes
|
|
|
|
A composite boundary has two edge-kinds rendered in the `where:` interior: input
|
|
roles and output re-exports. They are **not** symmetric, and this cycle makes the
|
|
asymmetry explicit in the rendering:
|
|
|
|
- An **input role** has no interior producer — the value enters from outside and
|
|
fans into one or more interior targets. There is no node to annotate it onto, so
|
|
it is correctly its own source node (`[price]`). **Unchanged.**
|
|
- An **output re-export** is always the value of an existing interior node. It has
|
|
a producer to annotate, so it is rendered as a binding *on that producer*
|
|
(`[macd := Sub(#Ef,#Es)]`), emitting no standalone node and no producer→output
|
|
edge.
|
|
|
|
The `:=` prefix becomes the precise visual signal that a node's value escapes the
|
|
composite boundary: a bound node is an output, a bare node is purely internal. The
|
|
interior graph then shows exactly the computation DAG — every drawn node is a real
|
|
compute step, every drawn edge a real dataflow.
|
|
|
|
### Multiple outputs from one node: tuple binding
|
|
|
|
The data model permits two `OutField`s re-exporting the same producer node under
|
|
different `field` indices (C8: one output port, K columns). When one node backs
|
|
more than one output, the binding is a tuple `(n1, n2) := <producer-label>`, names
|
|
ordered by `field` index (ties by author order). This does not occur in MACD —
|
|
each output is a distinct node — but the renderer must implement the general rule
|
|
so no test bakes in a MACD-only one-output-per-node assumption.
|
|
|
|
### The name/param collision is intended
|
|
|
|
MACD node 3 is the signal-length EMA: its param is aliased `signal` (0019) and its
|
|
output is also named `signal`, so it renders `[signal := EMA(signal)]`. This reads
|
|
punningly but is **truthful** — the signal output *is* the signal-length EMA — and
|
|
is specified as intended behaviour, not a defect to engineer around. The pinning
|
|
test asserts it verbatim.
|
|
|
|
### Invariant-neutrality
|
|
|
|
- **C23 honoured.** `OutField.name` stays a non-load-bearing render symbol dropped
|
|
at lowering; output identity is the `(node, field)` position, which survives.
|
|
The `:=` form changes only where the name is *printed*, never what it *is*. The
|
|
compiled view (`render_flat_graph`) is name-free and byte-identical —
|
|
`compiled_view_golden` is the regression guard, unchanged.
|
|
- **C8 / C7 untouched.** No change to the runtime record shape, the four base
|
|
scalars, or one-record-per-cycle. This is authoring-surface legibility only.
|
|
- **No engine / blueprint / bootstrap change.** The only files touched are
|
|
`render_definition` (+ its doc comment) in `crates/aura-cli/src/graph.rs` and the
|
|
pinning test in `crates/aura-cli/src/main.rs`. The `Composite` data model,
|
|
`OutField`, `compile_with_params`, and `param_space()` are all unchanged.
|
|
|
|
## Concrete code shapes
|
|
|
|
### Worked user-facing example (the acceptance evidence) — MACD `where:` body
|
|
|
|
The trader runs the blueprint view (`aura graph` over the MACD harness) and reads
|
|
the `where:` definition of the `macd` composite. The interior **node-label set** is
|
|
the empirical evidence (the ascii-dag pipe layout around these labels is
|
|
renderer-determined and not asserted byte-for-byte).
|
|
|
|
**Before** (today — 8 interior nodes: 5 compute + `price` entry + 3 output stubs):
|
|
|
|
```text
|
|
macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram):
|
|
|
|
[price] entry node (input role)
|
|
[EMA(fast)] node 0
|
|
[EMA(slow)] node 1
|
|
[Sub(#Ef,#Es)] node 2 (the macd line) ── also feeds 3 and 4
|
|
[EMA(signal)] node 3 (the signal line) ── also feeds 4
|
|
[Sub(#S,#Es)] node 4 (the histogram)
|
|
[macd] output stub ← edge from node 2 (false terminal)
|
|
[signal] output stub ← edge from node 3 (false terminal)
|
|
[histogram] output stub ← edge from node 4
|
|
```
|
|
|
|
**After** (this cycle — 6 interior nodes: 5 compute + `price` entry; output stubs
|
|
gone, names folded into their producers):
|
|
|
|
```text
|
|
macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram):
|
|
|
|
[price] entry node (input role) — unchanged
|
|
[EMA(fast)] node 0 — bare (not an output)
|
|
[EMA(slow)] node 1 — bare (not an output)
|
|
[macd := Sub(#Ef,#Es)] node 2 — bound; still feeds 3 and 4
|
|
[signal := EMA(signal)] node 3 — bound; still feeds 4
|
|
[histogram := Sub(#S,#Es)] node 4 — bound
|
|
```
|
|
|
|
The signature line `-> (macd, signal, histogram)` is unchanged (0020). The macd
|
|
line (node 2) keeps its real interior fan-out to the signal EMA and the histogram
|
|
`Sub`; it is no longer *also* drawn as a dead-end leaf. Node count drops from 8 to
|
|
6, and every drawn node is now a real compute step or the input entry.
|
|
|
|
### The render change — `render_definition` (before → after)
|
|
|
|
`crates/aura-cli/src/graph.rs`. Only the output handling changes; the interior-node
|
|
and input-role loops are as today.
|
|
|
|
**Before** (output loop appends a node + edge per `OutField`):
|
|
|
|
```rust
|
|
fn render_definition(c: &Composite, color: Color) -> String {
|
|
let mut labels: Vec<String> = Vec::with_capacity(c.nodes().len());
|
|
for (i, inner) in c.nodes().iter().enumerate() {
|
|
labels.push(match inner {
|
|
BlueprintNode::Leaf(factory) => leaf_label(c, i, factory),
|
|
BlueprintNode::Composite(inner_c) => inner_c.name().to_string(),
|
|
});
|
|
}
|
|
let mut edges: Vec<(usize, usize)> = Vec::new();
|
|
for e in c.edges() {
|
|
edges.push((e.from, e.to));
|
|
}
|
|
for role in c.input_roles() {
|
|
let in_id = labels.len();
|
|
labels.push(role.name.clone());
|
|
for t in &role.targets {
|
|
edges.push((in_id, t.node));
|
|
}
|
|
}
|
|
for of in c.output() {
|
|
let out_id = labels.len();
|
|
labels.push(of.name.clone());
|
|
edges.push((of.node, out_id));
|
|
}
|
|
|
|
format!("{}:\n\n{}", signature(c), render_flat(&labels, &edges, color))
|
|
}
|
|
```
|
|
|
|
**After** (each interior label is prefixed with its output binding, if any; the
|
|
output loop is gone):
|
|
|
|
```rust
|
|
fn render_definition(c: &Composite, color: Color) -> String {
|
|
let mut labels: Vec<String> = Vec::with_capacity(c.nodes().len());
|
|
for (i, inner) in c.nodes().iter().enumerate() {
|
|
let base = match inner {
|
|
BlueprintNode::Leaf(factory) => leaf_label(c, i, factory),
|
|
BlueprintNode::Composite(inner_c) => inner_c.name().to_string(),
|
|
};
|
|
labels.push(match output_binding(c, i) {
|
|
Some(prefix) => format!("{prefix}{base}"),
|
|
None => base,
|
|
});
|
|
}
|
|
let mut edges: Vec<(usize, usize)> = Vec::new();
|
|
for e in c.edges() {
|
|
edges.push((e.from, e.to));
|
|
}
|
|
for role in c.input_roles() {
|
|
let in_id = labels.len();
|
|
labels.push(role.name.clone());
|
|
for t in &role.targets {
|
|
edges.push((in_id, t.node));
|
|
}
|
|
}
|
|
// outputs are no longer standalone nodes — see output_binding (C23: the name
|
|
// is a render symbol folded onto its producer, never a wired terminal).
|
|
|
|
format!("{}:\n\n{}", signature(c), render_flat(&labels, &edges, color))
|
|
}
|
|
|
|
/// The `name := ` (single) or `(n1, n2) := ` (tuple) binding prefix for interior
|
|
/// node `i`, if any `OutField` re-exports it; `None` for a non-output node. Names
|
|
/// are ordered by re-exported `field` index (ties by author order). The producer
|
|
/// label follows the prefix; no standalone output node or producer→output edge is
|
|
/// emitted (the output is the producer, surfaced by name — C23).
|
|
fn output_binding(c: &Composite, i: usize) -> Option<String> {
|
|
let mut outs: Vec<&OutField> = c.output().iter().filter(|of| of.node == i).collect();
|
|
if outs.is_empty() {
|
|
return None;
|
|
}
|
|
outs.sort_by_key(|of| of.field);
|
|
let names: Vec<&str> = outs.iter().map(|of| of.name.as_str()).collect();
|
|
Some(match names.as_slice() {
|
|
[one] => format!("{one} := "),
|
|
many => format!("({}) := ", many.join(", ")),
|
|
})
|
|
}
|
|
```
|
|
|
|
`OutField` is already imported in `graph.rs` via the `aura_engine` use group; if
|
|
not, add it there (engine re-exports it from `lib.rs`).
|
|
|
|
## Components
|
|
|
|
| Component | File | Change |
|
|
|-----------|------|--------|
|
|
| `render_definition` | `crates/aura-cli/src/graph.rs` | drop the output-node loop; prefix each interior label via `output_binding`; update the doc comment |
|
|
| `output_binding` | `crates/aura-cli/src/graph.rs` | new private helper |
|
|
| `macd_blueprint_renders_a_nested_composite_definition` | `crates/aura-cli/src/main.rs` | re-pin to the `:=` form (see Testing strategy) |
|
|
|
|
The MACD author site (`fn macd` in `main.rs`) and every engine type are
|
|
**unchanged** — this is render-only.
|
|
|
|
## Data flow
|
|
|
|
1. **Author** writes a `Composite` with `Vec<OutField>` (unchanged).
|
|
2. **`param_space()` / `compile_with_params`** are untouched — outputs lower
|
|
exactly as today; the name is dropped at inline (C23).
|
|
3. **Blueprint render** (`render_definition`): for each interior node, the output
|
|
names re-exporting it are folded onto its label as a `:=` binding; no standalone
|
|
output node is emitted. Input roles still render as entry nodes.
|
|
4. **Compiled render** (`render_flat_graph`) is name-free and byte-identical
|
|
(`compiled_view_golden`).
|
|
|
|
## Error handling
|
|
|
|
No new failure modes. `render_definition` reads structure only and never `eval`s.
|
|
`output_binding` filters `c.output()` whose `node`/`field` indices were already
|
|
validated at `compile_with_params` (the rendering of a malformed blueprint is not a
|
|
correctness path — compile is the validator, #41). An `OutField` whose `node` is
|
|
out of range simply matches no interior index in the label loop and contributes no
|
|
binding — consistent with today's widening posture (no panic in the renderer).
|
|
|
|
## Testing strategy
|
|
|
|
CLI render test (`crates/aura-cli/src/main.rs`),
|
|
`macd_blueprint_renders_a_nested_composite_definition` — re-pinned to the `:=` form:
|
|
|
|
1. **Bound producers.** The render contains `[macd := Sub(#Ef,#Es)]`,
|
|
`[signal := EMA(signal)]` (the intended name/param collision), and
|
|
`[histogram := Sub(#S,#Es)]`.
|
|
2. **Bare non-outputs.** `[EMA(fast)]` and `[EMA(slow)]` stay bare (nodes 0/1 are
|
|
not re-exported).
|
|
3. **No standalone output stubs.** The render does **not** contain the bracketed
|
|
exact forms `[signal]` or `[histogram]`. (`[macd]` is *not* a valid negative
|
|
assertion — it still appears in the **main** graph as the opaque composite node;
|
|
discriminate on signal/histogram, which appear only inside their bindings and in
|
|
the signature's `(…)` list, never as `[signal]`/`[histogram]`.)
|
|
4. **Unchanged anchors (regression).** `[price]` present; the signature line
|
|
`macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram)` present;
|
|
`macd(` occurs exactly once; no `[param:`, no `[out:`.
|
|
|
|
Build / determinism:
|
|
|
|
5. `cargo build/test/clippy --workspace` (`-D warnings`) green; `compiled_view_golden`
|
|
byte-identical (C23 — the compiled view never carried output names); the MACD run
|
|
stays deterministic and unchanged in metrics (render-only change).
|
|
|
|
> **Post-ship note (audit 0022).** This Testing strategy and the §Components table
|
|
> under-counted the test blast radius: besides the `contains`-style assertions, the
|
|
> full-render golden `blueprint_view_golden` (`crates/aura-cli/src/main.rs`) pins the
|
|
> entire sample (`sma_cross`) blueprint render and was forced to re-capture (the
|
|
> `[cross]` output stub + its edge collapse into `[cross := Sub(#Sf,#Ss)]`). The
|
|
> shipped change is correct and green; this note records the doc-vs-reality gap that
|
|
> the implement phase surfaced.
|
|
|
|
A dedicated multi-output-per-node tuple-binding unit test is **not** required for
|
|
acceptance (no such composite exists in the fixtures), but `output_binding`'s tuple
|
|
branch is specified above so a future multi-output composite renders `(a, b) :=`
|
|
without a renderer change.
|
|
|
|
## Acceptance criteria
|
|
|
|
- The MACD `where:` definition renders 6 interior nodes (5 compute + `price`), not
|
|
8 — the worked before → after node-label set is the evidence. Every drawn node is
|
|
a real compute step or the input entry; no output appears as a false terminal.
|
|
- Output re-exports render as `name := <producer-label>` bindings; the producer's
|
|
real interior fan-out is preserved (the macd line still visibly feeds the signal
|
|
EMA and the histogram).
|
|
- The renderer measurably **removes redundancy** (3 stub nodes + 3 edges per MACD
|
|
render gone; the names live once, on their producers) — the aura feature-acceptance
|
|
criterion this cycle leans on.
|
|
- C23 preserved: `compiled_view_golden` byte-identical; no output name reaches a
|
|
`Box<dyn Node>`. No engine/blueprint/bootstrap change.
|
|
- `cargo build/test/clippy --workspace` (`-D warnings`) green; `aura run --macd`
|
|
deterministic and unchanged in metrics.
|
|
- Closes #46.
|