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:
araharah-executor
2026-10-26 07:30:15 +08:00
parent 06624bb688
commit 4ca7a71d15
8 changed files with 524 additions and 132 deletions
+15 -12
View File
@@ -49,7 +49,7 @@ const { strings } = await import("../src/i18n/strings.ts");
const { encodePolyline6 } = await import("@araharah/routing/src/polyline.ts");
const { deriveCumulativeDistanceM, haversineM } = await import("@araharah/routing/src/geo.ts");
const { encodeGraph, decodeGraph } = await import("@araharah/routing/src/graph.ts");
const { LANDMARK_QUANTUM_S, landmarkTables } = await import("@araharah/routing/src/landmark.ts");
const { contractGraph } = await import("@araharah/routing/src/contraction.ts");
/* ————— geometry helpers (1e-6-grid-exact construction, the codec's grid law) ————— */
@@ -250,23 +250,26 @@ test("GC6b unnamed matched edge → cardStreetUnnamed ('UNNAMED ROAD') — still
// A purpose-built unnamed way through the same encodeGraph contract
// (names: [] → every edge carries the UNNAMED_NAME_ID sentinel).
const meters = haversineM({ lat: 3.15, lng: 101.69 }, { lat: 3.1505, lng: 101.69 });
const edges = [
// A 3-node star through the same encodeGraph contract (names: [] →
// every edge carries the UNNAMED_NAME_ID sentinel). ARG3 (Phase 08.1):
// the ch block comes from the ONE contraction core, and the MID owns
// the LOWEST id — the priority tie contracts lowest-id-first, so a mid
// with id 1 would starve to zero shortcuts and an ARG3 with no
// shortcuts is not a road graph. Geometry: north leg (0->1), east leg
// (0->2) — the turn at the shared mid vertex.
const starEdges = [
{ from: 0, to: 1, seconds: meters / (30 / 3.6) },
{ from: 1, to: 0, seconds: meters / (30 / 3.6) },
{ from: 0, to: 2, seconds: meters / (30 / 3.6) },
{ from: 2, to: 0, seconds: meters / (30 / 3.6) },
];
// A 2-node graph has no degree>=2 farthest-point candidate (the selection
// filter), so the anchor pins directly — the tables still come from the
// ONE integer-Dijkstra core (08-02), through the ONE encode authority.
const landmarkInput = { nodeCount: 2, edges, quantumS: LANDMARK_QUANTUM_S };
const anchors = Uint32Array.of(0);
const { to, from } = landmarkTables(landmarkInput, anchors);
const buf = encodeGraph({
lat: [3.15, 3.1505],
lng: [101.69, 101.69],
edges,
lat: [3.15, 3.1505, 3.15],
lng: [101.69, 101.69, 101.6905],
edges: starEdges,
names: [],
maxSpeedKmh: 30,
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
ch: contractGraph({ nodeCount: 3, edges: starEdges }),
});
const graph = decodeGraph(buf);
assert.notEqual(graph, null);
+70 -20
View File
@@ -472,31 +472,37 @@ test("GW11 PUT /api/guidance → 405", async () => {
const { encodeGraph } = await import("../../routing/src/graph.ts");
const { haversineM } = await import("../../routing/src/geo.ts");
const { LANDMARK_QUANTUM_S, farthestPointAnchors, landmarkTables } = await import(
"../../routing/src/landmark.ts"
);
const { contractGraph } = await import("../../routing/src/contraction.ts");
/** Tiny REAL landmark block through the ONE preprocessing core (08-02):
* farthest-point anchor + integer ĉ tables — computed, never faked.
* `{ pin }` pins the anchor for graphs with no degree>=2 candidate (the
* selection filter's law — e.g. a single-segment graph); the tables still
* come from the ONE integer-Dijkstra core. */
function landmarkBlock(nodeCount, edges, { pin } = {}) {
const input = { nodeCount, edges, quantumS: LANDMARK_QUANTUM_S };
const anchors = pin !== undefined ? Uint32Array.of(pin) : farthestPointAnchors(input, 1);
if (anchors.length !== 1) throw new Error("graph has no degree-2 anchor candidate");
const { to, from } = landmarkTables(input, anchors);
return { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from };
/** The ARG3 ch block through the ONE contraction core (Phase 08.1):
* computed, never faked. A shape that contracts to ZERO shortcuts is a
* builder bug — an ARG3 with chShortcutCount = 0 is not a road graph
* (the decode law refuses it), so fail loud here instead of shipping an
* undecodable test binary. */
function chBlock(nodeCount, edges) {
const ch = contractGraph({ nodeCount, edges });
if (ch.stats.shortcutCount === 0) {
throw new Error("graph contracted to zero shortcuts — ARG3 has no CH-less layout");
}
return ch;
}
/** The committed 06-01 ground truth (25 nodes around KL center) via fs. */
const MINI_GRAPH = readFileSync(new URL("../../routing/test/fixtures/mini-graph.bin", import.meta.url));
/** Public gazetteer landmarks only — no user coordinate enters a test (D-12). */
/** Public gazetteer POIs only — no user coordinate enters a test (D-12). */
const KL_TOWER = { lat: 3.1528, lng: 101.7038 };
const KL_SENTRAL = { lat: 3.1343, lng: 101.6869 };
/** 8-node straight corridor kl-tower → suria-klcc (public coords), two-way. */
/** 8-node straight corridor kl-tower → suria-klcc (public coords), two-way.
* ARG3 (Phase 08.1): the corridor gains ONE fast nonstop chord (0<->7,
* 120 km/h vs the 60 km/h local road) — a pure line contracts to ZERO
* shortcuts (endpoints starve the interior on the priority tie) and a
* shortcut-less ARG3 is not a road graph. The chord gives the contraction
* a real hierarchy; the split variant keeps the gap AND drops the chord,
* so its two line components stay disconnected (RT9's A* miss) — the
* resulting binary decodes to null under the CH-less law, which flows to
* the same 200-null contract. */
function corridorGraphBuffer({ gap = false } = {}) {
const n = 8;
const lat = [];
@@ -512,26 +518,40 @@ function corridorGraphBuffer({ gap = false } = {}) {
const seconds = meters / (60 / 3.6);
edges.push({ from: i, to: i + 1, seconds, nameId: 0 }, { from: i + 1, to: i, seconds, nameId: 0 });
}
if (!gap) {
const meters = haversineM({ lat: lat[0], lng: lng[0] }, { lat: lat[n - 1], lng: lng[n - 1] });
const seconds = meters / (120 / 3.6);
edges.push({ from: 0, to: n - 1, seconds, nameId: 0 }, { from: n - 1, to: 0, seconds, nameId: 0 });
}
return encodeGraph({
lat,
lng,
edges,
names: ["Jalan Koridor Ujian"],
maxSpeedKmh: 60,
landmarks: { ...landmarkBlock(n, edges) },
ch: chBlock(n, edges),
});
}
/** Nodes cluster at two far-apart cells with a nodeless middle: an in-bbox
* origin between them is the snap-miss carrier (3x3-neighborhood law). */
* origin between them is the snap-miss carrier (3x3-neighborhood law).
* ARG3: node 0 is the star's hub (two unnamed legs, ids kept so the
* contraction's lowest-id tie-break contracts the MID first — a star
* whose mid id is higher starves to zero shortcuts) — the node set is
* unchanged from the ARG2-era fixture, keeping RT8's snap-miss geometry
* exact. */
function sparseGraphBuffer() {
const lat = [3.15, 3.1501, 3.1515];
const lng = [101.69, 101.6905, 101.7];
const meters = haversineM({ lat: lat[0], lng: lng[0] }, { lat: lat[1], lng: lng[1] });
const seconds = meters / (50 / 3.6);
const meters2 = haversineM({ lat: lat[0], lng: lng[0] }, { lat: lat[2], lng: lng[2] });
const seconds2 = meters2 / (50 / 3.6);
const edges = [
{ from: 0, to: 1, seconds, nameId: 0 },
{ from: 1, to: 0, seconds, nameId: 0 },
{ from: 0, to: 2, seconds: seconds2, nameId: 0 },
{ from: 2, to: 0, seconds: seconds2, nameId: 0 },
];
return encodeGraph({
lat,
@@ -539,12 +559,42 @@ function sparseGraphBuffer() {
edges,
names: ["Jalan Sparse Ujian"],
maxSpeedKmh: 50,
landmarks: { ...landmarkBlock(lat.length, edges, { pin: 0 }) },
ch: chBlock(lat.length, edges),
});
}
/** RT9's disconnected carrier: two independent 3-node stars — each
* decodable (real shortcuts) but the component A stars around kl-tower
* and the component B stars around kl-sentra never touch, so the route
* is the honest A* miss (NOT a decode refusal). */
function splitGraphBuffer() {
const lat = [3.1528, 3.1533, 3.1523, 3.1343, 3.1348, 3.1338];
const lng = [101.7038, 101.7043, 101.7033, 101.6869, 101.6874, 101.6864];
const edges = [];
const star = (mid, a, b) => {
const m1 = haversineM({ lat: lat[mid], lng: lng[mid] }, { lat: lat[a], lng: lng[a] });
const m2 = haversineM({ lat: lat[mid], lng: lng[mid] }, { lat: lat[b], lng: lng[b] });
edges.push(
{ from: mid, to: a, seconds: m1 / (60 / 3.6), nameId: 0 },
{ from: a, to: mid, seconds: m1 / (60 / 3.6), nameId: 0 },
{ from: mid, to: b, seconds: m2 / (60 / 3.6), nameId: 0 },
{ from: b, to: mid, seconds: m2 / (60 / 3.6), nameId: 0 },
);
};
star(0, 1, 2); // component A around kl-tower
star(3, 4, 5); // component B around kl-sentra — no edge bridges the two
return encodeGraph({
lat,
lng,
edges,
names: ["Jalan Split Ujian"],
maxSpeedKmh: 60,
ch: chBlock(lat.length, edges),
});
}
const CORRIDOR_BUF = corridorGraphBuffer();
const SPLIT_BUF = corridorGraphBuffer({ gap: true });
const SPLIT_BUF = splitGraphBuffer();
const SPARSE_BUF = sparseGraphBuffer();
/** An R2 fake over one fixed binary; counts + records its gets (the memo probe).