// 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);