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
This commit is contained in:
araharah-executor
2026-10-26 06:48:24 +08:00
parent f75dc9e8c6
commit d3f0b4577f
3 changed files with 742 additions and 701 deletions
+517
View File
@@ -0,0 +1,517 @@
#!/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");
});