Files
arah2/routing/test/landmark.test.mjs
T
araharah-executor e77bbe9f72 fix(08): WR-04 — quantized weight validated at the i32 boundary before the Int32Array store
- new named constant LANDMARK_I32_WEIGHT_CEILING (0x7fffffff) in
  landmark.ts: a seconds/quantumS quotient at or past 2^31 would wrap
  NEGATIVE on the Int32Array store, silently bypassing the
  LANDMARK_MAX_QUANTA throw in dijkstraPass (a negative contribution
  never exceeds it) and — at a -1 wrap — becoming the 0xFFFF sentinel;
  prepare() now fails loud at the table-build entry, naming the edge,
  the ceiling and the fix (raise landmarkQuantumS)
- 'nothing is clamped, ever' now holds at the storage boundary;
  validation is per-edge at build entry — the search hot path stays
  allocation-free and validation-free
- L5b regression vector: 3e10 s edge (3e11 quanta) throws in both
  landmarkTables and farthestPointAnchors; just-under-ceiling
  (2147483646) still fails via the documented 65000 law; ordinary
  weights untouched
2026-10-25 22:35:47 +08:00

270 lines
12 KiB
JavaScript
Raw 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
/*
* landmark.test.mjs — the pure landmark compute core gates (L1–L6).
* ------------------------------------------------------------------
* RED first (plan 08-02 task 1): every case fails while
* routing/src/landmark.ts does not exist (lazy-import pattern — each
* case reports its own ERR_MODULE_NOT_FOUND).
*
* The ALT preprocessing core (research Pattern 2): distances live in the
* ĉ metric — edge weights floor-quantized ONCE to fixed 0.1 s units —
* and every table cell is an EXACT integer Dijkstra result. Hand-computed
* expectations below; JS arithmetic is deterministic so the pinned quanta
* (verified: floor(2.55/0.1)=25, floor(3/0.1)=30, floor(1.5/0.1)=15,
* grid perimeter hop=33, interior hop=66) are stable.
*
* L1 anchors — farthestPointAnchors: deterministic, degree>=2
* filtered, well-spread on the 5-node path
* L2 quantum exactness — 2.55 s edge -> 25 quanta; two edges -> 50
* exactly (integer arithmetic, no float drift)
* L3 sentinel + self — unreachable pairs -> 0xFFFF; the landmark's
* own cell = 0 (oneway chain + isolated node)
* L4 asymmetry — to/from rows differ on a oneway graph
* L5 overflow throws — any label > 65000 quanta throws, naming the
* distance and the fix (raise landmarkQuantumS)
* L6 5x5 grid shape — anchors [0, 18] (interior node 18 is the ĉ
* farthest at 297 quanta); coverage 1.0
*
* Run: npm run routing:test (exit 0 = all green)
*/
import { strict as assert } from "node:assert";
import { test } from "./harness.mjs";
let landmarkPromise = null;
function loadLandmark() {
if (!landmarkPromise) landmarkPromise = import("../src/landmark.ts");
return landmarkPromise;
}
/* ————— hand-built vectors ————— */
/** 5-node path 0-1-2-3-4, two-way, 3.0 s per edge (30 quanta @ 0.1). */
function pathInput() {
const edges = [];
for (let u = 0; u < 4; u++) {
edges.push({ from: u, to: u + 1, seconds: 3, flags: 0 });
edges.push({ from: u + 1, to: u, seconds: 3, flags: 0 });
}
return { nodeCount: 5, edges, quantumS: 0.1 };
}
/** Oneway chain 0->1->2 (bit0 mirrors) + isolated node 3, 1.5 s/edge. */
function onewayInput() {
return {
nodeCount: 4,
edges: [
{ from: 0, to: 1, seconds: 1.5, flags: 0 },
{ from: 1, to: 0, seconds: 1.5, flags: 1 }, // blocked mirror
{ from: 1, to: 2, seconds: 1.5, flags: 0 },
{ from: 2, to: 1, seconds: 1.5, flags: 1 }, // blocked mirror
],
quantumS: 0.1,
};
}
/** The fixture builder's exact 5x5 grid shape (25 nodes, 80 edge entries,
* perimeter 60 km/h / interior 30 km/h, one oneway mirror at 3->2). */
async function gridInput() {
const { haversineM } = await import("../src/geo.ts");
const GRID = 5;
const lat = [];
const lng = [];
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID; c++) {
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, perimeter) {
const kmh = perimeter ? 60 : 30;
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), flags: 0 });
}
for (let r = 0; r < GRID; r++) {
for (let c = 0; c < GRID - 1; c++) {
const perimeter = r === 0 || r === GRID - 1;
addEdge(id(r, c), id(r, c + 1), perimeter);
addEdge(id(r, c + 1), id(r, c), perimeter);
}
}
for (let c = 0; c < GRID; c++) {
for (let r = 0; r < GRID - 1; r++) {
const perimeter = c === 0 || c === GRID - 1;
addEdge(id(r, c), id(r + 1, c), perimeter);
addEdge(id(r + 1, c), id(r, c), perimeter);
}
}
const reverse = edges.find((e) => e.from === 3 && e.to === 2);
reverse.flags = 1; // the fixture's single oneway-reverse mirror
return { nodeCount: GRID * GRID, edges, quantumS: 0.1 };
}
/* ————— L1–L6 ————— */
test("L1 anchors — deterministic, degree>=2-filtered, well-spread on the 5-node path", async () => {
const { farthestPointAnchors } = await loadLandmark();
const input = pathInput();
const a = farthestPointAnchors(input, 2);
assert.ok(a instanceof Uint32Array, "anchors are a Uint32Array");
assert.equal(a.length, 2);
// Candidates (degree>=2) are nodes 1,2,3 — endpoints excluded. First
// anchor = first candidate by id; pass-from-1 minima are [30,0,30,60,90]
// quanta, so the farthest candidate is node 3.
assert.deepEqual([...a], [1, 3], "first candidate by id, then farthest spread");
// L=3: after folding anchor 3, minima are [30,0,30,0,30] -> node 2.
assert.deepEqual([...farthestPointAnchors(input, 3)], [1, 3, 2]);
// Determinism: identical input -> identical anchors (no randomness).
assert.deepEqual([...farthestPointAnchors(pathInput(), 2)], [...a]);
// More anchors than candidates: stop early, never pad.
assert.equal(farthestPointAnchors(input, 10).length, 3, "only 3 candidates exist");
});
test("L2 quantum exactness — floor(2.55/0.1)=25 per edge; two edges sum to exactly 50 quanta", async () => {
const { landmarkTables, LANDMARK_QUANTUM_S } = await loadLandmark();
assert.equal(LANDMARK_QUANTUM_S, 0.1, "the default quantum is the doc'd 0.1 s");
const input = {
nodeCount: 3,
edges: [
{ from: 0, to: 1, seconds: 2.55, flags: 0 },
{ from: 1, to: 0, seconds: 2.55, flags: 0 },
{ from: 1, to: 2, seconds: 2.55, flags: 0 },
{ from: 2, to: 1, seconds: 2.55, flags: 0 },
],
quantumS: 0.1,
};
const { to, from } = landmarkTables(input, Uint32Array.of(0));
assert.equal(from.length, 1 * 3, "row-major L*N cells");
assert.equal(from[0 * 3 + 1], 25, "one 2.55 s edge = exactly 25 quanta (floor(25.4999...))");
assert.equal(from[0 * 3 + 2], 50, "two edges = exactly 50 quanta — integer arithmetic, no drift");
assert.equal(to[0 * 3 + 2], 50, "the to-row mirrors on a two-way path");
});
test("L3 sentinel + self-cell — unreachable pairs -> 0xFFFF; the landmark's own cell = 0", async () => {
const { landmarkTables, LANDMARK_SENTINEL } = await loadLandmark();
assert.equal(LANDMARK_SENTINEL, 0xffff);
const input = onewayInput();
const { to, from } = landmarkTables(input, Uint32Array.of(0));
assert.equal(to[0 * 4 + 0], 0, "landmark's own to-cell is 0");
assert.equal(from[0 * 4 + 0], 0, "landmark's own from-cell is 0");
assert.equal(from[0 * 4 + 1], 15, "0->1 traverses (floor(1.5/0.1)=15 quanta)");
assert.equal(from[0 * 4 + 2], 30, "0->1->2 traverses");
assert.equal(to[0 * 4 + 1], LANDMARK_SENTINEL, "1 cannot reach 0 against the oneway -> sentinel");
assert.equal(to[0 * 4 + 2], LANDMARK_SENTINEL, "2 cannot reach 0 -> sentinel");
// Isolated node: every cell is the sentinel.
assert.equal(to[0 * 4 + 3], LANDMARK_SENTINEL, "isolated node -> sentinel in the to-row");
assert.equal(from[0 * 4 + 3], LANDMARK_SENTINEL, "isolated node -> sentinel in the from-row");
});
test("L4 asymmetry — reversed vs forward rows differ on an oneway graph", async () => {
const { landmarkTables } = await loadLandmark();
const { to, from } = landmarkTables(onewayInput(), Uint32Array.of(1));
// Rows are indexed by anchor POSITION (row-major [ℓ*N+v], ℓ = 0 here),
// not by the anchor's node id. Anchor at the middle node 1: forward
// reaches 2 (15 quanta) but the to-row for 2 is the sentinel (2 cannot
// get back to 1).
assert.equal(from[0 * 4 + 2], 15, "1->2 traverses");
assert.equal(to[0 * 4 + 2], 0xffff, "2->1 does not (oneway) — asymmetry is representable");
assert.notEqual(to[0 * 4 + 2], from[0 * 4 + 2]);
});
test("L5 overflow throws — any label > 65000 quanta fails loud, naming the fix", async () => {
const { landmarkTables, farthestPointAnchors, LANDMARK_MAX_QUANTA } = await loadLandmark();
assert.equal(LANDMARK_MAX_QUANTA, 65000);
// 3-node path so node 1 is a degree-2 candidate — the anchor-selection
// pass actually runs (a 2-node graph has no candidates and returns
// empty before any Dijkstra).
const input = {
nodeCount: 3,
edges: [
{ from: 0, to: 1, seconds: 10, flags: 0 }, // floor(10/0.0001) = 100000 quanta
{ from: 1, to: 0, seconds: 10, flags: 0 },
{ from: 1, to: 2, seconds: 10, flags: 0 },
{ from: 2, to: 1, seconds: 10, flags: 0 },
],
quantumS: 0.0001,
};
assert.throws(
() => landmarkTables(input, Uint32Array.of(0)),
/100000 exceeds 65000 \(raise landmarkQuantumS\)/,
"the table pass names the offending distance and the fix",
);
assert.throws(
() => farthestPointAnchors(input, 1),
/exceeds 65000 \(raise landmarkQuantumS\)/,
"the anchor-selection pass fails loud on the same law",
);
});
test("L5b i32 wrap throws — an edge quantizing to >= 2^31 fails at prepare, before the Int32Array store", async () => {
const { landmarkTables, farthestPointAnchors } = await loadLandmark();
// seconds 3e10 at the 0.1 s quantum = 3e11 quanta — an absurd but FINITE
// positive weight. Stored unchecked it would wrap NEGATIVE in the
// Int32Array (-64771072 for 3e10), silently bypassing the LANDMARK_MAX_QUANTA
// throw (a negative contribution never exceeds it) and poisoning the
// tables. The wrap must be caught at the table-build entry instead.
const input = {
nodeCount: 3,
edges: [
{ from: 0, to: 1, seconds: 3e10, flags: 0 }, // floor(3e10/0.1) = 3e11 quanta >= 2^31
{ from: 1, to: 0, seconds: 10, flags: 0 },
{ from: 1, to: 2, seconds: 10, flags: 0 },
{ from: 2, to: 1, seconds: 10, flags: 0 },
],
quantumS: 0.1,
};
assert.throws(
() => landmarkTables(input, Uint32Array.of(0)),
/i32 weight ceiling 2147483647 \(raise landmarkQuantumS\)/,
"landmarkTables throws at the storage boundary, naming the edge, the ceiling and the fix",
);
assert.throws(
() => farthestPointAnchors(input, 1),
/i32 weight ceiling 2147483647/,
"the anchor-selection entry shares the same prepare-time law",
);
// The ceiling itself stays inside the legal domain: 2147483646 quanta on
// one edge does NOT throw here (the dijkstraPass 65000 ceiling is the law
// that fires later — prepare only rejects the i32 WRAP class).
const justUnder = {
nodeCount: 2,
edges: [
{ from: 0, to: 1, seconds: 214748364.6, flags: 0 }, // floor(/0.1) = 2147483646 < 2^31
{ from: 1, to: 0, seconds: 10, flags: 0 },
],
quantumS: 0.1,
};
assert.throws(
() => landmarkTables(justUnder, Uint32Array.of(0)),
/exceeds 65000/,
"just-under-ceiling still fails — via the documented 65000 law, never silently",
);
assert.doesNotThrow(
() => landmarkTables(
{ nodeCount: 2, edges: [{ from: 0, to: 1, seconds: 25.5, flags: 0 }, { from: 1, to: 0, seconds: 10, flags: 0 }], quantumS: 0.1 },
Uint32Array.of(0),
),
"ordinary weights are untouched by the new check",
);
});
test("L6 5x5 grid shape — anchors [0,18]; farthest 297 quanta; coverage 1.0", async () => {
const { farthestPointAnchors, landmarkTables, landmarkCoverage } = await loadLandmark();
const input = await gridInput();
const anchors = farthestPointAnchors(input, 2);
// Farthest-point over the ĉ metric: interior hops cost 66 quanta (2x the
// perimeter's 33), so the farthest node from corner 0 is the INTERIOR
// node 18 at 297 quanta — NOT the opposite corner 24 (264). Verified
// against an independent brute-force Dijkstra (297 unique max).
assert.deepEqual([...anchors], [0, 18], "first candidate is node 0; farthest is node 18");
const { to, from } = landmarkTables(input, anchors);
assert.equal(to.length, 2 * 25, "L*N cells (L=2, N=25)");
assert.equal(to[0 * 25 + 0], 0, "self-cell 0");
assert.equal(from[1 * 25 + 18], 0, "anchor 18's own from-cell 0 (row = position 1)");
assert.equal(from[0 * 25 + 18], 297, "0->18 = 3 perimeter + 3 interior hops = 99+198, exact");
assert.equal(to[1 * 25 + 0], 297, "the to-row mirrors on the grid");
const coverage = landmarkCoverage(to, anchors, 25);
assert.deepEqual(coverage, [1, 1], "the grid is strongly connected — full coverage");
});