From ed1d22bbfa2d38d1dbf1b4ca278357631b776a78 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 9 Jun 2026 17:47:18 +0200 Subject: [PATCH] spec: 0025 render root like any composite (slot stubs + role-name entries) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settled source: issue #49, enabled by cycle 0024 (1b39093, root is now a Composite with input_roles). The cycle removes the last two render special-cases the root still carries, both pre-0024 vestiges: (A) render_blueprint threads stub_ctx: None, so a top-level multi-input leaf (SimBroker) renders bare [SimBroker] instead of the #… slot stubs an interior leaf already gets. Fix: pass the root composite as stub_ctx (it IS a &Composite since 0024); stub_ctx drops its Option (both render_graph callers pass &Composite). The existing slot_source/fan_in_identifiers/signature_of serve a top-level leaf verbatim — no second stub path (#49 acceptance #3). -> [SimBroker(#E,#price)] (#E = exposure producer's signature prefix, #price = the source role name). (B) render_blueprint names root entries source:{kind} while render_definition names interior entries by role.name — the marker/stub asymmetry. Fix: build root entries from role.name (bound-only filter retained), identical to the interior. -> [price] instead of [source:F64], byte-symmetric with how the same role renders in a where: definition. render_compilat is left as [source:F64]: post-inline the role name has dissolved (C23), only the kind survives — [price] pre-inline / [source:F64] post-inline is C23 made visible, and compiled_view_golden is the negative control. Scope: crates/aura-cli/src/graph.rs only. Read-only render (C9), behaviour-preserving for the run path (C1); no engine change. Grounding-check PASS (10 assumptions ratified against green tests). Render goldens are the accepted value-asserted regression; closes #49 on land. --- docs/specs/0025-render-root-slot-stubs.md | 256 ++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/specs/0025-render-root-slot-stubs.md diff --git a/docs/specs/0025-render-root-slot-stubs.md b/docs/specs/0025-render-root-slot-stubs.md new file mode 100644 index 0000000..bbe607d --- /dev/null +++ b/docs/specs/0025-render-root-slot-stubs.md @@ -0,0 +1,256 @@ +# 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`. 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_identifiers` → `slot_source` → +`signature_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_compilat` +(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 compilat), 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: + +```text +# 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 + +```rust +// render_blueprint entry construction (crates/aura-cli/src/graph.rs ~line 70) +// before — kind-named source marker (pre-0024 vestige) +let entries: Vec = 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 = bp.input_roles().iter() + .filter(|role| role.source.is_some()) + .map(|role| Entry { name: role.name.clone(), targets: &role.targets }) + .collect(); +``` + +```rust +// render_graph signature (crates/aura-cli/src/graph.rs ~line 111) +// before: stub_ctx: Option<&Composite> after: stub_ctx: &Composite +``` + +```rust +// 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 = 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 = if slots.len() > 1 { + fan_in_identifiers(stub_ctx, index, &slots) +} else { + Vec::new() +}; +``` + +```rust +// 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_compilat`** (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_compilat`, 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_compilat`, 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.