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
|
#!/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
|
* RED first (plan 06-01 task 3): behavior cases fail while
|
||||||
* routing/src/astar.ts does not exist (lazy-import pattern).
|
* routing/src/astar.ts does not exist (lazy-import pattern).
|
||||||
@@ -32,6 +32,22 @@
|
|||||||
* original pin named this plan as the owner)
|
* original pin named this plan as the owner)
|
||||||
* A8 structural — no `new Map(` in astar.ts (allocation-free
|
* A8 structural — no `new Map(` in astar.ts (allocation-free
|
||||||
* hot loop: typed arrays only)
|
* 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)
|
* 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 () => {
|
test("A1 exhaustive equivalence — A* matches brute-force Dijkstra on EVERY 25x25 node pair", async () => {
|
||||||
const { routeAstar } = await loadAstar();
|
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");
|
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||||||
assert.ok(!code.includes("new Map("), "no Map allocation — the hot loop is typed arrays");
|
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