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:
@@ -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");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user