Files
arah2/routing/test/corpus.test.mjs
T
araharah-executor 63ef408197 test(05-02): add failing offline corpus tolerance gate suite RT1-RT10 (RED)
- 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)
2026-10-24 05:18:16 +08:00

173 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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");
});