feat(routing): 08-01 task 2 — pre-ALT optimality baseline: recorder + committed 100-pair record (D-03 anchor, measured before ARG2 lands, immutable)

This commit is contained in:
araharah-executor
2026-10-25 17:13:09 +08:00
parent 0fca2e08db
commit 643df1a07b
2 changed files with 1640 additions and 0 deletions
@@ -0,0 +1,217 @@
#!/usr/bin/env node
/*
* measure-optimality-baseline.mjs — the pre-ALT optimality baseline recorder
* (Phase 8 plan 08-01; D-03's comparison anchor).
* ------------------------------------------------------------------------------
* A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
* web/src, or routing/src imports it. It records the PRE-ALT route seconds for
* 100 coordinate-pinned cross-metro pairs into a COMMITTED JSON record — the
* anchor the D-03 optimality gate (Plan 08-05's measure-optimality-gate.mjs)
* compares against after the ALT preprocessing lands.
*
* RUN THIS BEFORE THE ARG2 CODEC LANDS (research Pitfall 1): once graph.ts
* versions to 2, decodeGraph rejects the v1 binary and "old code + old binary"
* is unreproducible from the working tree. The recorder enforces this
* mechanically — it reads the binary's version field and REFUSES anything
* that is not ARG1, so a post-codec run can never silently fake a pre-ALT
* baseline.
*
* THE RECORD IS IMMUTABLE: the script refuses to overwrite an existing record
* ("delete by explicit git revert only"). Pairs are pinned by COORDINATES
* (lat/lng drawn from the graph's order-independent bbox by a seeded PRNG) —
* never node indices (Pitfall 2: a rebuild reorders nodes; coordinates do not
* move). Route seconds are stored as JSON numbers — routeAstar returns exact
* f64 accumulations of f32 edgeSeconds and JSON round-trips IEEE doubles
* losslessly (research A8).
*
* Echo law: pair ids + verdict classes + seconds ONLY — coordinates in the
* record are public bbox-derived values, never user data (the corpus's D-12
* posture, applied to a maintainer tool).
*
* Usage:
* node routing/scripts/measure-optimality-baseline.mjs
*
* Exit codes: 0 recorded; 1 already recorded, graph absent/not ARG1, or
* decode failure. A run that measures but fails a gate writes NOTHING.
*/
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts
const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin");
const RECORD_PATH = resolve(here, "..", "test", "fixtures", "alt-baseline.json");
/** D-04's sample size for the optimality record (~100 cross-metro pairs). */
const BASELINE_PAIR_COUNT = 100;
/** Shared pair-set seed — the SAME seed measure-route-cpu.mjs --extended
* reuses, so the baseline and the CPU verdict measure identical pairs. */
const BASELINE_SEED = 0x08a1;
/** Cross-metro floor: straight-line endpoint distance below this is NOT a
* worst-case pair (the 20–30 km class is where the budget breaks — §1.9). */
const MIN_PAIR_KM = 12;
/** The binary version the baseline MUST be recorded on (Pitfall 1's guard). */
const REQUIRED_ARG_VERSION = 1;
/** The epsilon the D-03 gate will assert against (documentary here; the gate
* mirrors this value — a record edit alone cannot fake equality). */
const OPTIMALITY_EPSILON_PCT = 0.1;
/** Honest-termination guard for the rejection sampler. */
const MAX_SAMPLER_ATTEMPTS = 50_000;
function fail(message) {
console.error(`measure-optimality-baseline: ${message}`);
process.exit(1);
}
function toArrayBuffer(buf) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
/** mulberry32 — tiny deterministic PRNG (fixed seed => comparable runs).
* Copied verbatim from measure-route-cpu.mjs (one PRNG shape in the repo). */
function mulberry32(seed) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/** Exact-decimal rounding to 1e-6 (the codec's coordinate grid law). */
const round6 = (v) => Math.round(v * 1e6) / 1e6;
/**
* The pair generator — exported so the extended CPU sample (Plan 08-01 task 3)
* measures the SAME pairs from ONE definition (no drift between reporters).
* Deterministic: same bbox + seed ⇒ identical pairs, always.
*
* @param bbox {{ minLat: number, minLng: number, maxLat: number, maxLng: number }}
* @param opts {{ count: number, minPairKm: number, seed: number, haversineM: (a, b) => number }}
* @returns Array<{ pairId, origin: {lat, lng}, destination: {lat, lng}, straightLineKm: number }>
*/
export function generateCrossMetroPairs(bbox, opts) {
const { count, minPairKm, seed, haversineM } = opts;
const rand = mulberry32(seed);
const draw = (axis) =>
axis.min + rand() * (axis.max - axis.min);
const pairs = [];
let attempts = 0;
while (pairs.length < count) {
if (++attempts > MAX_SAMPLER_ATTEMPTS) {
fail(`sampler gave up after ${MAX_SAMPLER_ATTEMPTS} attempts — bbox too small for the ${minPairKm} km floor`);
}
const origin = { lat: round6(draw({ min: bbox.minLat, max: bbox.maxLat })), lng: round6(draw({ min: bbox.minLng, max: bbox.maxLng })) };
const destination = { lat: round6(draw({ min: bbox.minLat, max: bbox.maxLat })), lng: round6(draw({ min: bbox.minLng, max: bbox.maxLng })) };
const straightLineKm = haversineM(origin, destination) / 1000;
if (straightLineKm < minPairKm) continue; // not a cross-metro worst case
pairs.push({
pairId: `x${String(pairs.length).padStart(3, "0")}`,
origin,
destination,
straightLineKm: Math.round(straightLineKm * 1000) / 1000,
});
}
return pairs;
}
/**
* The measurement core — exported for in-process mirrors of the same numbers.
* Honest nulls are MEASUREMENTS: a snap miss records verdict "unsnappable",
* a disconnected pair records "unreachable" — neither is skipped silently.
*
* @returns Array<{ pairId, origin, destination, straightLineKm, seconds: number|null, verdict: "reachable"|"unreachable"|"unsnappable" }>
*/
export function measureBaselinePairs(graph, pairs, opts) {
const { snap, astar } = opts;
return pairs.map((pair) => {
const originSnap = snap(graph, pair.origin);
const goalSnap = snap(graph, pair.destination);
if (originSnap === null || goalSnap === null) {
return { ...pair, seconds: null, verdict: "unsnappable" };
}
const result = astar(graph, originSnap.node, goalSnap.node);
if (result === null) {
return { ...pair, seconds: null, verdict: "unreachable" };
}
return { ...pair, seconds: result.seconds, verdict: "reachable" };
});
}
async function main() {
if (!existsSync(GRAPH_PATH)) {
fail(`no graph at ${GRAPH_PATH} — build it first: node routing/scripts/build-road-graph.mjs`);
}
if (existsSync(RECORD_PATH)) {
fail(`baseline already recorded at ${RECORD_PATH} — the record is immutable; delete by explicit git revert only`);
}
const buffer = readFileSync(GRAPH_PATH);
if (buffer.byteLength < 64) fail(`${GRAPH_PATH} is not a road-graph binary`);
const version = new DataView(toArrayBuffer(buffer)).getUint32(4, true);
if (version !== REQUIRED_ARG_VERSION) {
fail(`the on-disk binary is ARG${version} — the baseline MUST be recorded on the ARG1 binary, BEFORE the ARG2 codec lands (Pitfall 1)`);
}
const { decodeGraph } = await import("../src/graph.ts");
const { snapToGraph } = await import("../src/snap.ts");
const { routeAstar } = await import("../src/astar.ts");
const { haversineM } = await import("../src/geo.ts");
const graph = decodeGraph(toArrayBuffer(buffer));
if (graph === null) fail(`${GRAPH_PATH} does not decode (fail-honest null) — rebuild the binary`);
const pairs = generateCrossMetroPairs(graph.bbox, {
count: BASELINE_PAIR_COUNT,
minPairKm: MIN_PAIR_KM,
seed: BASELINE_SEED,
haversineM,
});
const rows = measureBaselinePairs(graph, pairs, { snap: snapToGraph, astar: routeAstar });
// Print the measurement (every pair, then the summary) — print-the-measurement law.
for (const row of rows) {
const value = row.seconds === null ? row.verdict : `${row.seconds.toFixed(1)}s ${row.verdict}`;
console.log(` ${row.pairId}: ${value}`);
}
const routed = rows.filter((r) => r.verdict === "reachable");
const unreachable = rows.filter((r) => r.verdict === "unreachable");
const unsnappable = rows.filter((r) => r.verdict === "unsnappable");
const seconds = routed.map((r) => r.seconds);
const mean = seconds.reduce((acc, v) => acc + v, 0) / (seconds.length || 1);
const max = seconds.length > 0 ? Math.max(...seconds) : 0;
console.log(
`baseline: ${routed.length}/${rows.length} routed` +
(unreachable.length > 0 ? `, ${unreachable.length} unreachable` : "") +
(unsnappable.length > 0 ? `, ${unsnappable.length} unsnappable` : "") +
` — mean ${mean.toFixed(1)}s, max ${max.toFixed(1)}s`,
);
// Collect-then-write: the record is written ONLY after every pair measured.
const record = {
recordedAt: new Date().toISOString(),
tool: "routing/scripts/measure-optimality-baseline.mjs",
purpose:
"pre-ALT baseline for the D-03 optimality gate (Phase 8); measured BEFORE the ARG2 codec landed (Pitfall 1) — the record is immutable, delete by explicit git revert only",
epsilon: { name: "OPTIMALITY_EPSILON_PCT", pct: OPTIMALITY_EPSILON_PCT },
graph: {
path: "routing/data/kl-roadgraph.v1.bin",
byteLength: buffer.byteLength,
nodeCount: graph.nodeCount,
edgeCount: graph.edgeCount,
argVersion: REQUIRED_ARG_VERSION,
},
pairRules: { seed: `0x${BASELINE_SEED.toString(16)}`, count: BASELINE_PAIR_COUNT, minPairKm: MIN_PAIR_KM },
pairs: rows,
};
writeFileSync(RECORD_PATH, JSON.stringify(record, null, 2) + "\n");
console.log(`wrote ${RECORD_PATH} — ${rows.length} pairs, epsilon ±${OPTIMALITY_EPSILON_PCT}% (the D-03 gate mirrors this constant)`);
process.exit(0);
}
const invoked = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
if (invoked === import.meta.url) {
await main();
}