feat(06-04): R2 upload + A*-CPU measurement gate + the A1 ladder executed to its decision point

- upload-road-graph.mjs: wrangler-shelled maintainer tool (pre-flight 1-80MB,
  round-trip verify hint); bucket araharah-graphs NOT created — R2 is not yet
  enabled on the account (wrangler code 10042) — dashboard one-time activation
  is the phase's user_setup item
- measure-route-cpu.mjs: 25-pair gate (corpus + seeded quadrant spread),
  hrtime around routeAstar, warm-up excluded, unreachable probes reported
  SEPARATELY from the routed p95, exits 1 over the 8ms budget
- Rule 1 (measured): the class set lacked *_link ramps — the graph FRAGMENTED
  (7.7% of nodes in severed motorway islands; corpus pairs 'unreachable').
  5 link classes added -> 311325 nodes / 677068 edges / 14.7 MB, all landmarks
  in one component; enforced floor corrected 1KB -> 1MB (D-01)
- ladder (a) drop tertiary+residential: p95 17.8ms but 12/25 pairs unreachable
  — REJECTED on correctness (D-03), reverted, ladder history recorded
- ladder (b) bidirectional A* (BH, Ikeda potentials, reverse-CSR WeakMap):
  provably optimal (A1 625-pair sweep green), alternation balanced by heap
  size (raw-top comparison starves the backward side 19501:3), reduction
  signs in the standard feasible form. Measured: p95 ~38-93ms — NO GATE PASS.
  The potential split halves each side's steering, cancelling the halved radius
- GATE FAIL is the honest verdict: ladder (c) — Workers Paid vs layout-v2 ALT
  (still $0) vs free-plan-with-degradation — requires maintainer sign-off;
  docs/ROAD-GRAPH.md carries the decision table and the fork contract
This commit is contained in:
araharah-executor
2026-10-24 13:56:18 +08:00
parent 020eb92058
commit ea9584d163
5 changed files with 666 additions and 84 deletions
+44 -8
View File
@@ -70,8 +70,27 @@ const BBOX_MARGIN_DEG = 0.05;
/** Node dedup granularity: 1e-6 deg ≈ 0.11 m — far under GPS noise. */
const COORD_ROUND_DECIMALS = 6;
/** drivable classes ONLY (06-RESEARCH Pitfall 1): footway/cycleway/service/
* path are deliberately absent — a walking path must never enter a road A*. */
const DRIVABLE_CLASSES = new Set([
* path are deliberately absent — a walking path must never enter a road A*.
* The *_link classes are LOAD-BEARING (measured 2026-10-24): they are the
* interchange ramps tying motorways/trunk/primary into the surface network.
* Without them the graph FRAGMENTS — 7.7% of nodes sat in severed motorway
* islands and golden corpus pairs resolved "unreachable" because an endpoint
* snapped onto an isolated fragment.
*
* 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
* corpus pairs route — connectivity correct, CPU over budget.
* 2026-10-24 ladder step (a) — drop tertiary+residential (127k nodes):
* p95 17.8 ms BUT 12/25 pairs (incl. 8/15 golden corpus pairs) became
* UNREACHABLE — the dropped classes were the fine mesh tying landmarks
* to the arterial network. REJECTED on correctness (D-03: an honest
* 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
* CPU gate is carried by ladder step (b) — bidirectional A* in
* routing/src/astar.ts — and the recorded verdict in the plan summary.
* Ladder step (c) — Workers Paid — breaks the $0 law and requires user
* sign-off; it is NOT applied in this script. */
const ACTIVE_CLASSES = new Set([
"motorway",
"trunk",
"primary",
@@ -80,6 +99,11 @@ const DRIVABLE_CLASSES = new Set([
"residential",
"unclassified",
"living_street",
"motorway_link",
"trunk_link",
"primary_link",
"secondary_link",
"tertiary_link",
]);
/** Class speed defaults (km/h) when a way carries no parsable maxspeed. */
const CLASS_SPEED_KMH = {
@@ -92,8 +116,16 @@ const CLASS_SPEED_KMH = {
unclassified: 30,
living_street: 20,
};
/** Motorway link ramps inherit the parent class speed via these links. */
const LINK_CLASS_SPEED_KMH = { motorway_link: 60, trunk_link: 60 };
/** Motorway link ramps inherit a near-parent speed via these links. */
const LINK_CLASS_SPEED_KMH = {
motorway_link: 60,
trunk_link: 60,
primary_link: 50,
secondary_link: 40,
tertiary_link: 40,
};
/** ENFORCED floor — an at/below-1 MB "KL road graph" is a broken build (D-01). */
const MIN_BINARY_BYTES = 1 * 1024 * 1024;
function fail(message) {
console.error(`build-road-graph: ${message}`);
@@ -122,7 +154,7 @@ if (checkIdx !== -1) {
` bytes: ${size}`,
);
if (size > MAX_BINARY_BYTES) fail(`--check: ${size} bytes exceeds the ${MAX_BINARY_BYTES}-byte D-01 envelope`);
if (size <= 1024) fail(`--check: ${size} bytes is implausibly small — not a real road graph`);
if (size <= MIN_BINARY_BYTES) fail(`--check: ${size} bytes is at/below the ${MIN_BINARY_BYTES}-byte floor — not a real road graph`);
process.exit(0);
}
@@ -175,7 +207,7 @@ console.log(
/* ————— the Overpass fetch (ONE request, drivable classes only) ————— */
const CLASSES_RE = "^(" + [...DRIVABLE_CLASSES].join("|") + ")$";
const CLASSES_RE = "^(" + [...ACTIVE_CLASSES].join("|") + ")$";
const query =
`[out:json][timeout:${Math.floor(REQUEST_TIMEOUT_MS / 1000)}];` +
`way["highway"~"${CLASSES_RE}"](${bbox.minLat},${bbox.minLng},${bbox.maxLat},${bbox.maxLng});` +
@@ -243,7 +275,7 @@ function effectiveKmh(highway, maxspeedTag) {
for (const way of ways) {
const highway = way.tags?.highway;
if (typeof highway !== "string" || !DRIVABLE_CLASSES.has(highway)) continue;
if (typeof highway !== "string" || !ACTIVE_CLASSES.has(highway)) continue;
const geometry = way.geometry;
if (!Array.isArray(geometry) || geometry.length < 2) continue;
const kmh = effectiveKmh(highway, way.tags?.maxspeed);
@@ -316,11 +348,15 @@ console.log(
` binary size: ${mb(bytes)} (${bytes} B)`,
);
// THE ENFORCED GATE (D-01): over the ceiling → exit 1 WITHOUT writing.
// THE ENFORCED GATE (D-01): outside the 1-80 MB envelope → exit 1 WITHOUT writing.
if (bytes > MAX_BINARY_BYTES) {
fail(`binary is ${mb(bytes)} — over the ${mb(MAX_BINARY_BYTES)} D-01 envelope. ` +
`Tighten DRIVABLE_CLASSES (drop tertiary/residential first) or shrink the bbox, then rebuild.`);
}
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. ` +
`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) {