Files
arah2/routing/test/fixtures/build-mini-graph.mjs
T
araharah-executor 4ca7a71d15 feat(08.1-01): rebuild all fixtures and bot graph builders through the ARG3 path
- mini-graph-arg2.bin: the frozen ARG2-era bytes preserved once as the
  D-12 decode-rejection vector (never regenerated — the arg1 precedent)
- build-mini-graph.mjs: landmark stage out, CH stage in — contractGraph
  over the 25-node grid (41 shortcuts, 1.64/node, 0 budget hits) with
  fail-loud invariants: total-order rank, grid shortcut band, upward
  shortcuts, overlay-exact constituent weights (f64 replay, f32 compare),
  strong connectivity; deterministic ARG3 bytes
- graph.test.mjs: committed-fixture G4 added (ARG3 magic/version, 25
  nodes, 80 edges, rank 25, >=1 shortcut, ARG2 vector -> null); G7
  restored (full ARG3 pin)
- astar.test.mjs: A1-A6 restored — the 625-pair Dijkstra equivalence,
  oneway law, deriveSteps, depart/arrive, classifyTurn all green against
  the CH query on the committed ARG3 fixture
- snap.test.mjs: S1-S6 restored on the ARG3 fixture; S3's synthetic
  graph rebuilt as the ARG3 ch-block star
- webhook.test.mjs: landmarkBlock -> chBlock (contractGraph); corridor
  gains the fast nonstop chord (a pure line starves to zero shortcuts);
  sparse star keeps node ids 0-hub; RT9's split carrier is now two
  disconnected decodable stars (the honest A\* miss)
- guidance-cron.test.mjs: GC6b's 2-node graph becomes the lowest-id-mid
  3-node star (a 2-node graph cannot carry a valid shortcut)
- routing 87 passed / bot 126 passed; routing+bot+web typechecks green
2026-10-26 07:30:15 +08:00

