bc88e18247
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
64 lines
2.8 KiB
JavaScript
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);
|