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
+130
View File
@@ -0,0 +1,130 @@
# The Road Graph (engine A's data) — build, host, and the laws
Engine A routes over a **CSR road-graph binary** built from OpenStreetMap and
hosted on Cloudflare R2. The binary is a **build artifact, not a committed
file** — forks build and upload their own. Everything needed is committed:
two maintainer scripts and this doc. All of it is **$0** (Overpass public
instance, R2 free tier, Workers free plan).
## The fork contract (your graph, your bucket)
1. **Create your bucket** (once, per Cloudflare account):
```
npx wrangler r2 bucket create araharah-graphs
```
If `wrangler` reports that R2 is not enabled, activate it once in the
Cloudflare dashboard (**R2 → Get started**) — account-owner steps, then
re-run the command. Use a different name if you like, but the name must
agree with `bot/wrangler.jsonc`'s `ROAD_GRAPH` binding (`bucket_name`).
2. **Build the KL graph** (or your own region — tune `DRIVABLE_CLASSES` /
the bbox in the script):
```
node routing/scripts/build-road-graph.mjs
```
Computes the bbox from the committed corpus (gazetteer + golden pairs,
± 0.05°), fetches ONE Overpass extract (drivable classes only), encodes
the pinned CSR binary through `encodeGraph`, and writes
`routing/data/kl-roadgraph.v1.bin` — **only** after the encode round-trip
and the size gate pass. Exit 1 leaves any previous binary untouched.
3. **Check the artifact** (decode + stats, no network):
```
node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin
```
4. **Upload to R2**:
```
node routing/scripts/upload-road-graph.mjs
# = npx wrangler r2 object put araharah-graphs/kl-roadgraph.v1.bin \
# --file routing/data/kl-roadgraph.v1.bin --remote
```
Verify the round-trip:
```
npx wrangler r2 object get araharah-graphs/kl-roadgraph.v1.bin \
--file /tmp/opencode/graph-roundtrip.bin --remote
node routing/scripts/build-road-graph.mjs --check /tmp/opencode/graph-roundtrip.bin
```
5. **Deploy the Worker** (`npm run deploy -w bot`). `wrangler.jsonc` binds
the bucket as `ROAD_GRAPH`; `worker-router.ts` loads the object
`kl-roadgraph.v1.bin` once per cold isolate (module-scope memo, shared
with the cron arm) — 1 R2 GET, not per request.
**Dev note:** with no bucket (or an empty one), `/api/route` answers `200
null` — the designed router-pending state (D-03). Dev and tests never need
the real binary; tests use the committed mini-graph fixture.
## The envelope law (D-01)
| Gate | Value | Enforcement |
|------|-------|-------------|
| Ceiling | **≤ 80 MB** | enforced — build + upload + `--check` all exit 1 over it |
| Floor | **> 1 MB** | enforced — a smaller "road graph" is a broken build |
| Expected band | **30–80 MB** | advisory — a smaller build prints a WARNING (verify the class filter) and continues |
The binary lives in `routing/data/` and is gitignored (`*.bin`): it is
ODbL-derived data, regenerated at will, never source.
## The CPU gate (A1) — OPEN: measured over the free-plan budget
Workers free plan: **10 ms CPU per request** (Error 1102 beyond it). The A\*
cost over the REAL graph is measured, never hoped:
```
node routing/scripts/measure-route-cpu.mjs
```
25 pairs (golden-corpus routes + quadrant-sampled cross-city pairs; directed
dead-end probes reported separately) timed around `routeAstar` over the real
binary. **The gate is p95 ≤ 8 ms** (20% margin under the ceiling).
**Measured state (2026-10-24, the 311k-node linked graph): GATE FAIL.** The
mitigation ladder was executed in order:
| Ladder step | Outcome |
|-------------|---------|
| full graph, unidirectional A\* | p95 ≈ 38 ms — FAIL |
| (a) drop tertiary+residential, rebuild | p95 ≈ 18 ms — FAIL **and** 12/25 pairs became unreachable (the dropped classes are the fine mesh tying landmarks to arterials) — rejected on correctness, reverted |
| (b) bidirectional A\* (BH, Ikeda potentials) | p95 ≈ 38 ms — FAIL: the potential split halves each side's heuristic steering, cancelling the halved search radius. Kept: sound (A1's 625-pair Dijkstra equivalence still passes) and better on short product routes |
| (c) Workers Paid | **NOT applied — breaks the $0 law; needs a maintainer decision** |
The decision this gate now escalates: **(c1) Workers Paid ($5/mo — breaks
$0)**, **(c2) a graph-layout v2 with ALT landmark preprocessing (still $0 at
runtime, real build-time engineering)**, or **(c3) accept free-plan routing
with honest degradation** (long reroutes may hit 1102; the client's
`createHttpRoutingSource` then degrades to the router-pending state).
Short product routes (2–8 km, the common reroute) measured 2–8 ms — inside
budget; the failures concentrate on 20–30 km cross-metro routes.
## Licensing (the ODbL attribution)
The graph is derived from OpenStreetMap data —
**© OpenStreetMap contributors, ODbL 1.0**
(<https://opendatacommons.org/licenses/odbl/1-0/>), fetched via the public
Overpass API. Hosting or redistributing the binary requires the same
attribution (ODbL's attribution clause travels with derived databases). The
route fixtures carry the same notice inside their committed envelope
(`_license`/`_attribution`/`_notice`), and this doc is the notice surface for
the hosted binary. Keep it intact in forks.
## Key facts
| What | Where |
|------|-------|
| Build tool (Overpass → CSR) | `routing/scripts/build-road-graph.mjs` |
| Upload tool (wrangler shell) | `routing/scripts/upload-road-graph.mjs` |
| CPU measurement gate | `routing/scripts/measure-route-cpu.mjs` |
| Binary (gitignored artifact) | `routing/data/kl-roadgraph.v1.bin` |
| R2 object key | `kl-roadgraph.v1.bin` (pinned in `worker-router.ts` as `GRAPH_KEY`) |
| Binding | `ROAD_GRAPH` in `bot/wrangler.jsonc` (bucket, NEVER a KV namespace — GE5) |
| Loader (isolate-memoized) | `bot/src/routing/worker-router.ts` → `loadGraph` |
| Binary layout authority | `routing/src/graph.ts` (`encodeGraph`/`decodeGraph`, pinned CSR v1) |