feat(08.1-02): replace the ALT build stage with measured CH preprocessing (GREEN)
- The build no longer imports landmark.ts (deleted in 08.1-01) and has no --landmarks knob: the compacted directed edges feed contractGraph (the ONE CH construction authority) and the tables encode through the single encodeGraph authority as the ch: block (never landmarks:). - Any contraction/encode throw names the CH preprocessing stage and exits 1 BEFORE any write — collect-then-write keeps the previous binary intact (T-08.1-10). - The build prints the measured CH stats block (D-08/D-09): contracted nodes, shortcuts + ratio per node, witness searches + settle-budget hits, chRank bytes, shortcut bytes, wall-clock over contraction + encode, and total binary bytes. No ARG3 payload ceiling exists (D-08) — the enforced 1-80 MB D-01 envelope is unchanged and still fails before any write. - The stale ARG2-era 30 MB forecast advisory is retired with the layout it forecasted; the measurement itself is the record. - --check becomes a pure decode+facts tool for any valid ARG3 (the committed mini fixture included): prints chShortcutCount, shortcut ratio, chRank length; the envelope appears as an advisory line — the enforced gates stay in the build path.
This commit is contained in:
@@ -18,24 +18,24 @@
|
|||||||
* Pitfall 1: an unfiltered highway=* graph is the classic CPU-budget kill).
|
* Pitfall 1: an unfiltered highway=* graph is the classic CPU-budget kill).
|
||||||
* 3. Ways → nodes (deduplicated by rounded coordinate) → directed edges with
|
* 3. Ways → nodes (deduplicated by rounded coordinate) → directed edges with
|
||||||
* oneway flags, speed-derived seconds, and street-name pool ids.
|
* oneway flags, speed-derived seconds, and street-name pool ids.
|
||||||
* 4. THE ALT PREPROCESSING STAGE (Phase 8, ladder rung c2): the compacted
|
* 4. THE CH PREPROCESSING STAGE (Phase 08.1): the compacted edges feed the
|
||||||
* edges feed the ONE landmark module (../src/landmark.ts — this script
|
* ONE contraction module (../src/contraction.ts — this script quantizes
|
||||||
* quantizes nothing and implements nothing itself): farthestPointAnchors
|
* nothing and implements nothing itself): contractGraph runs the
|
||||||
* picks L = clamp(round(N/13000), 16, 32) road-network anchors (D-02
|
* deterministic ordering, the overlay-only bounded witness search, and
|
||||||
* scaling rule; anchors are NOT POIs), landmarkTables runs the integer
|
* the directed shortcut emission. Any throw names the stage and its
|
||||||
* ĉ-metric Dijkstras, and landmarkCoverage gates every anchor at
|
* reason and exits 1 BEFORE a byte is written.
|
||||||
* ≥ LANDMARK_MIN_COVERAGE_FRACTION node reach — any gate failure exits 1
|
|
||||||
* before a byte is written.
|
|
||||||
* 5. encodeGraph (../src/graph.ts — the layout's SINGLE authority) produces
|
* 5. encodeGraph (../src/graph.ts — the layout's SINGLE authority) produces
|
||||||
* the pinned ARG2 CSR binary from the edge set + the landmark block.
|
* the pinned ARG3 CSR binary from the edge set + the CH tables (the `ch:`
|
||||||
* COLLECT-THEN-WRITE: routing/data/kl-roadgraph.v1.bin is written ONLY
|
* input — rank + shortcut CSR with unpacking vias). COLLECT-THEN-WRITE:
|
||||||
* after the encode and the stats gate both pass — any failure exits 1
|
* routing/data/kl-roadgraph.v1.bin is written ONLY after the encode and
|
||||||
* with the previous binary untouched.
|
* the stats gate both pass — any failure exits 1 with the previous
|
||||||
* 6. THE STATS GATE prints nodeCount/edgeCount/nameCount/bbox/landmark
|
* binary untouched.
|
||||||
* facts/binary size and enforces the D-01 envelope: size ≤
|
* 6. THE STATS GATE prints nodeCount/edgeCount/bbox/CH facts (contracted
|
||||||
* MAX_BINARY_BYTES or exit 1. A result below the expected 30–80 MB band
|
* nodes, shortcuts + ratio, witness searches + budget hits, CH-section
|
||||||
* prints an ADVISORY warning (A2's forecast) — the ENFORCED floor is the
|
* bytes, build wall-clock, total bytes — D-08/D-09: MEASURED, never
|
||||||
* 1 MB honesty check in --verify, never the forecast band.
|
* hoped) and enforces the D-01 envelope: outside 1-80 MB → exit 1
|
||||||
|
* without writing. There is NO fixed ARG3 payload ceiling (D-08): the
|
||||||
|
* build ships whatever contraction produces, measured.
|
||||||
*
|
*
|
||||||
* The binary is a BUILD ARTIFACT, not committed: routing/data/.gitignore
|
* The binary is a BUILD ARTIFACT, not committed: routing/data/.gitignore
|
||||||
* ignores *.bin — forks build + upload their own (ODbL-derived data stays out
|
* ignores *.bin — forks build + upload their own (ODbL-derived data stays out
|
||||||
@@ -45,19 +45,21 @@
|
|||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node routing/scripts/build-road-graph.mjs
|
* node routing/scripts/build-road-graph.mjs
|
||||||
* # full build: fetch → landmarks → encode → gate → write
|
* # full build: fetch → contract (CH) → encode → gate → write
|
||||||
* # (network fetch; minutes on the KL bbox)
|
* # (network fetch; the CH stage is Dijkstra-heavy —
|
||||||
* node routing/scripts/build-road-graph.mjs --landmarks <n>
|
* # D-09: minutes, not hours, and the wall-clock is
|
||||||
* # full build with an explicit landmark count — the D-02
|
* # printed either way)
|
||||||
* # MEASURED knob (raise L within/at the top of the rule for a
|
|
||||||
* # CPU-gate retry; hard-capped at 128, never a table bomb)
|
|
||||||
* node routing/scripts/build-road-graph.mjs --check <file>
|
* node routing/scripts/build-road-graph.mjs --check <file>
|
||||||
* # decode an existing binary with decodeGraph, print stats
|
* # decode an existing ARG3 binary with decodeGraph, print the
|
||||||
* # incl. the landmark facts, exit 0/1 — NO network
|
* # stats incl. the CH facts, exit 0/1 — NO network. --check is
|
||||||
|
* # a decode+facts tool for ANY valid ARG3 (the committed test
|
||||||
|
* # fixture included): the envelope appears as an ADVISORY line;
|
||||||
|
* # the ENFORCED 1-80 MB gates run in the build path, before
|
||||||
|
* # any write.
|
||||||
*
|
*
|
||||||
* Exit codes: 0 built and gated (or checked green); 1 pre-flight failure,
|
* Exit codes: 0 built and gated (or checked green); 1 pre-flight failure,
|
||||||
* fetch failure, landmark coverage/quantum-gate failure, envelope-gate
|
* fetch failure, CH contraction/encode failure, envelope-gate failure, or
|
||||||
* failure, or --check decode failure.
|
* --check decode failure.
|
||||||
*/
|
*/
|
||||||
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, resolve } from "node:path";
|
import { dirname, resolve } from "node:path";
|
||||||
@@ -76,20 +78,17 @@ const USER_AGENT = "araharah-roadgraph/1.0 (build-time KL road graph; repo: arah
|
|||||||
/** D-01 envelope ceiling — a larger binary must NOT be written (shrink the
|
/** D-01 envelope ceiling — a larger binary must NOT be written (shrink the
|
||||||
* class filter or the bbox instead). */
|
* class filter or the bbox instead). */
|
||||||
const MAX_BINARY_BYTES = 80 * 1024 * 1024;
|
const MAX_BINARY_BYTES = 80 * 1024 * 1024;
|
||||||
/** A2's honest-build forecast floor — crossing it downward is an ADVISORY
|
|
||||||
* (verify the class filter), never a gate. */
|
|
||||||
const EXPECTED_MIN_BYTES = 30 * 1024 * 1024;
|
|
||||||
/** How far the computed bbox extends beyond the committed corpus extremes. */
|
/** How far the computed bbox extends beyond the committed corpus extremes. */
|
||||||
const BBOX_MARGIN_DEG = 0.05;
|
const BBOX_MARGIN_DEG = 0.05;
|
||||||
/** Node dedup granularity: 1e-6 deg ≈ 0.11 m — far under GPS noise. */
|
/** Node dedup granularity: 1e-6 deg ≈ 0.11 m — far under GPS noise. */
|
||||||
const COORD_ROUND_DECIMALS = 6;
|
const COORD_ROUND_DECIMALS = 6;
|
||||||
/** drivable classes ONLY (06-RESEARCH Pitfall 1): footway/cycleway/service/
|
/** drivable classes ONLY (06-RESEARCH Pitfall 1): footway/cycleway/service/
|
||||||
* path are deliberately absent — a walking path must never enter a road A*.
|
* path are deliberately absent — a walking path must never enter a road
|
||||||
* The *_link classes are LOAD-BEARING (measured 2026-10-24): they are the
|
* search. The *_link classes are LOAD-BEARING (measured 2026-10-24): they
|
||||||
* interchange ramps tying motorways/trunk/primary into the surface network.
|
* are the interchange ramps tying motorway/trunk/primary into the surface
|
||||||
* Without them the graph FRAGMENTS — 7.7% of nodes sat in severed motorway
|
* network. Without them the graph FRAGMENTS — 7.7% of nodes sat in severed
|
||||||
* islands and golden corpus pairs resolved "unreachable" because an endpoint
|
* motorway islands and golden corpus pairs resolved "unreachable" because
|
||||||
* snapped onto an isolated fragment.
|
* an endpoint snapped onto an isolated fragment.
|
||||||
*
|
*
|
||||||
* A1 MITIGATION LADDER HISTORY (measure-route-cpu.mjs is the arbiter):
|
* A1 MITIGATION LADDER HISTORY (measure-route-cpu.mjs is the arbiter):
|
||||||
* 2026-10-24 full 13-class linked graph (311k nodes): p95 38.4 ms, ALL
|
* 2026-10-24 full 13-class linked graph (311k nodes): p95 38.4 ms, ALL
|
||||||
@@ -99,14 +98,17 @@ const COORD_ROUND_DECIMALS = 6;
|
|||||||
* UNREACHABLE — the dropped classes were the fine mesh tying landmarks
|
* UNREACHABLE — the dropped classes were the fine mesh tying landmarks
|
||||||
* to the arterial network. REJECTED on correctness (D-03: an honest
|
* to the arterial network. REJECTED on correctness (D-03: an honest
|
||||||
* router-pending for half the product's destinations is not a smaller
|
* router-pending for half the product's destinations is not a smaller
|
||||||
* graph, it is a broken one). REVERTED to the full 13-class set; the
|
* graph, it is a broken one). REVERTED to the full 13-class set.
|
||||||
* CPU gate is carried by ladder step (b) — bidirectional A* in
|
* Ladder step (b) — bidirectional A*: sound, no worse, better on short
|
||||||
* routing/src/astar.ts — and the recorded verdict in the plan summary.
|
* product routes; kept until superseded.
|
||||||
* Ladder step (c) — Workers Paid — breaks the $0 law and requires user
|
* Ladder step (c) — Workers Paid — breaks the $0 law and requires user
|
||||||
* sign-off; it is NOT applied in this script.
|
* sign-off; it is NOT applied in this script.
|
||||||
* Ladder rung (c2) — EXECUTED in this script's preprocessing stage (Phase 8):
|
* Ladder rung (c2) — EXECUTED in Phase 8 (build-time ALT tables): the
|
||||||
* build-time landmark tables + ALT steering; the verdict lives in
|
* CPU verdict measured over budget; the c3 degradation class stayed.
|
||||||
* measure-route-cpu.mjs's history and docs/ROAD-GRAPH.md. */
|
* Ladder lever (Phase 08.1) — Contraction Hierarchies: THIS script's CH
|
||||||
|
* preprocessing stage (step 4 below) replaces steering with structure.
|
||||||
|
* The verdict lives in measure-route-cpu.mjs's history and the budget
|
||||||
|
* record — the class retires only on a passing measurement (D-01). */
|
||||||
const ACTIVE_CLASSES = new Set([
|
const ACTIVE_CLASSES = new Set([
|
||||||
"motorway",
|
"motorway",
|
||||||
"trunk",
|
"trunk",
|
||||||
@@ -141,51 +143,19 @@ const LINK_CLASS_SPEED_KMH = {
|
|||||||
secondary_link: 40,
|
secondary_link: 40,
|
||||||
tertiary_link: 40,
|
tertiary_link: 40,
|
||||||
};
|
};
|
||||||
/** ENFORCED floor — an at/below-1 MB "KL road graph" is a broken build (D-01). */
|
/** ENFORCED floor — an at/below-1 MB "KL road graph" is a broken build (D-01).
|
||||||
|
* The floor and the ceiling above are the ONLY byte gates: the ARG3 payload
|
||||||
|
* itself is measured-first (D-08) — no pre-committed CH payload ceiling. */
|
||||||
const MIN_BINARY_BYTES = 1 * 1024 * 1024;
|
const MIN_BINARY_BYTES = 1 * 1024 * 1024;
|
||||||
/** D-02 scaling rule: the landmark count is NODE-COUNT-PROPORTIONAL, never a
|
|
||||||
* fixed generous constant — per-landmark table bytes scale with N (~1.19
|
const mb = (b) => `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
* MB/landmark at KL's 311k nodes), so over-provisioning explodes at Malaysia
|
|
||||||
* scale exactly where ALT's advantage itself narrows (that tier is
|
|
||||||
* Contraction Hierarchies' job, still deferred). L = clamp(round(N/13,000),
|
|
||||||
* 16, 32) → ~24 at KL scale; a future Malaysia-wide build re-derives the
|
|
||||||
* count from the same rule at its own scale. */
|
|
||||||
const LANDMARK_COUNT_MIN = 16;
|
|
||||||
const LANDMARK_COUNT_MAX = 32;
|
|
||||||
const NODES_PER_LANDMARK = 13000; // 311,325 / 13,000 ≈ 24 at KL scale
|
|
||||||
/** Build-time coverage gate: every anchor's ĉ-Dijkstra must reach at least
|
|
||||||
* this fraction of nodes (the measured KL giant SCC covers ~0.98 — the
|
|
||||||
* headroom is honest). The SAME fraction gates ANY --landmarks value: the
|
|
||||||
* override cannot buy a weaker gate (T-08-11). */
|
|
||||||
const LANDMARK_MIN_COVERAGE_FRACTION = 0.9;
|
|
||||||
/** --landmarks hard cap — the knob exists for measured retries within/at the
|
|
||||||
* top of the D-02 rule, not for unbounded table bombs (T-08-10). The 80 MB
|
|
||||||
* envelope gate fires before any write regardless. */
|
|
||||||
const LANDMARK_COUNT_OVERRIDE_MAX = 128;
|
|
||||||
|
|
||||||
function fail(message) {
|
function fail(message) {
|
||||||
console.error(`build-road-graph: ${message}`);
|
console.error(`build-road-graph: ${message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ————— --landmarks override (the D-02 measured knob; parsed BEFORE --check) ————— */
|
/* ————— --check mode: decode + CH facts, no network ————— */
|
||||||
|
|
||||||
const landmarksIdx = process.argv.indexOf("--landmarks");
|
|
||||||
let landmarksOverride = null;
|
|
||||||
if (landmarksIdx !== -1) {
|
|
||||||
const raw = process.argv[landmarksIdx + 1];
|
|
||||||
const n = raw === undefined ? NaN : Number(raw);
|
|
||||||
if (!Number.isInteger(n) || n < 1) {
|
|
||||||
fail(`--landmarks requires a positive integer count, got ${raw === undefined ? "(missing value)" : `"${raw}"`}`);
|
|
||||||
}
|
|
||||||
if (n > LANDMARK_COUNT_OVERRIDE_MAX) {
|
|
||||||
fail(`--landmarks ${n} exceeds the hard cap ${LANDMARK_COUNT_OVERRIDE_MAX} — the knob exists for measured ` +
|
|
||||||
`retries within/at the top of the D-02 rule, not for unbounded table bombs`);
|
|
||||||
}
|
|
||||||
landmarksOverride = n;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ————— --check mode: decode + stats, no network ————— */
|
|
||||||
|
|
||||||
const checkIdx = process.argv.indexOf("--check");
|
const checkIdx = process.argv.indexOf("--check");
|
||||||
if (checkIdx !== -1) {
|
if (checkIdx !== -1) {
|
||||||
@@ -195,22 +165,10 @@ if (checkIdx !== -1) {
|
|||||||
const { decodeGraph } = await import("../src/graph.ts");
|
const { decodeGraph } = await import("../src/graph.ts");
|
||||||
const buffer = readFileSync(file);
|
const buffer = readFileSync(file);
|
||||||
const graph = decodeGraph(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
const graph = decodeGraph(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
||||||
if (graph === null) fail(`--check: ${file} is NOT a valid GRAPH_VERSION CSR binary (decodeGraph null)`);
|
if (graph === null) fail(`--check: ${file} is NOT a valid ARG3 CSR binary (decodeGraph null)`);
|
||||||
const size = statSync(file).size;
|
const size = statSync(file).size;
|
||||||
// ARG2 landmark facts (Phase 8): decoded straight off the binary — the
|
// ARG3 CH facts (Phase 08.1): read straight off the decoded views — the
|
||||||
// SAME ONE module computes the coverage audit here; NO network.
|
// SAME ONE codec module decoded the bytes; NO network.
|
||||||
const { LANDMARK_SENTINEL, landmarkCoverage } = await import("../src/landmark.ts");
|
|
||||||
let toSentinels = 0;
|
|
||||||
for (let i = 0; i < graph.landmarkTo.length; i++) {
|
|
||||||
if (graph.landmarkTo[i] === LANDMARK_SENTINEL) toSentinels += 1;
|
|
||||||
}
|
|
||||||
let fromSentinels = 0;
|
|
||||||
for (let i = 0; i < graph.landmarkFrom.length; i++) {
|
|
||||||
if (graph.landmarkFrom[i] === LANDMARK_SENTINEL) fromSentinels += 1;
|
|
||||||
}
|
|
||||||
const coverage = landmarkCoverage(graph.landmarkTo, graph.landmarkNodes, graph.nodeCount);
|
|
||||||
const minCoverage = Math.min(...coverage);
|
|
||||||
const meanCoverage = coverage.reduce((a, b) => a + b, 0) / coverage.length;
|
|
||||||
console.log(
|
console.log(
|
||||||
`--check ${file}\n` +
|
`--check ${file}\n` +
|
||||||
` nodeCount: ${graph.nodeCount}\n` +
|
` nodeCount: ${graph.nodeCount}\n` +
|
||||||
@@ -219,14 +177,17 @@ if (checkIdx !== -1) {
|
|||||||
` maxSpeedKmh: ${graph.maxSpeedKmh}\n` +
|
` maxSpeedKmh: ${graph.maxSpeedKmh}\n` +
|
||||||
` bbox: [${graph.bbox.minLat}, ${graph.bbox.minLng}, ${graph.bbox.maxLat}, ${graph.bbox.maxLng}]\n` +
|
` bbox: [${graph.bbox.minLat}, ${graph.bbox.minLng}, ${graph.bbox.maxLat}, ${graph.bbox.maxLng}]\n` +
|
||||||
` bytes: ${size}\n` +
|
` bytes: ${size}\n` +
|
||||||
` landmarkCount: ${graph.landmarkCount}\n` +
|
` chShortcutCount: ${graph.chShortcutCount}\n` +
|
||||||
` landmarkQuantumS: ${graph.landmarkQuantumS.toFixed(3)}\n` +
|
` shortcut ratio: ${(graph.chShortcutCount / graph.nodeCount).toFixed(2)} per node\n` +
|
||||||
` landmarkNodes: ${[...graph.landmarkNodes].join(", ")}\n` +
|
` chRank length: ${graph.chRank.length}`,
|
||||||
` landmark coverage min/mean: ${minCoverage.toFixed(3)} / ${meanCoverage.toFixed(3)}\n` +
|
|
||||||
` landmark sentinel cells: ${toSentinels} to / ${fromSentinels} from`,
|
|
||||||
);
|
);
|
||||||
if (size > MAX_BINARY_BYTES) fail(`--check: ${size} bytes exceeds the ${MAX_BINARY_BYTES}-byte D-01 envelope`);
|
// Advisory only (see the usage note): --check serves any valid ARG3, the
|
||||||
if (size <= MIN_BINARY_BYTES) fail(`--check: ${size} bytes is at/below the ${MIN_BINARY_BYTES}-byte floor — not a real road graph`);
|
// committed test fixture included. The ENFORCED gates run in the build path.
|
||||||
|
if (size > MAX_BINARY_BYTES || size <= MIN_BINARY_BYTES) {
|
||||||
|
console.log(
|
||||||
|
` envelope advisory: ${mb(size)} sits outside the 1-80 MB D-01 envelope a built KL metro graph lands in (advisory only)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +333,7 @@ for (const way of ways) {
|
|||||||
// forward direction: blocked when the way is reverse-only
|
// forward direction: blocked when the way is reverse-only
|
||||||
rawEdges.push({ from, to, seconds, flags: reverseOnly ? 1 : 0, nameId });
|
rawEdges.push({ from, to, seconds, flags: reverseOnly ? 1 : 0, nameId });
|
||||||
// reverse direction: blocked when the way is forward-only (bit0 set —
|
// reverse direction: blocked when the way is forward-only (bit0 set —
|
||||||
// stays visible to snapping, never traversed by A*)
|
// stays visible to snapping, never traversed by the search)
|
||||||
rawEdges.push({ from: to, to: from, seconds, flags: forwardOnly ? 1 : 0, nameId });
|
rawEdges.push({ from: to, to: from, seconds, flags: forwardOnly ? 1 : 0, nameId });
|
||||||
}
|
}
|
||||||
from = to;
|
from = to;
|
||||||
@@ -380,7 +341,8 @@ for (const way of ways) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Degree-0 cleanup: a node that lost every segment (collapsed self-loops) is
|
/* Degree-0 cleanup: a node that lost every segment (collapsed self-loops) is
|
||||||
* dead weight — compact the id space so A* and the snap grid never scan it. */
|
* dead weight — compact the id space so the search and the snap grid never
|
||||||
|
* scan it. */
|
||||||
const degree = new Uint32Array(latOut.length);
|
const degree = new Uint32Array(latOut.length);
|
||||||
for (const edge of rawEdges) {
|
for (const edge of rawEdges) {
|
||||||
degree[edge.from] += 1;
|
degree[edge.from] += 1;
|
||||||
@@ -402,86 +364,53 @@ const edges = rawEdges
|
|||||||
const droppedNodes = latOut.length - cLat.length;
|
const droppedNodes = latOut.length - cLat.length;
|
||||||
if (cLat.length === 0 || edges.length === 0) fail("the extract produced an empty graph — refusing to write");
|
if (cLat.length === 0 || edges.length === 0) fail("the extract produced an empty graph — refusing to write");
|
||||||
|
|
||||||
/* ————— ALT preprocessing stage (Phase 8 rung c2): landmark tables through the ONE module —————
|
/* ————— CH preprocessing stage (Phase 08.1): the hierarchy through the ONE module —————
|
||||||
*
|
*
|
||||||
* This script quantizes nothing and implements nothing itself: the graph's
|
* This script quantizes nothing and implements nothing itself: the graph's
|
||||||
* edges feed ../src/landmark.ts (farthestPointAnchors + landmarkTables +
|
* edges feed ../src/contraction.ts (contractGraph — ordering + contraction +
|
||||||
* landmarkCoverage) — there is no second integer-Dijkstra implementation here
|
* the overlay-only bounded witness search + shortcut CSR emission). There is
|
||||||
* (single-authority law). Gates run BEFORE any write:
|
* no second CH implementation here (single-authority law). The stage and the
|
||||||
* - quantum ceiling: a landmarkTables throw means the ĉ metric overflowed
|
* encode share one wall-clock measurement (D-09: the number prints either
|
||||||
* the u16 domain → raise LANDMARK_QUANTUM_S in landmark.ts, never clamp
|
* way — an over-run is an honest measurement, not a hidden failure), and any
|
||||||
* (Pitfall 5);
|
* throw names the stage and exits 1 BEFORE a byte is written (T-08.1-10:
|
||||||
* - coverage: every anchor reaches ≥ LANDMARK_MIN_COVERAGE_FRACTION of the
|
* the previous binary survives every build failure — collect-then-write).
|
||||||
* graph — a fragmented extract fails honestly, it is never forced through.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { LANDMARK_QUANTUM_S, LANDMARK_SENTINEL, farthestPointAnchors, landmarkTables, landmarkCoverage } =
|
const { contractGraph } = await import("../src/contraction.ts");
|
||||||
await import("../src/landmark.ts");
|
|
||||||
|
|
||||||
// D-02 scaling rule: the count derives from THIS graph's node count; a
|
|
||||||
// --landmarks override (validated at argv time) replaces the rule wholesale.
|
|
||||||
const ruleRaw = Math.round(cLat.length / NODES_PER_LANDMARK);
|
|
||||||
const ruleCount = Math.min(LANDMARK_COUNT_MAX, Math.max(LANDMARK_COUNT_MIN, ruleRaw));
|
|
||||||
const landmarkCount = landmarksOverride ?? ruleCount;
|
|
||||||
console.log(
|
|
||||||
`landmarks: rule round(${cLat.length} / ${NODES_PER_LANDMARK}) = ${ruleRaw} → clamped ${ruleCount}` +
|
|
||||||
(landmarksOverride !== null ? `; --landmarks override → L = ${landmarkCount}` : `; L = ${landmarkCount}`),
|
|
||||||
);
|
|
||||||
|
|
||||||
// The module's input shape — projected ONCE (no mutation of the encode-side
|
|
||||||
// edge array; encodeGraph re-groups edges itself).
|
|
||||||
const lmEdges = edges.map((e) => ({ from: e.from, to: e.to, seconds: e.seconds, flags: e.flags }));
|
|
||||||
const lmInput = { nodeCount: cLat.length, edges: lmEdges, quantumS: LANDMARK_QUANTUM_S };
|
|
||||||
const anchors = farthestPointAnchors(lmInput, landmarkCount);
|
|
||||||
if (anchors.length === 0) {
|
|
||||||
fail("farthestPointAnchors found no degree>=2 candidate — the extract has no junction-ish node (degenerate/fragmented graph)");
|
|
||||||
}
|
|
||||||
if (anchors.length < landmarkCount) {
|
|
||||||
fail(`farthestPointAnchors returned only ${anchors.length} of ${landmarkCount} requested anchors (candidates exhausted) — ` +
|
|
||||||
`refusing to encode a weaker ALT than the rule asked for; investigate the extract before rebuilding`);
|
|
||||||
}
|
|
||||||
let to;
|
|
||||||
let from;
|
|
||||||
try {
|
|
||||||
({ to, from } = landmarkTables(lmInput, anchors));
|
|
||||||
} catch (err) {
|
|
||||||
fail(`landmark tables exceeded the u16 quantum ceiling (${err.message}) — raise LANDMARK_QUANTUM_S in ` +
|
|
||||||
`routing/src/landmark.ts and rebuild (Pitfall 5: raise the quantum, never clamp)`);
|
|
||||||
}
|
|
||||||
// Coverage gate (research Pattern 3, layer 1): the SAME fraction for ANY L —
|
|
||||||
// the override cannot lower it (T-08-11).
|
|
||||||
const coverage = landmarkCoverage(to, anchors, cLat.length);
|
|
||||||
const minCoverage = Math.min(...coverage);
|
|
||||||
if (minCoverage < LANDMARK_MIN_COVERAGE_FRACTION) {
|
|
||||||
const worst = coverage.indexOf(minCoverage);
|
|
||||||
fail(
|
|
||||||
`landmark coverage gate FAILED at L=${landmarkCount}: anchor ${worst} (node ${anchors[worst]}) reaches only ` +
|
|
||||||
`${(minCoverage * 100).toFixed(1)}% of nodes (gate: ≥ ${(LANDMARK_MIN_COVERAGE_FRACTION * 100).toFixed(0)}%). ` +
|
|
||||||
`The KL extract is expected to be ONE giant SCC covering ~98% of nodes (2026-10-24 build fact) — ` +
|
|
||||||
`a miss this large means a fragmented extract; investigate the fetch, do NOT lower the gate.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const meanCoverage = coverage.reduce((a, b) => a + b, 0) / coverage.length;
|
|
||||||
// Sentinel audit — cheap honesty; --check mirrors it from the written bytes.
|
|
||||||
let toSentinels = 0;
|
|
||||||
for (let i = 0; i < to.length; i++) if (to[i] === LANDMARK_SENTINEL) toSentinels += 1;
|
|
||||||
let fromSentinels = 0;
|
|
||||||
for (let i = 0; i < from.length; i++) if (from[i] === LANDMARK_SENTINEL) fromSentinels += 1;
|
|
||||||
|
|
||||||
/* ————— encode (the layout's single authority) + stats gate ————— */
|
|
||||||
|
|
||||||
const names = [...nameIds.keys()];
|
const names = [...nameIds.keys()];
|
||||||
const buffer = encodeGraph({
|
// The module's input shape — projected ONCE (no nameId: the CH overlay runs
|
||||||
|
// on { from, to, seconds, flags }; encodeGraph re-groups the full edges itself).
|
||||||
|
const chEdges = edges.map((e) => ({ from: e.from, to: e.to, seconds: e.seconds, flags: e.flags }));
|
||||||
|
|
||||||
|
const buildT0 = process.hrtime.bigint();
|
||||||
|
let ch;
|
||||||
|
let buffer;
|
||||||
|
try {
|
||||||
|
ch = contractGraph({ nodeCount: cLat.length, edges: chEdges });
|
||||||
|
buffer = encodeGraph({
|
||||||
lat: cLat,
|
lat: cLat,
|
||||||
lng: cLng,
|
lng: cLng,
|
||||||
edges,
|
edges,
|
||||||
names,
|
names,
|
||||||
maxSpeedKmh: maxSeenKmh,
|
maxSpeedKmh: maxSeenKmh,
|
||||||
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
|
ch: {
|
||||||
|
rank: ch.rank,
|
||||||
|
shortcutStart: ch.shortcutStart,
|
||||||
|
shortcutTarget: ch.shortcutTarget,
|
||||||
|
shortcutSeconds: ch.shortcutSeconds,
|
||||||
|
shortcutVia: ch.shortcutVia,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
fail(`CH preprocessing stage failed: ${err.message}`);
|
||||||
|
}
|
||||||
|
const buildMs = Number(process.hrtime.bigint() - buildT0) / 1e6;
|
||||||
const bytes = buffer.byteLength;
|
const bytes = buffer.byteLength;
|
||||||
|
|
||||||
const mb = (b) => `${(b / (1024 * 1024)).toFixed(1)} MB`;
|
/* ————— stats print (measured, BEFORE the gates — a gate refusal still
|
||||||
|
* leaves every number on the terminal) + the D-01 envelope gate ————— */
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`graph stats:\n` +
|
`graph stats:\n` +
|
||||||
` nodeCount: ${cLat.length}${droppedNodes > 0 ? ` (${droppedNodes} degree-0 dropped)` : ""}\n` +
|
` nodeCount: ${cLat.length}${droppedNodes > 0 ? ` (${droppedNodes} degree-0 dropped)` : ""}\n` +
|
||||||
@@ -493,16 +422,17 @@ console.log(
|
|||||||
` binary size: ${mb(bytes)} (${bytes} B)`,
|
` binary size: ${mb(bytes)} (${bytes} B)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// The landmark facts print BEFORE the size gates — the gate numbers exist
|
// The CH facts (D-08/D-09): shortcut economy, witness behavior, section
|
||||||
// outside the terminal even when a gate then refuses the write.
|
// bytes, wall-clock, total bytes — the measured payload, never a forecast.
|
||||||
const tableBytes = 2 * landmarkCount * cLat.length * 2 + 4 * landmarkCount;
|
|
||||||
console.log(
|
console.log(
|
||||||
`landmark stats (ALT preprocessing through the ONE module):\n` +
|
`CH stats (contraction through the ONE module):\n` +
|
||||||
` L: ${landmarkCount} (D-02 rule ${ruleCount}${landmarksOverride !== null ? `, --landmarks override ${landmarksOverride}` : ""})\n` +
|
` contracted nodes: ${cLat.length}\n` +
|
||||||
` quantum: ${LANDMARK_QUANTUM_S} s (the ĉ unit)\n` +
|
` shortcuts: ${ch.stats.shortcutCount} (${ch.stats.ratioPerNode.toFixed(2)} per node)\n` +
|
||||||
` coverage min/mean: ${minCoverage.toFixed(3)} / ${meanCoverage.toFixed(3)} (gate ≥ ${LANDMARK_MIN_COVERAGE_FRACTION})\n` +
|
` witness searches: ${ch.stats.witnessSearches} (${ch.stats.witnessBudgetHits} hit the settle budget)\n` +
|
||||||
` table bytes: ${tableBytes} (${mb(tableBytes)} = 2 families × L·N u16 + L u32 anchors)\n` +
|
` chRank bytes: ${4 * cLat.length}\n` +
|
||||||
` sentinel cells: ${toSentinels} to / ${fromSentinels} from`,
|
` shortcut bytes: ${12 * ch.stats.shortcutCount}\n` +
|
||||||
|
` build wall-clock: ${buildMs.toFixed(0)} ms\n` +
|
||||||
|
` total binary: ${mb(bytes)} (${bytes} B)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// THE ENFORCED GATE (D-01): outside the 1-80 MB envelope → exit 1 WITHOUT writing.
|
// THE ENFORCED GATE (D-01): outside the 1-80 MB envelope → exit 1 WITHOUT writing.
|
||||||
@@ -514,19 +444,11 @@ if (bytes <= MIN_BINARY_BYTES) {
|
|||||||
fail(`binary is ${bytes} bytes — at/below the ${MIN_BINARY_BYTES}-byte floor; a real metro graph is never this small. ` +
|
fail(`binary is ${bytes} bytes — at/below the ${MIN_BINARY_BYTES}-byte floor; a real metro graph is never this small. ` +
|
||||||
`Verify the Overpass query actually returned ways, then rebuild.`);
|
`Verify the Overpass query actually returned ways, then rebuild.`);
|
||||||
}
|
}
|
||||||
// THE ADVISORY BAND (A2's 18–50 MB forecast vs D-01's 30–80 MB expectation):
|
|
||||||
// a small graph is a review prompt, not a failure — the enforced gate stays.
|
|
||||||
if (bytes < EXPECTED_MIN_BYTES) {
|
|
||||||
console.log(
|
|
||||||
`WARNING: ${mb(bytes)} is below the expected 30–80 MB band — verify the drivable-class filter ` +
|
|
||||||
`(A2 forecast: 18–50 MB for KL metro). Advisory only; the enforced gate is 1–80 MB.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ————— collect-then-write (the committed/previous binary survives failures) ————— */
|
/* ————— collect-then-write (the committed/previous binary survives failures) ————— */
|
||||||
|
|
||||||
mkdirSync(dirname(OUT_PATH), { recursive: true });
|
mkdirSync(dirname(OUT_PATH), { recursive: true });
|
||||||
writeFileSync(OUT_PATH, Buffer.from(buffer));
|
writeFileSync(OUT_PATH, Buffer.from(buffer));
|
||||||
console.log(`wrote ${OUT_PATH} — ${mb(bytes)}, ${cLat.length} nodes, ${edges.length} edges`);
|
console.log(`wrote ${OUT_PATH} — ${mb(bytes)}, ${cLat.length} nodes, ${edges.length} edges, ${ch.stats.shortcutCount} shortcuts`);
|
||||||
console.log("next: node routing/scripts/upload-road-graph.mjs # push the binary to your R2 bucket");
|
console.log("next: node routing/scripts/upload-road-graph.mjs # push the binary to your R2 bucket");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user