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).
132 lines
5.7 KiB
JavaScript
132 lines
5.7 KiB
JavaScript
#!/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`,
|
||
);
|