fix(06): revise plans per checker feedback — D-12 street-bearing cards by default, split 06-05 task 2, dep/wording/doc fixes

This commit is contained in:
araharah-executor
2026-10-24 08:06:42 +08:00
parent 7fe8c6aa65
commit 4212d1acd0
7 changed files with 761 additions and 59 deletions
@@ -30,7 +30,7 @@ user_setup:
must_haves:
truths:
- "A maintainer can rebuild the KL road graph from Overpass with one committed script — $0, fork-friendly (D-01)"
- "The real graph binary lands inside D-01's 30–80 MB envelope with the stats gate printing its measurement"
- "The real graph binary passes the enforced size gate (> 1 MB, ≤ 80 MB) with the stats gate printing its measurement — a sub-30 MB result prints D-01's expected-band warning for review rather than failing"
- "/api/route answers an arbitrary current position with a real A* route + named steps — engine A is the guidance engine (D-03)"
- "/api/route without a valid Bearer claim is 401 — the routing oracle is closed (T-06-01)"
- "A missing R2 graph degrades to honest null → the client's router-pending state (D-03), never an error"
@@ -43,6 +43,7 @@ must_haves:
exports: ["createHttpRoutingSource"]
- path: "bot/src/routing/worker-router.ts"
provides: "R2 graph load (isolate-memoized) + snap + A* + steps → RouteResult"
exports: ["handleRoute", "loadGraph"]
- path: "bot/wrangler.jsonc"
provides: "r2_buckets binding ROAD_GRAPH (still exactly ONE kv_namespaces — GE5)"
- path: "docs/ROAD-GRAPH.md"
@@ -128,6 +129,7 @@ names: u32 nameOffset[cnt+1] ++ u8 pool[] -->
<behavior>
- `node routing/scripts/build-road-graph.mjs` fetches the Overpass extract for the KL bbox, encodes the pinned CSR binary, writes routing/data/kl-roadgraph.v1.bin, prints node/edge counts + binary size + bbox, exits 0
- binary size > 80 MB (D-01 envelope) → exit 1 with the stats printed (the stats gate)
- binary size < 30 MB → printed WARNING naming D-01's expected band and A2's 18–50 MB forecast ("below the expected 30–80 MB band — verify the drivable-class filter") — advisory only; the ENFORCED gate stays > 1 MB, ≤ 80 MB
- ways with highway in the drivable class set only: motorway, trunk, primary, secondary, tertiary, residential, unclassified, living_street (footway/cycleway/service/path excluded — Pitfall 1)
- oneway=yes → flags bit0 on the reverse direction entry; maxspeed tags → edge seconds; class defaults: motorway 100, trunk 80, primary 60, secondary 50, tertiary 40, residential 30, unclassified 30, living_street 20 km/h
- way name tags → the name pool (unnamed ways → UNNAMED_NAME_ID)
@@ -135,7 +137,7 @@ names: u32 nameOffset[cnt+1] ++ u8 pool[] -->
- a second run with the committed file present and `--check` green needs no refetch (artifact, not input)
</behavior>
<action>
Write `routing/scripts/build-road-graph.mjs` in the fetch-route-fixtures.mjs shell: header naming the D-01 boundary ("A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, web/src, or routing/src imports it"), named constants (`OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter"`, `REQUEST_TIMEOUT_MS = 300_000`, `USER_AGENT`, `MAX_BINARY_BYTES = 80 * 1024 * 1024`, `DRIVABLE_CLASSES` + `CLASS_SPEED_KMH` tables, `BBOX_MARGIN_DEG = 0.05`), bbox COMPUTED at run start from `@araharah/search/src/gazetteer.ts` getEntries() + `routing/test/fixtures/golden-pairs.json` pair endpoints ± margin (printed; Open Q 4's mechanical pinning), the Overpass query `way[highway~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|living_street)$"](bbox); out geom;`, response → nodes deduplicated by rounded coord → edges with oneway/flags/speed/name → `encodeGraph` (import `../src/graph.ts`) → collect-then-write (committed file untouched on any failure, exit 1) → stats gate (size ≤ MAX_BINARY_BYTES else exit 1) → post-write summary + `next:` hint naming the upload command. `--check` mode decodes and re-prints stats. Also create `routing/data/.gitignore` containing `*.bin` (the binary is an ODbL-derived build artifact; committed artifacts are the script, docs, and the tiny test mini-graph).
Write `routing/scripts/build-road-graph.mjs` in the fetch-route-fixtures.mjs shell: header naming the D-01 boundary ("A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, web/src, or routing/src imports it"), named constants (`OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter"`, `REQUEST_TIMEOUT_MS = 300_000`, `USER_AGENT`, `MAX_BINARY_BYTES = 80 * 1024 * 1024`, `DRIVABLE_CLASSES` + `CLASS_SPEED_KMH` tables, `BBOX_MARGIN_DEG = 0.05`), bbox COMPUTED at run start from `@araharah/search/src/gazetteer.ts` getEntries() + `routing/test/fixtures/golden-pairs.json` pair endpoints ± margin (printed; Open Q 4's mechanical pinning), the Overpass query `way[highway~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|living_street)$"](bbox); out geom;`, response → nodes deduplicated by rounded coord → edges with oneway/flags/speed/name → `encodeGraph` (import `../src/graph.ts`) → collect-then-write (committed file untouched on any failure, exit 1) → stats gate (size ≤ MAX_BINARY_BYTES else exit 1; size < 30 MB prints the expected-band warning per the behavior list) → post-write summary + `next:` hint naming the upload command. `--check` mode decodes and re-prints stats. Also create `routing/data/.gitignore` containing `*.bin` (the binary is an ODbL-derived build artifact; committed artifacts are the script, docs, and the tiny test mini-graph).
Then RUN it (network fetch, minutes) and confirm the stats gate passes; record the printed counts in the plan summary.
</action>
<verify>
@@ -173,7 +175,7 @@ Then RUN it (network fetch, minutes) and confirm the stats gate passes; record t
</behavior>
<action>
1. `routing/src/http-source.ts`: `createHttpRoutingSource(opts: { endpoint: string; bearer: () => string | null; fetchImpl?: typeof fetch }): RoutingSource` — name "engine-a"; POST JSON {origin, destinationId}; Authorization only when bearer() returns non-null; response.json() → defensive shape check (copy fixture-source's isUsablePair style: every field type-checked, non-finite refused, try/catch → null); export from `routing/src/index.ts` alongside a header-note update (backend #2 live, ladder: fixture → engine-a → engine B documented post-v1).
2. `bot/src/routing/worker-router.ts`: module-scope `let graphP: Promise<RoadGraph | null> | null = null;` + `loadGraph(env)` — `env.ROAD_GRAPH.get(GRAPH_KEY)` with `GRAPH_KEY = "kl-roadgraph.v1.bin"`, `.then(o => o === null ? null : decodeGraph(await o.arrayBuffer()))`, catch → null (a corrupt binary degrades honest); memo never reset (per-isolate). `handleRoute(req, env): Promise<Response>`: Bearer claim (extract + getClaimByToken — reuse webhook.ts's exact helper posture), body-cap 4096 (`MAX_ROUTE_BODY_CHARS`), origin {lat,lng} numeric + inside graph.bbox (400 otherwise), `getEntryById(destinationId)` (import `@araharah/search/src/gazetteer.ts`) → null miss → 200 null; snapToGraph origin + destination → null miss → 200 null; routeAstar → null → 200 null; else build RouteResult `{ destinationId, origin, options: [{ geometry: { points: nodePath points, cumulativeDistanceM: deriveCumulativeDistanceM(points) }, summary: { distanceM, durationS }, steps: deriveSteps(...) }], backend: "engine-a" }` → 200 JSON.
2. `bot/src/routing/worker-router.ts`: module-scope `let graphP: Promise<RoadGraph | null> | null = null;` + EXPORTED `loadGraph(env)` — `env.ROAD_GRAPH.get(GRAPH_KEY)` with `GRAPH_KEY = "kl-roadgraph.v1.bin"`, `.then(o => o === null ? null : decodeGraph(await o.arrayBuffer()))`, catch → null (a corrupt binary degrades honest); memo never reset (per-isolate). Exporting loadGraph is deliberate: the cron arm (06-07 scheduled()) imports this SAME memo — one R2 GET per cold isolate regardless of which arm warms it; a second R2 loader anywhere in bot/src fails review. `handleRoute(req, env): Promise<Response>`: Bearer claim (extract + getClaimByToken — reuse webhook.ts's exact helper posture), body-cap 4096 (`MAX_ROUTE_BODY_CHARS`), origin {lat,lng} numeric + inside graph.bbox (400 otherwise), `getEntryById(destinationId)` (import `@araharah/search/src/gazetteer.ts`) → null miss → 200 null; snapToGraph origin + destination → null miss → 200 null; routeAstar → null → 200 null; else build RouteResult `{ destinationId, origin, options: [{ geometry: { points: nodePath points, cumulativeDistanceM: deriveCumulativeDistanceM(points) }, summary: { distanceM, durationS }, steps: deriveSteps(...) }], backend: "engine-a" }` → 200 JSON.
3. `bot/src/webhook.ts`: add the `/api/route` arm delegating to handleRoute (405 guard for non-POST). `bot/src/config.ts`: add `ROAD_GRAPH: R2Bucket; // D-01 graph binary — R2 binding, first in the repo (never a KV namespace — GE5)`. `bot/wrangler.jsonc`: add `"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }]` with a law comment; kv_namespaces stays exactly one entry.
4. RE-PIN `bot/test/route-privacy.test.mjs` in the same change: RP1 → the /api/route arm must reference the Bearer-claim derivation (structural: the handler path calls the claim helper; a bare unauthenticated fetch-forward shape fails); no external routing hostnames (`grep` for non-Telegram/non-Cloudflare API hosts). RP2 → allow-list `@araharah/search` + `@araharah/routing` in bot/src imports; any other `@araharah/*` fails. Extend `bot/test/webhook.test.mjs` with the /api/route behavior block (R2 mini-graph fake + mini graph binary loaded from routing/test/fixtures/mini-graph.bin via fs).
</action>
@@ -182,7 +184,7 @@ Then RUN it (network fetch, minutes) and confirm the stats gate passes; record t
</verify>
<acceptance_criteria>
- routing/src/http-source.ts exports createHttpRoutingSource (name "engine-a"); index.ts re-exports it
- bot/src/routing/worker-router.ts: module-scope graphP memo (grep `graphP ??=` or equivalent), GRAPH_KEY = "kl-roadgraph.v1.bin", null on absent/corrupt graph
- bot/src/routing/worker-router.ts: module-scope graphP memo (grep `graphP ??=` or equivalent), exported loadGraph, GRAPH_KEY = "kl-roadgraph.v1.bin", null on absent/corrupt graph
- webhook.test.mjs: 401 unauth, 413 cap, 400 malformed origin, 200-null on unknown dest / snap miss / absent graph, 200 RouteResult happy path via R2 fake, backend === "engine-a", options.length === 1
- route-privacy.test.mjs: RP1 (auth-bound routing) + RP2 (allow-list search+routing only) re-pinned and green; GE5 green (one kv_namespaces)
- bot/src/config.ts carries the ROAD_GRAPH: R2Bucket line with a law comment
@@ -247,7 +249,7 @@ Then RUN it (network fetch, minutes) and confirm the stats gate passes; record t
<verification>
- `npm run routing:test && npm test -w bot` green; both typechecks clean
- Real graph: `--check` green, size inside the 30–80 MB envelope, stats printed
- Real graph: `--check` green, size inside the enforced gate (1–80 MB; sub-30 MB prints the band warning), stats printed
- CPU gate: p95 ≤ 8 ms printed (A1 retired) or escalation recorded
- RP1/RP2 re-pinned green; GE5 (one KV namespace) still green
</verification>