#!/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`, );