Files
Aura/docs/specs/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

13 KiB

Blueprint view as main graph + composite definitions — Design Spec

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

Goal

Redesign the aura graph blueprint view (render_blueprint in crates/aura-cli/src/graph.rs) from the current "composites as ascii-dag subgraph cluster boxes, interiors expanded inline" model to a program-with-subroutines model: a small main graph that wires the harness with each composite shown as a single opaque node [name], plus a definitions section that renders each distinct composite type's interior exactly once.

This is the faithful representation of the blueprint as authored structure (C9/C12: a composite is an authoring-level node), distinct from the compiled view which is the inlined machine form (C23). It scales to real strategies, renders a reused composite's body once, removes the current nested-composite unimplemented!, and renders on ascii-dag's flat layout only — sidestepping the cluster-layout bug at the root.

The compiled view (render_flat_graph, flat fully-inlined, C23) is unchanged, byte-for-byte.

Architecture

Two ontological tiers map onto two render shapes:

View What it shows Mechanism
Blueprint (render_blueprint) authored source: harness wiring + composites as named subroutines, each body rendered once main flat graph (composite = opaque node) + a where: block of per-type definitions
Compiled (render_flat_graph) inlined machine form, boundaries dissolved (C23) one flat graph (unchanged)

The redesign is CLI-render-only. No change to the engine, the Node/Blueprint data model, or render_flat_graph. The blueprint data model already carries everything needed (Composite::name/nodes/edges/input_roles/output, Blueprint::nodes/sources/edges).

Why the model (substance, not effort):

  1. Correctness under width. ascii-dag 0.9.1's subgraph level-centering rounds sibling x-positions with an integer /2; wide sibling labels inside a cluster overlap (e.g. [SimpleMovingAverage(length)[SimpleMovingAverage(length)]). The garble is width/parity-sensitive and has no config or padding dodge that survives unequal-width siblings (Positioning exposes only Compact; Barycentric/BrandesKopf are unimplemented stubs; render() takes no config). The flat layout reserves width + 3 per node and is collision-free at all widths. The new model renders only flat graphs, so the bug cannot arise.
  2. Scale. The blueprint is authored structure, not the flat graph. A composite with 50 interior nodes is one node in the main graph; blueprint size tracks top-level wiring, not inlined node count. Real strategies stay displayable.
  3. Render-once. A composite reused N times appears N times as an opaque node in the main graph, but its body is rendered exactly once in the definitions section (deduplicated by composite type).

These satisfy aura's feature-acceptance criterion: the intended audience (a human or LLM inspecting an authored strategy) naturally reaches for a structural view that does not explode with strategy size; it removes a redundancy (a reused body drawn N times) and a latent failure (the cluster garble); and it reintroduces no look-ahead / determinism / scalar-typing violation (it is a read-only structural render, no eval).

Concrete code shapes

Worked example — what aura graph emits (the acceptance evidence)

For the built-in sample (build_sample() in crates/aura-cli/src/main.rs): the sma_cross composite (two SMA leaves → Sub) wired into Exposure → SimBroker → Recorder, with the price source feeding the composite and the broker, and exposure also tapped to a second Recorder.

The new blueprint view (real ascii-dag flat renders; final byte-exact goldens captured at implementation — the assembly format below is the shape):

        [source:F64]
          ┌───└─────┐
          ↓         │
     [sma_cross]    │
          └┐        │
           ↓        │
      [Exposure]    │
       ┌───┘────────│
       ↓            ↓
  [Recorder]   [SimBroker]
              ┌─────┘
              ↓
         [Recorder]

where:

sma_cross:
     [in:0]
    ┌───└───┐
    ↓       ↓
  [SMA]   [SMA]
    └───┌───┘
        ↓
      [Sub]
        │
        ↓
      [out]

