From 11e55c11bc2d9881433321e5f75ecf82bb5a2dd5 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sun, 25 Oct 2026 22:35:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(08):=20WR-01=20=E2=80=94=20oracle-sweep=20o?= =?UTF-8?q?ptimality=20compare=20is=20symmetric=20(|=CE=94|=20both=20direc?= =?UTF-8?q?tions)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sweepOracle: Math.abs(deltaS) > ORACLE_EPSILON_S replaces the one-sided 'seconds > optimal + eps' check — a result FASTER than the Dijkstra oracle (the traversal-law-break signature, e.g. a oneway relaxed) is now a violation, stamped ORACLE-MISMATCH, never a pass - worstDeltaPct tracks MAGNITUDE not sign — a -40% law break moves it - pass gate: suboptimal === 0 && oracleMismatch === 0 && classBreaks === 0 - header gate description + exit codes updated: the check is |Δ| in both directions; GATE FAIL names both violation classes; GATE PASS reads 0/N |Δ| violations - row-level signed Δ printout unchanged (sign/direction reporting kept) --- .../scripts/measure-astar-oracle-sweep.mjs | 57 +++++++++++++------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/routing/scripts/measure-astar-oracle-sweep.mjs b/routing/scripts/measure-astar-oracle-sweep.mjs index f2690f4..11e22fc 100644 --- a/routing/scripts/measure-astar-oracle-sweep.mjs +++ b/routing/scripts/measure-astar-oracle-sweep.mjs @@ -33,9 +33,15 @@ * 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 + * 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 @@ -48,8 +54,9 @@ * 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. + * 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"; @@ -188,12 +195,13 @@ function dijkstraSeconds(graph, origin, goal) { * @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 }} + * @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; @@ -227,21 +235,30 @@ export function sweepOracle(graph, pairs, opts) { 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; + // 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: suboptimalHere ? "SUBOPTIMAL" : "pass", + verdict: !violates ? "pass" : deltaS > 0 ? "SUBOPTIMAL" : "ORACLE-MISMATCH", }); } - const pass = suboptimal === 0 && classBreaks === 0; - return { rows, suboptimal, classBreaks, routed, unreachable, unsnappable, worstDeltaPct, pass }; + const pass = suboptimal === 0 && oracleMismatch === 0 && classBreaks === 0; + return { rows, suboptimal, oracleMismatch, classBreaks, routed, unreachable, unsnappable, worstDeltaPct, pass }; } /* ————— CLI reporter ————— */ @@ -306,20 +323,24 @@ async function main() { 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 Δ +${results.worstDeltaPct.toFixed(3)}%), ` + + `summary: ${results.routed} routed (worst |Δ| ${worst >= 0 ? "+" : ""}${worst.toFixed(3)}%), ` + `${results.unreachable} unreachable, ${results.unsnappable} unsnappable, ` + - `${results.suboptimal} SUBOPTIMAL, ${results.classBreaks} class-breaks`, + `${results.suboptimal} SUBOPTIMAL, ${results.oracleMismatch} ORACLE-MISMATCH, ` + + `${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.`, + `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} suboptimal — shipped routeAstar equals the brute-force oracle on every fresh pair`, + `GATE PASS: 0/${results.rows.length} |Δ| violations — shipped routeAstar equals the brute-force oracle in both directions on every fresh pair`, ); process.exit(0); }