Files
Aura/crates/aura-cli/tests/viewer_gang_param.mjs
Brummel bc88e18247 feat(engine,cli): GraphBuilder gang cadence + render carrier (#61 tasks 5-6)
Builder: NodeHandle::param -> ParamRef, GraphBuilder::gang, build() resolves
the gang table (collect-then-reject, BuildError::{UnknownParam,AmbiguousParam,
BadGang}) and terminates in Composite::with_gangs — the same predicate the
op-script and serde boundaries use. C24 lockstep extended: an op-script gang
replay serializes byte-identically to the GraphBuilder twin (FlatGraph edges +
sources + canonical JSON).

Render: model_to_json emits a per-scope gangs fragment (name, kind, [node,pos,
"param"] members) for ganged scopes only — the gang-free model stays
byte-identical (model_golden untouched). graph-viewer.js annotates a ganged
open param with its public knob name; guarded by a viewer_gang_param.{mjs,rs}
pair RED-first per the established viewer_bound_param convention (a welcome
extra beyond the plan's file list).

Ratified deviation: the model's kind string uses the serde form ("I64") — the
plan's implementation snippet (kind_str -> "i64") contradicted its own test;
resolved toward the test + the blueprint_serde form. Two quality nits held as
principled plan-holds (inline resolution block; the unreachable unwrap_or
placeholder check_gangs refuses first).

Verified: aura-engine lib 264/0 (model_golden, lockstep, builder, carrier all
green), viewer_gang_param 1/0, workspace clippy -D warnings clean. Slice
resumed across a transient API failure via workflow resume (cached prefix).

refs #61
2026-07-10 13:03:10 +02:00

64 lines
2.8 KiB
JavaScript

// Headless render guard for the `aura graph` viewer (graph-viewer.js).
//
// Property protected: a free param slot whose [nodeIndex, originalPos] is a
// member of the scope's gang table renders with its public gang name appended
// ("length (gang: channel_length)"), while an identical param slot in a
// gang-free scope renders bare — the gang table wired into normalizeModel/
// cellLabel (#61) actually reaches the rendered node card, not just the JSON
// model (which the Rust-side model_carries_gangs test already pins).
//
// It loads the real viewer module and drives the exported pure `genDot`. `node`
// is REQUIRED: a skipped guard is not a guard.
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));
global.window = { AURA_MODEL: { root: { nodes: {}, edges: [] }, composites: {} } };
const { normalizeModel, genDot } = require(join(here, "..", "assets", "graph-viewer.js"));
const dotFor = (model) => genDot(normalizeModel(model).root, "root", new Set(), false).dot;
const fail = (msg, dot) => {
console.error(msg + "\n--- dot ---\n" + dot);
process.exit(1);
};
// --- Model A: two sibling nodes, their "length" params ganged as "channel_length" ---
const ganged = {
root: {
nodes: {
"0": { prim: { name: "hi", type: "Sma", role: "node", params: [["length", "i64"]], ins: [], outs: [["value", "f64"]] } },
"1": { prim: { name: "lo", type: "Sma", role: "node", params: [["length", "i64"]], ins: [], outs: [["value", "f64"]] } },
},
edges: [],
gangs: [{ name: "channel_length", kind: "I64", members: [[0, 0, "length"], [1, 0, "length"]] }],
},
composites: {},
};
const dotGanged = dotFor(ganged);
const GANG_TAG = '<font color="#9399b2"> (gang: channel_length)</font>';
if (!dotGanged.includes(GANG_TAG)) fail("ganged param slot missing the gang-name annotation", dotGanged);
const occurrences = dotGanged.split(GANG_TAG).length - 1;
if (occurrences !== 2) fail(`expected the gang tag on BOTH member nodes (2 occurrences), got ${occurrences}`, dotGanged);
// --- Model B: identical shape, no gangs — must render bare, no annotation ---
const bare = {
root: {
nodes: {
"0": { prim: { name: "hi", type: "Sma", role: "node", params: [["length", "i64"]], ins: [], outs: [["value", "f64"]] } },
"1": { prim: { name: "lo", type: "Sma", role: "node", params: [["length", "i64"]], ins: [], outs: [["value", "f64"]] } },
},
edges: [],
},
composites: {},
};
const dotBare = dotFor(bare);
if (dotBare.includes("(gang:")) fail("gang-free scope must not render any gang annotation", dotBare);
console.log("OK — a ganged param slot renders its public gang name; a gang-free scope renders bare.");
process.exit(0);