The main graph shows sma_cross as a single opaque node; its body appears once under where:. A 50-node composite, or one reused three times, leaves the main graph the same size. Both SMA leaves render identically as [SMA] (bare type; values appear only in the compiled view, C22/#31).

Before → after: render_blueprint

The load-bearing change is that a composite maps to one display node, not a cluster of interior ids, and a second pass emits the definitions.

ItemDisplay — a composite is now one node, not a set of interior ids:

// before: a composite carried its interior display ids + boundary-resolution data
enum ItemDisplay {
    Leaf(usize),
    Composite { interior_ids: Vec<usize>, output_interior: usize, input_roles: Vec<Vec<Target>> },
}

// after: a composite is a single opaque main-graph node
enum ItemDisplay {
    Leaf(usize),
    Composite(usize),   // the one opaque display id
}

Boundary resolution collapses — every edge touching a composite attaches to its single node, regardless of slot:

fn producer_id(d: &ItemDisplay) -> usize {
    match d { ItemDisplay::Leaf(id) | ItemDisplay::Composite(id) => *id }
}
// a composite's input slots all land on the one opaque node
fn consumer_ids(d: &ItemDisplay, _slot: usize) -> Vec<usize> {
    match d { ItemDisplay::Leaf(id) | ItemDisplay::Composite(id) => vec![*id] }
}

render_blueprint becomes "render main graph (flat, composites opaque) + render each distinct composite definition once":

pub fn render_blueprint(bp: &Blueprint) -> String {
    // pass 1: one display node per top-level item (composite label = its name),
    //         plus source nodes; collect distinct composites by name in first-seen
    //         order for the definitions section.
    // pass 2: top-level + source edges, attaching to opaque composite nodes.
    let main = render_flat(/* main-graph nodes + edges, NO subgraphs */);

    // definitions: each distinct composite type once, recursively. A composite
    // nested inside another's interior is an opaque node there and queues its own
    // definition (dedup by name across the whole blueprint).
    let mut defs = collect_distinct_composites(bp);   // ordered, by name()
    let body = defs.iter().map(render_definition).collect::<Vec<_>>().join("\n");

    if defs.is_empty() { main } else { format!("{main}\nwhere:\n\n{body}") }
}

render_definition renders one composite's interior as a flat graph with port markers — input roles as entry nodes [in:k], the output port as [out]:

fn render_definition(c: &Composite) -> String {
    // interior leaves -> [type] nodes; nested composites -> opaque [name] nodes
    // (queued for their own definition). interior edges as-is.
    // role r -> add an [in:r] node with edges to each interior target in input_roles()[r].
    // output -> add an [out] node with an edge from the output interior node.
    // flat ascii-dag render, prefixed with "<name>:\n".
}

Components

  • render_blueprint(bp) -> String (rewritten). Produces main + optional where: block. Uses only flat ascii-dag graphs.
  • render_flat(labels, edges) -> String (new internal helper, or inline). A thin wrapper over the existing flat-graph build (Graph::with_mode(Vertical), add_node, add_edge, render) — exactly what render_flat_graph already does, with no add_subgraph / put_nodes.
  • collect_distinct_composites(bp) -> Vec<&Composite> (new). Walks top-level items and, recursively, composite interiors; returns distinct composites in first-seen order, keyed by name(). Recursion is what removes the unimplemented!.
  • render_definition(c) -> String (new). One composite interior → flat graph with [in:k] / [out] port markers, prefixed "<name>:\n".
  • ItemDisplay, producer_id, consumer_ids (simplified as above).
  • render_flat_graph — untouched.

Data flow

render_blueprint(bp):

  1. Main graph. For each bp.nodes() item: a leaf → one node labelled factory.label(); a composite → one node labelled c.name() (opaque). Each bp.sources() → one source:{kind} node. Edges: bp.edges() and source targets, resolved through producer_id/consumer_ids (composites attach at their single node). Render flat → main.
  2. Distinct composites. Traverse bp.nodes() and every composite's c.nodes() recursively; record each composite the first time its name() is seen (later same-name occurrences are skipped — dedup). Order = first-seen.
  3. Definitions. For each distinct composite, render_definition: interior leaves → [type], interior nested composites → opaque [name] (already queued in step 2), interior edges from c.edges(); add [in:k] nodes wired to c.input_roles()[k] targets, an [out] node wired from c.output().node. Render flat, prefix "<name>:\n".
  4. Assemble. main, then (if any composites) \nwhere:\n\n + definitions joined by a blank line.

Error handling

  • Same name, different structure. Dedup keys on name(). The authoring convention is that a composite name is its type, so same name ⇒ same structure; the first-seen body is rendered and later same-name composites are treated as the same type. This is a stated assumption, consistent with the ledger's "name is a non-load-bearing render symbol" (C23). A structural-identity key is a possible future refinement, out of scope here. No error is raised — this is a best-effort debug view, not a validator.
  • No composites. A blueprint of only leaves emits just the main graph, no where: block (the defs.is_empty() branch).
  • Recursion. Nested composites terminate because the blueprint is a finite DAG of items; dedup-by-name additionally guarantees each type is rendered once even under repeated nesting. (A blueprint cannot contain itself; composites are values built bottom-up.)
  • The render reads structure and label() only — never eval, no look-ahead, no determinism surface touched.

Testing strategy

Unit tests in crates/aura-cli/src/main.rs (replacing the two blueprint tests that assert the old cluster-box form):

  1. blueprint_view_main_graph_shows_composite_as_opaque_node — render the sample; assert the main graph contains [sma_cross] (the opaque node) and the top-level leaves [Exposure] / [SimBroker] / [Recorder], and that a where: section is present.
  2. blueprint_view_defines_each_composite_once — assert the sma_cross definition appears, containing [SMA], [Sub], and the port markers [in:0] / [out]; assert sma_cross: (the definition header) occurs exactly once.
  3. blueprint_view_golden (re-captured) — the exact bytes of the new render for the sample (ascii-dag's Sugiyama layout is deterministic; capture at implementation).
  4. nested_composite_renders_without_panic — construct a small blueprint with a composite whose interior contains another composite (one level of nesting); assert it renders (no unimplemented! panic), the outer definition shows the inner as an opaque [name] node, and the inner composite gets its own definition. This pins the removal of the graph.rs:75 unimplemented!.
  5. Reuse-renders-once — a blueprint using the same composite type twice; assert the main graph shows two opaque nodes but the definition header appears once.

Unchanged and must stay green: compiled_view_dissolves_the_composite_boundary, compiled_view_golden, swapped_param_vector_changes_the_compiled_render, run_sample_is_deterministic_and_non_trivial.

Acceptance criteria

  • aura graph (no flag) prints the sample as a small main graph with [sma_cross] opaque, followed by a where: block defining sma_cross once, on a flat layout (no ╔═╗ subgraph boxes).
  • aura graph --compiled output is byte-identical to before (render_flat_graph untouched; its golden unchanged).
  • The unimplemented!("nested-composite cluster rendering") at graph.rs:75 is gone; a one-level-nested composite renders.
  • A composite reused N times renders its body once.
  • cargo test --workspace green; cargo clippy --workspace --all-targets -D warnings clean.
  • Closes #38. (#37 — interactive enter/focus navigation — is the playground counterpart and explicitly out of scope.)