feat(06-01): CSR graph codec + committed mini-graph fixture + RouteStep.kind (GREEN)
- graph.ts: decodeGraph/encodeGraph over the pinned little-endian CSR layout; header magic+version+exact byte-size validation BEFORE any view allocation (T-06-03/T-06-03b fail-honest null); zero-copy typed-array views; street-name pool with UNNAMED_NAME_ID sentinel - header padded 60 -> 64 bytes so f64 views are 8-byte aligned — unaligned Float64Array throws RangeError, making plan-literal offsets incompatible with the plan's own zero-copy law (documented deviation; field order/content/endian unchanged; consumers inherit via encodeGraph) - build-mini-graph.mjs + committed mini-graph.bin: hand-built 5x5 grid, 25 nodes, 80 directed edges, 3 named streets, exactly ONE oneway pair, two speed classes (60/30 km/h) — the A* ground truth (G7 pins it) - types.ts: RouteStep gains optional kind (8 turn kinds) — Engine A populates it (06-01); fixture-world steps stay empty arrays (D-10)
This commit is contained in:
+85
@@ -0,0 +1,85 @@
|
||||
#!/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.
|
||||
*
|
||||
* Usage: node routing/test/fixtures/build-mini-graph.mjs
|
||||
* Exit: 0 on write; throws on any invariant violation (never writes).
|
||||
*
|
||||
* 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";
|
||||
|
||||
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}`);
|
||||
|
||||
const bin = encodeGraph({ lat, lng, edges, names, maxSpeedKmh: SPEED_PERIMETER_KMH });
|
||||
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, ${bin.byteLength} B`);
|
||||
Reference in New Issue
Block a user