Files
arah2/routing/test/fixtures/build-mini-graph.mjs
araharah-executor 32a00b5b0b feat(08-02): ARG2 codec — one version, one decode path (G-suite GREEN)
graph.ts versioned IN PLACE (no second codec): GRAPH_MAGIC 0x41524732,
GRAPH_VERSION 2, HEADER_SIZE 72 (72%8==0 keeps f64 views aligned);
header gains landmarkCount u32 @60 (the old pad), landmarkQuantumS f32
@64, reserved u32 @68; the landmark suffix (u16 toTable, u16 fromTable,
u32 landmarkNodes) extends layoutFor's total arithmetic — exact-size
law covers the variable suffix BEFORE any view allocation (T-08-01).
D-01 fail-honest: ARG1 bytes / version!=2 / landmarkCount=0 / hostile
quantum / size mismatch all decode null (committed mini-graph-arg1.bin
is the standing rejection vector). GraphInput.landmarks REQUIRED and
loud-validated (T-08-04). RoadGraph gains the five landmark fields
(Plan 08-03's ALT potentials consume them).

Fixture regenerated as ARG2 through the SAME encode authority: the
builder gains the tiny real landmark stage (L=2, anchors [0,18],
quantum 0.1s, coverage 1.0 — identical bytes every re-run).

Rule-3 call-site updates forced by the required-landmarks law (missed
by the plan's files list): astar.test A3 + snap.test S3 synthetics
carry inert hand landmark blocks. Fixture regen pulled forward from
task 3 so this commit's own battery is green (74 passed, 0 failed).
2026-10-25 18:48:23 +08:00

132 lines
5.7 KiB
JavaScript
Raw Permalink 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 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`,
);