#!/usr/bin/env node /* * measure-astar-oracle-sweep.mjs — the F1 real-graph optimality pin * (post-plan remediation; debug session alt-potential-4x-slowdown). * ------------------------------------------------------------------------------ * A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, * web/src, or routing/src imports it (the route-corpus/measure-route-cpu law). * * Purpose: D-03's frozen 100-pair record is a CORPUS — the same * coordinate-pinned pairs every run, recorded before ALT landed — and it * structurally cannot catch a stochastic per-edge inconsistency: the debug * session measured the shipped pre-F1 search SUBOPTIMAL on 1/194 FRESH * random real-graph pairs (+0.278% vs the Dijkstra oracle) while the D-03 * gate stayed green on sampling luck (its fixed pairs never cross a * violating edge harmfully). This sweep is the gate that WOULD have caught * RC-1: every run generates FRESH seeded cross-metro pairs via the baseline * recorder's exported generateCrossMetroPairs core — a seed deliberately * DIFFERENT from the D-03 corpus seed, so this sweep never measures the * corpus pairs — routes each pair with the SHIPPED routeAstar, and compares * seconds against a brute-force unidirectional Dijkstra oracle run on the * SAME snapped node pair. The traversal law is the shared one (edgeFlags * bit0 never relaxed — astar.ts's header law), so the only degrees of * freedom left are the search's own. * * COMPARED-PAIR law: random bbox coordinates often sit off-road (sea, * forest — the KL bbox is wide), and an unsnappable endpoint cannot * exercise the search at all. So the sampler OVERSAMPLES from the core * (one call, SWEEP_OVERSAMPLE_FACTOR x SWEEP_PAIR_COUNT raw pairs, ONE * pinned seed) and consumes pairs in order until SWEEP_PAIR_COUNT are * COMPARED — a pair compares when both endpoints snap (reachable rows AND * null-vs-null unreachable rows both compare; the honest null is part of * the pin). Rejected unsnappables are counted and printed, never hidden; * if the raw draw runs out first the run fails honestly. Same binary + * seed ⇒ the same compared set, always. * * Gate: 0 |Δ| pairs — the compare is SYMMETRIC (08-review WR-01): the * oracle is the ground truth in BOTH directions. A result slower than the * oracle stamps SUBOPTIMAL; a result FASTER than the oracle stamps * ORACLE-MISMATCH — the traversal-law-break signature (e.g. edgeFlags * bit0 relaxed would cut a oneway and come in BELOW the legal optimum), * which the original one-sided `seconds > optimal + ε` check wrongly * stamped pass. Equal-cost ties pass (seconds only, never node paths — * the D-03 tie law); null-vs-null (both sides unreachable) passes; a * verdict-class break (one side routes, the other does not) is a * violation. * * Honest-skip law: the real binary is gitignored — when it is absent this * script prints SKIP and exits 0 (the pin needs the real graph; a missing * binary is an environment fact, not a failure). * * Echo law: pair ids + measurements ONLY (the route-corpus posture) — the * generated coordinates are public bbox-derived values, never user data. * * Usage: * node routing/scripts/measure-astar-oracle-sweep.mjs * * Exit codes: 0 sweep passed (or the honest skip); 1 any |Δ| violation * (SUBOPTIMAL or ORACLE-MISMATCH) or class-break violation, or an * undecodable binary. */ import { existsSync, readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { decodeGraph } from "../src/graph.ts"; import { snapToGraph as snap } from "../src/snap.ts"; import { routeAstar as astar } from "../src/astar.ts"; import { haversineM } from "../src/geo.ts"; import { generateCrossMetroPairs } from "./measure-optimality-baseline.mjs"; const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts /** The current binary under test (same path law as the other gates). */ const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin"); /** Sample width — the debug session's bench7 shape: 200 fresh seeded pairs * caught the pre-F1 bug at 1/194; 200 is the standing width. */ const SWEEP_PAIR_COUNT = 200; /** Raw-draw multiplier: one generateCrossMetroPairs call draws this many * candidates so the sweep can still COMPARE SWEEP_PAIR_COUNT pairs after * the honest snap misses (the wide KL bbox leaves ~35-40% of random * coordinates off-road). Consumed in order; misses counted and printed. */ const SWEEP_OVERSAMPLE_FACTOR = 3; /** FRESH seed — pinned for reproducibility, deliberately NOT the D-03 * baseline's 0x08a1: this sweep must never measure the corpus pairs. */ const SWEEP_SEED = 0x08f1; /** Cross-metro floor — the baseline recorder's own worst-case rule. */ const SWEEP_MIN_PAIR_KM = 12; /** Summation-order noise floor: routeAstar and the oracle accumulate the * same f32 edgeSeconds in different orders — f64 association noise is * ~1e-12 relative; 1e-6 absolute is astar.test.mjs A1's epsilon. */ const ORACLE_EPSILON_S = 1e-6; function fail(message) { console.error(`measure-astar-oracle-sweep: ${message}`); process.exit(1); } function toArrayBuffer(buf) { return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); } /* ————— the brute-force oracle ————— */ /** Minimal binary min-heap (key, node) — maintainer-time allocation is * fine; the oracle's shape mirrors astar.ts's NodeHeap without its * preallocation (the oracle is the ground truth, not the budget). */ class MinHeap { constructor() { this.keys = []; this.nodes = []; } get size() { return this.keys.length; } push(key, node) { const keys = this.keys; const nodes = this.nodes; let i = keys.length; keys.push(key); nodes.push(node); while (i > 0) { const parent = (i - 1) >> 1; if (keys[parent] <= key) break; keys[i] = keys[parent]; nodes[i] = nodes[parent]; i = parent; } keys[i] = key; nodes[i] = node; } /** Returns the min-key node. Lazy deletion: the caller skips stale nodes * via the settled check (the astar.test.mjs Dijkstra law, heapified). */ pop() { const keys = this.keys; const nodes = this.nodes; const top = nodes[0]; const lastKey = keys.pop(); const lastNode = nodes.pop(); const size = keys.length; if (size > 0) { let i = 0; for (;;) { const left = 2 * i + 1; if (left >= size) break; const right = left + 1; const child = right < size && keys[right] < keys[left] ? right : left; if (keys[child] >= lastKey) break; keys[i] = keys[child]; nodes[i] = nodes[child]; i = child; } keys[i] = lastKey; nodes[i] = lastNode; } return top; } } /** * Brute-force unidirectional Dijkstra — the seconds oracle. Label-setting * over the SAME traversal law as routeAstar (edgeFlags bit0 never * relaxed); returns the exact optimal seconds once the goal settles, or * Infinity when the goal can never settle (unreachable). */ function dijkstraSeconds(graph, origin, goal) { const dist = new Float64Array(graph.nodeCount).fill(Number.POSITIVE_INFINITY); const settled = new Uint8Array(graph.nodeCount); const heap = new MinHeap(); dist[origin] = 0; heap.push(0, origin); while (heap.size > 0) { const u = heap.pop(); if (settled[u]) continue; // stale entry (lazy deletion) if (u === goal) return dist[goal]; // label-setting: the settled distance is exact settled[u] = 1; for (let e = graph.edgeStart[u]; e < graph.edgeStart[u + 1]; e++) { if ((graph.edgeFlags[e] & 1) !== 0) continue; // oneway-reverse: never relaxed const v = graph.edgeTarget[e]; const nd = dist[u] + graph.edgeSeconds[e]; if (nd < dist[v]) { dist[v] = nd; heap.push(nd, v); } } } return Number.POSITIVE_INFINITY; // goal never settled — unreachable } /* ————— the measurement core (route-corpus split: one measurement, two * reporters — the exported core + this CLI) ————— */ /** * @param graph decoded RoadGraph (the current binary) * @param pairs generateCrossMetroPairs output (fresh, seeded) * @param opts { snap, astar, oracle } — the live modules + the oracle * @returns {{ rows: Array<{ pairId, seconds: number|null, oracleSeconds: number|null, deltaPct: number|null, verdict: "pass"|"SUBOPTIMAL"|"ORACLE-MISMATCH"|"unreachable"|"unsnappable"|"class-break" }>, suboptimal: number, oracleMismatch: number, classBreaks: number, routed: number, unreachable: number, unsnappable: number, worstDeltaPct: number, pass: boolean }} */ export function sweepOracle(graph, pairs, opts) { const { snap, astar, oracle } = opts; const rows = []; let suboptimal = 0; let oracleMismatch = 0; let classBreaks = 0; let routed = 0; let unreachable = 0; let unsnappable = 0; let worstDeltaPct = 0; for (const pair of pairs) { const originSnap = snap(graph, pair.origin); const goalSnap = snap(graph, pair.destination); if (originSnap === null || goalSnap === null) { unsnappable += 1; rows.push({ pairId: pair.pairId, seconds: null, oracleSeconds: null, deltaPct: null, verdict: "unsnappable" }); continue; } const result = astar(graph, originSnap.node, goalSnap.node); const optimal = oracle(graph, originSnap.node, goalSnap.node); if (optimal === Number.POSITIVE_INFINITY && result === null) { unreachable += 1; // null-vs-null: the honest miss, agreed by both rows.push({ pairId: pair.pairId, seconds: null, oracleSeconds: null, deltaPct: null, verdict: "unreachable" }); continue; } if (optimal === Number.POSITIVE_INFINITY || result === null) { classBreaks += 1; // one side routes, the other does not — never legal rows.push({ pairId: pair.pairId, seconds: result === null ? null : result.seconds, oracleSeconds: optimal === Number.POSITIVE_INFINITY ? null : optimal, deltaPct: null, verdict: "class-break", }); continue; } routed += 1; // SYMMETRIC compare (08-review WR-01): |Δ| beyond the epsilon is a // violation in BOTH directions. A FASTER-than-oracle result is not a // pass — it is the traversal-law-break signature (e.g. a oneway // relaxed), labeled ORACLE-MISMATCH for a distinct escalation. const deltaS = result.seconds - optimal; const deltaPct = (deltaS / optimal) * 100; const violates = Math.abs(deltaS) > ORACLE_EPSILON_S; if (violates) { if (deltaS > 0) suboptimal += 1; else oracleMismatch += 1; } // Track MAGNITUDE, not sign: a -40% law break must move worstDeltaPct. if (Math.abs(deltaPct) > Math.abs(worstDeltaPct)) worstDeltaPct = deltaPct; rows.push({ pairId: pair.pairId, seconds: result.seconds, oracleSeconds: optimal, deltaPct, verdict: !violates ? "pass" : deltaS > 0 ? "SUBOPTIMAL" : "ORACLE-MISMATCH", }); } const pass = suboptimal === 0 && oracleMismatch === 0 && classBreaks === 0; return { rows, suboptimal, oracleMismatch, classBreaks, routed, unreachable, unsnappable, worstDeltaPct, pass }; } /* ————— CLI reporter ————— */ async function main() { // Honest-skip FIRST: the binary is gitignored; its absence is an // environment fact, not a failure. if (!existsSync(GRAPH_PATH)) { console.log( `SKIP: no graph at ${GRAPH_PATH} — the real-graph optimality pin needs the gitignored binary; ` + `build it first: node routing/scripts/build-road-graph.mjs`, ); process.exit(0); } const buffer = readFileSync(GRAPH_PATH); const graph = decodeGraph(toArrayBuffer(buffer)); if (graph === null) fail(`${GRAPH_PATH} does not decode (fail-honest null) — rebuild the binary`); const rawPairs = generateCrossMetroPairs(graph.bbox, { count: SWEEP_PAIR_COUNT * SWEEP_OVERSAMPLE_FACTOR, minPairKm: SWEEP_MIN_PAIR_KM, seed: SWEEP_SEED, haversineM, }); // The compared-pair consumption: in order, rejecting only unsnappable // endpoints (they cannot exercise the search). Reachable AND // null-vs-null unreachable rows both compare. Deterministic: same // binary + seed ⇒ the same compared set. const pairs = []; let snapMisses = 0; for (const pair of rawPairs) { if (pairs.length === SWEEP_PAIR_COUNT) break; const originSnap = snap(graph, pair.origin); const goalSnap = snap(graph, pair.destination); if (originSnap === null || goalSnap === null) { snapMisses += 1; continue; } pairs.push(pair); } if (pairs.length < SWEEP_PAIR_COUNT) { fail( `raw draw exhausted: only ${pairs.length}/${SWEEP_PAIR_COUNT} compared pairs after ${snapMisses} snap misses — ` + `raise SWEEP_OVERSAMPLE_FACTOR (currently ${SWEEP_OVERSAMPLE_FACTOR}x) or rebuild a denser binary`, ); } console.log( `F1 oracle sweep — ${pairs.length} COMPARED fresh seeded cross-metro pairs ` + `(seed 0x${SWEEP_SEED.toString(16)}, min ${SWEEP_MIN_PAIR_KM} km — deliberately NOT the D-03 corpus seed 0x8a1; ` + `${rawPairs.length} raw drawn, ${snapMisses} snap misses rejected)`, ); console.log(`graph: ${graph.nodeCount} nodes / ${graph.edgeCount} edges`); const results = sweepOracle(graph, pairs, { snap, astar, oracle: dijkstraSeconds }); // Print the measurement (every pair, then the summary) — print-the-measurement law. for (const row of results.rows) { const mine = row.seconds === null ? row.verdict : `${row.seconds.toFixed(1)}s`; const truth = row.oracleSeconds === null ? "—" : `${row.oracleSeconds.toFixed(1)}s`; const delta = row.deltaPct === null ? "Δ —" : `Δ ${row.deltaPct >= 0 ? "+" : ""}${row.deltaPct.toFixed(3)}%`; console.log(` ${row.pairId}: astar ${mine} vs oracle ${truth} (${delta}) ${row.verdict}`); } const worst = results.worstDeltaPct; console.log( `summary: ${results.routed} routed (worst |Δ| ${worst >= 0 ? "+" : ""}${worst.toFixed(3)}%), ` + `${results.unreachable} unreachable, ${results.unsnappable} unsnappable, ` + `${results.suboptimal} SUBOPTIMAL, ${results.oracleMismatch} ORACLE-MISMATCH, ` + `${results.classBreaks} class-breaks`, ); if (!results.pass) { console.error( `GATE FAIL: ${results.suboptimal} SUBOPTIMAL + ${results.oracleMismatch} ORACLE-MISMATCH + ` + `${results.classBreaks} class-break pair(s) across ${results.rows.length} fresh pairs — ` + `routeAstar seconds must equal the Dijkstra oracle in BOTH directions (gate: 0 |Δ| violations). ` + `An ORACLE-MISMATCH (faster than the oracle) is a traversal-law break; a SUBOPTIMAL is RC-1's signature. Escalate.`, ); process.exit(1); } console.log( `GATE PASS: 0/${results.rows.length} |Δ| violations — shipped routeAstar equals the brute-force oracle in both directions on every fresh pair`, ); process.exit(0); } // Run only when invoked directly — importing this module (an in-process // mirror of the same numbers) must never execute the CLI (route-corpus law). const invoked = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ""; if (invoked === import.meta.url) { await main(); }