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.
385 lines
16 KiB
Markdown
385 lines
16 KiB
Markdown
# Blueprint view as main graph + composite definitions — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0017-blueprint-view-definitions.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
|
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** Rewrite the `aura graph` blueprint view (`render_blueprint`) to a main
|
|
graph (composites as opaque nodes) + a `where:` definitions section, on a flat
|
|
ascii-dag layout only, and re-point its tests.
|
|
|
|
**Architecture:** `render_blueprint` builds a flat main graph (one node per
|
|
top-level item — a composite labelled by `name()`, opaque) plus a definitions
|
|
block where each distinct composite type (deduped by `name()`, collected
|
|
recursively) renders its interior once with `[in:k]`/`[out]` port markers. The
|
|
old `ItemDisplay`/`producer_id`/`consumer_ids`/subgraph machinery and the
|
|
nested-composite `unimplemented!` are removed. `render_flat_graph` is untouched.
|
|
|
|
**Tech Stack:** `crates/aura-cli/src/graph.rs` (render), `crates/aura-cli/src/main.rs`
|
|
(tests), `ascii-dag` flat `RenderMode::Vertical`, the `aura_engine`
|
|
Blueprint/Composite API.
|
|
|
|
---
|
|
|
|
**Files this plan creates or modifies:**
|
|
|
|
- Modify: `crates/aura-cli/src/graph.rs:1-132` — module doc-comment (1-10), drop
|
|
`ItemDisplay`/`producer_id`/`consumer_ids` (16-46), rewrite `render_blueprint`
|
|
(48-132) incl. removing the `unimplemented!` (74-77); add `render_flat`,
|
|
`collect_distinct_composites`, `render_definition`; fix imports (12-14).
|
|
- Modify: `crates/aura-cli/src/main.rs:184` — stale "clustered blueprint view"
|
|
comment in the `graph` arm.
|
|
- Test: `crates/aura-cli/src/main.rs:215-227,256-286` — replace the two old
|
|
blueprint tests; add `blueprint_view_main_graph_shows_composite_as_opaque_node`,
|
|
`blueprint_view_defines_each_composite_once`,
|
|
`nested_composite_renders_without_panic`, `reused_composite_defined_once`, and a
|
|
recaptured `blueprint_view_golden`.
|
|
|
|
---
|
|
|
|
### Task 1: Rewrite `render_blueprint` + behavioural tests (RED → GREEN)
|
|
|
|
**Files:**
|
|
- Modify: `crates/aura-cli/src/graph.rs:1-132`
|
|
- Test: `crates/aura-cli/src/main.rs:215-227,256-286` (+ new tests)
|
|
|
|
- [ ] **Step 1: Replace the two old blueprint tests with the four behavioural tests**
|
|
|
|
In `crates/aura-cli/src/main.rs`, DELETE the test
|
|
`blueprint_view_shows_cluster_and_param_generic_labels` (currently lines 215-227)
|
|
and the test `blueprint_view_golden` (currently lines 256-286). In their place put
|
|
the four behavioural tests below (the recaptured golden is added in Task 2). Insert
|
|
them in the `#[cfg(test)] mod tests` block (e.g. where the old blueprint tests were):
|
|
|
|
```rust
|
|
#[test]
|
|
fn blueprint_view_main_graph_shows_composite_as_opaque_node() {
|
|
let out = graph::render_blueprint(&sample_blueprint());
|
|
// the composite is a single opaque main-graph node, not an expanded cluster
|
|
assert!(out.contains("[sma_cross]"), "missing opaque composite node:\n{out}");
|
|
// top-level leaves render as their bare-type nodes
|
|
for needle in ["[Exposure]", "[SimBroker]", "[Recorder]"] {
|
|
assert!(out.contains(needle), "missing {needle}:\n{out}");
|
|
}
|
|
// a definitions section is present
|
|
assert!(out.contains("where:"), "missing where: section:\n{out}");
|
|
// the flat layout draws no subgraph cluster box
|
|
assert!(!out.contains('╔'), "blueprint view must not draw a cluster box:\n{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn blueprint_view_defines_each_composite_once() {
|
|
let out = graph::render_blueprint(&sample_blueprint());
|
|
// the sma_cross body is defined exactly once, with its interior + ports
|
|
assert_eq!(out.matches("sma_cross:").count(), 1, "definition not rendered once:\n{out}");
|
|
for needle in ["[SMA]", "[Sub]", "[in:0]", "[out]"] {
|
|
assert!(out.contains(needle), "missing {needle} in definition:\n{out}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_composite_renders_without_panic() {
|
|
// a composite whose interior contains another composite — render reads
|
|
// structure only (no compile/validate), so a minimal fixture suffices.
|
|
let inner = Composite::new(
|
|
"inner",
|
|
vec![Sma::factory().into()],
|
|
vec![],
|
|
vec![vec![Target { node: 0, slot: 0 }]],
|
|
OutPort { node: 0, field: 0 },
|
|
);
|
|
let outer = Composite::new(
|
|
"outer",
|
|
vec![BlueprintNode::Composite(inner), Sub::factory().into()],
|
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
|
vec![vec![Target { node: 0, slot: 0 }]],
|
|
OutPort { node: 1, field: 0 },
|
|
);
|
|
let bp = Blueprint::new(
|
|
vec![BlueprintNode::Composite(outer)],
|
|
vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
|
|
vec![],
|
|
);
|
|
let out = graph::render_blueprint(&bp); // must not panic (no unimplemented!)
|
|
// outer shows the inner composite as an opaque node, and both get a definition
|
|
assert!(out.contains("[outer]"), "missing opaque outer node:\n{out}");
|
|
assert!(out.contains("[inner]"), "inner must be opaque inside outer's definition:\n{out}");
|
|
assert_eq!(out.matches("outer:").count(), 1, "outer defined once:\n{out}");
|
|
assert_eq!(out.matches("inner:").count(), 1, "inner defined once:\n{out}");
|
|
}
|
|
|
|
#[test]
|
|
fn reused_composite_defined_once() {
|
|
// the same composite type used twice: two opaque nodes, one definition.
|
|
let bp = Blueprint::new(
|
|
vec![
|
|
BlueprintNode::Composite(sma_cross("dup")),
|
|
BlueprintNode::Composite(sma_cross("dup")),
|
|
Exposure::factory().into(),
|
|
],
|
|
vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
|
}],
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
|
],
|
|
);
|
|
let out = graph::render_blueprint(&bp);
|
|
assert_eq!(out.matches("[dup]").count(), 2, "two opaque uses expected:\n{out}");
|
|
assert_eq!(out.matches("dup:").count(), 1, "body defined once:\n{out}");
|
|
}
|
|
```
|
|
|
|
These tests need `Composite`, `OutPort`, `BlueprintNode`, `Blueprint`, `Edge`,
|
|
`SourceSpec`, `Target`, `ScalarKind`, `Sma`, `Sub`, `Exposure` in scope. They are
|
|
already imported at the top of `main.rs` (lines 11-16: `aura_core::{... ScalarKind ...}`,
|
|
`aura_engine::{... Blueprint, BlueprintNode, Composite, Edge, ... OutPort, ...
|
|
SourceSpec, Target}`, `aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}`). No
|
|
new imports needed in `main.rs`.
|
|
|
|
- [ ] **Step 2: Run the new tests to verify they fail (RED)**
|
|
|
|
Run: `cargo test -p aura-cli`
|
|
Expected: FAIL — the four new tests fail (the opaque-node / `where:` / definition
|
|
assertions fail against the current cluster render, and
|
|
`nested_composite_renders_without_panic` panics at the
|
|
`unimplemented!("...nested-composite cluster rendering...")`). The four
|
|
must-stay-green tests still pass; the build itself is clean (this is a behavioural
|
|
RED, not a compile error).
|
|
|
|
- [ ] **Step 3: Rewrite `graph.rs` — imports + module doc**
|
|
|
|
In `crates/aura-cli/src/graph.rs`, replace the module doc-comment (lines 1-10) and
|
|
imports (lines 12-14) with:
|
|
|
|
```rust
|
|
//! The `aura graph` ASCII-DAG adapter (#13, #38): turns the engine's
|
|
//! graph-as-data (C9) into an `ascii_dag::Graph` rendered to a `String`. Two
|
|
//! views. `render_blueprint` shows the authored structure — a flat main graph
|
|
//! wiring the harness with each composite as a single opaque node, plus a
|
|
//! `where:` section that defines each distinct composite type once (its interior
|
|
//! with `[in:k]`/`[out]` port markers). `render_flat_graph` shows the flat
|
|
//! post-inline graph (boundaries dissolved, C23). Rendering reads structure +
|
|
//! node `label()`s only — never `eval`.
|
|
//!
|
|
//! ascii-dag borrows its node labels as `&'a str`, so each function first
|
|
//! materializes the owned label `String`s (which outlive the `Graph`), then
|
|
//! borrows into them. `RenderMode::Vertical` is mandatory: Horizontal collapses a
|
|
//! fan-out onto one path. Both views build flat graphs (no subgraphs): the
|
|
//! subgraph layout mis-centres wide sibling labels, the flat layout does not.
|
|
|
|
use ascii_dag::graph::{Graph, RenderMode};
|
|
use aura_core::Node;
|
|
use aura_engine::{Blueprint, BlueprintNode, Composite, Edge, SourceSpec};
|
|
```
|
|
|
|
(`Composite` is added — named by the new helpers; `Target` is dropped — no longer
|
|
named after the `ItemDisplay` collapse. `Edge`/`SourceSpec`/`Node` remain: they are
|
|
`render_flat_graph`'s parameter types.)
|
|
|
|
- [ ] **Step 4: Rewrite `graph.rs` — replace `ItemDisplay`/`producer_id`/`consumer_ids`/`render_blueprint` (old lines 16-132)**
|
|
|
|
Delete the `ItemDisplay` enum, `producer_id`, `consumer_ids`, and the old
|
|
`render_blueprint` body (old lines 16-132) and replace with:
|
|
|
|
```rust
|
|
/// Blueprint view: the authored structure (#38). A flat main graph wires the
|
|
/// harness with each composite shown as a single opaque node; a `where:` section
|
|
/// defines each distinct composite type once. Flat layout only (no subgraphs).
|
|
pub fn render_blueprint(bp: &Blueprint) -> String {
|
|
// pass 1: one main-graph node per top-level item (leaf -> bare-type label;
|
|
// composite -> its name, opaque) and one node per source.
|
|
let mut labels: Vec<String> = Vec::new();
|
|
let mut item_ids: Vec<usize> = Vec::with_capacity(bp.nodes().len());
|
|
for item in bp.nodes() {
|
|
let id = labels.len();
|
|
labels.push(match item {
|
|
BlueprintNode::Leaf(factory) => factory.label(),
|
|
BlueprintNode::Composite(c) => c.name().to_string(),
|
|
});
|
|
item_ids.push(id);
|
|
}
|
|
let mut source_ids: Vec<usize> = Vec::with_capacity(bp.sources().len());
|
|
for src in bp.sources() {
|
|
let id = labels.len();
|
|
labels.push(format!("source:{:?}", src.kind));
|
|
source_ids.push(id);
|
|
}
|
|
|
|
// pass 2: edges — every endpoint is a single opaque node, so the slot that
|
|
// mattered for cluster fan-in is irrelevant here.
|
|
let mut edges: Vec<(usize, usize)> = Vec::new();
|
|
for e in bp.edges() {
|
|
edges.push((item_ids[e.from], item_ids[e.to]));
|
|
}
|
|
for (src, &sid) in bp.sources().iter().zip(&source_ids) {
|
|
for t in &src.targets {
|
|
edges.push((sid, item_ids[t.node]));
|
|
}
|
|
}
|
|
|
|
let main = render_flat(&labels, &edges);
|
|
|
|
// definitions: each distinct composite type, once, recursively.
|
|
let defs = collect_distinct_composites(bp);
|
|
if defs.is_empty() {
|
|
return main;
|
|
}
|
|
let body = defs.iter().map(|c| render_definition(c)).collect::<Vec<_>>().join("\n");
|
|
format!("{main}\nwhere:\n\n{body}")
|
|
}
|
|
|
|
/// Build and render a flat (no-subgraph) ascii-dag graph from owned labels and
|
|
/// edge pairs — the same idiom `render_flat_graph` uses.
|
|
fn render_flat(labels: &[String], edges: &[(usize, usize)]) -> String {
|
|
let mut g = Graph::with_mode(RenderMode::Vertical);
|
|
for (id, l) in labels.iter().enumerate() {
|
|
g.add_node(id, l);
|
|
}
|
|
for &(from, to) in edges {
|
|
g.add_edge(from, to, None);
|
|
}
|
|
g.render()
|
|
}
|
|
|
|
/// Every distinct composite type in the blueprint, in first-seen order, keyed by
|
|
/// `name()` (the authoring type identity — same name implies same structure).
|
|
/// Recurses into a composite's interior on first sight so nested composites get
|
|
/// their own definition; a later same-name occurrence is skipped (deduped).
|
|
fn collect_distinct_composites(bp: &Blueprint) -> Vec<&Composite> {
|
|
fn walk<'a>(items: &'a [BlueprintNode], seen: &mut Vec<&'a str>, out: &mut Vec<&'a Composite>) {
|
|
for item in items {
|
|
if let BlueprintNode::Composite(c) = item {
|
|
if !seen.contains(&c.name()) {
|
|
seen.push(c.name());
|
|
out.push(c);
|
|
walk(c.nodes(), seen, out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let mut seen: Vec<&str> = Vec::new();
|
|
let mut out: Vec<&Composite> = Vec::new();
|
|
walk(bp.nodes(), &mut seen, &mut out);
|
|
out
|
|
}
|
|
|
|
/// Render one composite's interior as a flat graph: interior leaves as `[type]`,
|
|
/// nested composites as opaque `[name]`, plus an `[in:k]` entry marker per input
|
|
/// role (wired to its interior targets) and an `[out]` marker (wired from the
|
|
/// output port). Prefixed `"<name>:\n"`.
|
|
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, targets) in c.input_roles().iter().enumerate() {
|
|
let in_id = labels.len();
|
|
labels.push(format!("in:{role}"));
|
|
for t in targets {
|
|
edges.push((in_id, t.node));
|
|
}
|
|
}
|
|
let out_id = labels.len();
|
|
labels.push("out".to_string());
|
|
edges.push((c.output().node, out_id));
|
|
|
|
format!("{}:\n{}", c.name(), render_flat(&labels, &edges))
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Build (compile gate — `render_blueprint`'s signature is unchanged, so `main()` still compiles)**
|
|
|
|
Run: `cargo build -p aura-cli --all-targets`
|
|
Expected: compiles, 0 errors. (If clippy flags an unused `Target` import, it was
|
|
not dropped in Step 3 — drop it.)
|
|
|
|
- [ ] **Step 6: Run the package suite to verify the new tests pass (GREEN)**
|
|
|
|
Run: `cargo test -p aura-cli`
|
|
Expected: PASS — the four new tests now pass, and the four must-stay-green tests
|
|
still pass. (`blueprint_view_golden` is not present yet; it is added in Task 2.)
|
|
|
|
---
|
|
|
|
### Task 2: Recapture the blueprint golden + full-suite gate
|
|
|
|
**Files:**
|
|
- Test: `crates/aura-cli/src/main.rs` (re-add `blueprint_view_golden`); comment at
|
|
`main.rs:184`.
|
|
|
|
- [ ] **Step 1: Capture the exact rendered bytes**
|
|
|
|
Run: `cargo run -q -p aura-cli -- graph`
|
|
Expected: the new flat main-graph + `where: sma_cross: ...` render prints to
|
|
stdout (deterministic Sugiyama layout, no RNG). Copy the **verbatim** stdout
|
|
(including the trailing blank lines `render()` emits) for the golden below.
|
|
|
|
- [ ] **Step 2: Add the recaptured `blueprint_view_golden` test**
|
|
|
|
In `crates/aura-cli/src/main.rs`, in the `#[cfg(test)] mod tests` block, add:
|
|
|
|
```rust
|
|
#[test]
|
|
fn blueprint_view_golden() {
|
|
let out = graph::render_blueprint(&sample_blueprint());
|
|
// ascii-dag's Sugiyama layout is deterministic (no RNG); these are the
|
|
// exact bytes `aura graph` emits — main graph (composites opaque) + the
|
|
// `where:` definitions section. Re-capture via `aura graph` if intended.
|
|
let expected = r#"<PASTE VERBATIM STDOUT FROM STEP 1 HERE>"#;
|
|
assert_eq!(out, expected, "blueprint render drifted; re-capture if intended");
|
|
}
|
|
```
|
|
|
|
Replace `<PASTE VERBATIM STDOUT FROM STEP 1 HERE>` with the exact bytes captured in
|
|
Step 1 (raw string `r#"..."#`; preserve every space, box glyph, arrow, and the
|
|
trailing newlines). Do not hand-edit the captured bytes.
|
|
|
|
- [ ] **Step 3: Fix the stale comment in the `graph` arm**
|
|
|
|
In `crates/aura-cli/src/main.rs`, in `main()`'s `Some("graph")` arm (around line
|
|
184), update the comment that calls the default view the "clustered blueprint
|
|
view":
|
|
|
|
Replace:
|
|
```rust
|
|
// `--compiled` selects the flat post-inline view; default is the
|
|
// clustered blueprint view. Strictness beyond this stays minimal (#16).
|
|
```
|
|
with:
|
|
```rust
|
|
// `--compiled` selects the flat post-inline view; default is the
|
|
// blueprint view (main graph + composite definitions). Strictness
|
|
// beyond this stays minimal (#16).
|
|
```
|
|
|
|
- [ ] **Step 4: Run the package suite to verify the golden passes**
|
|
|
|
Run: `cargo test -p aura-cli`
|
|
Expected: PASS — all aura-cli tests including the new `blueprint_view_golden`. If
|
|
the golden fails, the pasted bytes do not match — re-capture via Step 1 and
|
|
re-paste; do not hand-edit.
|
|
|
|
- [ ] **Step 5: Full workspace gate — tests + lint**
|
|
|
|
Run: `cargo test --workspace`
|
|
Expected: PASS, 0 failed. In particular the four must-stay-green tests
|
|
(`compiled_view_dissolves_the_composite_boundary`, `compiled_view_golden`,
|
|
`swapped_param_vector_changes_the_compiled_render`,
|
|
`run_sample_is_deterministic_and_non_trivial`) remain green — `render_flat_graph` is
|
|
untouched, so `compiled_view_golden` is byte-identical.
|
|
|
|
Run: `cargo clippy --workspace --all-targets -- -D warnings`
|
|
Expected: 0 warnings (no unused `Target` import, no dead `ItemDisplay`/`producer_id`/
|
|
`consumer_ids`).
|