Files
araharah-executor 4ca7a71d15 feat(08.1-01): rebuild all fixtures and bot graph builders through the ARG3 path
- mini-graph-arg2.bin: the frozen ARG2-era bytes preserved once as the
  D-12 decode-rejection vector (never regenerated — the arg1 precedent)
- build-mini-graph.mjs: landmark stage out, CH stage in — contractGraph
  over the 25-node grid (41 shortcuts, 1.64/node, 0 budget hits) with
  fail-loud invariants: total-order rank, grid shortcut band, upward
  shortcuts, overlay-exact constituent weights (f64 replay, f32 compare),
  strong connectivity; deterministic ARG3 bytes
- graph.test.mjs: committed-fixture G4 added (ARG3 magic/version, 25
  nodes, 80 edges, rank 25, >=1 shortcut, ARG2 vector -> null); G7
  restored (full ARG3 pin)
- astar.test.mjs: A1-A6 restored — the 625-pair Dijkstra equivalence,
  oneway law, deriveSteps, depart/arrive, classifyTurn all green against
  the CH query on the committed ARG3 fixture
- snap.test.mjs: S1-S6 restored on the ARG3 fixture; S3's synthetic
  graph rebuilt as the ARG3 ch-block star
- webhook.test.mjs: landmarkBlock -> chBlock (contractGraph); corridor
  gains the fast nonstop chord (a pure line starves to zero shortcuts);
  sparse star keeps node ids 0-hub; RT9's split carrier is now two
  disconnected decodable stars (the honest A\* miss)
- guidance-cron.test.mjs: GC6b's 2-node graph becomes the lowest-id-mid
  3-node star (a 2-node graph cannot carry a valid shortcut)
- routing 87 passed / bot 126 passed; routing+bot+web typechecks green
2026-10-26 07:30:15 +08:00

