Files
arah2/routing/test/ch-query.test.mjs
T
araharah-executor d3f0b4577f test(08.1-01): re-pin codec + search suites for ARG3 and the CH query (RED)
- graph.test.mjs: G1-G11 re-pinned for the ARG3 layout (chShortcutCount
  @60, CH suffix); G7 suspended until task 3 regenerates the fixture
- astar.test.mjs: committed-fixture cases A1/A2/A4/A5/A6 suspended;
  A3 re-pinned on an in-memory ch block; A8 extended to ch-query.ts;
  A11 becomes the D-04 one-search-path pin; ALT-era A9/A10/A12/A13 fold
- ch-query.test.mjs: Q1-Q5 (625-pair equivalence, directed oneway law,
  original-edge seconds, island nulls, structural deletion) + G1-G3
  in-memory ARG3 pins; all fixtures through contractGraph + encodeGraph
2026-10-26 06:48:24 +08:00

518 lines
21 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
/*
* ch-query.test.mjs — the uphill bidirectional CH query gates (Q1–Q5)
* plus the in-memory ARG3 codec pins (G1–G3).
* ----------------------------------------------------------------------------
* RED first (plan 08.1-01 task 2): every case fails while
* routing/src/ch-query.ts does not exist (lazy-import pattern).
*
* This suite is where the deleted ALT cases (astar.test.mjs A9/A10/A12/A13)
* FOLDED per D-04/D-05: their surviving content — Dijkstra equivalence over
* directed fixtures, the unreachable miss, the oneway law — is re-pinned
* against the CH query; the ALT table assertions are gone with the tables.
* ALL fixtures here are IN-MEMORY (encodeGraph → decodeGraph through the
* ONE authority); the committed mini-graph.bin is still ARG2 in this task
* and is NOT read (task 3 regenerates it and re-pins the committed-fixture
* cases in graph/astar.test.mjs).
*
* Q1 exhaustive equivalence — the CH query equals brute-force Dijkstra
* for EVERY pair of the 25-node in-memory
* grid; path endpoints are origin/goal
* Q2 directed oneway law — ring + chord + one-way-in spur: every pair
* matches the oracle in BOTH directions and
* every returned hop is a traversable
* original edge (bit0 never relaxed)
* Q3 original-edge seconds — reported seconds equal a fresh sum over
* the returned ORIGINAL-edge path (shortcut
* weights steer only — Pitfall 3)
* Q4 unreachable -> null — the island fold (A9/A10): cross-component
* pairs resolve null; internal pairs stay
* Dijkstra-equal
* Q5 structural deletion — no ALT tokens in astar.ts; landmark.ts and
* landmark.test.mjs ABSENT; index.ts exports
* no landmark symbols (D-04/D-05)
* G1 ARG3 round-trip — contractGraph output round-trips rank +
* shortcut CSR exactly through zero-copy views
* G2 rejection vectors — ARG1 bytes -> null; a version-2 tamper -> null
* G3 fail-honest decode — truncated / hostile-count / chShortcutCount=0
* buffers -> null BEFORE any typed-array
* allocation (T-08.1-01/T-08.1-02)
*
* Run: npm run routing:test (exit 0 = all green)
*/
import { strict as assert } from "node:assert";
import { readFileSync, existsSync } 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 chQueryPromise = null;
function loadChQuery() {
if (!chQueryPromise) chQueryPromise = import("../src/ch-query.ts");
return chQueryPromise;
}
let graphPromise = null;
function loadGraph() {
if (!graphPromise) graphPromise = import("../src/graph.ts");
return graphPromise;
}
let contractionPromise = null;
function loadContraction() {
if (!contractionPromise) contractionPromise = import("../src/contraction.ts");
return contractionPromise;
}
function toArrayBuffer(buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
let geoPromise = null;
async function haversineM(a, b) {
if (!geoPromise) geoPromise = import("../src/geo.ts");
const { haversineM: h } = await geoPromise;
return h(a, b);
}
/** Brute-force O(n^2) Dijkstra — plain arrays, SAME traversal law. */
function dijkstra(g, origin, goal) {
const dist = new Array(g.nodeCount).fill(Infinity);
const done = new Array(g.nodeCount).fill(false);
dist[origin] = 0;
for (;;) {
let u = -1;
let best = Infinity;
for (let i = 0; i < g.nodeCount; i++) {
if (!done[i] && dist[i] < best) {
best = dist[i];
u = i;
}
}
if (u === -1 || u === goal) return dist[goal];
done[u] = true;
for (let e = g.edgeStart[u]; e < g.edgeStart[u + 1]; e++) {
if ((g.edgeFlags[e] & 1) !== 0) continue; // oneway-reverse: never relaxed
const v = g.edgeTarget[e];
const nd = dist[u] + g.edgeSeconds[e];
if (nd < dist[v]) dist[v] = nd;
}
}
}
/** The traversable (bit0 clear) original edge u -> v, or -1. */
function findTraversableEdge(g, u, v) {
for (let e = g.edgeStart[u]; e < g.edgeStart[u + 1]; e++) {
if (g.edgeTarget[e] === v && (g.edgeFlags[e] & 1) === 0) return e;
}
return -1;
}
/** Fresh sum over the returned ORIGINAL-edge path (the Pitfall 3 oracle). */
function resumSeconds(g, nodes) {
let seconds = 0;
for (let i = 1; i < nodes.length; i++) {
const e = findTraversableEdge(g, nodes[i - 1], nodes[i]);
if (e < 0) return -1; // a non-traversable hop: the caller asserts loud
seconds += g.edgeSeconds[e];
}
return seconds;
}
/* ————— in-memory fixtures (encodeGraph → decodeGraph, the ONE authority) ————— */
/**
* The 25-node grid, the committed fixture's shape rebuilt in memory:
* 5x5, 80 directed edge entries, one oneway pair (2->3), perimeter 60
* km/h + interior 30 km/h, three street names. Contracted through the
* ONE CH core, encoded through the ONE codec.
*/
async function buildGridGraph() {
const { encodeGraph, decodeGraph } = await loadGraph();
const { contractGraph } = await loadContraction();
const lat = [];
const lng = [];
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 5; c++) {
lat.push((3150000 + r * 500) / 1e6);
lng.push((101690000 + c * 500) / 1e6);
}
}
const id = (r, c) => r * 5 + c;
const edges = [];
const addEdge = async (u, v, perimeter) => {
const kmh = perimeter ? 60 : 30;
const meters = await haversineM({ lat: lat[u], lng: lng[u] }, { lat: lat[v], lng: lng[v] });
edges.push({ from: u, to: v, seconds: meters / (kmh / 3.6) });
};
for (let r = 0; r < 5; r++) {
for (let c = 0; c < 4; c++) {
await addEdge(id(r, c), id(r, c + 1), r === 0 || r === 4);
await addEdge(id(r, c + 1), id(r, c), r === 0 || r === 4);
}
}
for (let c = 0; c < 5; c++) {
for (let r = 0; r < 4; r++) {
await addEdge(id(r, c), id(r + 1, c), c === 0 || c === 4);
await addEdge(id(r + 1, c), id(r, c), c === 0 || c === 4);
}
}
const reverse = edges.find((e) => e.from === 3 && e.to === 2);
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 ch = contractGraph({ nodeCount: 25, edges });
const g = decodeGraph(
encodeGraph({
lat,
lng,
edges,
names: ["Jalan Mini 1", "Jalan Mini 2", "Jalan Persiaran Mini"],
maxSpeedKmh: 60,
ch,
}),
);
assert.ok(g, "the in-memory ARG3 grid decodes");
assert.ok(g.chShortcutCount > 0, "the contracted grid carries a hierarchy");
return g;
}
/**
* The directed oneway fixture (the A12 fold): a directed 8-node ring
* (10 s one-way hops; every reverse mirror stored with bit0 — never
* relaxed), a bidirectional chord (0<->4, 15 s), and a one-way-in spur
* head (node 8 -> 3: nothing reaches 8). Directed reach asymmetry is
* exactly what broke the ALT four-family potential; under CH the
* shortcuts inherit direction structurally.
*/
async function buildRingGraph() {
const { encodeGraph, decodeGraph } = await loadGraph();
const { contractGraph } = await loadContraction();
const lat = [];
const lng = [];
for (let i = 0; i < 8; i++) {
lat.push((3100000 + Math.round(400 * Math.sin((i / 8) * 2 * Math.PI))) / 1e6);
lng.push((101700000 + Math.round(400 * Math.cos((i / 8) * 2 * Math.PI))) / 1e6);
}
lat.push(3095000 / 1e6); // node 8: the spur head
lng.push(101705000 / 1e6);
const edges = [];
for (let i = 0; i < 8; i++) {
const next = (i + 1) % 8;
edges.push({ from: i, to: next, seconds: 10, flags: 0 }); // the one-way hop
edges.push({ from: next, to: i, seconds: 10, flags: 1 }); // bit0: stored, never relaxed
}
edges.push({ from: 0, to: 4, seconds: 15, flags: 0 }); // bidirectional chord
edges.push({ from: 4, to: 0, seconds: 15, flags: 0 });
edges.push({ from: 8, to: 3, seconds: 10, flags: 0 }); // one-way-in spur
const ch = contractGraph({ nodeCount: 9, edges });
const g = decodeGraph(
encodeGraph({
lat,
lng,
edges,
names: ["Ring Road"],
maxSpeedKmh: 60,
ch,
}),
);
assert.ok(g, "the oneway-ring synthetic decodes");
assert.equal(g.nodeCount, 9);
return g;
}
/**
* The island fixture (the A9/A10 fold): component A is a 3x3 grid
* (nodes 0-8), component B a separate 4-node path (nodes 9-12) far
* apart. Cross-component pairs are unreachable — the honest miss.
*/
async function buildIslandGraph() {
const { encodeGraph, decodeGraph } = await loadGraph();
const { contractGraph } = await loadContraction();
const lat = [];
const lng = [];
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
lat.push((3150000 + r * 500) / 1e6);
lng.push((101690000 + c * 500) / 1e6);
}
}
for (let i = 0; i < 4; i++) {
lat.push((3200000 + i * 500) / 1e6);
lng.push((101750000 + i * 500) / 1e6);
}
const edges = [];
const link = (u, v) => {
edges.push({ from: u, to: v, seconds: 10, flags: 0 });
edges.push({ from: v, to: u, seconds: 10, flags: 0 });
};
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
if (c < 2) link(r * 3 + c, r * 3 + c + 1);
if (r < 2) link(r * 3 + c, (r + 1) * 3 + c);
}
}
for (let i = 0; i < 3; i++) link(9 + i, 10 + i);
const ch = contractGraph({ nodeCount: 13, edges });
const g = decodeGraph(
encodeGraph({
lat,
lng,
edges,
names: ["Island Road"],
maxSpeedKmh: 60,
ch,
}),
);
assert.ok(g, "the island synthetic decodes");
assert.equal(g.nodeCount, 13);
return g;
}
/* ————— Q1–Q5 ————— */
test("Q1 exhaustive equivalence — the CH query matches brute-force Dijkstra on EVERY 25x25 node pair", async () => {
const { routeCH } = await loadChQuery();
const g = await buildGridGraph();
let pairs = 0;
let reachable = 0;
for (let o = 0; o < g.nodeCount; o++) {
for (let t = 0; t < g.nodeCount; t++) {
pairs++;
const optimal = dijkstra(g, o, t);
const result = routeCH(g, o, t);
if (optimal === Infinity) {
assert.equal(result, null, `pair ${o}->${t}: unreachable must resolve null, never a path`);
continue;
}
reachable++;
assert.ok(result, `pair ${o}->${t}: reachable must route`);
assert.ok(
Math.abs(result.seconds - optimal) < 1e-6,
`pair ${o}->${t}: CH cost ${result.seconds} vs Dijkstra ${optimal}`,
);
assert.equal(result.nodes[0], o, `pair ${o}->${t}: path starts at origin`);
assert.equal(result.nodes[result.nodes.length - 1], t, `pair ${o}->${t}: path ends at goal`);
}
}
assert.equal(pairs, 625, "the full 25x25 sweep ran");
assert.ok(reachable > 600, `the grid is densely connected (${reachable} reachable pairs)`);
});
test("Q2 directed oneway law — ring + chord + spur matches the oracle in BOTH directions; every hop is a traversable original edge", async () => {
const { routeCH } = await loadChQuery();
const g = await buildRingGraph();
let pairs = 0;
let reachable = 0;
let crossNulls = 0;
for (let o = 0; o < g.nodeCount; o++) {
for (let t = 0; t < g.nodeCount; t++) {
pairs++;
const optimal = dijkstra(g, o, t);
const result = routeCH(g, o, t);
if (optimal === Infinity) {
assert.equal(result, null, `pair ${o}->${t}: unreachable must resolve null`);
crossNulls++;
continue;
}
reachable++;
assert.ok(result, `pair ${o}->${t}: reachable must route`);
assert.ok(
Math.abs(result.seconds - optimal) < 1e-6,
`pair ${o}->${t}: CH cost ${result.seconds} vs Dijkstra ${optimal}`,
);
// The unpacking law: every returned hop is a TRAVERSABLE original
// edge — no bit0 mirror and no unpacked shortcut survives.
for (let i = 1; i < result.nodes.length; i++) {
assert.ok(
findTraversableEdge(g, result.nodes[i - 1], result.nodes[i]) >= 0,
`pair ${o}->${t}: hop ${result.nodes[i - 1]}->${result.nodes[i]} is a traversable original edge`,
);
}
}
}
assert.equal(pairs, 81, "the full 9x9 sweep ran");
assert.ok(reachable > 40, `the ring is reachable (${reachable} pairs)`);
assert.ok(crossNulls >= 8, `the spur head is one-way-in (${crossNulls} pairs can never reach it)`);
});
test("Q3 original-edge seconds — reported seconds equal a fresh sum over the unpacked ORIGINAL-edge path", async () => {
const { routeCH } = await loadChQuery();
const g = await buildGridGraph();
// A long cross-grid pair (the route rides shortcuts) and the oneway
// detour pair: both must re-sum exactly over original edges.
for (const [o, t] of [[0, 24], [3, 2], [6, 19], [12, 12]]) {
const result = routeCH(g, o, t);
if (o === t) {
assert.deepEqual(result.nodes, [o], "the trivial pair is the single node");
assert.equal(result.seconds, 0, "the trivial pair costs 0");
continue;
}
assert.ok(result, `${o}->${t} routes`);
const fresh = resumSeconds(g, result.nodes);
assert.ok(fresh >= 0, `${o}->${t}: every returned hop is a traversable original edge`);
assert.ok(
Math.abs(result.seconds - fresh) < 1e-6,
`${o}->${t}: reported ${result.seconds} === original-edge re-sum ${fresh} (shortcut weights never enter seconds)`,
);
const optimal = dijkstra(g, o, t);
assert.ok(Math.abs(result.seconds - optimal) < 1e-6, `${o}->${t}: and it is the Dijkstra optimum`);
// distanceM is the haversine sum over the SAME unpacked path.
let meters = 0;
for (let i = 1; i < result.nodes.length; i++) {
const a = { lat: g.lat[result.nodes[i - 1]], lng: g.lng[result.nodes[i - 1]] };
const b = { lat: g.lat[result.nodes[i]], lng: g.lng[result.nodes[i]] };
meters += await haversineM(a, b);
}
assert.ok(
Math.abs(result.distanceM - meters) < 1e-6,
`${o}->${t}: distanceM is the haversine sum over the unpacked path`,
);
}
});
test("Q4 unreachable -> null — the island fold: cross pairs resolve null, internal pairs stay Dijkstra-equal", async () => {
const { routeCH } = await loadChQuery();
const g = await buildIslandGraph();
let pairs = 0;
let crossNulls = 0;
for (let o = 0; o < g.nodeCount; o++) {
for (let t = 0; t < g.nodeCount; t++) {
pairs++;
const optimal = dijkstra(g, o, t);
const result = routeCH(g, o, t);
const inA = o < 9 && t < 9;
const inB = o >= 9 && t >= 9;
if (!inA && !inB) {
assert.equal(optimal, Infinity, `cross pair ${o}->${t}: Dijkstra agrees unreachable`);
assert.equal(result, null, `cross pair ${o}->${t}: resolves null, never a fabricated path`);
crossNulls++;
continue;
}
assert.ok(result, `pair ${o}->${t} (${inA ? "A" : "B"}-internal) routes`);
assert.ok(
Math.abs(result.seconds - optimal) < 1e-6,
`pair ${o}->${t}: CH cost ${result.seconds} vs Dijkstra ${optimal}`,
);
}
}
assert.equal(pairs, 169, "the full 13x13 sweep ran");
assert.equal(crossNulls, 72, "every cross-component pair is a null");
});
test("Q5 structural deletion — no ALT tokens in astar.ts; landmark.ts + landmark.test.mjs ABSENT; index exports none (D-04/D-05)", () => {
const src = readFileSync(path.join(here, "..", "src", "astar.ts"), "utf8");
const code = src
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
for (const token of ["landmarkTo", "landmarkFrom", "cleanColumnsFor", "LANDMARK_SENTINEL"]) {
assert.ok(!code.includes(token), `comment-stripped astar.ts must not contain ${token}`);
}
assert.equal(existsSync(path.join(here, "..", "src", "landmark.ts")), false, "landmark.ts is deleted (D-05)");
assert.equal(
existsSync(path.join(here, "landmark.test.mjs")),
false,
"landmark.test.mjs is deleted (D-05)",
);
const indexSrc = readFileSync(path.join(here, "..", "src", "index.ts"), "utf8");
const indexCode = indexSrc
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
assert.ok(!indexCode.includes("landmark"), "index.ts exports no landmark symbols");
});
/* ————— G1–G3 — the in-memory ARG3 codec pins ————— */
test("G1 ARG3 round-trip — contractGraph output round-trips rank + shortcut CSR exactly through zero-copy views", async () => {
const { encodeGraph, decodeGraph } = await loadGraph();
const { contractGraph } = await loadContraction();
const g0 = await buildGridGraph(); // builds through encode/decode already
assert.ok(g0, "the grid decoded (G1's round-trip ran inside the builder)");
// Explicit round-trip on a fresh contraction output.
const { contractGraph: contract2 } = await loadContraction();
const edges = [
{ from: 0, to: 1, seconds: 1, flags: 0 },
{ from: 1, to: 0, seconds: 1, flags: 0 },
{ from: 0, to: 2, seconds: 1, flags: 0 },
{ from: 2, to: 0, seconds: 1, flags: 0 },
];
const ch = contract2({ nodeCount: 3, edges });
const { encodeGraph: enc, decodeGraph: dec } = await loadGraph();
const buf = enc({
lat: [3.15, 3.1505, 3.151],
lng: [101.69, 101.6905, 101.691],
edges,
names: [],
maxSpeedKmh: 60,
ch,
}).slice(0);
const g = dec(buf);
assert.ok(g, "decode succeeds");
assert.equal(g.chShortcutCount, ch.shortcutTarget.length, "the header count matches the contraction");
assert.deepEqual([...g.chRank], [...ch.rank], "rank restores exactly");
assert.deepEqual([...g.shortcutStart], [...ch.shortcutStart], "shortcutStart restores exactly");
assert.deepEqual([...g.shortcutTarget], [...ch.shortcutTarget], "shortcutTarget restores exactly");
assert.deepEqual([...g.shortcutSeconds], [...ch.shortcutSeconds], "shortcutSeconds restores exactly");
assert.deepEqual([...g.shortcutVia], [...ch.shortcutVia], "shortcutVia restores exactly");
assert.equal(g.chRank.buffer, buf, "chRank is a zero-copy view");
assert.equal(g.shortcutVia.buffer, buf, "shortcutVia is a zero-copy view");
});
test("G2 rejection vectors — ARG1 bytes -> null; a version-2 tamper -> null (D-07 one decode path)", async () => {
const { decodeGraph } = await loadGraph();
// The frozen Plan 08-01 vector (the ARG1 rejection fixture).
const arg1 = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph-arg1.bin")));
assert.equal(decodeGraph(arg1), null, "ARG1 bytes -> null, never a throw or a fallback decode");
// A version-2 tamper of an in-memory ARG3 buffer refuses too.
const { encodeGraph } = await loadGraph();
const { contractGraph } = await loadContraction();
const edges = [
{ from: 0, to: 1, seconds: 1, flags: 0 },
{ from: 1, to: 0, seconds: 1, flags: 0 },
{ from: 0, to: 2, seconds: 1, flags: 0 },
{ from: 2, to: 0, seconds: 1, flags: 0 },
];
const buf = encodeGraph({
lat: [3.15, 3.1505, 3.151],
lng: [101.69, 101.6905, 101.691],
edges,
names: [],
maxSpeedKmh: 60,
ch: contractGraph({ nodeCount: 3, edges }),
});
const v2 = buf.slice(0);
new DataView(v2).setUint32(4, 2, true);
assert.equal(decodeGraph(v2), null, "version 2 -> null (ARG2 bytes decode to nothing — D-07)");
});
test("G3 fail-honest decode — truncated / hostile-count / chShortcutCount=0 buffers -> null BEFORE any allocation", async () => {
const { decodeGraph, GRAPH_MAGIC, GRAPH_VERSION } = await loadGraph();
const { encodeGraph } = await loadGraph();
const { contractGraph } = await loadContraction();
const edges = [
{ from: 0, to: 1, seconds: 1, flags: 0 },
{ from: 1, to: 0, seconds: 1, flags: 0 },
{ from: 0, to: 2, seconds: 1, flags: 0 },
{ from: 2, to: 0, seconds: 1, flags: 0 },
];
const ch = contractGraph({ nodeCount: 3, edges });
const buf = encodeGraph({
lat: [3.15, 3.1505, 3.151],
lng: [101.69, 101.6905, 101.691],
edges,
names: [],
maxSpeedKmh: 60,
ch,
});
assert.equal(decodeGraph(buf.slice(0, buf.byteLength - 1)), null, "truncated -> null");
const hostile = new ArrayBuffer(72);
const dv = new DataView(hostile);
dv.setUint32(0, GRAPH_MAGIC, true);
dv.setUint32(4, GRAPH_VERSION, true);
dv.setUint32(8, 1_000_000, true); // a million nodes on a 72-byte buffer
assert.equal(decodeGraph(hostile), null, "hostile count -> null on arithmetic, no allocation");
const zeroed = buf.slice(0);
new DataView(zeroed).setUint32(60, 0, true);
assert.equal(decodeGraph(zeroed), null, "chShortcutCount = 0 -> null even when the size matches");
});