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:
+145
-26
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user