202 lines
9.3 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
/*
* snap.test.mjs — GPS-fix snapping gates (S1–S6).
* ------------------------------------------------
* RED first (plan 06-01 task 2): every case fails while
* routing/src/snap.ts does not exist (lazy-import pattern).
*
* D-05: the ROUTER snaps raw fixes to the graph (engine A owns snapping —
* one authority). The mini-graph fixture (mini-graph.bin) is the shared
* ground truth; a synthetic unnamed-edge graph covers the null-nameId half.
*
* S1 exact-node hit — a fix at a node's coordinates -> that node,
* distanceM ~ 0
* S2 mid-edge projection — a fix partway along a segment snaps to the
* CLOSER ENDPOINT (never a fabricated mid-node);
* distanceM is the haversine distance to it
* S3 nameId — the best segment's edgeNameId resolves;
* an unnamed segment resolves null
* S4 bbox margin — outside bbox + SNAP_BBOX_MARGIN_DEG -> null
* (honest miss, D-03); just outside the bbox
* but near roads still snaps
* S5 oneway ignored — a fix near the oneway segment still snaps to
* its endpoints (snapping ignores direction)
* S6 memoized grid — the coarse grid builds ONCE per RoadGraph
* (WeakMap; build-counter, kept loose) +
* structural: WeakMap and cos() in the source
*
* Phase 08.1 re-pin (plan 01 tasks 2-3): the committed mini-graph.bin is
* now ARG3 (regenerated through the ONE encode authority) and every case
* runs unchanged. S3's synthetic unnamed-edge graph moved to the ARG3 ch
* block (a 3-node star — a 2-node graph can carry no valid shortcut, and
* a shortcut-less ARG3 is not a road graph).
*/
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "./harness.mjs";
const here = path.dirname(fileURLToPath(import.meta.url)); // routing/test
let snapPromise = null;
function loadSnap() {
if (!snapPromise) snapPromise = import("../src/snap.ts");
return snapPromise;
}
let graphPromise = null;
function loadGraph() {
if (!graphPromise) graphPromise = import("../src/graph.ts");
return graphPromise;
}
async function loadGeo() {
return import("../src/geo.ts");
}
/** bytes -> exact ArrayBuffer copy (Buffer pools are larger). */
function toArrayBuffer(buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
/** The committed 5x5 mini-graph, decoded. */
async function loadMiniGraph() {
const { decodeGraph } = await loadGraph();
const bin = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph.bin")));
const g = decodeGraph(bin);
assert.ok(g, "the committed mini-graph decodes");
return g;
}
/** Grid coordinates — MUST match build-mini-graph.mjs (1e-6 integer grid). */
function nodeLat(r) {
return (3150000 + r * 500) / 1e6;
}
function nodeLng(c) {
return (101690000 + c * 500) / 1e6;
}
/* ————— S1–S6 ————— */
test("S1 exact-node hit — a fix at node 12's coordinates snaps to node 12 at ~0 m", async () => {
const { snapToGraph } = await loadSnap();
const g = await loadMiniGraph();
const result = snapToGraph(g, { lat: nodeLat(2), lng: nodeLng(2) });
assert.ok(result, "an on-graph fix snaps");
assert.equal(result.node, 12, "the exact node wins");
assert.ok(result.distanceM < 0.001, `distance ~ 0 (got ${result.distanceM})`);
});
test("S2 mid-edge projection — a fix a quarter along edge 6->7 snaps to the closer endpoint 6, haversine distance", async () => {
const { snapToGraph } = await loadSnap();
const { haversineM } = await loadGeo();
const g = await loadMiniGraph();
// Point 25% of the way from node 6 (r1,c1) to node 7 (r1,c2).
const point = { lat: nodeLat(1), lng: (101690000 + 625) / 1e6 };
const result = snapToGraph(g, point);
assert.ok(result, "the mid-edge fix snaps");
assert.equal(result.node, 6, "the CLOSER endpoint wins — never a fabricated mid-node");
assert.equal(result.nameId, 0, "the best segment is 'Jalan Mini 1' (nameId 0)");
const expected = haversineM(point, { lat: nodeLat(1), lng: nodeLng(1) });
assert.ok(
Math.abs(result.distanceM - expected) < 1e-9,
`distanceM is the haversine distance to the endpoint (${result.distanceM} vs ${expected})`,
);
});
test("S3 nameId — a perimeter fix resolves 'Jalan Persiaran Mini'; an unnamed segment resolves null", async () => {
const { snapToGraph } = await loadSnap();
const { encodeGraph } = await loadGraph();
const g = await loadMiniGraph();
const atNode1 = snapToGraph(g, { lat: nodeLat(0), lng: nodeLng(1) });
assert.ok(atNode1, "perimeter fix snaps");
assert.notEqual(atNode1.nameId, null, "perimeter edges carry names");
assert.equal(g.nameAt(atNode1.nameId), "Jalan Persiaran Mini");
// Synthetic 3-node star with UNNAMED edges -> nameId null.
// ARG3 (Phase 08.1): the ch block is required by the one encode
// authority; it comes from the ONE contraction core. A 2-node graph can
// carry no valid shortcut (no via node exists), so the fixture is the
// C4 star — mid node 0, unnamed legs to 1 and 2 — whose contraction
// emits the two endpoint shortcuts (decode refuses a shortcut-less
// ARG3). The snapped segment 0->1 covers the unnamed fix.
const { contractGraph } = await import("../src/contraction.ts");
const starEdges = [
{ from: 0, to: 1, seconds: 4 },
{ from: 1, to: 0, seconds: 4 },
{ from: 0, to: 2, seconds: 4 },
{ from: 2, to: 0, seconds: 4 },
];
const bare = encodeGraph({
lat: [3.15, 3.1505, 3.16],
lng: [101.69, 101.6905, 101.7],
edges: starEdges,
names: [],
maxSpeedKmh: 60,
ch: contractGraph({ nodeCount: 3, edges: starEdges }),
});
const { decodeGraph, UNNAMED_NAME_ID } = await loadGraph();
const bg = decodeGraph(bare);
assert.ok(bg, "synthetic graph decodes");
assert.equal(bg.edgeNameId[0], UNNAMED_NAME_ID, "the synthetic edge is unnamed");
const unnamed = snapToGraph(bg, { lat: 3.15025, lng: 101.69025 });
assert.ok(unnamed, "the unnamed segment still snaps");
assert.equal(unnamed.nameId, null, "UNNAMED_NAME_ID resolves to null for consumers");
});
test("S4 bbox margin — outside bbox + SNAP_BBOX_MARGIN_DEG -> null; just outside the bbox still snaps", async () => {
const { snapToGraph, SNAP_BBOX_MARGIN_DEG } = await loadSnap();
assert.equal(SNAP_BBOX_MARGIN_DEG, 0.01, "the named margin constant is 0.01 deg");
const g = await loadMiniGraph();
// bbox maxLat = 3.152; +0.008 above it is inside the margin AND near row-4 roads.
const justOutside = snapToGraph(g, { lat: 3.1528, lng: nodeLng(1) });
assert.ok(justOutside, "a fix just outside the bbox but near roads still snaps");
assert.ok(justOutside.node >= 20 && justOutside.node <= 24, "snaps to a row-4 node (ids 20..24)");
// 0.011 deg above the bbox top -> outside bbox + margin -> honest null.
assert.equal(
snapToGraph(g, { lat: 3.163, lng: nodeLng(1) }),
null,
"beyond bbox + margin -> null (router-pending upstream, D-03)",
);
assert.equal(
snapToGraph(g, { lat: nodeLat(1), lng: 101.703 }),
null,
"beyond bbox + margin in lng -> null",
);
});
test("S5 oneway ignored — a fix near the oneway segment snaps to its endpoints like any segment", async () => {
const { snapToGraph } = await loadSnap();
const g = await loadMiniGraph();
// The fixture's single oneway-reverse edge is 3 -> 2 (segment 2-3, perimeter).
// A fix 25% along segment 2->3 snaps to node 2 — direction plays no part.
const point = { lat: nodeLat(0), lng: (101690000 + 1125) / 1e6 };
const result = snapToGraph(g, point);
assert.ok(result, "the oneway segment's fix snaps");
assert.equal(result.node, 2, "the closer endpoint of the oneway segment wins");
assert.notEqual(result.nameId, null);
assert.equal(g.nameAt(result.nameId), "Jalan Persiaran Mini");
});
test("S6 memoized grid — the coarse grid builds once per RoadGraph (WeakMap) + structural gates", async () => {
const mod = await loadSnap();
const g = await loadMiniGraph();
const before = mod.gridBuildCount();
mod.snapToGraph(g, { lat: nodeLat(2), lng: nodeLng(2) });
assert.equal(mod.gridBuildCount(), before + 1, "first snap on a graph builds the grid once");
mod.snapToGraph(g, { lat: nodeLat(0), lng: nodeLng(1) });
mod.snapToGraph(g, { lat: nodeLat(4), lng: nodeLng(4) });
assert.equal(mod.gridBuildCount(), before + 1, "subsequent snaps REUSE the memoized grid");
const g2 = await loadMiniGraph(); // a second decode -> a fresh RoadGraph instance
mod.snapToGraph(g2, { lat: nodeLat(2), lng: nodeLng(2) });
assert.equal(mod.gridBuildCount(), before + 2, "a new graph instance gets its own grid");
// Structural: the memo is a WeakMap and the projection scales lng by cos.
const src = readFileSync(path.join(here, "..", "src", "snap.ts"), "utf8");
const code = src
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
assert.ok(code.includes("WeakMap"), "the per-graph memo is a WeakMap (no global leak)");
assert.ok(/Math\.cos\(/.test(code), "lng is cos(lat)-scaled — Pitfall 8 (never raw degrees)");
});