Files
Aura/docs/specs/0026-graph-render-redesign.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

17 KiB

aura graph render redesign — pin-graph via model + WASM-Graphviz — Design Spec

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

Goal

Replace the aura graph view. The current renderer (crates/aura-cli/src/graph.rs) is built on ascii-dag 0.9.1, whose node model is a single point: it cannot draw the one structure aura's data actually has — a node with n input pins and m output pins, where an Edge { from, from_field, to, slot } wires an output pin to an input pin. To compensate, today's renderer invents notation (#Sf, histogram →, := ) and omits most of the declared interface (input-pin kinds, the firing policy, leaf output names/kinds) even though PrimitiveBuilder::schema() has exposed all of it pre-build since #43 (cycle 0024).

The redesign renders homogeneously: every node is its body (type + param signature) plus its declared input pins and output pins, drawn from the data, inventing nothing and omitting nothing. Edges dock pin-to-pin. The graph is laid out and drawn by Graphviz compiled to WebAssembly, run in a browser — so aura ships no layout engine and stays a serializer (C9: graph-as-data, read-only, never eval). The interactive surface (drill-down into composites, inline-expand, per-pin tooltips) is the near-term graph viewer; the eventual egui playground (C22) is a separate, later track and is not in scope here.

This design was ratified interactively against a working prototype at docs/design/prototypes/graph-render/ (run via its fetch-deps.sh + a static server). The prototype is the visual source of truth; this spec lifts its worked shapes.

Architecture

aura emits a model, not pixels and not even DOT. The pipeline is three stages, and aura owns only the first:

 aura (Rust)            viewer (JS, shipped asset)        Graphviz-WASM (browser)
 ───────────            ──────────────────────────        ───────────────────────
 Blueprint  ──to_json──▶  model (JSON)  ──genDot──▶  DOT  ──render──▶  interactive SVG
 (C9 read-only)           drill/expand/tooltips
  • Stage 1 — model serializer (Rust, this engine). A new read-only serializer walks the root Composite (since 0024 the harness root is an ordinary Composite) and every distinct composite type it contains, and emits a deterministic, hand-rolled JSON model — following the existing house pattern RunReport::to_json (crates/aura-engine/src/report.rs:80), which is hand-rolled, serde-free, and determinism-tested. No serde dependency is added.
  • Stage 2 — viewer (JS asset). genDot (model → DOT) plus the chrome (drill, inline-expand, styled tooltips, breadcrumb), ported verbatim from the prototype's index.html. It is a render asset, not authoring logic — it carries no node / strategy / experiment logic, so C10 ("all logic is Rust", no DSL) is untouched.
  • Stage 3 — Graphviz-WASM. @viz-js/viz (Graphviz compiled to WASM) does layout, SVG, and edge routing entirely in the browser. No native dot binary anywhere. Validated on a thin client (Chromebook) over LAN.

aura graph emits one self-contained .html to stdout: the JSON model inlined, the viewer JS inlined, and the WASM-Graphviz asset inlined. The user redirects it to a file and opens it in any browser; nothing else is fetched at view time.

The deterministic, golden-tested contract aura owns is the JSON model (Stage 1 output) — one step before DOT, exactly as RunReport::to_json is the tested contract for a run. DOT and SVG are downstream of Graphviz's version and are never asserted.

Why this shape (acceptance rationale, applied to the worked code below)

The intended audience — a trader authoring/debugging a strategy in aura via Claude Code — reaches for aura graph to see the graph they wrote: which producer feeds which consumer slot, what type rides each wire, where a composite boundary sits. The redesign measurably improves on ascii-dag (it surfaces the full declared interface that #43 exposed and the old view dropped) and reintroduces no failure class the core constraints forbid: it is read-only (C9), its emitted contract is deterministic (golden-tested like RunReport), and it adds no eval-path code.

Concrete code shapes

The user-facing program (north star — the acceptance evidence)

# emit the self-contained interactive graph viewer
aura graph > graph.html
# open graph.html in any browser (locally, or served to a thin client):
#   python3 -m http.server 8000 --bind 0.0.0.0   # then open http://<host>:8000/graph.html

What the user gets, for the sample MACD harness: every node a box with its input pins (top), body macd(fast, slow, signal), and output pins (bottom); wires docking pin-to-pin and coloured by scalar type; price/volume sources green at the top rank, Recorder sinks maroon; clicking [+] on macd expands it inline as a framed cluster, clicking its body drills into its interior with a breadcrumb; hovering any pin/body/edge shows a styled tooltip. This is the prototype, rendered from aura's emitted model.

The deterministic contract aura emits (the golden-tested artefact)

The JSON model for the sample MACD harness (shape, abridged — keys in a fixed, canonical order, exactly as RunReport::to_json fixes its order):

{
  "root": {
    "nodes": {
      "price":  { "prim": { "type": "price", "role": "source",
                            "params": [], "ins": [], "outs": [["price","f64"]] } },
      "macd":   { "comp": "macd" },
      "expo":   { "prim": { "type": "Exposure", "role": "node",
                            "params": [["scale","f64"]],
                            "ins": [["signal","f64"]], "outs": [["exposure","f64"]] } },
      "broker": { "prim": { "type": "SimBroker", "role": "node",
                            "params": [["spread","f64"]],
                            "ins": [["exposure","f64"],
                                    ["price","f64",{"firing":"barrier 0"}]],
                            "outs": [["equity","f64"]] } },
      "rec_e":  { "prim": { "type": "Recorder", "role": "sink",
                            "params": [], "ins": [["exposure","f64"]], "outs": [] } }
    },
    "edges": [["price.o0","macd.i0"], ["macd.o2","expo.i0"],
              ["price.o0","broker.i1"], ["expo.o0","broker.i0"],
              ["expo.o0","rec_e.i0"], ["broker.o0","rec_q.i0"]]
  },
  "composites": {
    "macd": {
      "inputs": [["price","f64"]],
      "outputs": [["macd","f64"],["signal","f64"],["histogram","f64"]],
      "nodes": { "ema_f": { "prim": { "type":"EMA","role":"node",
                                      "params":[["fast","i64"]],
                                      "ins":[["price","f64"]],"outs":[["ema","f64"]] } },
                 "sub_h": { "prim": { "type":"Sub","role":"node","params":[],
                                      "ins":[["a","f64"],["b","f64"]],
                                      "outs":[["diff","f64"]] } } },
      "edges": [["@price","ema_f.i0"], ["sub_h.o0","#2"]]
    }
  }
}

Endpoint encoding in edges re-expresses the engine's wiring: "key.oN" = node key output field N; "key.iN" = node key input slot N; "@role" = a composite input role (from input_roles().targets); "#N" = a composite output binding (from output()[N]). This is the exact endpoint vocabulary the prototype's genDot consumes.

The viewer's DOT (what genDot produces in the browser — reference, not aura's output)

For one view, genDot turns the model into DOT (lifted from the prototype). The node template is a Graphviz HTML-like-label table with per-cell ids; edges dock by port:

digraph g {
  bgcolor="#16161a"; rankdir=TB; nodesep=0.5; ranksep=0.65;
  node [shape=plaintext, fontname="Courier", fontsize=12];   // core font: box sizing matches text
  edge [penwidth=1.7, arrowsize=0.8];

  macd [label=<<table border="0" cellborder="1" cellspacing="0" cellpadding="6">
    <tr><td port="i0" id="P_macd_i0" href="#" bgcolor="#222b33"><font color="#89b4fa">price</font></td></tr>
    <tr><td colspan="3" bgcolor="#5b4b8a"><table border="0" cellborder="0" cellspacing="3"><tr>
      <td id="plus_macd" href="#" align="center" valign="middle" fixedsize="true" width="24" height="24"
          border="1" color="#a594c8" bgcolor="#7c6ba8"><font color="#f5f5f5"><b>+</b></font></td>
      <td id="B_macd" href="#"><font color="#f5f5f5"><b>macd</b></font><font color="#bac2de">(fast, slow, signal)</font></td>
    </tr></table></td></tr>
    <tr><td port="o0" id="P_macd_o0" href="#" bgcolor="#332b22"><font color="#89b4fa">macd</font></td>
        <td port="o1" id="P_macd_o1" href="#" bgcolor="#332b22"><font color="#89b4fa">signal</font></td>
        <td port="o2" id="P_macd_o2" href="#" bgcolor="#332b22"><font color="#89b4fa">histogram</font></td></tr>
  </table>>];

  price:o0:s -> macd:i0:n [color="#89b4fa"];      // pin-to-pin, wire coloured by scalar kind
  {rank=min; price; volume; price -> volume [style=invis];}   // sources/inputs: top rank, signature order
  {rank=max; bout_0;}                                          // outputs: bottom rank, free order; sinks unpinned
}

The scalar-type palette is exactly the four base types (C4): i64 #f9e2af, f64 #89b4fa, bool #a6e3a1, timestamp #cba6f7. Body colours encode role: leaf #45475a, composite #5b4b8a, source #2e5a3a, sink #5a2e3e.

Implementation shape — before → after (secondary)

crates/aura-cli/src/graph.rs (the ascii-dag adapter, ~505 lines) is retired. Its public entry points (render_blueprint, render_flat_graph) and the ascii-dag dependency in crates/aura-cli/Cargo.toml are removed.

// BEFORE — ascii-dag adapter (graph.rs), point-nodes + invented label notation:
pub fn render_blueprint(bp: &Composite, color: Color) -> String { /* ascii_dag::Graph … */ }
pub fn render_flat_graph(nodes: &[Box<dyn Node>], ) -> String  { /* ascii_dag::Graph … */ }

// AFTER — a read-only model serializer (new module, e.g. graph_model.rs),
// hand-rolled deterministic JSON in the RunReport::to_json house style (no serde):
/// Serialize the harness root composite + every distinct composite type into the
/// canonical graph model the viewer consumes. Read-only (C9); never calls eval.
pub fn model_to_json(root: &Composite) -> String { /* walk nodes()/edges()/input_roles()/
                                                       output()/schema(); stable key order */ }
// crates/aura-cli/src/main.rs — the `graph` command emits the self-contained viewer:
["graph"] => print!("{}", graph::render_html(&sample_blueprint())),
//                         render_html = model_to_json + inlined viewer JS + inlined WASM-Graphviz

The current golden tests blueprint_view_golden and compiled_view_golden (crates/aura-cli/src/main.rs) and the aura graph --compiled / --macd flag plumbing are replaced; the new golden test asserts model_to_json (see Testing).

Components

  1. graph_model (new, aura-cli or aura-engine). The Rust serializer: model_to_json(&Composite) -> String. Walks the blueprint via existing read-only accessors (nodes, edges, input_roles, output, params, schema, name); maps each BlueprintNode::Primitive to a prim record (type, role, params, ins, outs from NodeSchema), each BlueprintNode::Composite to a {comp: name} reference; re-encodes edges/roles/outputs into the @role / key.oN / key.iN / #N endpoint vocabulary; collects each distinct composite type once (like today's collect_distinct_composites). Deterministic key order, hand-rolled like RunReport::to_json. Role derivation: source = a root node with no inputs and a bound source role; sink = output: vec![] (C8 pure consumer); else node.
  2. Viewer asset (new, vendored JS). genDot + chrome, ported from the prototype index.html. Lives as a static asset under the cli crate (e.g. crates/aura-cli/assets/graph-viewer.js). Plus the vendored WASM-Graphviz (@viz-js/viz@3.7.0) and svg-pan-zoom@3.6.1, restored by the prototype's fetch-deps.sh pattern (git-ignored blob, build-time fetch or checked-in per the planner's call).
  3. render_html (new, aura-cli). Assembles the self-contained page: a template shell with the model JSON, the viewer JS, and the WASM asset inlined. Emitted by aura graph.

