From 97d6e35c3afd6e55b4142f2263ae96c55e419949 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sun, 25 Oct 2026 22:11:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(08):=20committed=20200-pair=20real-graph?= =?UTF-8?q?=20Dijkstra-oracle=20sweep=20=E2=80=94=20the=20optimality=20pin?= =?UTF-8?q?=20D-03's=20fixed=20corpus=20cannot=20be?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../scripts/measure-astar-oracle-sweep.mjs | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 routing/scripts/measure-astar-oracle-sweep.mjs diff --git a/routing/scripts/measure-astar-oracle-sweep.mjs b/routing/scripts/measure-astar-oracle-sweep.mjs new file mode 100644 index 0000000..f2690f4 --- /dev/null +++ b/routing/scripts/measure-astar-oracle-sweep.mjs @@ -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(); +}