Files
Aura/docs/plans/0017-blueprint-view-definitions.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

16 KiB

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):

    #[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:

//! 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:

/// 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:

    #[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:

            // `--compiled` selects the flat post-inline view; default is the
            // clustered blueprint view. Strictness beyond this stays minimal (#16).

with:

            // `--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).