Files
Aura/docs/specs/0025-render-root-slot-stubs.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

12 KiB

Render the root composite like any other composite (slot stubs + role-name entries) — Design Spec

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

Goal

Close issue #49, and in doing so remove the last render special-case the root still carries. After the render-path unification (#48) the blueprint main graph and a composite's where:-interior share one core (render_graph, crates/aura-cli/src/graph.rs), but the root is still rendered specially in two places, both vestiges of the pre-0024 struct Blueprint:

  1. A composite-interior multi-input leaf renders its wired slots as #… source-derived stubs ([cross := Sub(#Sf,#Ss)]); a top-level leaf does not — SimBroker renders as bare [SimBroker] — because render_blueprint threads stub_ctx: None.
  2. An interior role renders as a [price] entry marker (the role name); a root role renders as [source:F64] (the ingestion kind), because render_blueprint builds entries from format!("source:{kind}").

Cycle 0024 (1b39093) deleted struct Blueprint: the root is now an ordinary Composite with input_roles: Vec<Role>. So both special-cases are obsolete — the root has roles, and the stub machinery is already keyed on &Composite + &[Role]. This cycle makes the root render identically to an interior: a root role becomes [price] marker + #price stub, byte-symmetric with how the same role renders inside a where: definition. The payoff is the C12 "render the structure so any mis-wiring is visible before a run" guarantee, now complete and uniform at every level — a swapped exposure/price wiring at a top-level SimBroker becomes visible, and the root no longer reads as a different kind of graph than its interiors.

Architecture

Two coordinated changes, both dissolving root-specialness; neither adds code that branches on "is this the root":

(A) Thread the stub context. render_blueprint passes the root composite as stub_ctx instead of None. Since 0024 the root is a &Composite carrying input_roles + edges, exactly what fan_in_identifiersslot_sourcesignature_of already consume. stub_ctx drops its now-vestigial Option (both render_graph callers pass &Composite). No new stub function — acceptance #3 holds — the existing machinery serves a top-level leaf verbatim. This makes the stubs mechanically determined:

  • An interior-fed root slot (SimBroker slot 0, the Exposure edge) takes the producer's sibling-unique signature_of prefix → #E.
  • A source-fed root slot (SimBroker slot 1, the price role) takes the role name verbatim via slot_source's role branch → #price.

(B) Name root entries by role. render_blueprint builds its external-entry markers from role.name (filtered to bound roles, source.is_some()), the same construction render_definition already uses for interior roles. The source:{kind} naming is removed. A bound root role therefore renders [price], and its slot stub #price now refers to a marker that carries the same name — the marker/stub mismatch the source:{kind} vestige caused is gone at the source, not patched at the stub.

What stays special is only what is genuinely special. The root still filters its entries to bound roles (source: Some) — an interior role is an open port, a root role is a bound ingestion feed (C3). That filter is semantic, not cosmetic, and is the sole remaining distinction; the rendering of a role is now identical across levels.

The compiled view is deliberately left as [source:F64]. render_flat_graph (graph.rs:495) builds source labels from the flat SourceSpec.kind — post-inline the role name has dissolved (C23, names are non-load-bearing debug symbols that do not reach the flat graph), so only the kind survives there. Thus [price] pre-inline (name alive) and [source:F64] post-inline (name gone) is not an inconsistency — it is C23 made visible, and it is the negative control proving the change is confined to the structural blueprint view.

Scope: the CLI render layer only (crates/aura-cli/src/graph.rs). No engine change, no run-loop change; read-only render (C9), behaviour-preserving for the run path (C1). signature_of (crates/aura-engine/src/blueprint.rs) is read, not modified.

Concrete code shapes

User-facing: aura graph --macd (the empirical evidence)

The MACD strategy root wires SimBroker (node 2) slot 0 (exposure) from the interior Exposure edge and slot 1 (price) from the price source role. Two lines of the main graph change — the source marker and the broker leaf:

# before — root reads as a different graph: kind-named source, bare fan-in leaf
       [source:F64]
       ...
  [Recorder]   [SimBroker]

