94af4c788c
The built-in sample that `aura graph` renders and `aura sweep` runs is now a
"trend + momentum, weighted blend" strategy that exercises four authoring/viewer
capabilities the single-level sample left dark:
- multiply-nested composites: root -> signals -> {trend = sma_cross, momentum = macd};
- a multi-param node inside a composite: blend = LinComb(3) (weights[0]/[1] shown);
- a multi-output node: momentum (the macd composite) re-exports macd/signal/histogram,
two of which the blend consumes;
- bind(): blend.weights[2] is fixed as a structural constant, so it drops out of the
sweepable param surface (and renders absent).
The new `signals` composite reuses the existing sma_cross/macd builders unchanged; the
sweep re-paths its axes to the eight free params (still a 4-point grid) and runs an
18-tick warm-up stream (showcase_prices) so the MACD EMA-of-EMA chain and the all-Any
LinComb join warm up. The render fixture (sample-model.json) is re-captured, and a new
headless guard (viewer_nested_depth) pins that the viewer renders the nesting to depth 2
with valid Graphviz ids. Pure authoring over already-shipped primitives — no engine or
graph-viewer.js change.
The flat run_sample, run_macd/macd(), the inline model_golden test, and the viewer are
untouched. A third sweep-param pin in tests/cli_run.rs (the aura sweep E2E) was re-pathed
in lockstep with its in-crate twin.
closes #62
82 lines
3.6 KiB
JavaScript
82 lines
3.6 KiB
JavaScript
// Headless depth-2 nesting guard for the `aura graph` viewer (graph-viewer.js).
|
|
//
|
|
// Property protected: the viewer renders MULTIPLY-nested composites — a composite
|
|
// inside a composite inside the root — with valid Graphviz node identifiers at
|
|
// every depth. genDot builds ids as `n` + the node-key path joined by `__`
|
|
// (graph-viewer.js: `cid = "n" + cpath.join("__")`), so a depth-2 interior node
|
|
// gets a three-segment id like `n0__0__0`. A digit-led non-numeral id (`0__0__0`)
|
|
// would be misparsed by Graphviz and collapse the interior; this guard pins that
|
|
// the `n`-prefix rule holds at depth 2, beyond the depth-1 viewer_dot_ids.mjs guard.
|
|
//
|
|
// It loads the real viewer module and the re-captured sample fixture (which is now
|
|
// the enriched root → signals → {trend, momentum} graph), then EXPANDS RECURSIVELY:
|
|
// discover the top-level expandable (signals), expand, re-discover the newly
|
|
// revealed expandables (trend, momentum), expand those — until none remain — and
|
|
// asserts (a) every DOT id is valid and (b) at least one three-segment (depth-2) id
|
|
// was actually produced, so the guard cannot pass vacuously on a flat model.
|
|
|
|
import { createRequire } from "node:module";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const model = JSON.parse(readFileSync(join(here, "fixtures", "sample-model.json"), "utf8"));
|
|
|
|
// Stub the global BEFORE requiring the viewer (mirrors the browser bootstrap).
|
|
global.window = { AURA_MODEL: model };
|
|
const { normalizeModel, genDot } = require(join(here, "..", "assets", "graph-viewer.js"));
|
|
const ROOT = normalizeModel(model).root;
|
|
|
|
// Expand every expandable composite, re-discovering deeper ones each pass, until
|
|
// the expandable set stops growing (reaches every nesting level).
|
|
let expanded = new Set();
|
|
let dot = "";
|
|
for (let pass = 0; pass < 16; pass++) {
|
|
const g = genDot(ROOT, "root", expanded, false);
|
|
dot = g.dot;
|
|
const fresh = g.expandable.filter((id) => !expanded.has(id));
|
|
if (!fresh.length) break;
|
|
fresh.forEach((id) => expanded.add(id));
|
|
}
|
|
|
|
// Collect every node identifier referenced in the final DOT (node-def lines + edge
|
|
// endpoints), mirroring viewer_dot_ids.mjs.
|
|
const ids = new Set();
|
|
for (const m of dot.matchAll(/^\s*([^\s\[]+)\s*\[label=/gm)) ids.add(m[1]);
|
|
for (const m of dot.matchAll(/(\S+?):[a-z]\w*:[a-z]\s*->\s*(\S+?):[a-z]\w*:[a-z]/g)) {
|
|
ids.add(m[1]);
|
|
ids.add(m[2]);
|
|
}
|
|
|
|
const QUOTED = /^".*"$/s;
|
|
const NUMERAL = /^-?(\.[0-9]+|[0-9]+(\.[0-9]*)?)$/;
|
|
const BARE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
const isValid = (id) => QUOTED.test(id) || NUMERAL.test(id) || BARE.test(id);
|
|
|
|
const bad = [...ids].filter((id) => !isValid(id));
|
|
if (bad.length) {
|
|
console.error(
|
|
"INVALID DOT node identifier(s) at nesting depth — Graphviz will misparse a\n" +
|
|
"digit-prefixed, non-numeral id and collapse the nested composite:\n " +
|
|
bad.map((id) => JSON.stringify(id)).join("\n ")
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Anti-vacuous: at least one three-segment (depth-2) id must exist, proving the
|
|
// guard actually expanded two composite levels (root → signals → trend/momentum).
|
|
const depth2 = [...ids].filter((id) => /^n\d+__\d+__\d+/.test(id));
|
|
if (!depth2.length) {
|
|
console.error(
|
|
"no depth-2 (three-segment `nA__B__C`) id found — the guard did not reach two\n" +
|
|
"nesting levels; the fixture is not multiply-nested or the expand loop stalled."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`OK — ${ids.size} DOT ids valid; ${depth2.length} reached nesting depth 2.`);
|
|
process.exit(0);
|