From 92b97fc97fa180acd929f38bc983eb4377b7680b Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sun, 25 Oct 2026 19:00:56 +0800 Subject: [PATCH] =?UTF-8?q?test(08-03):=20ALT=20containment=20cases=20?= =?UTF-8?q?=E2=80=94=20A9=20island=20sweep=20+=20A10=20search-scoped=20ski?= =?UTF-8?q?p=20+=20A11=20structural=20pin=20(RED)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- routing/test/astar.test.mjs | 166 +++++++++++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) diff --git a/routing/test/astar.test.mjs b/routing/test/astar.test.mjs index f00923f..b5b6c14 100644 --- a/routing/test/astar.test.mjs +++ b/routing/test/astar.test.mjs @@ -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"); +});