#!/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 A* correctness ground truth (plan 06-01) and the * snap/astar 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. * * Usage: node routing/test/fixtures/build-mini-graph.mjs * Exit: 0 on write; throws on any invariant violation (never writes). * * ARG2 (plan 08-02): the fixture carries a tiny real landmark stage — * L=2 anchors via farthestPointAnchors + quantized c-metric tables via * landmarkTables (the SAME pure module the real build script uses), passed * through the ONE encodeGraph authority. 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 { LANDMARK_QUANTUM_S, farthestPointAnchors, landmarkTables, landmarkCoverage } from "../../src/landmark.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}`); // LANDMARK STAGE (plan 08-02): the fixture regenerates as ARG2 THROUGH the // one encode authority — the same farthestPointAnchors + landmarkTables the // real build script (08-04) will run, at a tiny L. Fail-loud invariants // before any write (the `edges.length !== 80` style). const LANDMARK_COUNT = 2; // tiny L: the 5x5 grid needs exactly 2 well-spread anchors const landmarkInput = { nodeCount: GRID * GRID, edges, quantumS: LANDMARK_QUANTUM_S }; const anchors = farthestPointAnchors(landmarkInput, LANDMARK_COUNT); const { to, from } = landmarkTables(landmarkInput, anchors); if (anchors.length !== LANDMARK_COUNT) throw new Error(`expected ${LANDMARK_COUNT} anchors, got ${anchors.length}`); if (to.length !== LANDMARK_COUNT * GRID * GRID || from.length !== LANDMARK_COUNT * GRID * GRID) { throw new Error("landmark tables are not L*N row-major"); } for (let l = 0; l < anchors.length; l++) { const a = anchors[l]; if (to[l * GRID * GRID + a] !== 0 || from[l * GRID * GRID + a] !== 0) { throw new Error(`anchor ${l} (node ${a}) own cells are not 0 — table law broken`); } } const coverage = landmarkCoverage(to, anchors, GRID * GRID); if (coverage.some((c) => c !== 1)) { throw new Error(`the grid is strongly connected — coverage must be 1.0, got ${coverage.join(", ")}`); } const bin = encodeGraph({ lat, lng, edges, names, maxSpeedKmh: SPEED_PERIMETER_KMH, landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from }, }); 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, ` + `L=${LANDMARK_COUNT} landmarks (nodes ${[...anchors].join(",")}), quantum ${LANDMARK_QUANTUM_S}s, ` + `coverage ${coverage.map((c) => c.toFixed(2)).join("/")}, ${bin.byteLength} B`, );