Files
arah2/routing/test/astar.test.mjs
T
araharah-executor 149636ce0a fix(08-05): D-03 optimality gate green — fromTable families removed from the ALT potential
Task 1: the D-03 gate script (measure-optimality-gate.mjs) reads the frozen
pre-ALT record, re-routes the same coordinate-pinned pairs on the current
ARG2 binary, and gates on seconds-equality (±0.1%) + verdict-class
preservation — never on node paths (equal-cost ties are documented behavior).

Rule-1 deviation the gate forced on its first run: routeAstar returned
SUBOPTIMAL real-graph routes (x038: 1234.6s vs the 1214.8s Dijkstra optimum;
x045: 1475.5s vs 1460.6s) — the fixture sweep could not see it. Root cause:
the fromTable bound d̂(ℓ,t) − d̂(ℓ,v) is one-directional Lipschitz (its u-side
term needs the REVERSE edge; a oneway does not provide one — the anchor
reached node 264611 126.8s faster than its own 1.49s upstream neighbour
264610), and the shared Ikeda average p = (h_f − h_b)/2 is consistent only
when every family inside h_f/h_b is two-sided. The toTable bound
d̂(v,ℓ) − d̂(t,ℓ) rides stored traversable edges in BOTH directions; it alone
joins the haversine now. Stop rule, reductions, meet probes, reconstruction:
untouched. landmarkTables/build verified EXACT (0 mismatches vs independent
integer Dijkstras, both rows, all 24 anchors).

- measure-optimality-gate.mjs: exported core + CLI guard (route-corpus
  pattern); epsilon mirror + argVersion asserts precede any measurement;
  per-pair rows, extract-drift WARNING (never a re-record), honest exit law
- astar.ts: potential() consults landmarkTo only; doc-comments carry the
  one-directional-Lipschitz law and the x038/x045 evidence
- astar.test.mjs: A11 re-pinned (landmarkFrom must NOT appear in the
  comment-stripped search — re-adding it trips the pin); A12 oneway-ring
  sweep added (directed reach asymmetry stays Dijkstra-equal on every pair)

Battery: routing 80/80, bot 126/126, both typechecks green.
D-03 gate: GATE PASS — 53/53 routed pairs equal within ±0.1% (max |Δ| 0.005%),
verdict classes preserved (6 unreachable + 41 unsnappable, 0 re-snapped).
2026-10-25 19:54:24 +08:00

