Files
Aura/docs/specs/0020-composite-signature-render.md
T
Brummel 21c1621bd0 docs,engine: drop coined "compilat", use FlatGraph / "flat graph"
"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.
2026-06-14 17:02:15 +02:00

14 KiB

Composite Signature Render — Design Spec

Date: 2026-06-08 Status: Draft — awaiting user spec review Authors: orchestrator + Claude

Goal

Render a composite's blueprint definition as a typed signature line plus a de-cluttered body, replacing the [param:<name>] marker nodes #41 introduced (which bloat the ASCII graph). The interface moves into the title; the params and ordered inputs fold into the node labels that already exist.

This is render-only — no engine logic changes. The #41 ParamAlias data and param_space() are untouched.

For the MACD composite, the definition title becomes:

macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram)

and the three [param:*] marker nodes vanish. Output names only (not kinds): a value-empty LeafFactory exposes no output schema pre-build (the mc_4/#42 finding), so the typed output arrow (-> (macd:f64, …)) is deferred to #43. This cycle references #41 (the surface it refines) and #43 (the deferred typing).

Architecture

The blueprint-definition view (render_definition in crates/aura-cli/src/graph.rs) is the only renderer that changes. Five edits, all in render_definition except one doc-comment fix:

  1. Title = signature, not bare name. c.name()name(params) -> (outputs).
  2. Leaf labels fold their aliased param names. [EMA][EMA(fast)].
  3. Unnamed interior input slots render as positional stubs. [Sub][Sub(#A,#B)], derived from the interior wiring.
  4. Output markers drop the out: prefix. [out:macd][macd].
  5. The [param:<name>] marker nodes are removed.

Plus a stale-prose fix in crates/aura-core/src/node.rs.

Why this is renderable now (the ascii-dag history). LeafFactory::label historically stayed the bare type (SMA, not SMA(length)) because ascii-dag 0.9.1 garbles wide sibling labels inside a cluster subgraph. Cycle 0017 removed clusters — both aura graph views build flat graphs (crates/aura-cli/src/graph.rs:13-14: "the subgraph layout mis-centres wide sibling labels, the flat layout does not"). Wide labels are now safe, which is what makes [EMA(fast)] / [Sub(#A,#B)] renderable. The doc-comment at node.rs:93-101 still cites the cluster constraint as the reason for bare-type labels — stale prose this cycle corrects.

Invariant-neutrality

  • Render-only. No engine logic change; ParamAlias, param_space(), the compile path, and the run are untouched. Files: graph.rs (render_definition), main.rs (render tests/goldens), node.rs (doc comment only).
  • C23. compiled_view_golden is byte-identical — only the blueprint-definition render changes; the compiled/flat view and the flat graph are unaffected. Boundary names still never reach a Box<dyn Node>.
  • C8/C7/C4 untouched — authoring-surface legibility only. aura run --macd is deterministic and unchanged (render-only).

Concrete code shapes

Worked example — aura graph --macd, before → after

The MACD definition block today (real output — note the three [param:*] markers and the loose [in:price] cluttering the top, and the bare [EMA]/[Sub]):

macd:
  [param:fast]   [in:price]   [param:slow]   [param:signal]
        │             └┐───────┌────┘ ┌─────────────┘
        └──────────────┐       │      │
                       ↓       ↓      │
                     [EMA]   [EMA]    │
                       └───┌───┘      │
                           ↓      ┌───┘
                         [Sub]    │
                       ┌───┘──────│────┐
                       ↓          └────↓
                  [out:macd]      │  [EMA]
                      ┌───────────┌────┘
                      ↓───────────↓
                    [Sub]   [out:signal]
                      └───────┐
                              ↓
                       [out:histogram]

After this cycle — title carries the signature, [param:*] markers gone, params folded into the EMAs, ordered Sub inputs shown as stubs, outputs de-prefixed (exact box-drawing layout is whatever the flat renderer produces; the load-bearing changes are the labels):

macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram):
              [in:price]
               └┐──────┌┘
                ↓      ↓
          [EMA(fast)] [EMA(slow)]
                └──┌────┘
                   ↓
              [Sub(#A,#B)]
              ┌────┘────┐
              ↓         ↓
            [macd]  [EMA(signal)]
                  ┌──────┘
                  ↓
             [Sub(#A,#B)]
              ┌────┘────┐
              ↓         ↓
          [signal]  [histogram]

The sma_cross sample (named role, no aliases) confirms the empty-param case renders cleanly — no params in the signature, leaves stay bare:

sma_cross() -> (cross):
        [in:price]
         └┐──────┌┘
          ↓      ↓
        [SMA]  [SMA]
          └──┌───┘
             ↓
        [Sub(#A,#B)]
             │
             ↓
          [cross]

The label-annotation rule (pinned)

A leaf item's label in render_definition is factory.label() plus an optional parenthesised annotation built in this order:

  1. Aliased param names for this leaf — every a.name in c.params() with a.node == this_index, in c.params() order.
  2. Input-slot stubs — only if the leaf has >1 distinct wired input slot: one positional stub per slot in slot order (#A, #B, #C, …). A single-input leaf gets no stub (no ordering ambiguity).

If both are present, the annotation is factory.label() + ( + params + ; + stubs + ) (params and stubs separated by ; ). If only one is present, no separator. If neither, the bare label (today's behaviour). In the worked MACD/SMA graphs every leaf hits exactly one branch — EMAs have one aliased param and one input (→ [EMA(fast)]); Subs have no params and two inputs (→ [Sub(#A,#B)]) — the combined ; form is defined for completeness, not exercised here.

A node's wired input slots are the distinct .slot values targeting it, collected from both c.edges() (entries with to == index) and c.input_roles() (each role's targets with node == index). Nested-composite interior items keep rendering as their opaque name() (unchanged; their own definition renders separately).

Before → after — render_definition (crates/aura-cli/src/graph.rs)

Today (the relevant parts): leaves labelled bare; an [in:k], [param:name], and [out:name] marker per role/alias/output; title is c.name().

fn render_definition(c: &Composite) -> String {
    let mut labels: Vec<String> = Vec::with_capacity(c.nodes().len());
    for inner in c.nodes() {
        labels.push(match inner {
            BlueprintNode::Leaf(factory) => factory.label(),
            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(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));
    }
    format!("{}:\n{}", c.name(), render_flat(&labels, &edges))
}

After: leaf labels annotated (params folded + input stubs), the [param:*] loop removed, outputs de-prefixed, the title built from a new signature(c) helper.

fn render_definition(c: &Composite) -> 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(format!("in:{}", role.name));
        for t in &role.targets {
            edges.push((in_id, t.node));
        }
    }
    // params are no longer marker nodes — they fold into leaf labels (leaf_label)
    // and the signature title (signature). The [param:*] loop is removed.
    for of in c.output() {
        let out_id = labels.len();
        labels.push(of.name.clone()); // [out:macd] -> [macd]
        edges.push((of.node, out_id));
    }
    format!("{}:\n{}", signature(c), render_flat(&labels, &edges))
}

with two new render-side helpers (shapes; exact bodies are the planner's):

// "macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram)"
// param kinds from the interior leaf's factory().params()[slot].kind;
// output names only (kinds deferred to #43).
fn signature(c: &Composite) -> String { /* ... */ }

// "EMA(fast)" / "Sub(#A,#B)" / bare "SMA" per the label-annotation rule above.
fn leaf_label(c: &Composite, index: usize, factory: &LeafFactory) -> String { /* ... */ }

node.rs doc-comment fix (crates/aura-core/src/node.rs:93-101)

The LeafFactory::label doc currently reads (paraphrased) "the label stays the bare type because ascii-dag overlaps two wide sibling boxes inside a cluster subgraph". Replace that rationale: the renderer is flat (no clusters) since cycle 0017, so wide labels are safe; the factory label stays the bare param-generic type because alias / handle names are composite-level concepts resolved at render (render_definition folds them via c.params()), not because of any ascii-dag constraint. Behaviour-neutral (a comment).

Components

Component File Change
render_definition crates/aura-cli/src/graph.rs title = signature(c); leaf labels via leaf_label; drop [param:*] loop; outputs de-prefixed
signature (new) crates/aura-cli/src/graph.rs builds the name(params) -> (outputs) line; param kinds from factory().params()[slot].kind; output names only
leaf_label (new) crates/aura-cli/src/graph.rs folds aliased param names + input-slot stubs per the pinned rule
LeafFactory::label doc crates/aura-core/src/node.rs:93-101 replace stale cluster rationale (comment only)
render tests / goldens crates/aura-cli/src/main.rs blueprint_view_golden re-capture; new assertions; compiled_view_golden untouched

Data flow

Render is a pure read over the blueprint (C9 graph-as-data): render_definition reads c.nodes(), c.edges(), c.input_roles(), c.params(), c.output() and the interior leaves' factory().label() / factory().params(). No build, no param vector, no engine call. signature resolves param kinds by indexing the aliased leaf's factory().params(); output kinds are intentionally not resolved (deferred to #43).

Error handling

Render is total over a well-formed blueprint (the same blueprints the existing render_definition already renders). An alias is validated at compile (BadInteriorIndex, #41); the renderer is reached only for renderable blueprints, so signature/leaf_label index c.params()/factory().params() within the bounds an already-constructed composite guarantees. No new error paths.

Testing strategy

  • blueprint_view_golden (crates/aura-cli/src/main.rs) re-captured — the definition render changed. Run the test, paste the actual rendered output wholesale into the golden literal.
  • New render assertions on the MACD definition: contains the signature substring macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram), contains [EMA(fast)], [Sub(#A,#B)], [macd], [in:price]; and asserts [param: is absent (markers removed) and [out: is absent (prefix dropped).
  • compiled_view_golden byte-identical (C23 guard — must stay green, untouched; a drift here is a bug, not a re-capture).
  • run_macd…deterministic green, metrics unchanged (render-only).
  • No-alias case: a sma_cross render assertion confirms sma_cross() -> (cross) (empty param list) and bare [SMA] leaves render cleanly.

Acceptance criteria

  • A trader reading aura graph --macd sees the composite's interface as a typed signature line (macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram)) and an uncluttered body — the [param:*] marker bloat is gone, params read on the EMAs ([EMA(fast)]), the ordered Sub inputs are visible ([Sub(#A,#B)]). The worked before→after is the evidence.
  • Render-only: compiled_view_golden byte-identical; aura run --macd deterministic and unchanged; no engine/ParamAlias/param_space() change.
  • Output kinds are honestly omitted (names only), with the typed arrow deferred to #43; the spec and commit reference #41 (surface) and #43 (deferred typing), and this cycle closes no issue.
  • cargo build/test/clippy --workspace (-D warnings) green.