#!/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"); });