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:
+101
-31
@@ -3,24 +3,28 @@
|
||||
* build-mini-graph.mjs — builds the committed mini-graph.bin fixture.
|
||||
* ----------------------------------------------------------------------
|
||||
* A MAINTAINER-TIME REPO TOOL: regenerates routing/test/fixtures/
|
||||
* mini-graph.bin — the A* correctness ground truth (plan 06-01) and the
|
||||
* snap/astar suites' shared fixture. NOT a runtime dependency; nothing in
|
||||
* bot/src/, web/src/, or routing/src/ imports it. Tests consume the
|
||||
* COMMITTED binary; only a deliberate fixture redesign re-runs this.
|
||||
* mini-graph.bin — the routing correctness ground truth (plan 06-01) and
|
||||
* the snap/search suites' shared fixture. NOT a runtime dependency;
|
||||
* nothing in bot/src/, web/src/, or routing/src/ imports it. Tests
|
||||
* consume the COMMITTED binary; only a deliberate fixture redesign re-runs
|
||||
* this.
|
||||
*
|
||||
* PROVENANCE (Phase 8 plan 08-01): routing/test/fixtures/mini-graph-arg1.bin
|
||||
* holds the frozen ARG1-era bytes preserved expressly as the v1
|
||||
* decode-rejection vector consumed by the G-suite — it is NOT regenerated
|
||||
* by this script, ever.
|
||||
* PROVENANCE (Phase 08.1 plan 01): routing/test/fixtures/mini-graph-arg2.bin
|
||||
* holds the frozen ARG2-era bytes preserved expressly as the ARG2
|
||||
* decode-rejection vector consumed by the G-suite (D-12) — it is NOT
|
||||
* regenerated by this script, ever.
|
||||
*
|
||||
* Usage: node routing/test/fixtures/build-mini-graph.mjs
|
||||
* Exit: 0 on write; throws on any invariant violation (never writes).
|
||||
*
|
||||
* ARG2 (plan 08-02): the fixture carries a tiny real landmark stage —
|
||||
* L=2 anchors via farthestPointAnchors + quantized c-metric tables via
|
||||
* landmarkTables (the SAME pure module the real build script uses), passed
|
||||
* through the ONE encodeGraph authority. Deterministic: identical output
|
||||
* bytes on every re-run.
|
||||
* ARG3 (Phase 08.1, D-12): the fixture carries a tiny real CH stage —
|
||||
* contraction via contractGraph (the SAME pure module the real build
|
||||
* script uses), passed through the ONE encodeGraph authority as the `ch`
|
||||
* block. Deterministic: identical output bytes on every re-run.
|
||||
*
|
||||
* Design (pinned by 06-01): a hand-built 5x5 grid in a ~0.002 deg x 0.002 deg
|
||||
* bbox near KL center (lat 3.1500–3.1520, lng 101.6900–101.6920):
|
||||
@@ -34,7 +38,7 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { encodeGraph } from "../../src/graph.ts";
|
||||
import { haversineM } from "../../src/geo.ts";
|
||||
import { LANDMARK_QUANTUM_S, farthestPointAnchors, landmarkTables, landmarkCoverage } from "../../src/landmark.ts";
|
||||
import { contractGraph, CH_SHORTCUT_EXPLOSION_RATIO } from "../../src/contraction.ts";
|
||||
|
||||
const GRID = 5;
|
||||
const LAT0 = 3.15;
|
||||
@@ -92,27 +96,86 @@ reverse.flags = 1;
|
||||
|
||||
if (edges.length !== 80) throw new Error(`expected 80 directed edges, built ${edges.length}`);
|
||||
|
||||
// LANDMARK STAGE (plan 08-02): the fixture regenerates as ARG2 THROUGH the
|
||||
// one encode authority — the same farthestPointAnchors + landmarkTables the
|
||||
// real build script (08-04) will run, at a tiny L. Fail-loud invariants
|
||||
// before any write (the `edges.length !== 80` style).
|
||||
const LANDMARK_COUNT = 2; // tiny L: the 5x5 grid needs exactly 2 well-spread anchors
|
||||
const landmarkInput = { nodeCount: GRID * GRID, edges, quantumS: LANDMARK_QUANTUM_S };
|
||||
const anchors = farthestPointAnchors(landmarkInput, LANDMARK_COUNT);
|
||||
const { to, from } = landmarkTables(landmarkInput, anchors);
|
||||
if (anchors.length !== LANDMARK_COUNT) throw new Error(`expected ${LANDMARK_COUNT} anchors, got ${anchors.length}`);
|
||||
if (to.length !== LANDMARK_COUNT * GRID * GRID || from.length !== LANDMARK_COUNT * GRID * GRID) {
|
||||
throw new Error("landmark tables are not L*N row-major");
|
||||
// CH STAGE (Phase 08.1): the fixture regenerates as ARG3 THROUGH the one
|
||||
// encode authority — the same contractGraph the real build script (plan 02)
|
||||
// runs. Fail-loud invariants before any write (the `edges.length !== 80`
|
||||
// style), per RESEARCH §Pattern 4.
|
||||
const ch = contractGraph({ nodeCount: GRID * GRID, edges });
|
||||
const rankValues = [...ch.rank];
|
||||
if (new Set(rankValues).size !== GRID * GRID) {
|
||||
throw new Error(`rank is not a total order — ${new Set(rankValues).size} distinct for ${GRID * GRID} nodes`);
|
||||
}
|
||||
for (let l = 0; l < anchors.length; l++) {
|
||||
const a = anchors[l];
|
||||
if (to[l * GRID * GRID + a] !== 0 || from[l * GRID * GRID + a] !== 0) {
|
||||
throw new Error(`anchor ${l} (node ${a}) own cells are not 0 — table law broken`);
|
||||
if (rankValues.some((r) => !Number.isInteger(r) || r < 0 || r >= GRID * GRID)) {
|
||||
throw new Error("a rank value is outside [0, nodeCount)");
|
||||
}
|
||||
// The shortcut band for this grid (measured 2026-10-26, this host: 41 —
|
||||
// the literature's healthy ~1.4-2.0 per node; the tripwire sits at 4x).
|
||||
const SHORTCUT_BAND_MIN = 20;
|
||||
const SHORTCUT_BAND_MAX = 80;
|
||||
if (ch.stats.shortcutCount < SHORTCUT_BAND_MIN || ch.stats.shortcutCount > SHORTCUT_BAND_MAX) {
|
||||
throw new Error(
|
||||
`shortcut count ${ch.stats.shortcutCount} outside the expected grid band ` +
|
||||
`[${SHORTCUT_BAND_MIN}, ${SHORTCUT_BAND_MAX}] — check the contraction/witness budget`,
|
||||
);
|
||||
}
|
||||
// Overlay-aware weight map: a cascading shortcut's constituents are the
|
||||
// OVERLAY edges it replaced (originals OR earlier shortcuts — emission
|
||||
// order is the contraction order, so replaying rank[via]-ascending
|
||||
// rebuilds every weight exactly).
|
||||
const weight = new Map(); // "a,b" -> min overlay weight
|
||||
for (const e of edges) {
|
||||
if ((e.flags & 1) !== 0) continue;
|
||||
const key = `${e.from},${e.to}`;
|
||||
const prev = weight.get(key);
|
||||
if (prev === undefined || e.seconds < prev) weight.set(key, e.seconds);
|
||||
}
|
||||
const shortcutEntries = [];
|
||||
for (let u = 0; u < GRID * GRID; u++) {
|
||||
for (let s = ch.shortcutStart[u]; s < ch.shortcutStart[u + 1]; s++) {
|
||||
shortcutEntries.push({ from: u, to: ch.shortcutTarget[s], seconds: ch.shortcutSeconds[s], via: ch.shortcutVia[s] });
|
||||
}
|
||||
}
|
||||
const coverage = landmarkCoverage(to, anchors, GRID * GRID);
|
||||
if (coverage.some((c) => c !== 1)) {
|
||||
throw new Error(`the grid is strongly connected — coverage must be 1.0, got ${coverage.join(", ")}`);
|
||||
shortcutEntries.sort((a, b) => ch.rank[a.via] - ch.rank[b.via] || a.from - b.from || a.to - b.to);
|
||||
for (const sc of shortcutEntries) {
|
||||
const via = sc.via;
|
||||
if (!Number.isInteger(via) || via < 0 || via >= GRID * GRID) {
|
||||
throw new Error(`shortcut ${sc.from}->${sc.to} has an out-of-range via ${via}`);
|
||||
}
|
||||
if (ch.rank[sc.from] <= ch.rank[via] || ch.rank[sc.to] <= ch.rank[via]) {
|
||||
throw new Error(`shortcut ${sc.from}->${sc.to} (via ${via}) is not upward`);
|
||||
}
|
||||
const w1 = weight.get(`${sc.from},${via}`);
|
||||
const w2 = weight.get(`${via},${sc.to}`);
|
||||
if (w1 === undefined || w2 === undefined) {
|
||||
throw new Error(`shortcut ${sc.from}->${sc.to} (via ${via}): a constituent overlay edge is missing`);
|
||||
}
|
||||
if (sc.seconds !== Math.fround(w1 + w2)) {
|
||||
throw new Error(`shortcut ${sc.from}->${sc.to} weight ${sc.seconds} != f32(${w1} + ${w2})`);
|
||||
}
|
||||
const key = `${sc.from},${sc.to}`;
|
||||
const prev = weight.get(key);
|
||||
// The contraction's overlay accumulates f64 sums (the f32 rounding
|
||||
// happens once, at CSR assembly) — replay the same f64 domain so the
|
||||
// next cascade level compares exactly.
|
||||
if (prev === undefined || w1 + w2 < prev) weight.set(key, w1 + w2);
|
||||
}
|
||||
|
||||
// Strong connectivity (the grid is bidirectional except the ONE oneway
|
||||
// mirror — every node reaches every node).
|
||||
const adj = Array.from({ length: GRID * GRID }, () => []);
|
||||
for (const e of edges) {
|
||||
if ((e.flags & 1) !== 0) continue;
|
||||
adj[e.from].push(e.to);
|
||||
}
|
||||
const seen = new Uint8Array(GRID * GRID);
|
||||
const stack = [0];
|
||||
seen[0] = 1;
|
||||
while (stack.length > 0) {
|
||||
const u = stack.pop();
|
||||
for (const v of adj[u]) if (!seen[v]) { seen[v] = 1; stack.push(v); }
|
||||
}
|
||||
if (seen.some((s) => s === 0)) {
|
||||
throw new Error("the grid is not strongly connected — fixture invariant broken");
|
||||
}
|
||||
|
||||
const bin = encodeGraph({
|
||||
@@ -121,11 +184,18 @@ const bin = encodeGraph({
|
||||
edges,
|
||||
names,
|
||||
maxSpeedKmh: SPEED_PERIMETER_KMH,
|
||||
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
|
||||
ch: {
|
||||
rank: ch.rank,
|
||||
shortcutStart: ch.shortcutStart,
|
||||
shortcutTarget: ch.shortcutTarget,
|
||||
shortcutSeconds: ch.shortcutSeconds,
|
||||
shortcutVia: ch.shortcutVia,
|
||||
},
|
||||
});
|
||||
writeFileSync(new URL("./mini-graph.bin", import.meta.url), Buffer.from(bin));
|
||||
console.log(
|
||||
`mini-graph.bin: ${GRID * GRID} nodes, ${edges.length} edges, 3 names, ` +
|
||||
`L=${LANDMARK_COUNT} landmarks (nodes ${[...anchors].join(",")}), quantum ${LANDMARK_QUANTUM_S}s, ` +
|
||||
`coverage ${coverage.map((c) => c.toFixed(2)).join("/")}, ${bin.byteLength} B`,
|
||||
`CH shortcuts ${ch.stats.shortcutCount} (${ch.stats.ratioPerNode.toFixed(2)}/node, ` +
|
||||
`witness searches ${ch.stats.witnessSearches}, budget hits ${ch.stats.witnessBudgetHits}), ` +
|
||||
`${bin.byteLength} B`,
|
||||
);
|
||||
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user