test(08-03): ALT containment cases — A9 island sweep + A10 search-scoped skip + A11 structural pin (RED)
- A9: two-component synthetic (3x3 grid + 4-node path), anchors HAND-PINNED inside component A only via landmarkTables; full 169-pair sweep asserts seconds-only Dijkstra equivalence (A-internal with ALT live, B-internal on the haversine fallback, 72 cross pairs -> null) - A10: origin AND goal inside the anchorless component — all families drop at SEARCH scope, the 16 B-internal pairs still route Dijkstra-equal - A11: comment-stripped astar.ts must reference landmarkTo/landmarkFrom/ landmarkQuantumS in CODE; one sentinel spelling (0xffff, never 65535); A8's no-Map law unchanged — FAILS now (RED: the potential is still pure haversine) - case-index doc-comment extended A1-A8 -> A1-A11; buildIslandFixture helper routes fixture tables through the ONE landmark module
This commit is contained in:
+164
-2
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* astar.test.mjs — typed-array A* + turn classifier + step derivation (A1–A8).
|
||||
* 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).
|
||||
@@ -32,6 +32,22 @@
|
||||
* 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)
|
||||
*/
|
||||
@@ -90,7 +106,68 @@ function dijkstra(g, origin, goal) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ————— A1–A8 ————— */
|
||||
/* ————— 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();
|
||||
@@ -290,3 +367,88 @@ test("A8 structural — no `new Map(` in astar.ts (typed arrays only in the hot
|
||||
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||||
assert.ok(!code.includes("new Map("), "no Map allocation — the hot loop is typed arrays");
|
||||
});
|
||||
|
||||
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", "landmarkFrom", "landmarkQuantumS"]) {
|
||||
assert.ok(code.includes(token), `comment-stripped astar.ts must reference ${token} in code`);
|
||||
}
|
||||
// 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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user