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.
302 lines
17 KiB
Markdown
302 lines
17 KiB
Markdown
# 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)
|
|
|
|
```sh
|
|
# 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):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```dot
|
|
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.
|
|
|
|
```rust
|
|
// 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 */ }
|
|
```
|
|
|
|
```rust
|
|
// 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 graph` → `sample_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.
|