// 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 = ' (gang: channel_length)'; 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);