#!/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(); }