From 63ef408197df4cdea86d7dae6ac67174b1cd8aa5 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sat, 24 Oct 2026 05:18:16 +0800 Subject: [PATCH] test(05-02): add failing offline corpus tolerance gate suite RT1-RT10 (RED) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - one test per check, mirroring search/test/corpus.test.mjs: the suite imports measurePairs from routing/scripts/route-corpus.mjs IN-PROCESS (never spawns the CLI) — one measurement, two reporters, no drift - RED: the measurement core does not exist yet -> 10 named ERR_MODULE_NOT_FOUND failures; the committed dataset itself is already green material (task 1) - fully offline by construction: zero network in the measurement path (T-05-05) --- routing/test/corpus.test.mjs | 172 +++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 routing/test/corpus.test.mjs diff --git a/routing/test/corpus.test.mjs b/routing/test/corpus.test.mjs new file mode 100644 index 0000000..7d69f78 --- /dev/null +++ b/routing/test/corpus.test.mjs @@ -0,0 +1,172 @@ +#!/usr/bin/env node +/* + * corpus.test.mjs — the offline tolerance gate over the committed fixture + * corpus (RT1–RT10), the phase's primary acceptance instrument (amended + * ROADMAP SC3): "the golden-route corpus routes cleanly through the fixture + * RoutingSource backend with bundle-size and type gates holding". + * ---------------------------------------------------------------------- + * The suite re-runs the corpus runner's measurements IN-PROCESS (it imports + * measurePairs from routing/scripts/route-corpus.mjs — it never spawns the + * CLI), so `npm run routing:test` fails whenever the committed dataset + * regresses: the corpus must be gate-green at every wave merge, not just at + * regeneration time. + * + * THE GATE IS FULLY OFFLINE (T-05-05): zero network in the measurement path — + * geometry↔summary consistency is recomputed by haversine, the codec + * round-trip is re-proven per route, and the no-user-coordinate law is proven + * mechanically against the committed gazetteer. The OSRM demo server is + * touched ONLY by the maintainer regeneration tool, never by this gate. + * + * Checks (one test per check, mirroring search/test/corpus.test.mjs): + * RT1 corpus size — 10..20 golden pairs (D-02's 10–20 window) + * RT2 schema + public origin — slug pairIds, non-empty labels, KL bbox, + * gazetteer-known destinations; RT2b proves every committed origin + * EQUALS its gazetteer landmark coordinate within 1e-4 deg — the + * mechanical no-user-coordinate proof (D-12) + * RT3 route shape — 1..3 routes, finite positive distance/duration, + * non-empty geometry strings + * RT4 probe exclusion — national-museum is NEVER a destinationId (it is + * plan 05-05's pending-state carrier) + * RT5 geometry↔summary consistency — haversine polyline length within + * [0.85, 1.10] × distanceM; points finite and inside the KL bbox + * (+0.05 deg slack) — poisoned or drifting geometry fails HERE + * RT6 duration sanity — average speed within [1.0, 30] m/s + * RT7 codec round-trip — encode(decode(geometry)) === geometry, every + * route (the 1e-6-grid law, verbatim-storage proof) + * RT8 licence header — ODbL-1.0 + OpenStreetMap attribution + ODbL notice + * + router.project-osrm.org source (T-05-06: the notice cannot be + * lost by a regeneration) + * RT9 size budget — gzipped committed dataset <= corpus budget (100 KB; + * T-05-12 early warning under the 200 KB BA9 ceiling) + * RT10 alternatives floor — >= 4 pairs carry >= 2 routes (RTNG-02: real + * data for the chips/redraw surface) + * + * Run: npm run routing:test (exit 0 = all green) + * CLI: npm run routing:corpus (same measurement, human report) + */ +import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; +import { test } from "./harness.mjs"; + +// Lazy runner-module load — a missing module => per-case ERR_MODULE_NOT_FOUND +// (the RED state), one FAIL per law instead of a lost suite load. +let runnerPromise = null; +function loadRunner() { + if (!runnerPromise) { + runnerPromise = import("../scripts/route-corpus.mjs"); + } + return runnerPromise; +} + +function loadInputs() { + return { + corpus: JSON.parse( + readFileSync(new URL("./fixtures/golden-pairs.json", import.meta.url), "utf8"), + ), + fixtures: JSON.parse( + readFileSync(new URL("../src/data/route-fixtures.json", import.meta.url), "utf8"), + ), + gazetteer: JSON.parse( + readFileSync(new URL("../../search/src/data/kl-gazetteer.json", import.meta.url), "utf8"), + ), + }; +} + +// One measurement shared by all ten checks — the SAME numbers the CLI prints. +let reportPromise = null; +function getReport() { + if (!reportPromise) { + reportPromise = (async () => { + const { measurePairs } = await loadRunner(); + const { corpus, fixtures, gazetteer } = loadInputs(); + return { corpus, report: measurePairs(corpus, fixtures, gazetteer) }; + })(); + } + return reportPromise; +} + +/** Assert one check is violation-free with a named message. */ +async function expectClean(checkId, message) { + const { report } = await getReport(); + const check = report.checks[checkId]; + assert.ok(check, `check ${checkId} is missing from the measurement report`); + const details = check.violations.map((v) => ` ${v.pairId ?? "(dataset)"}: ${v.reason}`); + assert.equal( + check.violations.length, + 0, + `${message}\n${details.join("\n")}`, + ); +} + +/* ————— RT1 — corpus size ————— */ + +test("RT1 corpus size — the committed corpus carries 10..20 golden pairs (D-02)", async () => { + await expectClean("RT1", "pair count outside D-02's 10–20 window"); +}); + +/* ————— RT2 — schema + the public-origin proof (D-12) ————— */ + +test("RT2 schema + public origin — slug ids, labels, bbox, gazetteer destinations; RT2b pins every origin to a gazetteer coordinate", async () => { + await expectClean( + "RT2", + "schema/public-origin violation — a pair's shape or its pinned coordinate is not a committed gazetteer landmark (D-12: no user coordinate can enter a fixture)", + ); +}); + +/* ————— RT3 — route shape ————— */ + +test("RT3 route shape — 1..3 routes per pair; finite positive distanceM/durationS; non-empty geometry strings", async () => { + await expectClean("RT3", "route-shape violation"); +}); + +/* ————— RT4 — probe exclusion ————— */ + +test("RT4 probe exclusion — national-museum is never a destinationId (the 05-05 pending-state carrier)", async () => { + await expectClean("RT4", "the pending-probe id must never be a fixture destination"); +}); + +/* ————— RT5 — geometry↔summary consistency ————— */ + +test("RT5 geometry↔summary consistency — haversine polyline length within [0.85, 1.10] × distanceM; points finite and in-bbox (+0.05 deg)", async () => { + await expectClean( + "RT5", + "geometry inconsistent with its own summary — poisoned or drifted geometry must fail the gate BEFORE it can ship (T-05-05a)", + ); +}); + +/* ————— RT6 — duration sanity ————— */ + +test("RT6 duration sanity — average speed within [1.0, 30] m/s (KL congestion floor to DUKE highway ceiling)", async () => { + await expectClean("RT6", "implausible average speed"); +}); + +/* ————— RT7 — codec round-trip ————— */ + +test("RT7 codec round-trip — encode(decode(geometry)) === geometry for EVERY route (the 1e-6-grid law)", async () => { + await expectClean("RT7", "geometry is not a verbatim 1e-6-grid polyline6 string"); +}); + +/* ————— RT8 — licence header ————— */ + +test("RT8 licence header — ODbL-1.0 + OpenStreetMap attribution + ODbL notice + OSRM-demo source (T-05-06)", async () => { + await expectClean("RT8", "the ODbL attribution surfaces are damaged — a regeneration must never lose them"); +}); + +/* ————— RT9 — size budget (printed, so growth is visible) ————— */ + +test("RT9 size budget — the gzipped committed dataset is within the corpus budget (T-05-12 early warning)", async () => { + const { corpus, report } = await getReport(); + console.log( + `[corpus] gzipped fixture dataset: ${report.gzipBytes} B / budget ${corpus.fixtureGzipBudgetBytes} B`, + ); + assert.ok(report.gzipBytes > 0, "the gzip measurement must be a real byte count"); + await expectClean("RT9", "fixture payload exceeds the D-11 early-warning budget"); +}); + +/* ————— RT10 — alternatives floor ————— */ + +test("RT10 alternatives floor — >= 4 pairs carry >= 2 routes (RTNG-02 real-data requirement)", async () => { + const { report } = await getReport(); + console.log(`[corpus] pairs with >= 2 routes: ${report.multiRoutePairs}`); + await expectClean("RT10", "too few multi-route pairs for the chips/redraw surface"); +});