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)
This commit is contained in:
araharah-executor
2026-10-24 05:18:16 +08:00
parent 4e89cff552
commit 63ef408197
+172
View File
@@ -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");
});