# after — root reads like an interior: role-named source, stubbed fan-in leaf
       [price]
       ...
  [Recorder]   [SimBroker(#E,#price)]

#E is the exposure producer's sibling-unique signature prefix (Exposure's signature_of is Em at the macd root / Es at the sample root; the leading E already separates it from price). #price is the source role's name, verbatim — and now matches the [price] marker feeding the slot. The same two changes appear in the default aura graph (sample) view, whose root is wired identically.

Both marker and leaf change width, so the ascii-dag Sugiyama layout of the whole main graph re-flows. The full golden is re-captured from the actual aura graph output (not a line edit); the load-bearing facts are: the source marker reads [price], the SimBroker leaf carries the two #… stubs, and the where: section stays byte-identical.

Implementation before → after

// render_blueprint entry construction (crates/aura-cli/src/graph.rs ~line 70)
// before — kind-named source marker (pre-0024 vestige)
let entries: Vec<Entry> = bp.input_roles().iter()
    .filter_map(|role| role.source.map(|kind| Entry {
        name: format!("source:{kind:?}"),
        targets: &role.targets,
    }))
    .collect();
// after — role-named, identical to render_definition modulo the bound-only filter
let entries: Vec<Entry> = bp.input_roles().iter()
    .filter(|role| role.source.is_some())
    .map(|role| Entry { name: role.name.clone(), targets: &role.targets })
    .collect();
// render_graph signature (crates/aura-cli/src/graph.rs ~line 111)
// before:  stub_ctx: Option<&Composite>      after:  stub_ctx: &Composite
// leaf_label stub gate (crates/aura-cli/src/graph.rs ~line 283)
// before — Some/None gate; the None arm was the root carve-out
let stubs: Vec<String> = match stub_ctx {
    Some(c) if slots.len() > 1 => fan_in_identifiers(c, index, &slots),
    _ => Vec::new(),
};
// after — context always present; the fan-in test alone decides
let stubs: Vec<String> = if slots.len() > 1 {
    fan_in_identifiers(stub_ctx, index, &slots)
} else {
    Vec::new()
};
// the two render_graph call sites (crates/aura-cli/src/graph.rs ~80, ~454)
// render_blueprint:   None,    -> bp,
// render_definition:  Some(c), -> c,

The Entry doc-comment (graph.rs ~41, "the blueprint from SourceSpec (name = source:{kind})") and leaf_label's item-3 doc-comment (graph.rs ~243, "At the blueprint root no such context is threaded…") are rewritten: a root entry is named by its role like any interior role, and a multi-input fan-in renders its slot stubs in both views (the "root has no context" carve-out is removed).

Components

  • render_blueprint (graph.rs): entries built from role.name (bound-only filter retained); passes the root composite as stub_ctx.
  • render_graph / leaf_label (graph.rs): stub_ctx becomes &Composite; the fan-in gate keys on slots.len() > 1 alone.
  • render_definition (graph.rs): passes c instead of Some(c) (mechanical).
  • render_flat_graph (graph.rs): unchanged — keeps source:{kind} from the flat SourceSpec (C23).
  • slot_source / fan_in_identifiers / signature_of: unchanged, reused verbatim.

Data flow

render_blueprint(bp) builds entries { name: role.name, targets } for each bound root role, then calls render_graph(bp.nodes(), bp.edges(), entries, …, stub_ctx = bp). Per node, leaf_label(…, stub_ctx = bp); for a multi-input leaf, fan_in_identifiers(bp, index, slots) → per slot slot_source(bp, index, slot): an edge-fed slot resolves through signature_of(bp.nodes(), bp.edges(), bp.input_roles(), bp.params(), e.from); a role-fed slot returns the role name. The where: definitions still flow through render_definition(c)render_graph(…, stub_ctx = c), untouched and byte-stable. The compiled view flows through the independent render_flat_graph, untouched.

Error handling

No new failure modes. Read-only structural rendering (C9): the stub path indexes bp.nodes()/bp.edges()/bp.input_roles() the caller already holds; slot_source returns an empty signature for an unwired slot (never panics); fan_in_identifiers falls back to a positional #A for genuinely interchangeable inputs. The dropped Option and the dropped source:{kind} format both remove branches rather than add them. No engine validity surface is touched (the derive_signature raw-indexing gap is the separate, still-open #24).

Testing strategy

All affected tests live in crates/aura-cli/src/main.rs:

  1. blueprint_view_main_graph_shows_composite_as_opaque_node (~line 410): the needle list asserts the sample render contains [SimBroker] (bare) with the comment "paramless leaves (SimBroker, Recorder) stay bare". Update: the SimBroker needle becomes [SimBroker(#E,#price)], and the comment is corrected — a paramless single-input leaf (Recorder) stays bare, a paramless multi-input fan-in (SimBroker) now shows its slot stubs.

  2. blueprint_view_golden (~line 526): full byte golden of the sample blueprint render — re-captured from the actual aura graph output. Two changes: the source marker [source:F64][price], the SimBroker leaf gains (#E,#price); the main graph re-flows around both. The where: section stays byte-identical (acceptance #2).

  3. macd_blueprint_renders_a_nested_composite_definition (~line 616): add an assertion that the macd root SimBroker shows its two slot stubs — out.contains("[SimBroker(#E,#price)]"). The macd root source marker also becomes [price]; no existing assertion in this test pins the source marker, so the where:-interior and opaque-composite assertions are unchanged (acceptance #1 for --macd + acceptance #2).

  4. compiled_view_golden (~line 569): asserted unchanged — renders through render_flat_graph, a separate path that keeps [source:F64] (C23: name dissolved post-inline) and [SimBroker(0.0001)]. The negative control.

  5. nested_composite_renders_without_panic (~437) and reused_composite_defined_once (~475): use a src-named bound root role. Their [source:F64] marker becomes [src], but neither asserts on the source marker text (only on composite markers [outer]/[inner]/[dup] and definition counts), so both stay green unmodified. Confirms the entry-naming change carries the role name through generally, not just for price.

Build/test/lint gates: cargo build --workspace, cargo test --workspace, cargo clippy --workspace --all-targets -- -D warnings.

Acceptance criteria

Verbatim from #49:

  • aura graph --macd renders SimBroker's two input slots as #… stubs ([SimBroker(#E,#price)]).
  • The where: section stays byte-identical (the composite stub output is unchanged).
  • No second parallel stub function is introduced (the de-duplication from #48 holds) — stub_ctx loses its Option; both views call the same fan_in_identifiers/slot_source path.

Plus the cycle's structural-cleanliness goals:

  • A bound root role renders [price] (role name), byte-symmetric with how the same role renders as an interior entry marker; the source:{kind} special-case is removed from the blueprint view.
  • The sample aura graph view shows both [price] and [SimBroker(#E,#price)] (identical root wiring); blueprint_view_golden re-captured value-asserted.
  • compiled_view_golden is byte-unchanged: the compiled view keeps [source:F64] (C23 — name dissolved post-inline). Negative control.
  • Run-path determinism untouched (C1): only crates/aura-cli/src/graph.rs changes.
  • Closes #49 on land.