c3109bdfdd
Iteration-2 plan for cycle 0026 (graph render redesign): port the prototype viewer, vendor Graphviz-WASM (checked in, for hermetic offline builds), add a read-only render_html, wire `aura graph` to emit the self-contained HTML, and retire ascii-dag (adapter, dep, --compiled/--macd flags, 12 ascii-dag tests). Plus a stale-prose tidy in aura-core and the cycle-0026 ledger note. refs #51
848 lines
42 KiB
Markdown
848 lines
42 KiB
Markdown
# Graph Render Viewer (cycle 0026, iteration 2) — Implementation Plan
|
||
|
||
> **Parent spec:** `docs/specs/0026-graph-render-redesign.md`
|
||
>
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||
|
||
**Goal:** Replace `aura graph`'s ascii-dag output with a single self-contained
|
||
HTML page — the ported prototype viewer (pin-graph via Graphviz-WASM) driven by
|
||
the deterministic model serializer that shipped in iteration 1 — and retire
|
||
`ascii-dag` from the workspace. Closes Gitea #51.
|
||
|
||
**Architecture:** `aura graph` calls a new read-only `render_html(&Composite)`
|
||
(crate `aura-cli`, module `render`) that concatenates the prototype's `<head>`
|
||
shell, the inlined vendored Graphviz-WASM + pan-zoom blobs, the inlined model
|
||
JSON (`aura_engine::model_to_json`), and the inlined ported viewer JS into one
|
||
`.html` on stdout. The viewer JS is the prototype's `genDot`/chrome **verbatim**,
|
||
prefixed with a `normalizeModel` adapter that maps aura's real model tuple shape
|
||
(post-0027 named inputs: `ins = [kind, firing, name]`, index node keys, `comp`
|
||
references, `src_<role>` sources) into the viewer's native shape. The old
|
||
ascii-dag adapter (`graph.rs`), its dependency, the `--compiled`/`--macd` flag
|
||
plumbing, and all 12 ascii-dag-asserting tests are deleted in one
|
||
compile-coherent task. No `eval`/build on the render path (C9); no `serde` (C14);
|
||
the four-colour palette stays the four scalar base types (C4); the viewer carries
|
||
no authoring logic (C10).
|
||
|
||
**Tech Stack:** `aura-cli` (`render.rs`, `main.rs`, `Cargo.toml`, `assets/`,
|
||
`tests/cli_run.rs`), `aura-engine::model_to_json` (consumed, unchanged),
|
||
`aura-core::node.rs` (stale-prose tidy), `docs/design/INDEX.md` (ledger note).
|
||
Vendored JS/WASM assets are **checked in** (see Task 1 rationale).
|
||
|
||
---
|
||
|
||
## Files this plan creates or modifies
|
||
|
||
- Create: `crates/aura-cli/assets/graph-viewer.js` — ported viewer (normalizeModel + verbatim genDot/chrome)
|
||
- Create: `crates/aura-cli/assets/viz-standalone.js` — vendored `@viz-js/viz@3.7.0` (Graphviz-WASM, ~1.38 MB, checked in)
|
||
- Create: `crates/aura-cli/assets/svg-pan-zoom.min.js` — vendored `svg-pan-zoom@3.6.1` (~30 KB, checked in)
|
||
- Create: `crates/aura-cli/assets/refresh-assets.sh` — pinned-version refresh script (provenance; not run at build time)
|
||
- Create: `crates/aura-cli/src/render.rs` — `render_html(&Composite) -> String` + smoke test
|
||
- Modify: `crates/aura-cli/src/main.rs` — `mod render;`; flip `["graph"]` arm to `render_html`; delete `--compiled`/`--macd` graph arms, `render_compiled`, the `color` plumbing, `USAGE`; delete 12 ascii-dag tests + `swapped_point`/`sample_point` helpers; drop `mod graph;`
|
||
- Modify: `crates/aura-cli/Cargo.toml:16` — remove `ascii-dag = "0.9.1"`
|
||
- Delete: `crates/aura-cli/src/graph.rs` — the 504-line ascii-dag adapter (incl. the stale #43 comment at :324)
|
||
- Modify: `crates/aura-cli/tests/cli_run.rs:164-185` — replace the ascii-dag fan-in test with the HTML-contract test
|
||
- Modify: `crates/aura-core/src/node.rs:150-157,196-199` — re-justify two stale ascii-dag prose references
|
||
- Modify: `docs/design/INDEX.md:694` — add the cycle-0026 render-redesign realization note
|
||
|
||
---
|
||
|
||
## Task 1: Vendor the assets + port the viewer JS
|
||
|
||
**Vendoring decision (orchestrator call, deferred by the spec):** the blobs are
|
||
**checked into the repo**, not git-ignored + fetched at build time. Substance:
|
||
(a) `render_html` inlines the assets via `include_str!`, which requires them
|
||
present at **compile time** — a build-time `unpkg` fetch makes the build
|
||
non-hermetic and non-offline; (b) C8 freezes deploy artifacts and C1 demands
|
||
determinism — a build that pulls a remote URL drifts with the registry and the
|
||
network; (c) the shipped `.html` must be byte-deterministic for a given commit.
|
||
A checked-in blob is the only shape satisfying all three. `refresh-assets.sh`
|
||
records the pinned provenance for a future deliberate refresh; it is **not** part
|
||
of the build. The ~1.4 MB cost is the price of a hermetic, frozen render asset.
|
||
|
||
The validated blobs already exist on disk in the prototype dir — copy them (no
|
||
network fetch in this task).
|
||
|
||
**Files:**
|
||
- Create: `crates/aura-cli/assets/viz-standalone.js`
|
||
- Create: `crates/aura-cli/assets/svg-pan-zoom.min.js`
|
||
- Create: `crates/aura-cli/assets/refresh-assets.sh`
|
||
- Create: `crates/aura-cli/assets/graph-viewer.js`
|
||
|
||
- [ ] **Step 1: Create the assets dir and copy the two vendored blobs from the prototype**
|
||
|
||
Run:
|
||
```sh
|
||
mkdir -p crates/aura-cli/assets
|
||
cp docs/design/prototypes/graph-render/viz-standalone.js crates/aura-cli/assets/viz-standalone.js
|
||
cp docs/design/prototypes/graph-render/svg-pan-zoom.min.js crates/aura-cli/assets/svg-pan-zoom.min.js
|
||
wc -c crates/aura-cli/assets/viz-standalone.js crates/aura-cli/assets/svg-pan-zoom.min.js
|
||
```
|
||
Expected: `viz-standalone.js` ≈ `1379750` bytes, `svg-pan-zoom.min.js` ≈ `29768` bytes (both non-empty, copied).
|
||
|
||
If the prototype blobs are absent (a clean checkout that never ran the
|
||
prototype's `fetch-deps.sh`), fetch them instead with the pinned URLs from Step 3
|
||
below, then re-run the `wc -c` check.
|
||
|
||
- [ ] **Step 2: Verify the assets are not git-ignored (they are checked in)**
|
||
|
||
Run:
|
||
```sh
|
||
git check-ignore crates/aura-cli/assets/viz-standalone.js crates/aura-cli/assets/svg-pan-zoom.min.js; echo "exit=$?"
|
||
```
|
||
Expected: `exit=1` (no path printed) — neither file is ignored, so both will be
|
||
committed. If any path is printed, a parent `.gitignore` rule is catching them;
|
||
do **not** add a crate-level `.gitignore` for these (unlike the prototype, the
|
||
engine checks them in).
|
||
|
||
- [ ] **Step 3: Create the provenance refresh script**
|
||
|
||
Create `crates/aura-cli/assets/refresh-assets.sh`:
|
||
```sh
|
||
#!/usr/bin/env bash
|
||
# Refresh the vendored graph-viewer assets. NOT run at build time — the blobs are
|
||
# checked into the repo so `include_str!` and the build stay hermetic/offline
|
||
# (C1/C8). Run this only to deliberately bump a pinned version, then commit the
|
||
# updated blobs. Mirrors the prototype's fetch-deps.sh.
|
||
set -euo pipefail
|
||
cd "$(dirname "$0")"
|
||
curl -fsSL "https://unpkg.com/@viz-js/viz@3.7.0/lib/viz-standalone.js" -o viz-standalone.js
|
||
curl -fsSL "https://unpkg.com/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js" -o svg-pan-zoom.min.js
|
||
echo "refreshed: viz-standalone.js (@viz-js/viz@3.7.0), svg-pan-zoom.min.js (svg-pan-zoom@3.6.1)"
|
||
```
|
||
Run:
|
||
```sh
|
||
chmod +x crates/aura-cli/assets/refresh-assets.sh
|
||
head -1 crates/aura-cli/assets/refresh-assets.sh
|
||
```
|
||
Expected: `#!/usr/bin/env bash`.
|
||
|
||
- [ ] **Step 4: Create the ported viewer `crates/aura-cli/assets/graph-viewer.js`**
|
||
|
||
This is the prototype's `index.html` script (lines 43-256) **verbatim**, with the
|
||
hardcoded model (prototype lines 52-100, the `F`/`I`/`P`/`C`/`COMP`/`ROOT`
|
||
constants) replaced by `normalizeModel(window.AURA_MODEL)`. `genDot`, `cellLabel`,
|
||
`addInfo`, the tooltip/interaction chrome, and `chrome()`/`show()` are unchanged.
|
||
|
||
Create `crates/aura-cli/assets/graph-viewer.js`:
|
||
```js
|
||
// aura graph viewer — ported from docs/design/prototypes/graph-render/index.html.
|
||
// A render asset (C10): no node/strategy/experiment logic, no DSL. Reads aura's
|
||
// emitted model (window.AURA_MODEL, injected by render_html) and draws a
|
||
// homogeneous pin-graph via Graphviz-WASM. genDot/cellLabel/chrome are the
|
||
// prototype verbatim; normalizeModel adapts aura's real tuple shape (post-0027
|
||
// named inputs) to the viewer's native shape.
|
||
const TYPE_COLOR = { i64: "#f9e2af", f64: "#89b4fa", bool: "#a6e3a1", timestamp: "#cba6f7" };
|
||
const COLOR_TYPE = Object.fromEntries(Object.entries(TYPE_COLOR).map(([k, v]) => [v.toLowerCase(), k]));
|
||
const PURPLE = "#5b4b8a", TEAL = "#2c5a5a", PLUS = "#7c6ba8";
|
||
const LEAF = "#45475a", SOURCE = "#2e5a3a", SINK = "#5a2e3e";
|
||
const bodyBgFor = (role) => role === "source" ? SOURCE : role === "sink" ? SINK : LEAF;
|
||
const HEAD = `bgcolor="#16161a"; rankdir=TB; nodesep=0.5; ranksep=0.65;
|
||
node [shape=plaintext, fontname="Courier", fontsize=12];
|
||
edge [penwidth=1.7, arrowsize=0.8];`;
|
||
const col = (k) => TYPE_COLOR[k] || "#cdd6f4";
|
||
|
||
// === adapt aura's emitted model to the viewer's native shape =================
|
||
// aura input tuples are [kind, firing, name]; the viewer wants [name, kind, meta?]
|
||
// (non-"any" firing surfaces in the tooltip). outs/params are already [name, kind].
|
||
// aura composite inputs are [kind, firing, name]; the viewer wants [name, kind].
|
||
// node keys (indices / "src_<role>") and edge endpoints pass through unchanged.
|
||
function adaptIns(ins) {
|
||
return ins.map(([kind, firing, name]) =>
|
||
firing && firing !== "any" ? [name, kind, { firing }] : [name, kind]);
|
||
}
|
||
function adaptNodes(nodes) {
|
||
const out = {};
|
||
for (const key in nodes) {
|
||
const n = nodes[key];
|
||
if (n.prim) {
|
||
const p = n.prim;
|
||
out[key] = { prim: { type: p.type, role: p.role, params: p.params, ins: adaptIns(p.ins), outs: p.outs } };
|
||
} else {
|
||
out[key] = n; // composite reference { comp }
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
function normalizeModel(model) {
|
||
const composites = {};
|
||
for (const cname in (model.composites || {})) {
|
||
const c = model.composites[cname];
|
||
composites[cname] = {
|
||
inputs: c.inputs.map(([kind, firing, name]) => [name, kind]),
|
||
outputs: c.outputs,
|
||
nodes: adaptNodes(c.nodes),
|
||
edges: c.edges,
|
||
};
|
||
}
|
||
return {
|
||
root: { inputs: [], outputs: [], nodes: adaptNodes(model.root.nodes), edges: model.root.edges },
|
||
composites,
|
||
};
|
||
}
|
||
const MODEL = normalizeModel(window.AURA_MODEL);
|
||
const COMP = MODEL.composites;
|
||
const ROOT = MODEL.root;
|
||
|
||
// === ONE homogeneous, per-cell-addressable node template =====================
|
||
function cellLabel(id, o, showTypes) {
|
||
const cols = Math.max(o.ins.length, o.outs.length, 1);
|
||
const span = (n) => Math.max(1, Math.floor(cols / n));
|
||
const txt = (n, k) => (showTypes && k) ? `${n} : ${k}` : n;
|
||
const cell = (io, arr) => ([n, k], i) =>
|
||
`<td port="${io}${i}" id="P_${id}_${io}${i}" href="#" colspan="${span(arr.length)}" ` +
|
||
`bgcolor="${io === "i" ? "#222b33" : "#332b22"}"><font color="${col(k)}">${txt(n, k)}</font></td>`;
|
||
const sp = o.params.map(([n, k]) => `<font color="${col(k)}">${showTypes && k ? `${n}:${k}` : n}</font>`);
|
||
const sig = o.params.length ? `<font color="#9399b2">(</font>${sp.join('<font color="#9399b2">, </font>')}<font color="#9399b2">)</font>` : "";
|
||
const name = `<font color="#f5f5f5"><b>${o.type}</b></font>${sig}`;
|
||
let bodyRow;
|
||
if (o.composite) {
|
||
bodyRow = `<tr><td colspan="${cols}" bgcolor="${o.bodyBg}"><table border="0" cellborder="0" cellspacing="3"><tr>` +
|
||
`<td id="plus_${id}" href="#" align="center" valign="middle" fixedsize="true" width="24" height="24" border="1" color="#a594c8" bgcolor="${PLUS}"><font color="#f5f5f5"><b>+</b></font></td>` +
|
||
`<td id="B_${id}" href="#">${name}</td></tr></table></td></tr>`;
|
||
} else {
|
||
bodyRow = `<tr><td id="B_${id}" href="#" colspan="${cols}" bgcolor="${o.bodyBg}">${name}</td></tr>`;
|
||
}
|
||
let rows = "";
|
||
if (o.ins.length) rows += `<tr>${o.ins.map(cell("i", o.ins)).join("")}</tr>`;
|
||
rows += bodyRow;
|
||
if (o.outs.length) rows += `<tr>${o.outs.map(cell("o", o.outs)).join("")}</tr>`;
|
||
return `<<table border="0" cellborder="1" cellspacing="0" cellpadding="6">${rows}</table>>`;
|
||
}
|
||
function addInfo(INFO, id, o) {
|
||
const ps = o.params && o.params.length ? o.params.map(([n, k]) => `\`${n}:${k}\``).join(", ") : "<span class='dim'>none</span>";
|
||
INFO.B[id] = `**${o.type}** <span class='dim'>· ${o.role || "node"}</span>\nparams: ${ps}`;
|
||
(o.ins || []).forEach(([n, k, m], i) => INFO.P[`P_${id}_i${i}`] = `**${n}** · \`${k}\`` + (m && m.firing ? `\nfiring: \`${m.firing}\`` : ""));
|
||
(o.outs || []).forEach(([n, k], i) => INFO.P[`P_${id}_o${i}`] = `**${n}** · \`${k}\``);
|
||
}
|
||
|
||
// === recursive DOT generator (the mini-inliner) ==============================
|
||
function genDot(def, role, expandedSet, showTypes) {
|
||
const INFO = { B: {}, P: {}, C: {} }, drill = {}, expandable = [], clusters = [], edges = [], topRank = [], bottomRank = [];
|
||
function build(path, def, brole) {
|
||
const childRes = {}; let block = "";
|
||
for (const key in def.nodes) {
|
||
const inst = def.nodes[key], cpath = [...path, key], cid = cpath.join("__");
|
||
if (inst.prim) {
|
||
const s = inst.prim;
|
||
block += `${cid} [label=${cellLabel(cid, { type: s.type, params: s.params, ins: s.ins, outs: s.outs, bodyBg: bodyBgFor(s.role), composite: false }, showTypes)}];\n`;
|
||
addInfo(INFO, cid, s);
|
||
if (s.role === "source") topRank.push(cid); // sinks are placed freely (not rank-pinned)
|
||
childRes[key] = { type: "leaf", id: cid, spec: s };
|
||
} else {
|
||
const cname = inst.comp, cdef = COMP[cname];
|
||
if (expandedSet.has(cid)) {
|
||
const sub = build(cpath, cdef, "cluster"); clusters.push(cid);
|
||
INFO.C["clust_" + cid] = `**${cname}** <span class='dim'>· composite · click frame to collapse</span>`;
|
||
block += `subgraph cluster_${cid} { id="clust_${cid}"; style=filled; color="#5b4b8a"; fillcolor="#211c30"; penwidth=2;\n${sub.block}}\n`;
|
||
childRes[key] = { type: "exp", resolveIn: sub.resolveIn, resolveOut: sub.resolveOut };
|
||
} else {
|
||
block += `${cid} [label=${cellLabel(cid, { type: cname, params: [], ins: cdef.inputs, outs: cdef.outputs, bodyBg: PURPLE, composite: true }, showTypes)}];\n`;
|
||
addInfo(INFO, cid, { type: cname, role: "composite", params: [], ins: cdef.inputs, outs: cdef.outputs });
|
||
drill[cid] = cname; expandable.push(cid);
|
||
childRes[key] = { type: "opaque", id: cid, cdef };
|
||
}
|
||
}
|
||
}
|
||
const pk = (r, io, idx) => r.type === "leaf" ? (io === "i" ? r.spec.ins : r.spec.outs)[idx][1] : (io === "i" ? r.cdef.inputs : r.cdef.outputs)[idx][1];
|
||
const resTarget = (ep) => { const [k, p] = ep.split("."); const idx = +p.slice(1), r = childRes[k]; return r.type === "exp" ? r.resolveIn(idx) : [{ id: r.id, port: "i" + idx, kind: pk(r, "i", idx) }]; };
|
||
const resSource = (ep) => { const [k, p] = ep.split("."); const idx = +p.slice(1), r = childRes[k]; return r.type === "exp" ? r.resolveOut(idx) : { id: r.id, port: "o" + idx, kind: pk(r, "o", idx) }; };
|
||
const resolveIn = (i) => { const rn = "@" + def.inputs[i][0]; return def.edges.filter(e => e[0] === rn).flatMap(e => resTarget(e[1])); };
|
||
const resolveOut = (i) => { const e = def.edges.find(e => e[1] === "#" + i); return resSource(e[0]); };
|
||
for (const [f, t] of def.edges) { if (f[0] === "@" || t[0] === "#") continue; const s = resSource(f); for (const d of resTarget(t)) edges.push([s, d]); }
|
||
if (brole === "drill") {
|
||
def.inputs.forEach(([rn, rk], i) => { const bid = `bin_${i}`;
|
||
block += `${bid} [label=${cellLabel(bid, { type: rn, params: [], ins: [], outs: [[rn, rk]], bodyBg: SOURCE, composite: false }, showTypes)}];\n`;
|
||
addInfo(INFO, bid, { type: rn, role: "input", params: [], ins: [], outs: [[rn, rk]] });
|
||
topRank.push(bid);
|
||
resolveIn(i).forEach(d => edges.push([{ id: bid, port: "o0", kind: rk }, d])); });
|
||
def.outputs.forEach(([on, ok], i) => { const bid = `bout_${i}`;
|
||
block += `${bid} [label=${cellLabel(bid, { type: on, params: [], ins: [[on, ok]], outs: [], bodyBg: SINK, composite: false }, showTypes)}];\n`;
|
||
addInfo(INFO, bid, { type: on, role: "output", params: [], ins: [[on, ok]], outs: [] });
|
||
bottomRank.push(bid);
|
||
edges.push([resolveOut(i), { id: bid, port: "i0", kind: ok }]); });
|
||
}
|
||
return { block, resolveIn, resolveOut };
|
||
}
|
||
const top = build([], def, role);
|
||
let dot = `digraph g { ${HEAD}\n${top.block}`;
|
||
// annotation by position: sources/inputs share the top rank, outputs the bottom rank.
|
||
const rankRow = (ids, r, chain) => ids.length
|
||
? `{rank=${r};${ids.map(id => " " + id + ";").join("")}${chain && ids.length > 1 ? " " + ids.join(" -> ") + " [style=invis];" : ""}}\n`
|
||
: "";
|
||
dot += rankRow(topRank, "min", true);
|
||
dot += rankRow(bottomRank, "max", false);
|
||
for (const [s, d] of edges) dot += `${s.id}:${s.port}:s -> ${d.id}:${d.port}:n [color="${TYPE_COLOR[s.kind] || "#7f849c"}"];\n`;
|
||
return { dot: dot + "}", info: INFO, drill, expandable, clusters };
|
||
}
|
||
|
||
// === styled tooltip + interaction ============================================
|
||
const tip = document.getElementById("tip");
|
||
const md = (s) => s.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/`(.+?)`/g, "<code>$1</code>").replace(/\n/g, "<br>");
|
||
function showTip(text, x, y) {
|
||
tip.innerHTML = md(text); tip.style.display = "block";
|
||
const w = tip.offsetWidth, h = tip.offsetHeight;
|
||
let l = x + 14, t = y + 16;
|
||
if (l + w > innerWidth - 8) l = x - w - 14;
|
||
if (t + h > innerHeight - 8) t = y - h - 16;
|
||
tip.style.left = l + "px"; tip.style.top = t + "px";
|
||
}
|
||
const hideTip = () => { tip.style.display = "none"; };
|
||
|
||
const stage = document.getElementById("stage"), crumbEl = document.getElementById("crumb");
|
||
let viz, viewStack = [{ name: "root", def: ROOT }], expanded = new Set(), SHOW_TYPES = false, INFO = { B: {}, P: {}, C: {} };
|
||
|
||
function chrome() {
|
||
crumbEl.innerHTML = "";
|
||
viewStack.forEach((v, i) => {
|
||
if (i) { const s = document.createElement("span"); s.className = "sep"; s.textContent = " › "; crumbEl.appendChild(s); }
|
||
const a = document.createElement("a"); a.textContent = v.name;
|
||
a.onclick = () => { viewStack = viewStack.slice(0, i + 1); expanded = new Set(); show(); };
|
||
crumbEl.appendChild(a);
|
||
});
|
||
}
|
||
|
||
function show() {
|
||
const v = viewStack[viewStack.length - 1];
|
||
const G = genDot(v.def, v.name === "root" ? "root" : "drill", expanded, SHOW_TYPES);
|
||
INFO = G.info;
|
||
stage.innerHTML = ""; hideTip();
|
||
const svg = viz.renderSVGElement(G.dot);
|
||
stage.appendChild(svg);
|
||
const pz = svgPanZoom(svg, { zoomEnabled: true, controlIconsEnabled: true, fit: true, center: true, minZoom: 0.1, maxZoom: 12, dblClickZoomEnabled: false });
|
||
addEventListener("resize", () => { pz.resize(); pz.fit(); pz.center(); });
|
||
svg.querySelectorAll("title").forEach(t => t.remove());
|
||
svg.querySelectorAll("a").forEach(a => a.removeAttributeNS("http://www.w3.org/1999/xlink", "title"));
|
||
|
||
const idOf = (el) => { const a = el.closest && el.closest("a[id], g[id]"); return a ? a.id.replace(/^a_/, "") : ""; };
|
||
svg.addEventListener("mousemove", (e) => {
|
||
const id = idOf(e.target);
|
||
if (id.startsWith("P_") && INFO.P[id]) return showTip(INFO.P[id], e.clientX, e.clientY);
|
||
if (id.startsWith("B_")) { const k = id.slice(2); if (INFO.B[k]) return showTip(INFO.B[k], e.clientX, e.clientY); }
|
||
if (id.startsWith("plus_")) return showTip("**expand** inline", e.clientX, e.clientY);
|
||
if (id.startsWith("clust_") && INFO.C[id]) return showTip(INFO.C[id], e.clientX, e.clientY);
|
||
const eg = e.target.closest && e.target.closest("g.edge");
|
||
if (eg) { const p = eg.querySelector("path"); const c = p && p.getAttribute("stroke") ? p.getAttribute("stroke").toLowerCase() : ""; return showTip("`" + (COLOR_TYPE[c] || "?") + "`", e.clientX, e.clientY); }
|
||
hideTip();
|
||
});
|
||
svg.addEventListener("mouseleave", hideTip);
|
||
svg.addEventListener("click", (e) => { const a = e.target.closest && e.target.closest("a"); if (a) e.preventDefault(); });
|
||
|
||
const click = (id, fn) => { const el = svg.getElementById(id) || svg.getElementById("a_" + id); if (el) { el.style.cursor = "pointer"; el.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); fn(); }); } };
|
||
for (const id in G.drill) click("B_" + id, () => { viewStack.push({ name: G.drill[id], def: COMP[G.drill[id]] }); expanded = new Set(); show(); });
|
||
for (const id of G.expandable) click("plus_" + id, () => { expanded.add(id); show(); });
|
||
for (const id of G.clusters) click("clust_" + id, () => { expanded.delete(id); show(); });
|
||
chrome();
|
||
}
|
||
|
||
Viz.instance().then(v => { viz = v; show(); }).catch(e => { document.getElementById("err").textContent = String(e && e.stack || e); });
|
||
```
|
||
|
||
- [ ] **Step 5: Sanity-check the viewer asset for the load-bearing markers**
|
||
|
||
Run:
|
||
```sh
|
||
grep -c "Viz.instance" crates/aura-cli/assets/graph-viewer.js
|
||
grep -c "normalizeModel(window.AURA_MODEL)" crates/aura-cli/assets/graph-viewer.js
|
||
grep -F 'i64: "#f9e2af"' crates/aura-cli/assets/graph-viewer.js | head -1
|
||
```
|
||
Expected: first two print `1`; the third prints the palette line (C4 four-colour
|
||
table present). These substrings are asserted by the Task 2 / Task 3 tests, so
|
||
they must be present contiguously.
|
||
|
||
---
|
||
|
||
## Task 2: `render_html` module + smoke test (coexists with graph.rs)
|
||
|
||
`render.rs` is a new module that compiles **alongside** the still-present
|
||
`graph.rs` — no retirement yet, so the workspace stays green. `render_html` is
|
||
read-only (C9): it calls `aura_engine::model_to_json` (a pure `&Composite ->
|
||
String` walk) and string-concatenates the inlined assets. No `eval`, no build, no
|
||
`serde`.
|
||
|
||
**Files:**
|
||
- Create: `crates/aura-cli/src/render.rs`
|
||
- Modify: `crates/aura-cli/src/main.rs:9` (add `mod render;`)
|
||
- Test: `crates/aura-cli/src/render.rs` (the `#[cfg(test)]` smoke test)
|
||
|
||
- [ ] **Step 1: Create `crates/aura-cli/src/render.rs`**
|
||
|
||
Create `crates/aura-cli/src/render.rs`:
|
||
```rust
|
||
//! Self-contained HTML graph viewer assembly (C9: read-only render path).
|
||
//!
|
||
//! `render_html` walks the blueprint into the deterministic JSON model
|
||
//! (`aura_engine::model_to_json`) and inlines it, the vendored Graphviz-WASM, the
|
||
//! pan-zoom helper, and the ported viewer JS into one standalone `.html`. The
|
||
//! emitted page fetches nothing at view time. No `eval`/build is on this path
|
||
//! (C9); the model is hand-rolled JSON (C14, owned by `model_to_json`).
|
||
|
||
use aura_core::Composite;
|
||
|
||
/// The prototype's `<head>` shell + body skeleton (header, stage, tooltip, error
|
||
/// pane), verbatim from `docs/design/prototypes/graph-render/index.html` lines
|
||
/// 1-38 — minus the two `<script src="./…">` tags, since `render_html` inlines
|
||
/// the assets instead.
|
||
const SHELL_HEAD: &str = r#"<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>aura graph</title>
|
||
<style>
|
||
html, body { margin: 0; height: 100%; background: #16161a; color: #cdd6f4;
|
||
font-family: ui-monospace, monospace; }
|
||
header { padding: 8px 14px; border-bottom: 1px solid #313244; font-size: 13px;
|
||
display: flex; gap: 16px; align-items: baseline; flex-wrap: nowrap;
|
||
white-space: nowrap; overflow: hidden; }
|
||
header b { color: #f5e0dc; } .sub { color: #6c7086; }
|
||
#crumb a { color: #89b4fa; cursor: pointer; text-decoration: none; }
|
||
#crumb a:hover { text-decoration: underline; }
|
||
#crumb .sep { color: #6c7086; }
|
||
#status { color: #6c7086; margin-left: auto; }
|
||
#stage { width: 100%; height: calc(100% - 44px); }
|
||
#stage svg { width: 100%; height: 100%; cursor: default; }
|
||
#stage svg text { cursor: inherit; user-select: none; -webkit-user-select: none; }
|
||
#stage svg a { cursor: inherit; }
|
||
#err { padding: 14px; color: #f38ba8; white-space: pre-wrap; }
|
||
#tip { position: fixed; display: none; pointer-events: none; z-index: 10;
|
||
background: #1e1e2e; border: 1px solid #585b70; border-radius: 6px; padding: 6px 9px;
|
||
font-family: ui-monospace, monospace; font-size: 12px; color: #cdd6f4;
|
||
max-width: 420px; box-shadow: 0 6px 18px rgba(0,0,0,.55); line-height: 1.5; }
|
||
#tip b { color: #f5e0dc; } #tip code { color: #89b4fa; } #tip .dim { color: #7f849c; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<b>aura graph</b>
|
||
<span id="crumb"></span>
|
||
<span class="sub">hover · [+] expand · body drill</span>
|
||
</header>
|
||
<div id="stage"></div>
|
||
<div id="tip"></div>
|
||
<pre id="err"></pre>
|
||
"#;
|
||
|
||
const VIZ_JS: &str = include_str!("../assets/viz-standalone.js");
|
||
const PANZOOM_JS: &str = include_str!("../assets/svg-pan-zoom.min.js");
|
||
const VIEWER_JS: &str = include_str!("../assets/graph-viewer.js");
|
||
|
||
/// Assemble the self-contained interactive graph viewer page for `root`.
|
||
///
|
||
/// Order is load-bearing: the Graphviz-WASM and pan-zoom globals (`Viz`,
|
||
/// `svgPanZoom`) and the model (`window.AURA_MODEL`) must be defined before the
|
||
/// viewer script runs.
|
||
pub fn render_html(root: &Composite) -> String {
|
||
let model = aura_engine::model_to_json(root);
|
||
let mut html = String::with_capacity(
|
||
SHELL_HEAD.len() + VIZ_JS.len() + PANZOOM_JS.len() + VIEWER_JS.len() + model.len() + 256,
|
||
);
|
||
html.push_str(SHELL_HEAD);
|
||
html.push_str("<script>");
|
||
html.push_str(VIZ_JS);
|
||
html.push_str("</script>\n<script>");
|
||
html.push_str(PANZOOM_JS);
|
||
html.push_str("</script>\n<script>window.AURA_MODEL = ");
|
||
html.push_str(&model);
|
||
html.push_str(";</script>\n<script>");
|
||
html.push_str(VIEWER_JS);
|
||
html.push_str("</script>\n</body>\n</html>\n");
|
||
html
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::sample_blueprint;
|
||
|
||
/// `aura graph`'s page is self-contained (no remote fetch), embeds the
|
||
/// deterministic model as the viewer's data source, carries the Graphviz-WASM
|
||
/// bootstrap, and keeps the C4 four-colour palette. Structural, not pixel:
|
||
/// the DOT/SVG are Graphviz-version-dependent and are exercised by hand
|
||
/// against the prototype (spec Testing strategy).
|
||
#[test]
|
||
fn render_html_is_self_contained_and_embeds_the_model() {
|
||
let html = render_html(&sample_blueprint());
|
||
// a complete HTML document
|
||
assert!(html.starts_with("<!doctype html>"), "not an HTML doc");
|
||
assert!(html.trim_end().ends_with("</html>"), "HTML doc not closed");
|
||
// self-contained: every asset is inlined, no remote <script src>
|
||
assert!(!html.contains("<script src"), "assets must be inlined, found a remote script tag");
|
||
// the deterministic model is injected as the viewer's data source
|
||
assert!(html.contains("window.AURA_MODEL = {\"root\":"), "model JSON not injected");
|
||
// a known node from the sample harness round-trips via the model
|
||
assert!(html.contains("\"type\":\"Exposure\""), "sample model content missing");
|
||
// the vendored Graphviz-WASM is present (its bootstrap global)
|
||
assert!(html.contains("Viz.instance"), "viewer bootstrap (Viz.instance) missing");
|
||
// C4: the four-scalar palette, no fifth colour
|
||
assert!(
|
||
html.contains("i64: \"#f9e2af\"") && html.contains("timestamp: \"#cba6f7\""),
|
||
"C4 four-colour palette missing"
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Declare the module**
|
||
|
||
In `crates/aura-cli/src/main.rs`, add `mod render;` immediately after the existing
|
||
`mod graph;` at line 9:
|
||
```rust
|
||
mod graph;
|
||
mod render;
|
||
```
|
||
(Both modules coexist this task; `graph` is retired in Task 3.)
|
||
|
||
- [ ] **Step 3: Build and run the smoke test**
|
||
|
||
Run:
|
||
```sh
|
||
cargo test -p aura-cli render_html_is_self_contained_and_embeds_the_model
|
||
```
|
||
Expected: PASS (`test result: ok. 1 passed`). The model golden contract is
|
||
unchanged, so `cargo build --workspace` stays green with `graph.rs` still present.
|
||
|
||
---
|
||
|
||
## Task 3: Retire ascii-dag (atomic — flip `aura graph`, delete graph.rs + dep + dead tests/helpers)
|
||
|
||
This is one compile-coherent unit: removing `graph.rs` and the `ascii-dag` dep
|
||
breaks every `graph::` reference (the production arms + 12 tests) at once, so the
|
||
"0 errors" build gate is only satisfiable when **all** of them are removed in the
|
||
same task (planner self-review item 7 — signature/removal-breaks-all-callers).
|
||
|
||
**Files:**
|
||
- Modify: `crates/aura-cli/src/main.rs` (graph arm, flags, `render_compiled`, `color`, `USAGE`, `mod graph;`, 12 tests, `swapped_point`/`sample_point` helpers)
|
||
- Modify: `crates/aura-cli/Cargo.toml:16` (remove `ascii-dag`)
|
||
- Delete: `crates/aura-cli/src/graph.rs`
|
||
- Modify: `crates/aura-cli/tests/cli_run.rs:164-185` (replace the ascii-dag fan-in test)
|
||
|
||
- [ ] **Step 1: Flip the `["graph"]` arm and delete the `--compiled`/`--macd` graph arms**
|
||
|
||
In `crates/aura-cli/src/main.rs`, the `match` block (lines 379-395) currently reads:
|
||
```rust
|
||
match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
|
||
["run"] => println!("{}", run_sample().to_json()),
|
||
["run", "--macd"] => println!("{}", run_macd().to_json()),
|
||
["graph"] => println!("{}", graph::render_blueprint(&sample_blueprint(), color)),
|
||
["graph", "--compiled"] => {
|
||
println!("{}", render_compiled(sample_blueprint(), &sample_point(), color));
|
||
}
|
||
["graph", "--macd"] => println!("{}", graph::render_blueprint(&macd_blueprint(), color)),
|
||
["graph", "--macd", "--compiled"] => {
|
||
println!("{}", render_compiled(macd_blueprint(), &macd_point(), color));
|
||
}
|
||
["--help"] | ["-h"] => println!("{USAGE}"),
|
||
_ => {
|
||
eprintln!("aura: {USAGE}");
|
||
std::process::exit(2);
|
||
}
|
||
}
|
||
```
|
||
Replace it with (only `run`/`run --macd`/`graph`/help survive; `graph` emits the
|
||
HTML viewer via `print!` since `render_html` ends with a newline):
|
||
```rust
|
||
match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
|
||
["run"] => println!("{}", run_sample().to_json()),
|
||
["run", "--macd"] => println!("{}", run_macd().to_json()),
|
||
["graph"] => print!("{}", render::render_html(&sample_blueprint())),
|
||
["--help"] | ["-h"] => println!("{USAGE}"),
|
||
_ => {
|
||
eprintln!("aura: {USAGE}");
|
||
std::process::exit(2);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Delete the `color` plumbing and the `render_compiled` helper**
|
||
|
||
In `crates/aura-cli/src/main.rs`, delete the `color` binding (lines 372-378):
|
||
```rust
|
||
// Edge colouring only when stdout is an interactive terminal; a redirect to a
|
||
// file or pipe stays plain (no escape codes). `run` output is JSON, never coloured.
|
||
let color = if std::io::stdout().is_terminal() {
|
||
graph::Color::Ansi
|
||
} else {
|
||
graph::Color::Plain
|
||
};
|
||
```
|
||
And delete the `render_compiled` helper (lines 358-363):
|
||
```rust
|
||
/// Compile a blueprint under its point vector and render the flat post-inline
|
||
/// (C23) view — the shared body of the `--compiled` paths.
|
||
fn render_compiled(bp: Composite, point: &[Scalar], color: graph::Color) -> String {
|
||
let flat = bp.compile_with_params(point).expect("valid blueprint");
|
||
graph::render_compilat(&flat.nodes, &flat.sources, &flat.edges, color)
|
||
}
|
||
```
|
||
If `is_terminal`/`IsTerminal` was imported only for the `color` binding, its `use`
|
||
becomes unused — remove it (clippy will name it in Step 7; the import is near the
|
||
top of `main.rs`).
|
||
|
||
- [ ] **Step 3: Shrink the `USAGE` string (drop the retired graph flags)**
|
||
|
||
In `crates/aura-cli/src/main.rs:365`, replace:
|
||
```rust
|
||
const USAGE: &str = "usage: aura run [--macd] | aura graph [--compiled | --macd [--compiled]]";
|
||
```
|
||
with:
|
||
```rust
|
||
const USAGE: &str = "usage: aura run [--macd] | aura graph";
|
||
```
|
||
|
||
- [ ] **Step 4: Drop `mod graph;` and delete the file**
|
||
|
||
In `crates/aura-cli/src/main.rs:9`, remove the `mod graph;` line (leaving
|
||
`mod render;`). Then (plain `rm`, not `git rm` — the deletion stays unstaged with
|
||
the rest of the iter's work for the orchestrator to commit):
|
||
```sh
|
||
rm crates/aura-cli/src/graph.rs
|
||
```
|
||
Expected: `graph.rs` removed (this also retires the stale #43 comment at :324,
|
||
which dies with the file).
|
||
|
||
- [ ] **Step 5: Delete the 12 ascii-dag-asserting tests + the orphaned `swapped_point`/`sample_point` helpers**
|
||
|
||
In `crates/aura-cli/src/main.rs`'s `#[cfg(test)] mod tests`, delete these test
|
||
functions in full (each asserts retired ascii-dag output and references
|
||
`graph::render_blueprint` / `graph::render_compilat` / `graph::Color`):
|
||
```text
|
||
blueprint_view_main_graph_shows_composite_as_opaque_node (~409-425)
|
||
blueprint_view_defines_each_composite_once (~427-435)
|
||
nested_composite_renders_without_panic (~437-473)
|
||
reused_composite_defined_once (~475-501)
|
||
compiled_view_dissolves_the_composite_boundary (~503-512)
|
||
swapped_param_vector_changes_the_compiled_render (~514-527)
|
||
blueprint_view_golden (~529-566)
|
||
compiled_view_golden (~568-593)
|
||
macd_blueprint_renders_a_nested_composite_definition (~619-646)
|
||
fan_in_identifiers_are_source_derived_and_scoped_per_node_call (~648-670)
|
||
fan_in_identifiers_descend_into_bare_combinators (~672-723)
|
||
ansi_colour_emits_escapes_only_when_requested (~725-735)
|
||
```
|
||
**Keep** every test that does not reference `graph::`:
|
||
`run_macd_compiles_from_nested_composite_and_is_deterministic` (~595-617),
|
||
`macd_param_space_surfaces_the_three_named_aliases` (~743-759),
|
||
`run_sample_is_deterministic_and_non_trivial` (~761+).
|
||
|
||
Then delete the now-orphaned test helper `swapped_point` (~405-407, used only by
|
||
the deleted `swapped_param_vector_changes_the_compiled_render`).
|
||
|
||
Completeness check — no `graph::` reference may survive in the file:
|
||
```sh
|
||
grep -n "graph::" crates/aura-cli/src/main.rs; echo "exit=$?"
|
||
```
|
||
Expected: `exit=1` (no matches). Any surviving match is a test the deletion list
|
||
missed — remove it.
|
||
|
||
- [ ] **Step 6: Remove the `ascii-dag` dependency**
|
||
|
||
In `crates/aura-cli/Cargo.toml`, delete line 16:
|
||
```toml
|
||
ascii-dag = "0.9.1"
|
||
```
|
||
|
||
- [ ] **Step 7: Build + clippy; remove whatever dead-code/unused-import the gate names**
|
||
|
||
`sample_point()` (main.rs ~199) loses its last caller when the compiled-view
|
||
tests go (its only uses were the `--compiled` arm + the deleted tests). The
|
||
`dead_code` lint under `-D warnings` will name it and any import (e.g.
|
||
`graph`-only or `IsTerminal`-only `use`s, the fixture imports `Role`/`Target`/
|
||
`Edge`/`OutField`/`BlueprintNode`/`ParamAlias`/`Sub`/`Ema`/`Sma`/`Exposure`/
|
||
`sma_cross` used only by the deleted tests) that is now unused.
|
||
|
||
Run, then delete each item the lint names, and re-run until clean:
|
||
```sh
|
||
cargo build --workspace
|
||
cargo clippy --workspace --all-targets -- -D warnings
|
||
```
|
||
Expected (final): `cargo build` 0 errors; `clippy` 0 warnings. Remove `sample_point`
|
||
and every unused `use` clippy reports; do **not** remove `macd_blueprint`,
|
||
`macd_point`, `run_macd` (still used by the kept tests / `run --macd` arm) or
|
||
`sample_blueprint`/`build_sample` (used by the `graph` arm + `render` test).
|
||
|
||
- [ ] **Step 8: Replace the integration test that pinned the old ascii-dag output**
|
||
|
||
In `crates/aura-cli/tests/cli_run.rs`, the test
|
||
`graph_renders_source_derived_fan_in_identifiers` (lines 164-185) asserts the
|
||
retired ascii-dag notation (`[cross := Sub(#Sf,#Ss)]`) and now fails. Replace the
|
||
entire test (its doc-comment + function) with the HTML-contract test:
|
||
```rust
|
||
/// Property: `aura graph` emits a single self-contained HTML page — the
|
||
/// interactive pin-graph viewer with the deterministic model inlined and the
|
||
/// vendored Graphviz-WASM + pan-zoom inlined (no remote fetch). Driven through
|
||
/// the built binary so it pins the observable CLI contract. The DOT/SVG are
|
||
/// Graphviz-version-dependent and not asserted (the prototype is the by-hand
|
||
/// visual reference); this pins the page envelope + the retirement of the old
|
||
/// ascii-dag notation.
|
||
#[test]
|
||
fn graph_emits_self_contained_html_viewer() {
|
||
let out = Command::new(BIN).arg("graph").output().expect("spawn aura graph");
|
||
assert_eq!(out.status.code(), Some(0), "`aura graph` exit: {:?}", out.status);
|
||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||
// a self-contained HTML document...
|
||
assert!(
|
||
stdout.trim_start().starts_with("<!doctype html>"),
|
||
"not an HTML doc: {:?}",
|
||
&stdout[..stdout.len().min(80)]
|
||
);
|
||
// ...with the deterministic model inlined as the viewer's data source...
|
||
assert!(stdout.contains("window.AURA_MODEL = {\"root\":"), "model not inlined: {stdout:?}");
|
||
// ...the Graphviz-WASM bootstrap present, and no remote asset fetch.
|
||
assert!(stdout.contains("Viz.instance"), "viewer bootstrap missing");
|
||
assert!(!stdout.contains("<script src"), "assets must be inlined, found remote <script src>");
|
||
// the retired ascii-dag invented notation is gone.
|
||
assert!(!stdout.contains("Sub(#Sf,#Ss)"), "retired ascii-dag notation must be gone: {stdout}");
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 9: Full gate — build, test, lint, dep-tree, file-gone**
|
||
|
||
Run:
|
||
```sh
|
||
cargo build --workspace
|
||
cargo test --workspace
|
||
cargo clippy --workspace --all-targets -- -D warnings
|
||
cargo tree -p aura-cli 2>/dev/null | grep -i ascii-dag; echo "ascii-dag-grep-exit=$?"
|
||
test ! -e crates/aura-cli/src/graph.rs && echo "graph.rs gone"
|
||
```
|
||
Expected: build 0 errors; `cargo test --workspace` all green (every test that ran
|
||
passed, and the new `graph_emits_self_contained_html_viewer` +
|
||
`render_html_is_self_contained_and_embeds_the_model` are among them — confirm both
|
||
names appear in the test output, not "0 ran"); clippy 0 warnings;
|
||
`ascii-dag-grep-exit=1` (ascii-dag absent from the dep tree); `graph.rs gone`.
|
||
|
||
---
|
||
|
||
## Task 4: Tidy the two stale ascii-dag prose references in aura-core
|
||
|
||
The cycle retires ascii-dag; these two comments in `crates/aura-core/src/node.rs`
|
||
are its last prose footprint and carry now-false justifications (the second
|
||
already false since cycle 0017 dropped cluster boxes). The `label()` single-line
|
||
rule stays true (an HTML-table cell label is still one line) but is re-justified
|
||
generically; the param-generic rule is re-justified on C23, not on an ascii-dag
|
||
limitation. Methods and assertions are unchanged — prose only.
|
||
|
||
**Files:**
|
||
- Modify: `crates/aura-core/src/node.rs:154` and `:198-199`
|
||
|
||
- [ ] **Step 1: Re-justify the `Node::label()` single-line constraint (~line 150-157)**
|
||
|
||
Replace:
|
||
```rust
|
||
/// A one-line, **non-load-bearing** render label (C23): a debug symbol for
|
||
/// tracing / graph rendering (#13), never read by the run loop and never
|
||
/// part of wiring (which is by index). Overrides SHOULD carry the node's
|
||
/// identifying params so identical node types disambiguate (`SMA(2)` vs
|
||
/// `SMA(4)`). MUST be single-line (no `\n`): ascii-dag breaks box drawing on
|
||
/// a multiline label. The default is a placeholder; every shipped node
|
||
/// overrides it. Returns an owned `String` and takes `&self`, so `Node`
|
||
/// stays object-safe and `Box<dyn Node>::label()` dispatches.
|
||
```
|
||
with:
|
||
```rust
|
||
/// A one-line, **non-load-bearing** render label (C23): a debug symbol for
|
||
/// tracing / graph rendering (#13), never read by the run loop and never
|
||
/// part of wiring (which is by index). Overrides SHOULD carry the node's
|
||
/// identifying params so identical node types disambiguate (`SMA(2)` vs
|
||
/// `SMA(4)`). MUST be single-line (no `\n`): the graph render draws each
|
||
/// label in one cell. The default is a placeholder; every shipped node
|
||
/// overrides it. Returns an owned `String` and takes `&self`, so `Node`
|
||
/// stays object-safe and `Box<dyn Node>::label()` dispatches.
|
||
```
|
||
|
||
- [ ] **Step 2: Re-justify the param-generic builder-label test comment (~line 196-199)**
|
||
|
||
Replace:
|
||
```rust
|
||
fn primitive_builder_label_is_the_bare_type() {
|
||
// param-generic: the label is just the node type, no knob suffix (the
|
||
// ascii-dag blueprint view cannot render wide cluster-sibling labels).
|
||
```
|
||
with:
|
||
```rust
|
||
fn primitive_builder_label_is_the_bare_type() {
|
||
// param-generic: the label is just the node type, no knob suffix — the
|
||
// blueprint view is param-generic (C23), values appear only post-compile.
|
||
```
|
||
|
||
- [ ] **Step 3: Verify no ascii-dag prose remains in the workspace source**
|
||
|
||
Run:
|
||
```sh
|
||
grep -rni "ascii-dag\|ascii_dag" crates/ ; echo "exit=$?"
|
||
```
|
||
Expected: `exit=1` (no matches) — the dep, the adapter, and the prose are all
|
||
gone. (`Cargo.lock` may still list it transitively until regenerated; the build
|
||
in Task 3 Step 9 already regenerated it — if it appears there only, that is the
|
||
lockfile and not a source reference.)
|
||
|
||
- [ ] **Step 4: Build + test aura-core to confirm the prose edits compile**
|
||
|
||
Run:
|
||
```sh
|
||
cargo test -p aura-core
|
||
```
|
||
Expected: PASS (prose-only change; all aura-core tests green).
|
||
|
||
---
|
||
|
||
## Task 5: Ledger realization note (cycle 0026)
|
||
|
||
Add one consolidated realization note for cycle 0026 (both iterations: the model
|
||
serializer + the WASM-Graphviz viewer) to the C9 section of the design ledger,
|
||
right after the cycle-0024 render-lineage note.
|
||
|
||
**Files:**
|
||
- Modify: `docs/design/INDEX.md:694` (insert after the cycle-0024 realization note, before `### C20`)
|
||
|
||
- [ ] **Step 1: Insert the realization note**
|
||
|
||
In `docs/design/INDEX.md`, after the paragraph ending at line 694 (`…the C8 0024
|
||
realization).`) and before the blank line preceding `### C20 — Strategy ↔
|
||
harness` (line 696), insert:
|
||
```markdown
|
||
|
||
**Realization (cycle 0026 — graph render redesign: model + WASM-Graphviz viewer,
|
||
#51).** `aura graph` no longer renders ASCII. The render path is now two pieces:
|
||
a read-only **model serializer** (`aura_engine::model_to_json`, iteration 1) that
|
||
walks the root composite + every distinct composite type into a deterministic,
|
||
hand-rolled JSON model (C14, golden-tested like `RunReport::to_json`; the
|
||
swapped-param mis-wire property moved here from the old compiled-view test), and a
|
||
**self-contained HTML viewer** (`aura-cli::render::render_html`, iteration 2) that
|
||
inlines that model, the ported prototype viewer JS, and a vendored Graphviz-WASM
|
||
blob into one page emitted to stdout. Layout/SVG happen in the browser via
|
||
WebAssembly — aura ships no layout engine and stays a serializer (C9: graph-as-data,
|
||
no `eval`/build on the path). The viewer is a render asset (C10 — no node/strategy
|
||
logic, no DSL); it labels every input pin from the model's now-real names (C23
|
||
debug symbols, named in cycle 0027) and colours wires by the four scalar base
|
||
types (C4). This **retires `ascii-dag`** and its adapter (`graph.rs`), the
|
||
`--compiled`/`--macd` flag plumbing, and the invented `#Sf`/`:=`/`histogram →`
|
||
notation — superseding the cycle-0017 flat-ascii model and its renderer-defect
|
||
workaround. The DOT/SVG are Graphviz-version-dependent and not golden-tested; the
|
||
deterministic JSON model is the asserted contract.
|
||
```
|
||
|
||
- [ ] **Step 2: Confirm the note placed cleanly**
|
||
|
||
Run:
|
||
```sh
|
||
grep -n "Realization (cycle 0026" docs/design/INDEX.md
|
||
grep -n "### C20 — Strategy" docs/design/INDEX.md
|
||
```
|
||
Expected: the 0026 note line number is immediately above (a few lines before) the
|
||
`### C20` line — the insertion sits at the end of the C9 section, not inside C20.
|
||
|
||
---
|
||
|
||
## Iteration done-state
|
||
|
||
After Task 5, the working tree carries: the new assets + `render.rs`, the
|
||
retired `graph.rs` + dep + flags + tests, the prose tidy, and the ledger note —
|
||
all unstaged. The full gate (Task 3 Step 9) is green: `cargo build --workspace`
|
||
0 errors, `cargo test --workspace` all green (model golden unchanged; the two new
|
||
HTML tests present), `cargo clippy --workspace --all-targets -- -D warnings`
|
||
clean, `ascii-dag` absent from the dep tree. The orchestrator inspects the diff
|
||
and commits (suggested subject: `feat(aura-cli): render the graph as a
|
||
self-contained WASM-Graphviz viewer; retire ascii-dag`, body `closes #51`).
|