feat(08): committed 200-pair real-graph Dijkstra-oracle sweep — the optimality pin D-03's fixed corpus cannot be

measure-astar-oracle-sweep.mjs (maintainer-time, route-corpus law: exported
core sweepOracle + CLI guard, print-the-measurement, honest exit codes):

- FRESH seeded cross-metro pairs via measure-optimality-baseline.mjs's
  exported generateCrossMetroPairs core — seed 0x08f1, deliberately NOT
  the D-03 corpus seed 0x08a1, so this sweep never measures the corpus
- compared-pair law: one 3x-oversampled draw, consumed in order until 200
  COMPARED pairs (reachable + null-vs-null unreachable both compare;
  snap misses counted and printed; same binary + seed => same set) —
  random bbox coordinates are ~20% off-road and an unsnappable pair
  cannot exercise the search
- brute-force unidirectional Dijkstra oracle (label-setting, heap, the
  shared oneway traversal law) on the SAME snapped node pair
- gate: 0 suboptimal (epsilon 1e-6 s — f64 summation-order noise floor,
  A1's epsilon); class-breaks (one side routes, other doesn't) violate
- honest skip when the gitignored binary is absent (exit 0, SKIP printed)

First run on the fixed engine: 200 compared (182 routed, 18 unreachable),
0 SUBOPTIMAL, worst delta +0.000% — GATE PASS. The pre-F1 engine measured
1/194 suboptimal on this pair shape (bench7, debug session).
This commit is contained in:
araharah-executor
2026-10-25 22:11:02 +08:00
parent 2a5a0b5b6a
commit 97d6e35c3a
@@ -0,0 +1,332 @@
#!/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 suboptimal pairs. 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 suboptimal /
* 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"|"unreachable"|"unsnappable"|"class-break" }>, suboptimal: 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 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;
const deltaPct = ((result.seconds - optimal) / optimal) * 100;
const suboptimalHere = result.seconds > optimal + ORACLE_EPSILON_S;
if (suboptimalHere) suboptimal += 1;
if (deltaPct > worstDeltaPct) worstDeltaPct = deltaPct;
rows.push({
pairId: pair.pairId,
seconds: result.seconds,
oracleSeconds: optimal,
deltaPct,
verdict: suboptimalHere ? "SUBOPTIMAL" : "pass",
});
}
const pass = suboptimal === 0 && classBreaks === 0;
return { rows, suboptimal, 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}`);
}
console.log(
`summary: ${results.routed} routed (worst Δ +${results.worstDeltaPct.toFixed(3)}%), ` +
`${results.unreachable} unreachable, ${results.unsnappable} unsnappable, ` +
`${results.suboptimal} SUBOPTIMAL, ${results.classBreaks} class-breaks`,
);
if (!results.pass) {
console.error(
`GATE FAIL: ${results.suboptimal} suboptimal + ${results.classBreaks} class-break pair(s) across ${results.rows.length} fresh pairs — ` +
`routeAstar seconds must equal the Dijkstra oracle (gate: 0 suboptimal). Escalate: this is RC-1's signature.`,
);
process.exit(1);
}
console.log(
`GATE PASS: 0/${results.rows.length} suboptimal — shipped routeAstar equals the brute-force oracle 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();
}