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
This commit is contained in:
araharah-executor
2026-10-26 07:30:15 +08:00
parent 06624bb688
commit 4ca7a71d15
8 changed files with 524 additions and 132 deletions
+145 -26
View File
@@ -5,14 +5,14 @@
* RED first (plan 06-01 task 3): behavior cases fail while
* routing/src/astar.ts does not exist (lazy-import pattern).
*
* Phase 08.1 re-pin (plan 01 task 2): routeAstar is CH-backed — the ALT
* Phase 08.1 re-pin (plan 01 tasks 2-3): routeAstar is CH-backed — the ALT
* machinery is DELETED (D-04/D-05: one search path, no dormant engine).
* The exhaustive equivalence law (A1's 625-pair sweep) and the oneway /
* steps / classifier laws carry over UNCHANGED in intent; the
* committed-fixture cases (A1/A2/A4/A5/A6) are SUSPENDED here because
* mini-graph.bin is still ARG2 at this point in the plan — task 3
* regenerates the fixture through the ARG3 encode path and un-suspends
* them in place. The in-memory and structural cases are re-pinned HERE:
* committed-fixture cases (A1/A2/A4/A5/A6) were held out in task 2 while
* mini-graph.bin was still ARG2 and are restored here (task 3
* regenerated the fixture through the ARG3 encode path). The in-memory
* and structural cases were re-pinned in task 2:
*
* A3 unreachable -> null — honest miss (synthetic disconnected
* graph, ch block through the ONE authority)
@@ -24,13 +24,16 @@
* unpacking exist in ch-query.ts CODE;
* astar.ts contains NO landmark token
* (D-04's one-search-path pin)
* A1 exhaustive equivalence — SUSPENDED until task 3 (ARG3 fixture)
* A2 oneway respected — SUSPENDED until task 3 (ARG3 fixture)
* A4 deriveSteps L-path — SUSPENDED until task 3 (ARG3 fixture)
* A5 depart + arrive always — SUSPENDED until task 3 (ARG3 fixture)
* A6 classifyTurn pins — SUSPENDED until task 3 (ARG3 fixture)
* A1 exhaustive equivalence — 625 pairs, cost equality + path endpoints
* A2 oneway respected — the flagged edge is never traversed; the
* open direction is direct
* A4 deriveSteps L-path — 90-deg bearing changes classify left/right;
* steps carry name/distanceM/polyline6
* A5 depart + arrive always — straight paths still start "depart" and
* end "arrive"
* A6 classifyTurn pins — sign convention: negative delta = left
*
* The A9/A10/A12/A13 ALT-era cases (island / search-scoped skip / oneway
* The A9/A10/A12/A13 ALT-era cases (island / search-scoped drop / oneway
* ring / mixed columns) folded into ch-query.test.mjs: their surviving
* content is CH-law Dijkstra equivalence over directed fixtures — the
* ALT-specific table assertions are gone with the tables (D-05).
@@ -60,8 +63,13 @@ function toArrayBuffer(buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
// loadMiniGraph() returns in task 3, when the committed fixture is ARG3
// and the suspended cases un-suspend.
async function loadMiniGraph() {
const { decodeGraph } = await loadGraph();
const g = decodeGraph(toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph.bin"))));
assert.ok(g, "the committed mini-graph decodes");
return g;
}
/** Brute-force O(n^2) Dijkstra — plain arrays, SAME traversal law as CH. */
function dijkstra(g, origin, goal) {
@@ -192,25 +200,136 @@ test("A11 structural (CH law) — the uphill rank filter + unpacking live in ch-
/* ————— suspended committed-fixture cases (task 3 un-suspends) ————— */
test("A1 exhaustive equivalence — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// The committed fixture is still ARG2 here; the CH query's exhaustive
// 625-pair Dijkstra-equivalence law runs in ch-query.test.mjs (Q1) on
// an in-memory ARG3 grid until task 3 re-pins this case in place.
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
test("A1 exhaustive equivalence — A* matches brute-force Dijkstra on EVERY 25x25 node pair", async () => {
const { routeAstar } = await loadAstar();
const g = await loadMiniGraph();
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 = routeAstar(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}: A* 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.ok(result.distanceM > 0 || o === t, `pair ${o}->${t}: distanceM summed over the path`);
}
}
assert.equal(pairs, 625, "the full 25x25 sweep ran");
assert.ok(reachable > 600, `the grid is densely connected (${reachable} reachable pairs)`);
});
test("A2 oneway respected — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
test("A2 oneway respected — the flagged edge is never traversed; the open direction is direct", async () => {
const { routeAstar } = await loadAstar();
const g = await loadMiniGraph();
// The fixture's single oneway-reverse edge is 3 -> 2 (perimeter, row 0).
// Routing AGAINST it (3 -> 2) must take the long way around the block.
const against = routeAstar(g, 3, 2);
assert.ok(against, "3->2 routes via the detour");
assert.equal(
against.nodes.length > 2,
true,
`the flagged 1-edge path must NOT be used (path: ${against.nodes.join("->")})`,
);
assert.ok(
Math.abs(against.seconds - dijkstra(g, 3, 2)) < 1e-6,
"the detour cost matches Dijkstra",
);
// The open direction is the direct edge.
const with_flow = routeAstar(g, 2, 3);
assert.ok(with_flow, "2->3 routes");
assert.equal(with_flow.nodes.length, 2, "2->3 is the direct single edge");
assert.ok(Math.abs(with_flow.seconds - dijkstra(g, 2, 3)) < 1e-6);
});
test("A4 deriveSteps L-path — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
test("A4 deriveSteps L-path — 90-deg turns classify, steps carry name/distance/polyline6", async () => {
const { routeAstar, deriveSteps } = await loadAstar();
const { decodePolyline6 } = await import("../src/polyline.ts");
const g = await loadMiniGraph();
// 0=(r0,c0) -> 1=(r0,c1) -> 6=(r1,c1) -> 7=(r1,c2): east, then north
// (increasing row = increasing lat), then east.
const steps = deriveSteps(g, [0, 1, 6, 7]);
const kinds = steps.map((s) => s.kind);
assert.deepEqual(kinds, ["depart", "left", "right", "arrive"]);
assert.equal(steps[0].name, "Jalan Persiaran Mini", "step 0 rides the perimeter");
assert.equal(steps[1].name, "Jalan Mini 2", "the left turn turns onto Jalan Mini 2");
assert.equal(steps[2].name, "Jalan Mini 1", "the right turn turns onto Jalan Mini 1");
for (const step of steps) {
if (step.kind === "arrive") continue;
assert.ok(step.distanceM > 0, `${step.kind} step has positive distance`);
const points = decodePolyline6(step.geometry);
assert.equal(points.length, 2, `${step.kind} step geometry decodes to its two endpoints`);
}
const arrive = steps[steps.length - 1];
assert.equal(decodePolyline6(arrive.geometry).length, 1, "the arrive step anchors the goal node");
// round-trip: a full route's steps derive from A*'s own node path
const routed = routeAstar(g, 0, 11);
assert.ok(routed, "0->11 routes");
const routeSteps = deriveSteps(g, routed.nodes);
assert.equal(routeSteps[0].kind, "depart", "route-derived steps start with depart");
assert.equal(routeSteps[routeSteps.length - 1].kind, "arrive", "and end with arrive");
const total = routeSteps
.filter((s) => s.kind !== "arrive")
.reduce((sum, s) => sum + s.distanceM, 0);
assert.ok(
Math.abs(total - routed.distanceM) < 1e-6,
`step distances sum to the route distance (${total} vs ${routed.distanceM})`,
);
});
test("A5 depart + arrive always — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
test("A5 depart + arrive always — a straight same-name path still yields [depart, arrive]", async () => {
const { deriveSteps } = await loadAstar();
const g = await loadMiniGraph();
const steps = deriveSteps(g, [0, 1]);
assert.deepEqual(
steps.map((s) => s.kind),
["depart", "arrive"],
"even the simplest path starts depart and ends arrive",
);
assert.equal(steps[0].name, "Jalan Persiaran Mini");
assert.ok(steps[0].distanceM > 0);
assert.equal(deriveSteps(g, []).length, 0, "an empty path derives no steps");
assert.deepEqual(
deriveSteps(g, [12]).map((s) => s.kind),
["arrive"],
"a trivial single-node path is just the arrive step",
);
});
test("A6 classifyTurn pins — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
test("A6 classifyTurn pins — compass convention: negative bearing delta = left", async () => {
const { classifyTurn, TURN_STRAIGHT_MAX_DEG, TURN_SLIGHT_MAX_DEG, TURN_UTURN_MIN_DEG } = await loadAstar();
assert.equal(TURN_STRAIGHT_MAX_DEG, 25);
assert.equal(TURN_SLIGHT_MAX_DEG, 60);
assert.equal(TURN_UTURN_MIN_DEG, 150);
assert.equal(classifyTurn(0, -90), "left");
assert.equal(classifyTurn(0, 90), "right");
assert.equal(classifyTurn(0, 20), "straight");
assert.equal(classifyTurn(0, -20), "straight");
// The plan's "(0, 40) -> slight-left" contradicts its own (0,-90)->left /
// (0,90)->right cases; the compass-consistent reading is pinned:
assert.equal(classifyTurn(0, 40), "slight-right");
assert.equal(classifyTurn(0, -40), "slight-left");
assert.equal(classifyTurn(0, 175), "uturn");
assert.equal(classifyTurn(0, -175), "uturn");
assert.equal(classifyTurn(0, 180), "uturn");
// boundaries: 25 stays straight, 61 is a full turn, 150 is uturn
assert.equal(classifyTurn(0, 25), "straight");
assert.equal(classifyTurn(0, 26), "slight-right");
assert.equal(classifyTurn(0, 60), "slight-right");
assert.equal(classifyTurn(0, 61), "right");
assert.equal(classifyTurn(0, 149), "right");
assert.equal(classifyTurn(0, 150), "uturn");
// wrap-around normalization
assert.equal(classifyTurn(350, 10), "straight", "20 deg across north is straight");
assert.equal(classifyTurn(350, 80), "right", "90 deg across north is a right");
});
+101 -31
View File
@@ -3,24 +3,28 @@
* 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.
* mini-graph.bin — the routing correctness ground truth (plan 06-01) and
* the snap/search 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.
*
* PROVENANCE (Phase 8 plan 08-01): routing/test/fixtures/mini-graph-arg1.bin
* holds the frozen ARG1-era bytes preserved expressly as the v1
* decode-rejection vector consumed by the G-suite — it is NOT regenerated
* by this script, ever.
* PROVENANCE (Phase 08.1 plan 01): routing/test/fixtures/mini-graph-arg2.bin
* holds the frozen ARG2-era bytes preserved expressly as the ARG2
* decode-rejection vector consumed by the G-suite (D-12) — it is NOT
* regenerated by this script, ever.
*
* Usage: node routing/test/fixtures/build-mini-graph.mjs
* Exit: 0 on write; throws on any invariant violation (never writes).
*
* ARG2 (plan 08-02): the fixture carries a tiny real landmark stage —
* L=2 anchors via farthestPointAnchors + quantized c-metric tables via
* landmarkTables (the SAME pure module the real build script uses), passed
* through the ONE encodeGraph authority. Deterministic: identical output
* bytes on every re-run.
* ARG3 (Phase 08.1, D-12): the fixture carries a tiny real CH stage —
* contraction via contractGraph (the SAME pure module the real build
* script uses), passed through the ONE encodeGraph authority as the `ch`
* block. Deterministic: identical output bytes on every re-run.
*
* 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):
@@ -34,7 +38,7 @@
import { writeFileSync } from "node:fs";
import { encodeGraph } from "../../src/graph.ts";
import { haversineM } from "../../src/geo.ts";
import { LANDMARK_QUANTUM_S, farthestPointAnchors, landmarkTables, landmarkCoverage } from "../../src/landmark.ts";
import { contractGraph, CH_SHORTCUT_EXPLOSION_RATIO } from "../../src/contraction.ts";
const GRID = 5;
const LAT0 = 3.15;
@@ -92,27 +96,86 @@ reverse.flags = 1;
if (edges.length !== 80) throw new Error(`expected 80 directed edges, built ${edges.length}`);
// LANDMARK STAGE (plan 08-02): the fixture regenerates as ARG2 THROUGH the
// one encode authority — the same farthestPointAnchors + landmarkTables the
// real build script (08-04) will run, at a tiny L. Fail-loud invariants
// before any write (the `edges.length !== 80` style).
const LANDMARK_COUNT = 2; // tiny L: the 5x5 grid needs exactly 2 well-spread anchors
const landmarkInput = { nodeCount: GRID * GRID, edges, quantumS: LANDMARK_QUANTUM_S };
const anchors = farthestPointAnchors(landmarkInput, LANDMARK_COUNT);
const { to, from } = landmarkTables(landmarkInput, anchors);
if (anchors.length !== LANDMARK_COUNT) throw new Error(`expected ${LANDMARK_COUNT} anchors, got ${anchors.length}`);
if (to.length !== LANDMARK_COUNT * GRID * GRID || from.length !== LANDMARK_COUNT * GRID * GRID) {
throw new Error("landmark tables are not L*N row-major");
// CH STAGE (Phase 08.1): the fixture regenerates as ARG3 THROUGH the one
// encode authority — the same contractGraph the real build script (plan 02)
// runs. Fail-loud invariants before any write (the `edges.length !== 80`
// style), per RESEARCH §Pattern 4.
const ch = contractGraph({ nodeCount: GRID * GRID, edges });
const rankValues = [...ch.rank];
if (new Set(rankValues).size !== GRID * GRID) {
throw new Error(`rank is not a total order — ${new Set(rankValues).size} distinct for ${GRID * GRID} nodes`);
}
for (let l = 0; l < anchors.length; l++) {
const a = anchors[l];
if (to[l * GRID * GRID + a] !== 0 || from[l * GRID * GRID + a] !== 0) {
throw new Error(`anchor ${l} (node ${a}) own cells are not 0 — table law broken`);
if (rankValues.some((r) => !Number.isInteger(r) || r < 0 || r >= GRID * GRID)) {
throw new Error("a rank value is outside [0, nodeCount)");
}
// The shortcut band for this grid (measured 2026-10-26, this host: 41 —
// the literature's healthy ~1.4-2.0 per node; the tripwire sits at 4x).
const SHORTCUT_BAND_MIN = 20;
const SHORTCUT_BAND_MAX = 80;
if (ch.stats.shortcutCount < SHORTCUT_BAND_MIN || ch.stats.shortcutCount > SHORTCUT_BAND_MAX) {
throw new Error(
`shortcut count ${ch.stats.shortcutCount} outside the expected grid band ` +
`[${SHORTCUT_BAND_MIN}, ${SHORTCUT_BAND_MAX}] — check the contraction/witness budget`,
);
}
// Overlay-aware weight map: a cascading shortcut's constituents are the
// OVERLAY edges it replaced (originals OR earlier shortcuts — emission
// order is the contraction order, so replaying rank[via]-ascending
// rebuilds every weight exactly).
const weight = new Map(); // "a,b" -> min overlay weight
for (const e of edges) {
if ((e.flags & 1) !== 0) continue;
const key = `${e.from},${e.to}`;
const prev = weight.get(key);
if (prev === undefined || e.seconds < prev) weight.set(key, e.seconds);
}
const shortcutEntries = [];
for (let u = 0; u < GRID * GRID; u++) {
for (let s = ch.shortcutStart[u]; s < ch.shortcutStart[u + 1]; s++) {
shortcutEntries.push({ from: u, to: ch.shortcutTarget[s], seconds: ch.shortcutSeconds[s], via: ch.shortcutVia[s] });
}
}
const coverage = landmarkCoverage(to, anchors, GRID * GRID);
if (coverage.some((c) => c !== 1)) {
throw new Error(`the grid is strongly connected — coverage must be 1.0, got ${coverage.join(", ")}`);
shortcutEntries.sort((a, b) => ch.rank[a.via] - ch.rank[b.via] || a.from - b.from || a.to - b.to);
for (const sc of shortcutEntries) {
const via = sc.via;
if (!Number.isInteger(via) || via < 0 || via >= GRID * GRID) {
throw new Error(`shortcut ${sc.from}->${sc.to} has an out-of-range via ${via}`);
}
if (ch.rank[sc.from] <= ch.rank[via] || ch.rank[sc.to] <= ch.rank[via]) {
throw new Error(`shortcut ${sc.from}->${sc.to} (via ${via}) is not upward`);
}
const w1 = weight.get(`${sc.from},${via}`);
const w2 = weight.get(`${via},${sc.to}`);
if (w1 === undefined || w2 === undefined) {
throw new Error(`shortcut ${sc.from}->${sc.to} (via ${via}): a constituent overlay edge is missing`);
}
if (sc.seconds !== Math.fround(w1 + w2)) {
throw new Error(`shortcut ${sc.from}->${sc.to} weight ${sc.seconds} != f32(${w1} + ${w2})`);
}
const key = `${sc.from},${sc.to}`;
const prev = weight.get(key);
// The contraction's overlay accumulates f64 sums (the f32 rounding
// happens once, at CSR assembly) — replay the same f64 domain so the
// next cascade level compares exactly.
if (prev === undefined || w1 + w2 < prev) weight.set(key, w1 + w2);
}
// Strong connectivity (the grid is bidirectional except the ONE oneway
// mirror — every node reaches every node).
const adj = Array.from({ length: GRID * GRID }, () => []);
for (const e of edges) {
if ((e.flags & 1) !== 0) continue;
adj[e.from].push(e.to);
}
const seen = new Uint8Array(GRID * GRID);
const stack = [0];
seen[0] = 1;
while (stack.length > 0) {
const u = stack.pop();
for (const v of adj[u]) if (!seen[v]) { seen[v] = 1; stack.push(v); }
}
if (seen.some((s) => s === 0)) {
throw new Error("the grid is not strongly connected — fixture invariant broken");
}
const bin = encodeGraph({
@@ -121,11 +184,18 @@ const bin = encodeGraph({
edges,
names,
maxSpeedKmh: SPEED_PERIMETER_KMH,
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
ch: {
rank: ch.rank,
shortcutStart: ch.shortcutStart,
shortcutTarget: ch.shortcutTarget,
shortcutSeconds: ch.shortcutSeconds,
shortcutVia: ch.shortcutVia,
},
});
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, ` +
`L=${LANDMARK_COUNT} landmarks (nodes ${[...anchors].join(",")}), quantum ${LANDMARK_QUANTUM_S}s, ` +
`coverage ${coverage.map((c) => c.toFixed(2)).join("/")}, ${bin.byteLength} B`,
`CH shortcuts ${ch.stats.shortcutCount} (${ch.stats.ratioPerNode.toFixed(2)}/node, ` +
`witness searches ${ch.stats.witnessSearches}, budget hits ${ch.stats.witnessBudgetHits}), ` +
`${bin.byteLength} B`,
);
Binary file not shown.
Binary file not shown.
+78 -12
View File
@@ -31,6 +31,9 @@
* G3 bad version -> null — ARG1 fixture bytes -> null (D-01 one-path
* law); version 1, 2 and 4 tampers -> null
* (the accepted version is 3)
* G4 committed fixture — mini-graph.bin (ARG3) is 25 nodes / 80
* edges / rank 25 / >= 1 shortcut; the frozen
* mini-graph-arg2.bin decodes to null (D-12)
* G4 truncated/hostile -> null — byte-length ≠ header-declared size
* (incl. huge-count header on a tiny buffer,
* T-06-03b: size check BEFORE view allocation)
@@ -38,10 +41,12 @@
* flipping an input byte flips the decoded view
* G6 name pool — nameAt(0) === "Jalan Mini 1";
* nameAt(UNNAMED_NAME_ID) === undefined
* G7 committed fixture — SUSPENDED until task 3 regenerates
* mini-graph.bin through the ARG3 encode path
* (the committed bytes are still ARG2 here);
* task 3 re-pins this case in place
* G7 committed fixture — mini-graph.bin (ARG3, regenerated through
* the ONE encode authority) decodes: 25 nodes,
* 80 edges, 3 names, maxSpeed 60, exactly ONE
* oneway flag, a total-order rank, >= 1
* upward shortcut; the frozen ARG2 vector
* decodes to null
* G8 exported constants — GRAPH_MAGIC === 0x41524733, GRAPH_VERSION === 3
* G9 table round-trip — the CH fields restore exactly; the CH arrays
* are zero-copy views over the input buffer
@@ -164,9 +169,12 @@ test("G2 bad magic -> null — a tampered header never half-decodes (T-06-03)",
test("G3 bad version -> null — ARG1 fixture bytes refuse; version 1, 2 and 4 tampers to null (D-01 one-path law)", async () => {
const { encodeGraph, decodeGraph } = await loadGraph();
// The frozen Plan 08-01 vector: REAL ARG1 bytes decode to null — the
// The frozen rejection vectors: REAL ARG1 bytes decode to null — the
// Phase 6 router-pending state covers every pre-ARG3 binary (D-01).
// There is exactly one decode path; no v1/v2 fallback branch exists.
// (mini-graph-arg2.bin — the frozen ARG2-era bytes, D-12 — is pinned in
// the committed-fixture cases G4/G7; the version-2 tamper here covers
// the same law in memory.)
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");
const buf = encodeGraph(syntheticInput());
@@ -181,6 +189,26 @@ test("G3 bad version -> null — ARG1 fixture bytes refuse; version 1, 2 and 4 t
assert.equal(decodeGraph(v4), null, "version 4 -> null (the accepted version is 3)");
});
test("G4 committed ARG3 fixture — 25 nodes / 80 edges / rank 25 / >=1 shortcut; the preserved ARG2 vector decodes to null", async () => {
const { decodeGraph, GRAPH_MAGIC, GRAPH_VERSION } = await loadGraph();
// The committed ground truth, read as bytes: ARG3 header fields only.
const bin = readFileSync(path.join(here, "fixtures", "mini-graph.bin"));
assert.equal(bin.readUInt32LE(0), GRAPH_MAGIC, "the committed fixture is ARG3-magic");
assert.equal(bin.readUInt32LE(4), GRAPH_VERSION, "the committed fixture is version 3");
assert.equal(bin.readUInt32LE(8), 25, "25 nodes");
assert.equal(bin.readUInt32LE(12), 80, "80 directed edges");
assert.ok(bin.readUInt32LE(60) >= 1, "the committed hierarchy carries at least one shortcut");
// Decoding sees the rank table's length too.
const g = decodeGraph(toArrayBuffer(bin));
assert.ok(g, "the committed fixture decodes as ARG3");
assert.equal(g.chRank.length, 25, "one rank per node");
assert.equal(g.chShortcutCount, bin.readUInt32LE(60), "the header count agrees with the decoded graph");
// The preserved ARG2-era bytes (frozen before this fixture's
// regeneration — D-12) decode to null: one decode path, no fallback.
const arg2 = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph-arg2.bin")));
assert.equal(decodeGraph(arg2), null, "ARG2 bytes -> null (the D-07 rejection vector)");
});
test("G4 truncated + hostile-size buffer -> null — exact byte-size check BEFORE any view allocation (T-06-03b)", async () => {
const { encodeGraph, decodeGraph } = await loadGraph();
const buf = encodeGraph(syntheticInput());
@@ -227,13 +255,51 @@ test("G6 name pool — nameAt(0) resolves the first pool name; the unnamed senti
assert.equal(g.nameAt(999), undefined, "out-of-range id -> undefined");
});
test("G7 committed ARG3 fixture — SUSPENDED until task 3 regenerates mini-graph.bin through the ARG3 encode path", async () => {
// The committed mini-graph.bin is still ARG2 at this point in the plan
// (task 2 flips the codec; task 3 regenerates the fixture through the
// ONE encode authority and re-pins this case in place, together with
// the preserved mini-graph-arg2.bin rejection vector). Documented
// no-op — the ONLY suspended case in this suite.
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
test("G7 committed ARG3 fixture — decodes to 25 nodes / 80 edges / 3 names / a real hierarchy, maxSpeed 60, ONE oneway", async () => {
const { decodeGraph } = await loadGraph();
const bin = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph.bin")));
assert.ok(bin.byteLength > 1000, `the committed fixture is substantial (${bin.byteLength} B)`);
const g = decodeGraph(bin);
assert.ok(g, "the committed fixture decodes cleanly (ARG3 — regenerated through the ONE encode authority, task 3)");
assert.equal(g.nodeCount, 25, "5x5 grid");
assert.equal(g.edgeCount, 80, "20 H + 20 V segments, both directions");
assert.equal(g.nameCount, 3, "Jalan Mini 1 / Jalan Mini 2 / Jalan Persiaran Mini");
assert.equal(g.nameAt(0), "Jalan Mini 1");
assert.equal(g.nameAt(2), "Jalan Persiaran Mini");
assert.equal(g.maxSpeedKmh, 60, "the perimeter speed class is the header max");
// bbox of the hand-built grid (lat 3.1500–3.1520, lng 101.6900–101.6920)
assert.deepEqual(g.bbox, { minLat: 3.15, minLng: 101.69, maxLat: 3.152, maxLng: 101.692 });
let flagged = 0;
for (let e = 0; e < g.edgeCount; e++) if ((g.edgeFlags[e] & 1) !== 0) flagged++;
assert.equal(flagged, 1, "exactly ONE oneway-reverse edge in the whole fixture");
// both speed classes present: perimeter ~55.6 m / 16.67 ≈ 3.3 s; interior ≈ 6.7 s
let sawFast = false;
let sawSlow = false;
for (let e = 0; e < g.edgeCount; e++) {
if (g.edgeSeconds[e] < 5) sawFast = true;
if (g.edgeSeconds[e] > 5) sawSlow = true;
}
assert.ok(sawFast && sawSlow, "both speed classes (60 and 30 km/h) exist in the weights");
// ARG3 CH laws (Phase 08.1, D-12): the fixture regenerates THROUGH the
// encode authority with the tiny real contraction stage (S = 41 on the
// 5x5 grid — the healthy ~1.6/node band).
assert.equal(g.chRank.length, 25, "one rank per node");
assert.equal(g.shortcutStart.length, 26, "CSR row pointers: nodeCount + 1");
assert.equal(g.shortcutTarget.length, g.chShortcutCount, "shortcut CSR lengths agree with the header");
assert.equal(g.shortcutSeconds.length, g.chShortcutCount, "shortcut seconds length agrees");
assert.equal(g.shortcutVia.length, g.chShortcutCount, "shortcut via length agrees");
assert.ok(g.chShortcutCount >= 1, "the committed hierarchy carries at least one shortcut");
// Rank is a total order (Pitfall 9) and the shortcuts are upward.
assert.equal(new Set(g.chRank).size, 25, "ranks are DISTINCT");
for (let u = 0; u < g.nodeCount; u++) {
for (let s = g.shortcutStart[u]; s < g.shortcutStart[u + 1]; s++) {
const via = g.shortcutVia[s];
assert.ok(g.chRank[u] > g.chRank[via] && g.chRank[g.shortcutTarget[s]] > g.chRank[via], `shortcut ${u}->${g.shortcutTarget[s]} is upward against via ${via}`);
}
}
// The preserved ARG2-era bytes decode to null (D-12's rejection vector).
const arg2 = toArrayBuffer(readFileSync(path.join(here, "fixtures", "mini-graph-arg2.bin")));
assert.equal(decodeGraph(arg2), null, "ARG2 bytes -> null (the D-07 rejection vector)");
});
test("G8 exported constants — GRAPH_MAGIC === 0x41524733 ('ARG3'), GRAPH_VERSION === 3", async () => {
+115 -31
View File
@@ -25,15 +25,11 @@
* (WeakMap; build-counter, kept loose) +
* structural: WeakMap and cos() in the source
*
* Phase 08.1 re-pin: S1/S2/S3/S4/S5/S6 read the committed mini-graph.bin (S3's
* perimeter half) — they are
* which is still ARG2 mid-plan — they are SUSPENDED until task 3
* regenerates the fixture through the ARG3 encode path (the same
* suspension pattern as graph.test.mjs G7 and astar.test.mjs A1–A6).
* S3 was re-pinned NOW: its synthetic graph moved to the ARG3 ch block
* (a 3-node star — a 2-node graph can carry no valid shortcut).
*
* Run: npm run routing:test (exit 0 = all green)
* 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";
@@ -82,36 +78,124 @@ function nodeLng(c) {
/* ————— S1–S6 ————— */
test("S1 exact-node hit — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// The committed fixture is still ARG2 (decode refuses by the D-07 law); task 3 regenerates it through the ARG3 encode path and un-suspends this case in place (real body: git show f75dc9e:routing/test/snap.test.mjs).
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
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 — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// Suspended with S1; task 3 restores the real body.
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
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 — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// Both halves read ARG2-era data mid-plan: the perimeter half reads the
// committed fixture, and the unnamed half's re-pinned ch block rides the
// same restore. Task 3 un-suspends with the ARG3 fixture (the unnamed
// half's 3-node-star ch-block builder is already in this file's history:
// git show <task-2-green>:routing/test/snap.test.mjs).
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
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 — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// Suspended with S1; task 3 restores the real body.
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
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 — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// Suspended with S1; task 3 restores the real body.
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
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 — SUSPENDED until task 3 regenerates mini-graph.bin as ARG3", async () => {
// Suspended with S1 (the memo law re-pins against the ARG3 fixture in task 3).
assert.ok(true, "suspended: deferred to task 3 (fixture regeneration)");
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)");
});