Data flow

aura graphsample_blueprint() (existing fixture) → model_to_json(&root) → JSON string → render_html inlines {model, viewer JS, WASM} into one HTML → stdout. In the browser: viewer parses the model → genDot(model, view, expanded) → DOT → Graphviz-WASM → SVG → DOM; user interaction (drill/expand/tooltip) re-runs genDot for the new view/expansion and re-renders. No engine round-trip; the whole model ships once.

Error handling

  • The serializer is total over a well-formed blueprint (the engine already validates wiring at build, #41): every accessor returns concrete data. A malformed alias / missing field falls back to a ?-style placeholder rather than panicking, matching today's signature() discipline (graph.rs).
  • The serializer never calls eval, never constructs a sized instance — read-only (C9). It cannot fail a backtest invariant because it touches none.
  • Viewer-side: a model that references an unknown composite or an out-of-range field degrades to showing the opaque node / the field index (the prototype's multi_output_field_name fallback), never throws.

Testing strategy

  • Golden test on the model (the contract). model_to_json(&sample_blueprint()) asserted byte-for-byte against a checked-in expected string — the same form as the existing render goldens, but now over the deterministic JSON. Re-capturable on intended drift. This replaces blueprint_view_golden / compiled_view_golden.
  • Determinism test. model_to_json(&bp) == model_to_json(&bp) and a swapped-param / mis-wired blueprint produces a different model (the property the old swapped_param_vector_changes_the_compiled_render and the mc_2_miswire_render fieldtest protected — carried forward to the model).
  • Read-only assertion. The serializer signature takes &Composite and returns String; a test confirms no eval/build path is on the call (mirrors the C9 audit note for the old renderer).
  • Structural assertions (not pixels): the model for a multi-input composite has inputs.len() == 2; a sink has outs == []; a nested composite appears once in composites. The viewer/DOT/SVG are not golden-tested (Graphviz-version dependent); the prototype is the visual reference, exercised by hand.
  • No spec_validation parser is configured in the profile, so the fenced blocks above carry no automated parse gate (no-op); the dot/json/rust blocks are illustrative shapes lifted from the validated prototype, not asserted bytes.

Acceptance criteria

  1. aura graph emits a single self-contained .html that, opened in a browser with no native dependency, renders the sample harness as a homogeneous pin-graph (body + input pins + output pins per node), edges docked pin-to-pin and coloured by scalar type, sources/sinks distinguished, composites navigable by drill-down and inline expand, with per-element styled tooltips — i.e. the ratified prototype, driven by aura's emitted model.
  2. The model is produced by a read-only serializer (C9 — no eval, no build) and is deterministic (golden-tested byte-for-byte, like RunReport::to_json), with no serde dependency added.
  3. The model omits nothing and invents nothing: every node carries its declared NodeSchema (input ports with kind + firing, output fields with name + kind, params with name + kind); the invented #Sf / histogram → / := notation is gone.
  4. ascii-dag is removed from aura-cli's dependencies; graph.rs's ascii-dag entry points and the stale #43 comment are retired.
  5. The four-colour type palette is exactly the four scalar base types (C4); no fifth colour, no type outside i64/f64/bool/timestamp.

Iteration scope

Iteration 1 (foundation): the Rust graph_model serializer + its golden + determinism + read-only tests — the deterministic contract everything downstream consumes. No viewer wiring yet; aura graph may temporarily print the JSON model. This is the bite-sized, fully-testable core.

Iteration 2 (viewer): vendor the viewer JS (ported from the prototype) + WASM assets; render_html self-contained assembly; aura graph emits the HTML; retire ascii-dag and the old goldens/flags.

The planner decomposes from here.