202 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
* build-mini-graph.mjs — builds the committed mini-graph.bin fixture.
* ----------------------------------------------------------------------
* A MAINTAINER-TIME REPO TOOL: regenerates routing/test/fixtures/
* mini-graph.bin — the routing correctness ground truth (plan 06-01) and
* the snap/search suites' shared fixture. NOT a runtime dependency;
* nothing in bot/src/, web/src/, or routing/src/ imports it. Tests
* consume the COMMITTED binary; only a deliberate fixture redesign re-runs
* this.
*
* PROVENANCE (Phase 8 plan 08-01): routing/test/fixtures/mini-graph-arg1.bin
* holds the frozen ARG1-era bytes preserved expressly as the v1
* decode-rejection vector consumed by the G-suite — it is NOT regenerated
* by this script, ever.
* PROVENANCE (Phase 08.1 plan 01): routing/test/fixtures/mini-graph-arg2.bin
* holds the frozen ARG2-era bytes preserved expressly as the ARG2
* decode-rejection vector consumed by the G-suite (D-12) — it is NOT
* regenerated by this script, ever.
*
* Usage: node routing/test/fixtures/build-mini-graph.mjs
* Exit: 0 on write; throws on any invariant violation (never writes).
*
* ARG3 (Phase 08.1, D-12): the fixture carries a tiny real CH stage —
* contraction via contractGraph (the SAME pure module the real build
* script uses), passed through the ONE encodeGraph authority as the `ch`
* block. Deterministic: identical output bytes on every re-run.
*
* Design (pinned by 06-01): a hand-built 5x5 grid in a ~0.002 deg x 0.002 deg
* bbox near KL center (lat 3.1500–3.1520, lng 101.6900–101.6920):
* - 25 nodes, 80 directed edge entries (20 H + 20 V segments, both ways)
* - three named streets: "Jalan Mini 1" (interior E-W rows),
* "Jalan Mini 2" (interior N-S columns), "Jalan Persiaran Mini" (perimeter)
* - exactly ONE oneway pair: 2 -> 3 traversable, 3 -> 2 flagged bit0
* - two speed classes: perimeter 60 km/h, interior 30 km/h
* (weights = segment meters / (kmh / 3.6), meters via haversineM)
*/
import { writeFileSync } from "node:fs";
import { encodeGraph } from "../../src/graph.ts";
import { haversineM } from "../../src/geo.ts";
import { contractGraph, CH_SHORTCUT_EXPLOSION_RATIO } from "../../src/contraction.ts";
const GRID = 5;
const LAT0 = 3.15;
const LNG0 = 101.69;
const STEP_DEG = 0.0005;
/** The single oneway pair (grid ids: row 0, cols 2->3). */
const ONEWAY_U = 2;
const ONEWAY_V = 3;
const NAME_MINI_1 = 0; // interior E-W rows
const NAME_MINI_2 = 1; // interior N-S columns
const NAME_PERIMETER = 2;
const names = ["Jalan Mini 1", "Jalan Mini 2", "Jalan Persiaran Mini"];
const SPEED_PERIMETER_KMH = 60;
const SPEED_INTERIOR_KMH = 30;
const lat = [];
const lng = [];
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
// 1e-6 integer grid then divide — exact decimal coordinates (the codec's
// grid law); naive `3.15 + r * 0.0005` accumulates float error.
lat.push((3150000 + r * 500) / 1e6);
lng.push((101690000 + c * 500) / 1e6);
}
}
const id = (r, c) => r * GRID + c;
const edges = [];
function addEdge(u, v, nameId) {
const kmh = nameId === NAME_PERIMETER ? SPEED_PERIMETER_KMH : SPEED_INTERIOR_KMH;
const meters = haversineM({ lat: lat[u], lng: lng[u] }, { lat: lat[v], lng: lng[v] });
edges.push({ from: u, to: v, seconds: meters / (kmh / 3.6), nameId });
}
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID - 1; c++) {
const nameId = r === 0 || r === GRID - 1 ? NAME_PERIMETER : NAME_MINI_1;
addEdge(id(r, c), id(r, c + 1), nameId);
addEdge(id(r, c + 1), id(r, c), nameId);
}
}
for (let c = 0; c < GRID; c++) {
for (let r = 0; r < GRID - 1; r++) {
const nameId = c === 0 || c === GRID - 1 ? NAME_PERIMETER : NAME_MINI_2;
addEdge(id(r, c), id(r + 1, c), nameId);
addEdge(id(r + 1, c), id(r, c), nameId);
}
}
// Exactly ONE oneway: the reverse mirror of U->V carries flags bit0.
const reverse = edges.find((e) => e.from === ONEWAY_V && e.to === ONEWAY_U);
if (!reverse) throw new Error("oneway reverse edge not found — grid invariant broken");
reverse.flags = 1;
if (edges.length !== 80) throw new Error(`expected 80 directed edges, built ${edges.length}`);
// CH STAGE (Phase 08.1): the fixture regenerates as ARG3 THROUGH the one
// encode authority — the same contractGraph the real build script (plan 02)
// runs. Fail-loud invariants before any write (the `edges.length !== 80`
// style), per RESEARCH §Pattern 4.
const ch = contractGraph({ nodeCount: GRID * GRID, edges });
const rankValues = [...ch.rank];
if (new Set(rankValues).size !== GRID * GRID) {
throw new Error(`rank is not a total order — ${new Set(rankValues).size} distinct for ${GRID * GRID} nodes`);
}
if (rankValues.some((r) => !Number.isInteger(r) || r < 0 || r >= GRID * GRID)) {
throw new Error("a rank value is outside [0, nodeCount)");
}
// The shortcut band for this grid (measured 2026-10-26, this host: 41 —
// the literature's healthy ~1.4-2.0 per node; the tripwire sits at 4x).
const SHORTCUT_BAND_MIN = 20;
const SHORTCUT_BAND_MAX = 80;
if (ch.stats.shortcutCount < SHORTCUT_BAND_MIN || ch.stats.shortcutCount > SHORTCUT_BAND_MAX) {
throw new Error(
`shortcut count ${ch.stats.shortcutCount} outside the expected grid band ` +
`[${SHORTCUT_BAND_MIN}, ${SHORTCUT_BAND_MAX}] — check the contraction/witness budget`,
);
}
// Overlay-aware weight map: a cascading shortcut's constituents are the
// OVERLAY edges it replaced (originals OR earlier shortcuts — emission
// order is the contraction order, so replaying rank[via]-ascending
// rebuilds every weight exactly).
const weight = new Map(); // "a,b" -> min overlay weight
for (const e of edges) {
if ((e.flags & 1) !== 0) continue;
const key = `${e.from},${e.to}`;
const prev = weight.get(key);
if (prev === undefined || e.seconds < prev) weight.set(key, e.seconds);
}
const shortcutEntries = [];
for (let u = 0; u < GRID * GRID; u++) {
for (let s = ch.shortcutStart[u]; s < ch.shortcutStart[u + 1]; s++) {
shortcutEntries.push({ from: u, to: ch.shortcutTarget[s], seconds: ch.shortcutSeconds[s], via: ch.shortcutVia[s] });
}
}
shortcutEntries.sort((a, b) => ch.rank[a.via] - ch.rank[b.via] || a.from - b.from || a.to - b.to);
for (const sc of shortcutEntries) {
const via = sc.via;
if (!Number.isInteger(via) || via < 0 || via >= GRID * GRID) {
throw new Error(`shortcut ${sc.from}->${sc.to} has an out-of-range via ${via}`);
}
if (ch.rank[sc.from] <= ch.rank[via] || ch.rank[sc.to] <= ch.rank[via]) {
throw new Error(`shortcut ${sc.from}->${sc.to} (via ${via}) is not upward`);
}
const w1 = weight.get(`${sc.from},${via}`);
const w2 = weight.get(`${via},${sc.to}`);
if (w1 === undefined || w2 === undefined) {
throw new Error(`shortcut ${sc.from}->${sc.to} (via ${via}): a constituent overlay edge is missing`);
}
if (sc.seconds !== Math.fround(w1 + w2)) {
throw new Error(`shortcut ${sc.from}->${sc.to} weight ${sc.seconds} != f32(${w1} + ${w2})`);
}
const key = `${sc.from},${sc.to}`;
const prev = weight.get(key);
// The contraction's overlay accumulates f64 sums (the f32 rounding
// happens once, at CSR assembly) — replay the same f64 domain so the
// next cascade level compares exactly.
if (prev === undefined || w1 + w2 < prev) weight.set(key, w1 + w2);
}
// Strong connectivity (the grid is bidirectional except the ONE oneway
// mirror — every node reaches every node).
const adj = Array.from({ length: GRID * GRID }, () => []);
for (const e of edges) {
if ((e.flags & 1) !== 0) continue;
adj[e.from].push(e.to);
}
const seen = new Uint8Array(GRID * GRID);
const stack = [0];
seen[0] = 1;
while (stack.length > 0) {
const u = stack.pop();
for (const v of adj[u]) if (!seen[v]) { seen[v] = 1; stack.push(v); }
}
if (seen.some((s) => s === 0)) {
throw new Error("the grid is not strongly connected — fixture invariant broken");
}
const bin = encodeGraph({
lat,
lng,
edges,
names,
maxSpeedKmh: SPEED_PERIMETER_KMH,
ch: {
rank: ch.rank,
shortcutStart: ch.shortcutStart,
shortcutTarget: ch.shortcutTarget,
shortcutSeconds: ch.shortcutSeconds,
shortcutVia: ch.shortcutVia,
},
});
writeFileSync(new URL("./mini-graph.bin", import.meta.url), Buffer.from(bin));
console.log(
`mini-graph.bin: ${GRID * GRID} nodes, ${edges.length} edges, 3 names, ` +
`CH shortcuts ${ch.stats.shortcutCount} (${ch.stats.ratioPerNode.toFixed(2)}/node, ` +
`witness searches ${ch.stats.witnessSearches}, budget hits ${ch.stats.witnessBudgetHits}), ` +
`${bin.byteLength} B`,
);