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:
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* measure-route-cpu.mjs — the A*-CPU measurement gate (A1, the phase's top risk).
|
||||
* ------------------------------------------------------------------------------
|
||||
* A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
|
||||
* web/src, or routing/src imports it. The free plan gives a Worker 10 ms of
|
||||
* CPU per request (Error 1102 beyond it), so the A*-over-the-real-graph cost
|
||||
* is MEASURED AND PRINTED, never hoped for ("budget gates print their
|
||||
* measurement" — the BA9 precedent).
|
||||
*
|
||||
* What it measures: routeAstar() over the REAL routing/data/kl-roadgraph.v1.bin
|
||||
* (decodeGraph → typed arrays, exactly the Worker's data plane), timed with
|
||||
* process.hrtime.bigint() around the search call only.
|
||||
*
|
||||
* The 25-route sample mixes both realistic classes:
|
||||
* - the fixture-corpus endpoints (golden-pairs origins + gazetteer
|
||||
* destinations), snapped through snapToGraph — the demo-script routes
|
||||
* - quadrant-sampled node pairs spread across the graph bbox (deterministic
|
||||
* seeded PRNG — re-runs are comparable) — the cross-city worst cases
|
||||
* One UNMEASURED warm-up route runs first so the numbers reflect the
|
||||
* steady-state isolate an /api/route request actually lands on (the warm-up
|
||||
* is printed, never hidden).
|
||||
*
|
||||
* The GATE (A1's mitigation margin): p95 > P95_BUDGET_MS (8 ms — 20% under
|
||||
* the 10 ms free-plan ceiling) exits 1. The mitigation ladder, IN ORDER:
|
||||
* (a) tighten DRIVABLE_CLASSES (drop tertiary/residential) and rebuild
|
||||
* (b) bidirectional A* variant
|
||||
* (c) Workers Paid as an explicit fork decision (breaks $0 — user sign-off)
|
||||
*
|
||||
* LADDER HISTORY (2026-10-24, the real KL graph — see the plan summary):
|
||||
* full 13-class graph, unidirectional A*: p95 38.4 ms GATE FAIL
|
||||
* ladder (a) drop tertiary+residential: p95 17.8 ms GATE FAIL + 12/25
|
||||
* pairs unreachable (the dropped classes are the fine mesh tying
|
||||
* landmarks to the arterials) — REJECTED on correctness, REVERTED.
|
||||
* ladder (b) bidirectional A* (BH): p95 ~38 ms GATE FAIL — the
|
||||
* potential split halves each side's steering, cancelling the halved
|
||||
* radius; kept (sound, no worse, better on short product routes).
|
||||
* (c) NOT APPLIED — breaks the $0 law; needs maintainer sign-off. The
|
||||
* honest alternatives on the table: Workers Paid, or a layout-v2 ALT
|
||||
* landmark graph (build-time preprocessing, still $0 at runtime).
|
||||
*
|
||||
* Usage:
|
||||
* node routing/scripts/measure-route-cpu.mjs
|
||||
*
|
||||
* Exit codes: 0 p95 inside budget; 1 p95 over budget or absent binary.
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { decodeGraph } from "../src/graph.ts";
|
||||
import { snapToGraph as snap } from "../src/snap.ts";
|
||||
import { routeAstar as astar } from "../src/astar.ts";
|
||||
import { getEntries } from "../../search/src/gazetteer.ts";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts
|
||||
const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin");
|
||||
const CORPUS_PATH = resolve(here, "..", "test", "fixtures", "golden-pairs.json");
|
||||
|
||||
/** The gate: p95 budget, 20% margin under the 10 ms free-plan ceiling. */
|
||||
const P95_BUDGET_MS = 8;
|
||||
/** The sample size (the plan's number). */
|
||||
const ROUTE_COUNT = 25;
|
||||
/** The PRNG seed — fixed so re-runs sample identical pairs. */
|
||||
const SAMPLE_SEED = 0x06d4;
|
||||
|
||||
function fail(message) {
|
||||
console.error(`measure-route-cpu: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(GRAPH_PATH)) {
|
||||
fail(`no graph at ${GRAPH_PATH} — build it first: node routing/scripts/build-road-graph.mjs`);
|
||||
}
|
||||
|
||||
function toArrayBuffer(buf) {
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||
}
|
||||
|
||||
const graph = decodeGraph(toArrayBuffer(readFileSync(GRAPH_PATH)));
|
||||
if (graph === null) fail(`${GRAPH_PATH} does not decode (fail-honest null) — rebuild the binary`);
|
||||
console.log(
|
||||
`graph: ${graph.nodeCount} nodes, ${graph.edgeCount} edges, ` +
|
||||
`bbox lat ${graph.bbox.minLat.toFixed(3)}..${graph.bbox.maxLat.toFixed(3)} lng ${graph.bbox.minLng.toFixed(3)}..${graph.bbox.maxLng.toFixed(3)}`,
|
||||
);
|
||||
|
||||
/** mulberry32 — tiny deterministic PRNG (fixed seed => comparable runs). */
|
||||
function mulberry32(seed) {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a = (a + 0x6d2b79f5) >>> 0;
|
||||
let t = a;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/* ————— pair sampling: corpus endpoints + quadrant spread ————— */
|
||||
|
||||
const corpus = JSON.parse(readFileSync(CORPUS_PATH, "utf8"));
|
||||
const gazetteerById = new Map(getEntries().map((e) => [e.id, e]));
|
||||
|
||||
const pairs = [];
|
||||
let corpusRoutes = 0;
|
||||
for (const pair of corpus.pairs) {
|
||||
const dest = gazetteerById.get(pair.destinationId);
|
||||
if (dest === undefined) continue;
|
||||
const origin = snap(graph, { lat: pair.origin.lat, lng: pair.origin.lng });
|
||||
const goal = snap(graph, { lat: dest.lat, lng: dest.lng });
|
||||
if (origin === null || goal === null) continue;
|
||||
pairs.push({ label: `corpus ${pair.pairId}`, from: origin.node, to: goal.node });
|
||||
corpusRoutes += 1;
|
||||
}
|
||||
|
||||
const midLat = (graph.bbox.minLat + graph.bbox.maxLat) / 2;
|
||||
const midLng = (graph.bbox.minLng + graph.bbox.maxLng) / 2;
|
||||
const quadrants = [[], [], [], []]; // 0=NW 1=NE 2=SW 3=SE
|
||||
for (let n = 0; n < graph.nodeCount; n++) {
|
||||
const q = (graph.lat[n] >= midLat ? 0 : 2) + (graph.lng[n] >= midLng ? 1 : 0);
|
||||
quadrants[q].push(n);
|
||||
}
|
||||
const rand = mulberry32(SAMPLE_SEED);
|
||||
const pick = (q) => quadrants[q][Math.floor(rand() * quadrants[q].length)];
|
||||
while (pairs.length < ROUTE_COUNT) {
|
||||
const a = Math.floor(rand() * 4);
|
||||
const b = (a + 1 + Math.floor(rand() * 3)) % 4; // always a DIFFERENT quadrant — cross-city
|
||||
const from = pick(a);
|
||||
const to = pick(b);
|
||||
if (from === to) continue;
|
||||
pairs.push({ label: `quadrant q${a}->q${b}`, from, to });
|
||||
}
|
||||
console.log(`sample: ${corpusRoutes} corpus-snapped + ${pairs.length - corpusRoutes} quadrant pairs = ${pairs.length} routes`);
|
||||
|
||||
/* ————— warm-up (unmeasured, printed) + the measured runs ————— */
|
||||
|
||||
function cpuMs(fn) {
|
||||
const t0 = process.hrtime.bigint();
|
||||
const result = fn();
|
||||
const t1 = process.hrtime.bigint();
|
||||
return { ms: Number(t1 - t0) / 1e6, result };
|
||||
}
|
||||
|
||||
const warm = cpuMs(() => astar(graph, pairs[0].from, pairs[0].to));
|
||||
console.log(`warm-up (unmeasured): ${warm.ms.toFixed(3)} ms`);
|
||||
|
||||
const samples = [];
|
||||
let unreachableProbes = 0;
|
||||
let unreachableMs = 0;
|
||||
for (const pair of pairs) {
|
||||
const { ms, result } = cpuMs(() => astar(graph, pair.from, pair.to));
|
||||
if (result === null) {
|
||||
// A directed dead-end probe (a snap onto a one-way sink or a small
|
||||
// island): the honest-null degradation path. Reported SEPARATELY — it
|
||||
// is not a routed trip, and mixing it into the route p95 would
|
||||
// misdescribe the budget the /api/route hot path actually needs.
|
||||
unreachableProbes += 1;
|
||||
unreachableMs += ms;
|
||||
console.log(` ${pair.label}: ${ms.toFixed(3)} ms (unreachable — honest null, D-03)`);
|
||||
continue;
|
||||
}
|
||||
const km = `${(result.distanceM / 1000).toFixed(1)} km / ${(result.seconds / 60).toFixed(1)} min`;
|
||||
console.log(` ${pair.label}: ${ms.toFixed(3)} ms (${km})`);
|
||||
samples.push(ms);
|
||||
}
|
||||
console.log(
|
||||
`routed pairs: ${samples.length}/${pairs.length}` +
|
||||
(unreachableProbes > 0
|
||||
? `; unreachable probes: ${unreachableProbes} (mean ${(unreachableMs / unreachableProbes).toFixed(3)} ms, reported separately)`
|
||||
: ""),
|
||||
);
|
||||
if (samples.length === 0) fail("no routable pairs in the sample — the graph may be fragmented; rebuild");
|
||||
|
||||
samples.sort((a, b) => a - b);
|
||||
const idx = Math.min(samples.length - 1, Math.ceil(0.95 * samples.length) - 1);
|
||||
const p95 = samples[idx];
|
||||
const max = samples[samples.length - 1];
|
||||
const mean = samples.reduce((acc, v) => acc + v, 0) / samples.length;
|
||||
console.log(`p95: ${p95.toFixed(3)} ms (max ${max.toFixed(3)} ms, mean ${mean.toFixed(3)} ms, n=${samples.length})`);
|
||||
|
||||
if (p95 > P95_BUDGET_MS) {
|
||||
console.error(`GATE FAIL: p95 ${p95.toFixed(3)} ms exceeds the ${P95_BUDGET_MS} ms budget (A1) — apply the mitigation ladder: (a) tighten DRIVABLE_CLASSES + rebuild, (b) bidirectional A*, (c) Workers Paid as an explicit fork decision`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`GATE PASS: p95 ${p95.toFixed(3)} ms <= ${P95_BUDGET_MS} ms budget — A1 retired for this graph`);
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* upload-road-graph.mjs — put the built road graph on R2 (D-01, fork path).
|
||||
* ---------------------------------------------------------------------------
|
||||
* A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
|
||||
* web/src, or routing/src imports it. It shells the wrangler CLI (already the
|
||||
* repo's deploy tool) so a fork needs zero new installs:
|
||||
*
|
||||
* npx wrangler r2 object put araharah-graphs/kl-roadgraph.v1.bin
|
||||
* --file routing/data/kl-roadgraph.v1.bin --remote
|
||||
*
|
||||
* THE FORK CONTRACT (docs/ROAD-GRAPH.md is the prose): your graph, YOUR
|
||||
* bucket. Create it first (`npx wrangler r2 bucket create araharah-graphs`
|
||||
* — or your own name, and point bot/wrangler.jsonc's ROAD_GRAPH binding at
|
||||
* it), build the binary with build-road-graph.mjs, then run this. The
|
||||
* Worker side reads the binding, never a bucket name — the name here and in
|
||||
* wrangler.jsonc must simply agree.
|
||||
*
|
||||
* PRE-FLIGHT: the binary must EXIST and clear the enforced 1 MB floor (the
|
||||
* 80 MB ceiling is build-road-graph.mjs's gate — this script re-checks both
|
||||
* cheaply so a stale artifact never uploads silently).
|
||||
*
|
||||
* Usage:
|
||||
* node routing/scripts/upload-road-graph.mjs
|
||||
*
|
||||
* Exit codes: 0 uploaded; 1 pre-flight failure or wrangler failure.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts
|
||||
const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin");
|
||||
|
||||
/** Must agree with bot/wrangler.jsonc's ROAD_GRAPH binding bucket_name. */
|
||||
const BUCKET = "araharah-graphs";
|
||||
/** Must agree with worker-router.ts's GRAPH_KEY. */
|
||||
const KEY = "kl-roadgraph.v1.bin";
|
||||
/** The enforced floor — an at/below-floor file is a broken build, never an upload. */
|
||||
const MIN_BINARY_BYTES = 1 * 1024 * 1024;
|
||||
const MAX_BINARY_BYTES = 80 * 1024 * 1024;
|
||||
|
||||
function fail(message) {
|
||||
console.error(`upload-road-graph: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(GRAPH_PATH)) {
|
||||
fail(`no graph at ${GRAPH_PATH} — build it first: node routing/scripts/build-road-graph.mjs`);
|
||||
}
|
||||
const bytes = statSync(GRAPH_PATH).size;
|
||||
if (bytes <= MIN_BINARY_BYTES || bytes > MAX_BINARY_BYTES) {
|
||||
fail(
|
||||
`${GRAPH_PATH} is ${bytes} bytes — outside the enforced 1-80 MB envelope (D-01). ` +
|
||||
`Rebuild and heed the stats gate: node routing/scripts/build-road-graph.mjs`,
|
||||
);
|
||||
}
|
||||
console.log(`uploading ${GRAPH_PATH} (${(bytes / (1024 * 1024)).toFixed(1)} MB) -> r2://${BUCKET}/${KEY}`);
|
||||
|
||||
try {
|
||||
execFileSync(
|
||||
"npx",
|
||||
["wrangler", "r2", "object", "put", `${BUCKET}/${KEY}`, "--file", GRAPH_PATH, "--remote", "--content-type", "application/octet-stream"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
} catch (err) {
|
||||
fail(
|
||||
`wrangler failed (${err.status ?? "no status"}). If the bucket does not exist yet: ` +
|
||||
`npx wrangler r2 bucket create ${BUCKET} — and if R2 is not enabled on the account, ` +
|
||||
`activate it once in the Cloudflare dashboard (R2 -> Get started).`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`uploaded r2://${BUCKET}/${KEY}`);
|
||||
console.log("next: npm run dev -w bot # /api/route loads it through the ROAD_GRAPH binding (memoized per isolate)");
|
||||
console.log(`verify: npx wrangler r2 object get ${BUCKET}/${KEY} --file /tmp/opencode/graph-roundtrip.bin --remote && node routing/scripts/build-road-graph.mjs --check /tmp/opencode/graph-roundtrip.bin`);
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user