264 lines
24 KiB
Markdown
264 lines
24 KiB
Markdown
---
|
||
phase: 06-turn-by-turn-guidance-live-driving
|
||
plan: 04
|
||
type: execute
|
||
wave: 2
|
||
depends_on: [06-01, 06-03]
|
||
files_modified:
|
||
- routing/scripts/build-road-graph.mjs
|
||
- routing/scripts/upload-road-graph.mjs
|
||
- routing/scripts/measure-route-cpu.mjs
|
||
- routing/src/http-source.ts
|
||
- routing/src/index.ts
|
||
- routing/data/.gitignore
|
||
- bot/src/routing/worker-router.ts
|
||
- bot/src/webhook.ts
|
||
- bot/src/config.ts
|
||
- bot/wrangler.jsonc
|
||
- bot/test/route-privacy.test.mjs
|
||
- bot/test/webhook.test.mjs
|
||
- docs/ROAD-GRAPH.md
|
||
autonomous: true
|
||
requirements: [GUID-02]
|
||
user_setup:
|
||
- service: cloudflare-r2
|
||
why: "D-01: the KL road graph hosts in R2 (first repo binding); forks upload their own"
|
||
env_vars: []
|
||
dashboard_config:
|
||
- task: "R2 bucket araharah-graphs must exist before upload + real routing"
|
||
location: "`npx wrangler r2 bucket create araharah-graphs` (executor attempts it; on wrangler-auth failure this becomes the checkpoint — Cloudflare dashboard → R2 → Create bucket works too)"
|
||
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 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"
|
||
- "A* CPU on the real graph is measured and printed, p95 inside the 10 ms free-plan budget (A1)"
|
||
artifacts:
|
||
- path: "routing/scripts/build-road-graph.mjs"
|
||
provides: "Overpass extract → CSR binary (maintainer tool, collect-then-write, stats gate)"
|
||
- path: "routing/src/http-source.ts"
|
||
provides: "createHttpRoutingSource — web's RoutingSource backend #2"
|
||
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"
|
||
provides: "fork setup: bucket, upload, licensing, envelope law"
|
||
key_links:
|
||
- from: "bot/src/webhook.ts"
|
||
to: "bot/src/routing/worker-router.ts"
|
||
via: "/api/route arm (Bearer-claim gated like /api/location)"
|
||
pattern: "handleRoute|/api/route"
|
||
- from: "bot/src/routing/worker-router.ts"
|
||
to: "env.ROAD_GRAPH"
|
||
via: "module-scope promise memo — 1 R2 GET per cold isolate"
|
||
pattern: "ROAD_GRAPH\\.get"
|
||
- from: "routing/src/http-source.ts"
|
||
to: "RoutingSource"
|
||
via: "implements the Phase 5 seam — network fail → null (fail-honest)"
|
||
pattern: "RoutingSource"
|
||
---
|
||
|
||
<objective>
|
||
Engine A's server surface (D-01): the real KL graph build tool (Overpass → CSR binary), the R2 hosting + upload path with fork docs, the isolate-memoized Worker router behind a Bearer-gated `/api/route`, the client-side `createHttpRoutingSource` RoutingSource backend #2, RP1/RP2 re-pinned in the same change, and the A*-CPU measurement gate printed and enforced.
|
||
|
||
Purpose: GUID-02's rerouting becomes REAL — arbitrary current positions route for real, $0, no VPS. This is the phase's top technical risk (A1: 10 ms free-plan CPU), so the measurement gate is a deliverable, not a hope.
|
||
Output: build/upload/measure scripts + http-source + worker-router + `/api/route` + re-pinned gates + docs, real graph built and measured.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@.planning/PROJECT.md
|
||
@.planning/ROADMAP.md
|
||
@.planning/STATE.md
|
||
@.planning/phases/06-turn-by-turn-guidance-live-driving/06-CONTEXT.md
|
||
@.planning/phases/06-turn-by-turn-guidance-live-driving/06-RESEARCH.md
|
||
@.planning/phases/06-turn-by-turn-guidance-live-driving/06-PATTERNS.md
|
||
|
||
<interfaces>
|
||
<!-- Engine A pure core exists from plan 06-01 (routing/src, ALL exported from index.ts): -->
|
||
decodeGraph(buffer: ArrayBuffer): RoadGraph | null
|
||
snapToGraph(graph: RoadGraph, point: RoutePoint): { node: number; distanceM: number } | null
|
||
routeAstar(graph: RoadGraph, originNode: number, goalNode: number): { nodes: number[]; seconds: number; distanceM: number } | null
|
||
deriveSteps(graph: RoadGraph, nodes: number[]): RouteStep[]
|
||
// RoadGraph carries bbox + maxSpeedKmh + nameAt(id)
|
||
|
||
<!-- PINNED CSR BINARY LAYOUT (from 06-01 — the build script MUST produce exactly this):
|
||
header: magic u32 0x41524731 | version u32 1 | nodeCount u32 | edgeCount u32 | nameCount u32 |
|
||
namePoolLen u32 | bbox 4×f64 | maxSpeedKmh f32
|
||
nodes: f64 lat[] ++ f64 lng[] · offsets: u32 edgeStart[n+1] · edges: u32 target[] ++ u8 flags[]
|
||
(bit0 oneway-reversed) · weights: f32 seconds[] · edgeName: u32 nameId[] (0xffffffff unnamed)
|
||
names: u32 nameOffset[cnt+1] ++ u8 pool[] -->
|
||
|
||
<!-- The Worker route arm copies webhook.ts's Bearer-claim posture (plan 06-03 task 2). -->
|
||
|
||
<!-- Envelope service exists from plan 06-03; bot/package.json already has "@araharah/routing": "*". -->
|
||
|
||
<!-- RP gate re-pin (PATTERNS): RP1 shrinks to "no UNAUTHENTICATED / proxy-shaped routing"
|
||
(the handler MUST derive identity from the Bearer claim; no forwarding to external
|
||
routing hosts); RP2 now ALLOWS @araharah/routing in bot/src (subpath-with-extension
|
||
imports only) and bans any OTHER new workspace dependency beyond
|
||
@araharah/search + @araharah/routing. GE5 stays: exactly ONE kv_namespaces. -->
|
||
|
||
<!-- graph binary is a BUILD ARTIFACT, not committed: routing/data/.gitignore ignores *.bin
|
||
(forks build + upload their own; committed = scripts + docs + mini-graph tests). -->
|
||
</interfaces>
|
||
|
||
**Isolate memoization is the cache that works** (RESEARCH): `caches.default` is useless here — Bearer requests auto-bypass Workers Cache and cron is never cached. Module-scope promise memo, 1 R2 GET per cold isolate.
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>task 1: Road-graph build tool (Overpass → CSR binary) + real KL graph</name>
|
||
<files>routing/scripts/build-road-graph.mjs, routing/data/.gitignore</files>
|
||
<read_first>
|
||
- routing/scripts/fetch-route-fixtures.mjs lines 1–64, 110–142, 185–248 (THE maintainer-tool pattern to copy: #!/usr/bin/env node, dense MAINTAINER-TIME header + Usage + Exit codes, named constants up top, fail() helper, pre-flight validation, collect-then-write, post-write stats + "next:" hint)
|
||
- routing/test/fixtures/build-mini-graph.mjs + routing/src/graph.ts (the exact encodeGraph contract + pinned CSR layout this script must produce)
|
||
- .planning/phases/06-turn-by-turn-guidance-live-driving/06-RESEARCH.md A7 (Overpass response size/timeout) + Pitfall 1 (road-class filter) + Open Q 4 (bbox derivation)
|
||
- search/src/gazetteer.ts getEntries (bbox extremes source)
|
||
</read_first>
|
||
<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)
|
||
- `--check <file>` mode: decode with decodeGraph, print stats, exit 0/1 — no network
|
||
- 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; 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>
|
||
<automated>node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin && node -e "const s=require('fs').statSync('routing/data/kl-roadgraph.v1.bin'); if(s.size>80*1024*1024) process.exit(1); console.log('graph bytes:', s.size)"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- routing/scripts/build-road-graph.mjs exists with the maintainer-tool header, named constants (no inline magic), collect-then-write, and the 80 MB stats gate
|
||
- routing/data/kl-roadgraph.v1.bin exists, > 1 MB, ≤ 80 MB, and passes `--check` (decodeGraph green)
|
||
- routing/data/.gitignore contains `*.bin` and `git status --porcelain routing/data` shows no .bin staged
|
||
- printed stats include nodeCount, edgeCount, binary size, bbox (executor records them in the SUMMARY)
|
||
- grep `footway|cycleway|service|path` → absent from DRIVABLE_CLASSES (Pitfall 1)
|
||
</acceptance_criteria>
|
||
<done>The real KL graph builds reproducibly from Overpass inside the D-01 envelope with the class filter and stats gate standing guard</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>task 2: http-source (backend #2) + worker-router + Bearer-gated /api/route + RP re-pins</name>
|
||
<files>routing/src/http-source.ts, routing/src/index.ts, bot/src/routing/worker-router.ts, bot/src/webhook.ts, bot/src/config.ts, bot/wrangler.jsonc, bot/test/route-privacy.test.mjs, bot/test/webhook.test.mjs</files>
|
||
<read_first>
|
||
- routing/src/fixture-source.ts lines 35–95, 134–189 (the backend-factory skeleton: slug guard first, lazy memoize, defensive validation, D-03 honest nulls — copy the posture)
|
||
- bot/src/webhook.ts (the /api/location arm to mirror: Bearer claim → 401, body cap, category logs; route-dispatch skeleton)
|
||
- bot/src/config.ts (the typed Env contract — one line per field with the trailing law comment)
|
||
- bot/wrangler.jsonc (JSONC with law-bearing comments; GE5 asserts exactly ONE kv_namespaces entry)
|
||
- bot/test/route-privacy.test.mjs lines 1–24 (RP1/RP2 as they stand — the doc-comments name Phase 6 as the re-pinning owner)
|
||
- search/src/gazetteer.ts getEntryById(id) (destination resolution server-side)
|
||
- bot/test/webhook.test.mjs (offline pattern; extend for the /api/route block with an R2 fake: `{ get: async () => ({ arrayBuffer: async () => miniGraphBuffer }) }`)
|
||
</read_first>
|
||
<behavior>
|
||
- createHttpRoutingSource: POST {origin, destinationId} to endpoint with Bearer token from the injected getter; non-200, malformed body, or thrown fetch → null (fail-honest — T-05-04's .catch(()=>null) law, network edition); a valid RouteResult-shaped response passes defensive validation (destinationId match, options 1..3, finite numbers) → RouteResult
|
||
- worker-router handleRoute: no/invalid Bearer → 401 [route] rejected: unauthorized; body > 4096 → 413; non-numeric/out-of-bbox origin lat/lng → 400 [route] rejected: malformed (V5 clamp-to-bbox validation); unknown destinationId → 200 null-ish? NO — unknown destination or snap miss or absent graph → Response JSON `null` with 200 (the null contract carries over HTTP: honest router-pending, never a 500)
|
||
- happy path through the R2-faked mini-graph: real origin/dest resolve → 200 RouteResult with backend "engine-a", one option (NO alternatives — RESEARCH anti-pattern: alternatives never during the guidance path), steps carrying kind/name/distance/polyline6
|
||
- graph object absent (R2 fake returns null) → 200 null (D-03 designed degradation)
|
||
- RP1 re-pinned: a /api/route handler WITHOUT the Bearer-claim derivation fails the gate; RP2 re-pinned: @araharah/routing subpath imports allowed in bot/src; any other new workspace dep fails
|
||
- GE5 still green: exactly ONE kv_namespaces entry (r2_buckets is a different binding class)
|
||
</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;` + 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>
|
||
<verify>
|
||
<automated>npm test -w bot && npm run routing:test && npm run bot:typecheck && npm run routing:typecheck</automated>
|
||
</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), 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
|
||
- both typechecks clean
|
||
</acceptance_criteria>
|
||
<done>The routing oracle is Bearer-gated and live in dev (R2 faked in tests), the client seam has its backend #2, honest-null degradation is proven at every miss class, and the negative-space gates were re-pinned by the change that invalidated them</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>task 3: R2 bucket + upload script + fork docs + A*-CPU measurement gate</name>
|
||
<files>routing/scripts/upload-road-graph.mjs, routing/scripts/measure-route-cpu.mjs, docs/ROAD-GRAPH.md</files>
|
||
<read_first>
|
||
- routing/scripts/build-road-graph.mjs (task 1 — the "next:" hint names this script; keep the maintainer-tool shell identical)
|
||
- .planning/phases/06-turn-by-turn-guidance-live-driving/06-RESEARCH.md Pitfall 1 (CPU measurement mandate), Assumption A1 (mitigation ladder), Environment Availability (R2 bucket "not yet created")
|
||
- web/test/build-artifact.test.mjs BA9 (the "budget gates print their measurement" precedent this gate follows)
|
||
</read_first>
|
||
<behavior>
|
||
- upload-road-graph.mjs: verifies routing/data/kl-roadgraph.v1.bin exists (> 1 MB), shells `npx wrangler r2 object put araharah-graphs/kl-roadgraph.v1.bin --file routing/data/kl-roadgraph.v1.bin --remote`, prints the key + next steps; missing file → exit 1 with the build hint
|
||
- measure-route-cpu.mjs: loads the real binary, runs 25 A* routes between sampled node pairs spread across the bbox, prints per-route CPU ms + p95, exits 1 if p95 > 8 ms (20% margin under the 10 ms free-plan budget — A1); absent binary → exit 1 with the build hint
|
||
- R2 bucket `araharah-graphs` exists on the account (wrangler CLI; on auth failure the executor surfaces a human checkpoint with the exact command — the bucket create is the one step a keyless machine cannot do)
|
||
- docs/ROAD-GRAPH.md documents: build → check → upload → deploy sequence, the fork contract (your own bucket, your own upload), ODbL attribution for the OSM-derived graph, the 30–80 MB envelope law, and the p95 CPU gate
|
||
</behavior>
|
||
<action>
|
||
1. Attempt `npx wrangler r2 bucket create araharah-graphs`. If wrangler reports auth/permission failure, create a `checkpoint:human-action` note in the run log with the exact command + dashboard path (Cloudflare dashboard → R2 → Create bucket) and continue with dev-only verification — flag it in the SUMMARY as the phase's user_setup item. If it succeeds, proceed.
|
||
2. Write `routing/scripts/upload-road-graph.mjs` (fetch-route-fixtures.mjs shell) and RUN it (when the bucket exists) — verify with `npx wrangler r2 object get araharah-graphs/kl-roadgraph.v1.bin --file /tmp/opencode/graph-roundtrip.bin --remote` + `--check` round-trip (or `r2 object info`).
|
||
3. Write and RUN `routing/scripts/measure-route-cpu.mjs` (Node: `process.hrtime.bigint()` around routeAstar; pairs sampled from graph nodes across the bbox quadrants + the fixture-corpus endpoints snapped via snapToGraph). Record the printed p95 in the SUMMARY. If p95 > 8 ms, apply A1's mitigation ladder IN ORDER and re-measure: (a) drop tertiary/residential from DRIVABLE_CLASSES and rebuild (tighter graph), (b) bidirectional A* variant, (c) document Workers Paid as an explicit fork decision (breaks $0 — requires user sign-off; stop and surface it).
|
||
4. Write `docs/ROAD-GRAPH.md` per the behavior list (concise — one page).
|
||
</action>
|
||
<verify>
|
||
<automated>node routing/scripts/measure-route-cpu.mjs 2>&1 | tail -3 && node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- measure-route-cpu.mjs prints "p95" + a millisecond number and exits 0 (p95 ≤ 8 ms), or the SUMMARY records which A1 mitigation was applied and the new p95
|
||
- upload-road-graph.mjs exists with the maintainer shell; if the bucket exists the round-trip get + --check proves the upload
|
||
- docs/ROAD-GRAPH.md exists covering: bucket create, upload command, ODbL attribution, envelope law, CPU gate
|
||
- the R2 bucket state (created / needs-user) is recorded in the SUMMARY
|
||
</acceptance_criteria>
|
||
<done>Graph hosting is fork-ready ($0, scripted, documented), the binary is measured against the free-plan CPU ceiling with a printed verdict, and A1's risk is retired or explicitly escalated</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| Internet → /api/route | Untrusted callers would love a free routing oracle; identity via Bearer claim only |
|
||
| Request body → A* | Origin coordinates are untrusted input reaching a compute-heavy path |
|
||
| R2 object → decodeGraph | Fork-uploaded binary (covered by 06-01's header validation — decodeGraph null on any mismatch) |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|-------------|-----------------|
|
||
| T-06-01 | DoS / Elevation of Privilege | /api/route | mitigate | Bearer-gated exactly like /api/location (401 class tested); the 10 ms CPU cap is the natural per-request rate limiter; body cap 4096 (413). RP1 re-pinned to fail any unauthenticated/proxy-shaped routing arm. |
|
||
| T-06-01b | Denial of Service (compute) | handleRoute | mitigate | V5: origin lat/lng type+range-validated against graph.bbox BEFORE snap/A* (400 class); unknown destinationId short-circuits before any graph work; alternatives generation prohibited on this path (single best route — RESEARCH anti-pattern). |
|
||
| T-06-03 | Tampering | loadGraph → decodeGraph | mitigate | decodeGraph's magic+version+size check (06-01) → null → designed router-pending (D-03); upload script is maintainer-surface only; corrupt binary in R2 degrades honest, never crashes the Worker. |
|
||
| T-06-04 | Information Disclosure | handleRoute logs | mitigate | Category-only logging (`[route] rejected: <category>`) — never coordinates, ids, or payloads (the webhook law extended). |
|
||
| — | Information Disclosure (fork) | docs/ROAD-GRAPH.md | accept | The graph is public OSM-derived data (ODbL-attributed) — hosting it is not a privacy surface; documented licensing keeps forks honest. |
|
||
</threat_model>
|
||
|
||
<verification>
|
||
- `npm run routing:test && npm test -w bot` green; both typechecks clean
|
||
- 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>
|
||
|
||
<success_criteria>
|
||
Engine A routes real positions for real: build → upload → Bearer-gated `/api/route` → client `createHttpRoutingSource` — all $0, fork-friendly, honestly degrading when the graph is absent, and measured against the free-plan CPU ceiling. GUID-02's "recalculates from the current position" now has an engine behind it.
|
||
</success_criteria>
|
||
|
||
<output>
|
||
After completion, create `.planning/phases/06-turn-by-turn-guidance-live-driving/06-04-SUMMARY.md`
|
||
</output>
|