537 lines
24 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
/*
* astar.test.mjs — typed-array A* + turn classifier + step derivation (A1–A11).
* -----------------------------------------------------------------------------
* RED first (plan 06-01 task 3): behavior cases fail while
* routing/src/astar.ts does not exist (lazy-import pattern).
*
* The correctness gate RESEARCH mandates: A* must equal brute-force
* Dijkstra's optimal cost for EVERY (origin, goal) pair of the mini-graph —
* the exhaustive 25x25 equivalence sweep (A1). The test-local Dijkstra uses
* plain arrays (fine in tests) and the SAME traversal law as routeAstar:
* oneway-reverse edges (edgeFlags bit0) are never relaxed.
*
* A1 exhaustive equivalence — 625 pairs, cost equality + path endpoints
* A2 oneway respected — the pair behind the flagged edge routes the
* long way (matching Dijkstra); the open
* direction is direct
* A3 unreachable -> null — honest miss (synthetic disconnected graph)
* 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: compass bearings increase
* clockwise, so negative delta = left
* (NOTE: the plan's "classifyTurn(0, 40) ->
* slight-left" contradicts its own
* (0,-90)->left / (0,90)->right cases; the
* compass-consistent reading is pinned here)
* A7 public surface — index.ts exposes the engine-A exports;
* createHttpRoutingSource is LIVE (re-pinned
* by 06-04, the plan that added it — the
* original pin named this plan as the owner)
* A8 structural — no `new Map(` in astar.ts (allocation-free
* hot loop: typed arrays only)
* A9 ALT island (08-03) — two-component synthetic, anchors HAND-PINNED
* inside component A only: pairs inside A route
* Dijkstra-equal with real ALT bounds live,
* pairs inside B route Dijkstra-equal on the
* haversine fallback (per-node sentinel skip),
* cross-component pairs resolve null
* A10 ALT search-scoped skip — origin AND goal inside the anchorless
* component: every l's goal/origin-side
* constant is sentinel, all families drop for
* the WHOLE search (constants dropped at
* search scope, never per-node l-masking),
* results still Dijkstra-equal
* A11 structural (ALT) — comment-stripped astar.ts consults the
* ARG2 landmark tables (landmarkTo/
* landmarkFrom/landmarkQuantumS in CODE, not
* comments); `new Map(` still absent
*
* Run: npm run routing:test (exit 0 = all green)
*/
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 astarPromise = null;
function loadAstar() {
if (!astarPromise) astarPromise = import("../src/astar.ts");
return astarPromise;
}
let graphPromise = null;
function loadGraph() {
if (!graphPromise) graphPromise = import("../src/graph.ts");
return graphPromise;
}
function toArrayBuffer(buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
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 A*. */
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;
}
}
}
/* ————— A9/A10 island fixture (plan 08-03) ————— */
/**
* Two-component synthetic: component A is a 3x3 grid (nodes 0-8), component
* B a separate 4-node path (nodes 9-12), far apart. `anchors` are HAND-PINNED
* by the caller (never derived — A9 pins them inside A only, so every
* component-B node is the 0xFFFF sentinel in BOTH table families). Tables
* come from the ONE landmark module (landmark.ts); encoding through the ONE
* graph authority (encodeGraph) — the same path the real build uses.
*/
async function buildIslandFixture(anchors) {
const { encodeGraph, decodeGraph } = await loadGraph();
const { LANDMARK_QUANTUM_S, landmarkTables } = await import("../src/landmark.ts");
const lat = [];
const lng = [];
// Component A: 3x3 grid near (3.15, 101.69); integer grid / 1e6 (the
// codec's exact-decimal law — no float accumulation).
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);
}
}
// Component B: a straight 4-node path near (3.20, 101.75) — a different
// metro entirely as far as the haversine fallback is concerned.
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) => {
// 10 s per hop — quantizes to exactly 100 quanta at the 0.1 s unit.
edges.push({ from: u, to: v, seconds: 10, flags: 0, nameId: 0 });
edges.push({ from: v, to: u, seconds: 10, flags: 0, nameId: 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 { to, from } = landmarkTables(
{ nodeCount: 13, edges, quantumS: LANDMARK_QUANTUM_S },
anchors,
);
const g = decodeGraph(
encodeGraph({
lat,
lng,
edges,
names: ["Island Road"],
maxSpeedKmh: 60,
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
}),
);
assert.ok(g, "the island synthetic decodes");
assert.equal(g.nodeCount, 13, "13 nodes: 9 in the anchored grid + 4 on the anchorless path");
return g;
}
/* ————— A1–A11 ————— */
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 — 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("A3 unreachable -> null — a disconnected pair resolves the honest miss", async () => {
const { routeAstar } = await loadAstar();
const { encodeGraph, decodeGraph } = await loadGraph();
// Two disconnected components: 0<->1 and 2<->3.
const g = decodeGraph(
encodeGraph({
lat: [3.15, 3.1505, 3.16, 3.1605],
lng: [101.69, 101.6905, 101.69, 101.6905],
edges: [
{ from: 0, to: 1, seconds: 4, nameId: 0 },
{ from: 1, to: 0, seconds: 4, nameId: 0 },
{ from: 2, to: 3, seconds: 4, nameId: 0 },
{ from: 3, to: 2, seconds: 4, nameId: 0 },
],
names: ["Isolated Road"],
maxSpeedKmh: 60,
// ARG2 (plan 08-02): every encode carries a landmark block — inert
// steering data until ALT lands (08-03); hand values keep this case
// focused on the unreachable-pair law. Sentinels mark component 2.
landmarks: {
quantumS: 0.5,
nodes: [0],
to: Uint16Array.of(0, 30, 0xffff, 0xffff),
from: Uint16Array.of(0, 30, 0xffff, 0xffff),
},
}),
);
assert.ok(g, "synthetic graph decodes");
assert.equal(dijkstra(g, 0, 2), Infinity, "Dijkstra agrees: unreachable");
assert.equal(routeAstar(g, 0, 2), null, "unreachable -> null (never a fabricated path)");
const within = routeAstar(g, 0, 1);
assert.ok(within, "the connected pair routes");
assert.deepEqual(within.nodes, [0, 1]);
});
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 — 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 — 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");
});
test("A7 public surface — index.ts exposes engine A + the engine-a HTTP source (re-pinned 06-04)", async () => {
const mod = await import("../src/index.ts");
for (const name of [
"decodeGraph",
"encodeGraph",
"GRAPH_MAGIC",
"GRAPH_VERSION",
"snapToGraph",
"SNAP_BBOX_MARGIN_DEG",
"routeAstar",
"deriveSteps",
"classifyTurn",
"TURN_STRAIGHT_MAX_DEG",
"TURN_SLIGHT_MAX_DEG",
"TURN_UTURN_MIN_DEG",
"createHttpRoutingSource",
]) {
assert.notEqual(mod[name], undefined, `index.ts must export ${name}`);
}
assert.equal(typeof mod.createHttpRoutingSource, "function", "backend #2's client seam is live (D-01, 06-04)");
});
test("A8 structural — no `new Map(` in astar.ts (typed arrays only in the hot loop)", () => {
const src = readFileSync(path.join(here, "..", "src", "astar.ts"), "utf8");
const code = src
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
assert.ok(!code.includes("new Map("), "no Map allocation — the hot loop is typed arrays");
});
/* ————— A12: oneway ring under ALT (plan 08-05) ————— */
/**
* A directed 8-node ring (10 s one-way hops; every reverse direction stored
* with bit0 — never relaxed) + a bidirectional chord (0<->4, 15 s) + a
* one-way-in spur (8->3: nothing reaches 8, so its fromTable cells are the
* 0xFFFF sentinel while its toTable cells are finite). This is the fixture
* shape the 08-05 consistency fix is ABOUT: directed reach asymmetry (the
* anchor reaches a node far faster than its upstream neighbour) is exactly
* what made the four-family potential inconsistent on the real graph — the
* D-03 gate caught it as x038/x045 suboptimality. Under the amended
* toTable-only potential EVERY pair must stay Dijkstra-equal.
*/
test("A12 oneway ring under ALT — directed reach asymmetry stays Dijkstra-equal on every pair", async () => {
const { encodeGraph, decodeGraph } = await loadGraph();
const { routeAstar } = await loadAstar();
const { LANDMARK_QUANTUM_S, landmarkTables } = await import("../src/landmark.ts");
const lat = [];
const lng = [];
// Ring + chord + spur laid out around (3.10, 101.70), integer / 1e6.
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, off-ring
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, nameId: 0 }); // the one-way hop
edges.push({ from: next, to: i, seconds: 10, flags: 1, nameId: 0 }); // bit0: stored, never relaxed
}
edges.push({ from: 0, to: 4, seconds: 15, flags: 0, nameId: 0 }); // bidirectional chord
edges.push({ from: 4, to: 0, seconds: 15, flags: 0, nameId: 0 });
edges.push({ from: 8, to: 3, seconds: 10, flags: 0, nameId: 0 }); // one-way-in spur
const anchors = new Uint32Array([0, 4]); // hand-pinned, in the ring's SCC
const { to, from } = landmarkTables(
{ nodeCount: 9, edges, quantumS: LANDMARK_QUANTUM_S },
anchors,
);
const g = decodeGraph(
encodeGraph({
lat,
lng,
edges,
names: ["Ring Road"],
maxSpeedKmh: 60,
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
}),
);
assert.ok(g, "the oneway-ring synthetic decodes");
assert.equal(g.nodeCount, 9, "9 nodes: ring + spur head");
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`);
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(pairs, 81, "the full 9x9 sweep ran");
assert.ok(reachable > 60, `the ring is densely reachable (${reachable} pairs)`);
});
test("A9 ALT island — anchors pinned in component A only; EVERY pair still Dijkstra-equal", async () => {
const { routeAstar } = await loadAstar();
const g = await buildIslandFixture(Uint32Array.of(0, 8));
const n = g.nodeCount;
// Fixture law (the island precondition): every component-B node is the
// 0xFFFF sentinel in BOTH families for every landmark — 0xFFFF is DATA.
for (let l = 0; l < g.landmarkCount; l++) {
for (let v = 9; v < n; v++) {
assert.equal(g.landmarkTo[l * n + v], 0xffff, `B node ${v} cannot reach anchor ${l}`);
assert.equal(g.landmarkFrom[l * n + v], 0xffff, `anchor ${l} cannot reach B node ${v}`);
}
}
// The sweep: A-internal pairs run with real ALT bounds live, B-internal
// pairs run on the haversine fallback (per-node sentinel skip), cross
// pairs resolve the honest miss. Seconds-only assertions — never
// node-path equality (D-03's tie law).
let pairs = 0;
let crossNulls = 0;
for (let o = 0; o < n; o++) {
for (let t = 0; t < n; t++) {
pairs++;
const optimal = dijkstra(g, o, t);
const result = routeAstar(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}: ALT-steered 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("A10 ALT search-scoped skip — origin AND goal in the anchorless component stays optimal", async () => {
const { routeAstar } = await loadAstar();
const g = await buildIslandFixture(Uint32Array.of(0, 8));
const n = g.nodeCount;
// With origin and goal inside B (9..12), every l's goal/origin-side
// constant is the 0xFFFF sentinel — so ALL families drop for the WHOLE
// search (the skip lives in the per-search constant tables, never a
// per-node l-mask mid-max) and the run is pure-haversine. The skip law:
// results stay exactly optimal regardless.
let pairs = 0;
for (let o = 9; o < n; o++) {
for (let t = 9; t < n; t++) {
pairs++;
const optimal = dijkstra(g, o, t);
const result = routeAstar(g, o, t);
assert.ok(result, `anchorless pair ${o}->${t} routes (pure-haversine search)`);
assert.ok(
Math.abs(result.seconds - optimal) < 1e-6,
`pair ${o}->${t}: cost ${result.seconds} vs Dijkstra ${optimal}`,
);
}
}
assert.equal(pairs, 16, "the 4x4 anchorless sweep ran");
});
test("A11 structural (ALT) — astar.ts consults the landmark tables in CODE, not comments", () => {
const src = readFileSync(path.join(here, "..", "src", "astar.ts"), "utf8");
const code = src
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
// The potential provably consults Plan 08-02's ARG2 table fields — a
// comment-only mention would strip away and fail this pin.
for (const token of ["landmarkTo", "landmarkQuantumS"]) {
assert.ok(code.includes(token), `comment-stripped astar.ts must reference ${token} in code`);
}
// 08-05 amendment pin: the fromTable families are EXCLUDED from the
// potential — their bounds are one-directional Lipschitz (the u-side term
// needs the reverse edge; a oneway does not have one), which broke the
// shared Ikeda average's consistency and returned suboptimal real-graph
// routes (the D-03 gate's x038/x045 catch). Re-adding landmarkFrom to the
// search trips this pin and re-opens that bug.
assert.ok(!code.includes("landmarkFrom"), "comment-stripped astar.ts must NOT consult landmarkFrom (08-05 consistency law)");
// Raw u16 sentinel handling on the table families — one spelling of the
// sentinel (0xffff in code, never the decimal 65535).
assert.ok(code.includes("0xffff"), "the sentinel is compared raw (u16, before float conversion)");
assert.ok(!code.includes("65535"), "one spelling of the sentinel: 0xffff");
// A8's law is unchanged under ALT: the hot loop stays typed arrays.
assert.ok(!code.includes("new Map("), "no Map allocation — the hot loop is typed arrays");
});