plan: 0025 render root like any composite
Two-task plan for spec 0025 (#49). Task 1 is one compile unit in crates/aura-cli/src/graph.rs: thread the root composite as stub_ctx (drop the Option, update both render_graph callers) and name root entries by role.name instead of source:{kind}; gate is `cargo build -p aura-cli` (goldens stay red until Task 2 — the golden-recapture exception). Task 2 re-captures blueprint_view_golden from the live `aura graph` output, updates the SimBroker needle, adds a root-SimBroker-stub assertion to the macd test and a [src] role-name-passthrough assertion to reused_composite_defined_once; gate is `cargo test --workspace` + clippy. compiled_view_golden is the byte-unchanged negative control (render_compilat path, C23). plan-recon mapped exact line numbers and flagged the source:{kind} golden twin (blueprint changes / compiled must not) and the stale, out-of-scope render_clustered.txt fixture.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
# Render the root composite like any other composite — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0025-render-root-slot-stubs.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Close #49 by making a top-level blueprint leaf render its fan-in slot
|
||||
stubs (`[SimBroker(#E,#price)]`) and a bound root role render by name (`[price]`),
|
||||
byte-symmetric with how the same constructs render inside a `where:` definition —
|
||||
removing the last two pre-0024 root render special-cases.
|
||||
|
||||
**Architecture:** Two coordinated edits in `crates/aura-cli/src/graph.rs`, both in
|
||||
the CLI render layer: (A) thread the root composite as the fan-in stub context
|
||||
(`stub_ctx` drops its `Option`; both `render_graph` callers pass `&Composite`),
|
||||
and (B) build root entries from `role.name` instead of `format!("source:{kind}")`.
|
||||
The existing `slot_source`/`fan_in_identifiers`/`signature_of` are reused verbatim
|
||||
(no new stub path). `render_compilat` is untouched (the compiled view keeps
|
||||
`[source:F64]` — names dissolve post-inline, C23). Behaviour-preserving for the
|
||||
run path (C1); read-only render (C9); no engine change.
|
||||
|
||||
**Tech Stack:** `crates/aura-cli/src/graph.rs` (render core), golden + assertion
|
||||
updates in `crates/aura-cli/src/main.rs`. Goldens are value-asserted regressions
|
||||
captured from the live `aura graph` / `aura graph --macd` output.
|
||||
|
||||
---
|
||||
|
||||
## Files this plan creates or modifies
|
||||
|
||||
- Modify: `crates/aura-cli/src/graph.rs:37-46` — `Entry` doc-comment (drop the
|
||||
`source:{kind}` vestige; both views name entries by role).
|
||||
- Modify: `crates/aura-cli/src/graph.rs:62-89` — `render_blueprint` entry
|
||||
construction (role-named, bound-only filter) + the `render_graph` call
|
||||
(`None` → `bp`) + its leading comment.
|
||||
- Modify: `crates/aura-cli/src/graph.rs:106-117` — `render_graph` doc-comment +
|
||||
signature (`stub_ctx: Option<&Composite>` → `&Composite`).
|
||||
- Modify: `crates/aura-cli/src/graph.rs:243-258` — `leaf_label` doc-comment item 3
|
||||
+ signature (`stub_ctx: Option<&Composite>` → `&Composite`).
|
||||
- Modify: `crates/aura-cli/src/graph.rs:283-286` — `leaf_label` stub gate
|
||||
(`match` → `if slots.len() > 1`).
|
||||
- Modify: `crates/aura-cli/src/graph.rs:461` — `render_definition` `render_graph`
|
||||
call (`Some(c)` → `c`).
|
||||
- Test: `crates/aura-cli/src/main.rs:417` — needle `[SimBroker]` → captured
|
||||
`[SimBroker(#E,#price)]` + comment correction.
|
||||
- Test: `crates/aura-cli/src/main.rs:526-562` — `blueprint_view_golden` full
|
||||
re-capture.
|
||||
- Test: `crates/aura-cli/src/main.rs:615-639` — `macd_blueprint_renders_a_nested_composite_definition`
|
||||
add a root-`SimBroker`-stub assertion.
|
||||
- Test: `crates/aura-cli/src/main.rs:474-497` — `reused_composite_defined_once`
|
||||
add a `[src]` role-name-passthrough assertion.
|
||||
|
||||
**Explicitly OUT of scope (do not touch):**
|
||||
- `crates/aura-cli/src/graph.rs:486-505` (`render_compilat`) — the negative-control
|
||||
path; must stay byte-identical so `compiled_view_golden` does not move.
|
||||
- `crates/aura-cli/src/main.rs:564-589` (`compiled_view_golden`) — its expected
|
||||
block (`[source:F64]`, `[SimBroker(0.0001)]`) must NOT be edited.
|
||||
- `fieldtests/milestone-construction-layer/render_clustered.txt` — a stale manual
|
||||
fieldtest fixture (carries `[source:F64]` + a cluster box already stale since
|
||||
0017); not referenced by any `cargo test`, not part of this cycle.
|
||||
- Any file under `crates/aura-engine/` — `signature_of` is read, not modified.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: graph.rs — thread the stub context + name root entries by role
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/graph.rs`
|
||||
|
||||
This task is one compile unit: changing `stub_ctx`'s type from
|
||||
`Option<&Composite>` to `&Composite` breaks compilation until BOTH `render_graph`
|
||||
call sites (`render_blueprint` and `render_definition`) are updated, so all edits
|
||||
land together and the gate is a **build**, not a test (the goldens stay red until
|
||||
Task 2 re-captures them).
|
||||
|
||||
- [ ] **Step 1: Rewrite the `Entry` doc-comment (drop the `source:{kind}` vestige)**
|
||||
|
||||
Replace the doc-comment above `struct Entry` (`graph.rs:37-42`). Current:
|
||||
|
||||
```rust
|
||||
/// A CLI-local, *borrowed* render notion unifying a composite **input role** and a
|
||||
/// blueprint **source** for the shared graph core (#48): both are an external entry
|
||||
/// drawn as a marker node wired into its interior `targets`. The composite builds
|
||||
/// these from `Role` (name = role name), the blueprint from `SourceSpec` (name =
|
||||
/// `source:{kind}`) — no engine change, the list is assembled CLI-side and borrows
|
||||
/// the engine's `Target` slices.
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
/// A CLI-local, *borrowed* render notion unifying a composite **input role** and a
|
||||
/// blueprint **bound source role** for the shared graph core (#48): both are an
|
||||
/// external entry drawn as a marker node wired into its interior `targets`. Both
|
||||
/// views build these from `Role` (name = role name); the blueprint root filters to
|
||||
/// bound roles (`source.is_some()`), the only remaining root-vs-interior
|
||||
/// distinction (C3) — no engine change, the list is assembled CLI-side and borrows
|
||||
/// the engine's `Target` slices.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rewrite `render_blueprint`'s entry construction (role-named, bound-only filter)**
|
||||
|
||||
Replace the entry construction (`graph.rs:70-79`). Current:
|
||||
|
||||
```rust
|
||||
let entries: Vec<Entry> = bp
|
||||
.input_roles()
|
||||
.iter()
|
||||
.filter_map(|role| {
|
||||
role.source.map(|kind| Entry {
|
||||
name: format!("source:{kind:?}"),
|
||||
targets: &role.targets,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
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();
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `render_blueprint`'s leading comment and pass `bp` as the stub context**
|
||||
|
||||
Replace the comment block + `render_graph` call (`graph.rs:62-89`). Current
|
||||
(comment `62-69`, call `80-89`):
|
||||
|
||||
```rust
|
||||
// the main graph is the shared graph core (`render_graph`) over the root
|
||||
// composite's top-level (nodes, edges): leaves enriched exactly as `where:`
|
||||
// interior leaves are (param names folded in, a `<field> →` prefix for any input
|
||||
// fed by a multi-output producer — the headline: macd's `histogram` driving
|
||||
// Exposure), composites opaque. The deltas vs a composite definition: param names
|
||||
// come from the builder (no alias overlay at the root); the entries are the
|
||||
// root's source-bound roles, not interior roles; there is no output record
|
||||
// (terminals are sinks) and no title.
|
||||
let main = render_graph(
|
||||
bp.nodes(),
|
||||
bp.edges(),
|
||||
&entries,
|
||||
&[], // no output bindings: a blueprint's terminals are sinks
|
||||
ParamNames::Factory,
|
||||
None, // no fan-in stub context at the root (deterministic no-op, #48)
|
||||
None, // no title
|
||||
color,
|
||||
);
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
// the main graph is the shared graph core (`render_graph`) over the root
|
||||
// composite's top-level (nodes, edges), rendered exactly as a `where:` interior:
|
||||
// leaves enriched (param names, a `<field> →` prefix for a multi-output producer
|
||||
// — macd's `histogram` driving Exposure — and, now threaded at the root too,
|
||||
// fan-in slot stubs), composites opaque, entries named by their role. The only
|
||||
// deltas vs a composite definition: param names come from the builder (no alias
|
||||
// overlay at the root); entries are filtered to bound source roles (C3); there
|
||||
// is no output record (terminals are sinks) and no title.
|
||||
let main = render_graph(
|
||||
bp.nodes(),
|
||||
bp.edges(),
|
||||
&entries,
|
||||
&[], // no output bindings: a blueprint's terminals are sinks
|
||||
ParamNames::Factory,
|
||||
bp, // the root IS the fan-in stub context (it carries roles + edges)
|
||||
None, // no title
|
||||
color,
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `render_graph`'s doc-comment and signature**
|
||||
|
||||
Replace the tail of `render_graph`'s doc-comment + the signature
|
||||
(`graph.rs:106-120`). Current (doc tail `106-109`, signature `110-120`):
|
||||
|
||||
```rust
|
||||
/// `Some`. The two render-borders that differ — param-name source and fan-in stub
|
||||
/// availability — are passed as `param_names` / `stub_ctx`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_graph(
|
||||
nodes: &[BlueprintNode],
|
||||
edges: &[Edge],
|
||||
entries: &[Entry],
|
||||
output: &[OutField],
|
||||
param_names: ParamNames,
|
||||
stub_ctx: Option<&Composite>,
|
||||
title: Option<&str>,
|
||||
color: Color,
|
||||
) -> String {
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
/// `Some`. The render-border that differs — the param-name source — is passed as
|
||||
/// `param_names`; `stub_ctx` is the borrowed composite (root or interior) both
|
||||
/// views thread for fan-in slot resolution.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_graph(
|
||||
nodes: &[BlueprintNode],
|
||||
edges: &[Edge],
|
||||
entries: &[Entry],
|
||||
output: &[OutField],
|
||||
param_names: ParamNames,
|
||||
stub_ctx: &Composite,
|
||||
title: Option<&str>,
|
||||
color: Color,
|
||||
) -> String {
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Rewrite `leaf_label`'s doc-comment item 3 and update its signature**
|
||||
|
||||
Replace item 3 of `leaf_label`'s doc-comment (`graph.rs:243-246`). Current:
|
||||
|
||||
```rust
|
||||
/// 3. The leaf's input-slot stubs (`#Sf`, …) when it is a multi-input fan-in **and** a
|
||||
/// composite context is available (`stub_ctx = Some`). At the blueprint root no
|
||||
/// such context is threaded, so a top-level fan-in renders without stubs (a
|
||||
/// deterministic no-op, #48) rather than duplicating the signature machinery.
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
/// 3. The leaf's input-slot stubs (`#Sf`, …) when it is a multi-input fan-in. The
|
||||
/// `stub_ctx` is the borrowed composite (root or interior) the wired slots
|
||||
/// resolve against; both views thread it, so a top-level fan-in stubs exactly as
|
||||
/// an interior one does — one shared path, no root carve-out (#49).
|
||||
```
|
||||
|
||||
Then change the `leaf_label` signature parameter (`graph.rs:258`). Current:
|
||||
|
||||
```rust
|
||||
stub_ctx: Option<&Composite>,
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
stub_ctx: &Composite,
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Simplify the stub gate**
|
||||
|
||||
Replace the stub gate in `leaf_label` (`graph.rs:283-286`). Current:
|
||||
|
||||
```rust
|
||||
let stubs: Vec<String> = match stub_ctx {
|
||||
Some(c) if slots.len() > 1 => fan_in_identifiers(c, index, &slots),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
let stubs: Vec<String> = if slots.len() > 1 {
|
||||
fan_in_identifiers(stub_ctx, index, &slots)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Update `render_definition`'s `render_graph` call**
|
||||
|
||||
In `render_definition`, change the `stub_ctx` argument (`graph.rs:461`). Current:
|
||||
|
||||
```rust
|
||||
Some(c),
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```rust
|
||||
c,
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Build gate — graph.rs compiles**
|
||||
|
||||
Run: `cargo build -p aura-cli`
|
||||
Expected: finishes with `0 errors` (a `warning:`-free build; the
|
||||
`#[allow(clippy::too_many_arguments)]` stays, so no new clippy noise here).
|
||||
|
||||
- [ ] **Step 9: Observe the expected golden drift (RED confirmation, non-gating)**
|
||||
|
||||
Run: `cargo test -p aura-cli`
|
||||
Expected: `blueprint_view_golden` FAILS ("blueprint render drifted") and
|
||||
`blueprint_view_main_graph_shows_composite_as_opaque_node` FAILS (the
|
||||
`[SimBroker]` needle no longer matches — the leaf now reads
|
||||
`[SimBroker(#E,#price)]`). `compiled_view_golden`,
|
||||
`macd_blueprint_renders_a_nested_composite_definition`,
|
||||
`nested_composite_renders_without_panic`, and `reused_composite_defined_once`
|
||||
still PASS. This drift is expected and is fixed in Task 2 — do NOT hand-edit the
|
||||
goldens here.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: main.rs — re-capture goldens and add the new assertions
|
||||
|
||||
**Files:**
|
||||
- Test: `crates/aura-cli/src/main.rs`
|
||||
|
||||
The render bytes (stub text, ascii-dag re-flow) are produced by the live binary,
|
||||
not hand-written. Capture them, paste them, and reuse the exact captured
|
||||
`SimBroker` substring in the two `contains` assertions so the pin and the rendered
|
||||
output are byte-consistent.
|
||||
|
||||
- [ ] **Step 1: Capture the exact sample `aura graph` output**
|
||||
|
||||
Run: `cargo run -p aura-cli -- graph`
|
||||
Expected: the structural blueprint view prints to stdout. Two facts to read off
|
||||
it: (a) the full output bytes (for Step 2), and (b) the exact `SimBroker`
|
||||
bracket-substring on the `[Recorder] [SimBroker(…)]` line. The predicted value
|
||||
is `[SimBroker(#E,#price)]` (`#E` = the `Exposure` producer's sibling-unique
|
||||
`signature_of` prefix; `#price` = the `price` role name). If the capture differs,
|
||||
use the **captured** substring everywhere below — the golden is value-asserted,
|
||||
the prediction is not load-bearing.
|
||||
|
||||
- [ ] **Step 2: Replace the `blueprint_view_golden` expected block with the captured bytes**
|
||||
|
||||
In `blueprint_view_golden` (`main.rs:526`), replace the entire `expected`
|
||||
raw-string (`main.rs:531-560`, between `let expected = r#"` and `"#;`) with the
|
||||
exact stdout from Step 1. Keep the `// … Re-capture via aura graph if intended.`
|
||||
comment (`main.rs:528-530`) and the assert (`main.rs:561`).
|
||||
Verification of intent (not a code edit): the `where:` portion of the new block
|
||||
(the `sma_cross(fast:i64, slow:i64) -> (cross):` definition through
|
||||
`[cross := Sub(#Sf,#Ss)]`) must be **byte-identical** to the old block — only the
|
||||
main-graph portion (the `[source:F64]`→`[price]` marker and the `SimBroker` leaf)
|
||||
and its layout re-flow may differ. If a `where:` line changed, STOP — that means
|
||||
the interior render moved, which this cycle must not do.
|
||||
|
||||
- [ ] **Step 3: Run the golden test green**
|
||||
|
||||
Run: `cargo test -p aura-cli blueprint_view_golden`
|
||||
Expected: PASS (1 test). (Filter substring `blueprint_view_golden` matches exactly
|
||||
this named test in the current tree.)
|
||||
|
||||
- [ ] **Step 4: Update the needle and comment in the opaque-node test**
|
||||
|
||||
In `blueprint_view_main_graph_shows_composite_as_opaque_node`, replace the needle
|
||||
array (`main.rs:417`) and its comment (`main.rs:414-416`). Current:
|
||||
|
||||
```rust
|
||||
// top-level leaves render enriched (param names folded in, #48), exactly as
|
||||
// the `where:` interior leaves are — `Exposure` folds its `scale` param;
|
||||
// paramless leaves (SimBroker, Recorder) stay bare.
|
||||
for needle in ["[Exposure(scale)]", "[SimBroker]", "[Recorder]"] {
|
||||
```
|
||||
|
||||
New (use the exact `SimBroker` substring captured in Step 1 — shown here as the
|
||||
predicted `[SimBroker(#E,#price)]`):
|
||||
|
||||
```rust
|
||||
// top-level leaves render enriched exactly as the `where:` interior leaves:
|
||||
// `Exposure` folds its `scale` param; a paramless SINGLE-input leaf
|
||||
// (`Recorder`) stays bare, but a paramless MULTI-input fan-in (`SimBroker`)
|
||||
// now shows its slot stubs (#49).
|
||||
for needle in ["[Exposure(scale)]", "[SimBroker(#E,#price)]", "[Recorder]"] {
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add the root-`SimBroker`-stub assertion to the macd test**
|
||||
|
||||
First capture the macd view: `cargo run -p aura-cli -- graph --macd` and read its
|
||||
`SimBroker` bracket-substring (predicted `[SimBroker(#E,#price)]`; use the captured
|
||||
value if different). Then, in
|
||||
`macd_blueprint_renders_a_nested_composite_definition`, add one assertion after the
|
||||
`[histogram := Sub(#S,#Es)]` assertion (`main.rs:630`):
|
||||
|
||||
```rust
|
||||
// the root SimBroker is a top-level multi-input fan-in: its two slots now
|
||||
// render as #… stubs, mirroring the where: interior (#49).
|
||||
assert!(out.contains("[SimBroker(#E,#price)]"), "root SimBroker shows its two slot stubs: {out}");
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Add the `[src]` role-name-passthrough assertion**
|
||||
|
||||
In `reused_composite_defined_once`, add an assertion after the `dup(` count
|
||||
assertion (`main.rs:496`):
|
||||
|
||||
```rust
|
||||
// the bound root role named "src" carries its name into the entry marker
|
||||
// (general non-"price" role-name passthrough, not just the sample's price).
|
||||
assert!(out.contains("[src]"), "root role name renders as the entry marker: {out}");
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Test gate — the whole CLI crate is green**
|
||||
|
||||
Run: `cargo test -p aura-cli`
|
||||
Expected: all tests PASS (0 failed). In particular `compiled_view_golden` passes
|
||||
**unchanged** (its `[source:F64]` / `[SimBroker(0.0001)]` block was not edited —
|
||||
the negative control), and the four edited tests pass.
|
||||
|
||||
- [ ] **Step 8: Workspace gate — full suite + lint**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: all tests PASS (0 failed) — no run-path test moved (C1).
|
||||
|
||||
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
Expected: finishes clean (no warnings; exit 0).
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user