// Headless render guard for the `aura graph` viewer (graph-viewer.js). // // Property protected: a bind-bound param slot renders in the node signature as a // DIMMED `name=value`, in TRUE slot order (by original position), with the free // slots in their type colour — never dropped. Covers a trailing bind (the // cycle-0036 showcase shape) AND a non-trailing (middle) bind, which an // append-after-free design would render in the wrong order. // // 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)); // Load the viewer once with an empty model; normalizeModel/genDot are pure and // take the model/def explicitly, so each case is rendered by passing its own. 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: trailing bind (the cycle-0036 showcase shape) --- const trailing = { root: { nodes: { "0": { prim: { name: "blend", type: "LinComb", role: "node", params: [["weights[0]", "f64"], ["weights[1]", "f64"]], bound: [[2, "weights[2]", "f64", "0.5"]], ins: [["f64", "any", "term[0]"], ["f64", "any", "term[1]"], ["f64", "any", "term[2]"]], outs: [["value", "f64"]], } } }, edges: [] }, composites: {}, }; const dotA = dotFor(trailing); const BOUND = 'weights[2]=0.5'; const FREE0 = 'weights[0]'; const FREE1 = 'weights[1]'; if (!dotA.includes(BOUND)) fail("trailing: bound slot weights[2]=0.5 not rendered dimmed", dotA); if (!dotA.includes(FREE0) || !dotA.includes(FREE1)) fail("trailing: a free weight slot is missing or not type-coloured", dotA); const a0 = dotA.indexOf(FREE0), a1 = dotA.indexOf(FREE1), ab = dotA.indexOf(BOUND); if (!(a0 < a1 && a1 < ab)) fail("trailing: slot order is not weights[0] < weights[1] < weights[2]=0.5", dotA); // --- Model B: NON-TRAILING (middle) bind — pins slot-order faithfulness --- const middle = { root: { nodes: { "0": { prim: { name: "mid", type: "Probe", role: "node", params: [["a", "f64"], ["c", "f64"]], bound: [[1, "b", "f64", "9.0"]], ins: [["f64", "any", "t0"], ["f64", "any", "t1"], ["f64", "any", "t2"]], outs: [["value", "f64"]], } } }, edges: [] }, composites: {}, }; const dotB = dotFor(middle); const MA = 'a'; const MB = 'b=9.0'; const MC = 'c'; if (!dotB.includes(MA) || !dotB.includes(MB) || !dotB.includes(MC)) fail("middle: a free or bound slot is missing", dotB); const ba = dotB.indexOf(MA), bb = dotB.indexOf(MB), bc = dotB.indexOf(MC); if (!(ba < bb && bb < bc)) fail("middle: bound slot b=9.0 not placed between free a and c (append-only bug)", dotB); console.log("OK — bound slot renders dimmed name=value in true slot order (trailing + middle bind)."); process.exit(0);