Files

552 lines
56 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 6: Turn-by-Turn Guidance (Live Driving) - Research
**Researched:** 2026-10-24
**Domain:** Real-time navigation state machine (client) + Worker-side routing engine A over an R2-hosted KL road graph (server) + Telegram cron turn-cards
**Confidence:** HIGH on platform APIs and infrastructure; MEDIUM on performance envelope (free-plan CPU) and tuning constants (replay-tuned by design)
## Summary
Phase 6 has two interlocking workstreams. **Engine A (D-01)**: an OSM → compact road-graph build script (Overpass JSON extract → CSR typed-array binary, ~18–50 MB for KL metro — inside D-01's 30–80 MB envelope), R2-hosted, routed by a **Worker-side A\*** exposed as `/api/route`, which the web app consumes as RoutingSource backend #2 behind the Phase 5 seam. **Guidance (GUID-01..06, PX-01, PX-03)**: a pure, Node-testable client state machine (`ON_ROUTE → OFF_ROUTE → REROUTING → ON_ROUTE` per D-04) driving the banner/list/ETA/haptics surface, plus a Worker Cron Trigger that projects progress for minimized sessions and pushes glyph+text turn-cards (D-11/D-12) over the Phase 5-frozen `GuidanceSessionEnvelope`.
The single most consequential platform fact: **Workers Free plan = 10 ms CPU per HTTP request and per cron invocation** [VERIFIED: developers.cloudflare.com/workers/platform/limits], with 128 MB memory. A metro-scale A\* in typed arrays plausibly fits in a few ms, but this is the phase's top technical risk and demands an explicit CPU-measurement gate (wrangler dev invocation logs) plus designed mitigations (road-class filtering keeps the graph tight; bidirectional search; single best route on reroute — alternatives only at pre-drive consult). Second platform fact: **KV free tier = 1,000 writes/day** [VERIFIED] — the cron's per-session card-cursor state must be designed against this budget (or made stateless).
Two Phase 5 gates are **designed to be relaxed by this phase** (the established GE5–GE7 handoff pattern): RP1/RP2 ("Worker cannot route") are superseded by D-01 — the Worker gains `/api/route` and its first `@araharah/routing` import — and GE4–GE7 (guidance envelope types-only) are superseded when the envelope service lands. Both relaxations are that gate's documented Phase 6 responsibility, not gate violations.
**Primary recommendation:** Build the guidance state machine as a pure module first (replay-tested per D-16), engine A second behind the existing RoutingSource seam, and keep every fix on-device while on-route — the only positions that ever leave the phone are reroute origins and the minimize-time envelope, preserving GEO-03's memory law and minimizing the privacy surface by construction.
## User Constraints (from 06-CONTEXT.md)
### Locked Decisions (verbatim intent, D-01..D-16 — do NOT re-litigate)
- **D-01:** Engine A ships this phase: OSM → compact road-graph build script (KL ~30–80 MB typed arrays; one-ways + speed weights; turn restrictions deferred), graph hosted in **R2** (upload script committed — forks upload their own), **Worker-side A\*** (JS typed arrays, WASM optional) exposed as RoutingSource backend #2 via a JSON routing API on the Worker. Satisfies every law: $0, no VPS, fork-friendly, privacy.
- **D-02:** Valhalla-to-JS port ruled infeasible; geojson-path-finder rejected (no alternatives/one-ways). Engine B (client-side router) is A's documented evolution, deferred post-v1.
- **D-03:** Fixture backend remains #1 (golden-pairs demo); engine A is #2 and **becomes the guidance engine**. Router-pending state remains for graph-edge cases (e.g. R2 graph absent — a designed degradation, not an error).
- **D-04:** Reroute = thresholds + grace state machine `ON_ROUTE → OFF_ROUTE → REROUTING → ON_ROUTE` (deviation > threshold with grace window + N consecutive off-route fixes before acting); storm guard: minimum time between recalculations; missed-turn detection = off-route past the maneuver point. All thresholds named constants, replay-tested.
- **D-05:** The router snaps raw GPS fixes to the graph (engine A owns snapping — one authority). Watch fix streams raw.
- **D-06:** Layout: top maneuver banner (distance + arrow + street; location register compacts), collapsible full maneuver list (collapsed default, expands over map, tap-out collapses), bottom guidance card (ETA + remaining + progress).
- **D-07:** ETA smoothing = EMA + deadband — recompute from route model + snapped progress (never raw fix-to-destination distance); display updates only past ±1 min / ±5% remaining; constants replay-tested.
- **D-08:** Haptics: `HapticFeedback.selectionChanged()` at maneuver-approach (~300 m), `impactOccurred` at the maneuver point. Graceful no-op where unsupported.
- **D-09:** PX-03: closing during guidance requires the "abandon route?" Rams confirm card (en/ms); `disableVerticalSwipes()` while guidance active; safe-area insets respected. PX-01: BackButton walks the full graph — search → routes → guidance → back to search — never dead-ends.
- **D-10:** Camera: locked follow (center ≈65% down-screen, pitch 35°) easing along the snapped line; user pan unlocks follow → Phase 3 recenter control returns.
- **D-11:** Minimize during guidance → app POSTs the guidance session (envelope goes LIVE: KV writes begin, `guidance:<id>`, 7200 s TTL). Worker Cron Trigger (60–120 s) scans active guidance keys, projects progress along the snapshot, pushes a chat turn-card when the next maneuver changes (or every N minutes). App re-open resumes foreground; cron pushes pause while foregrounded.
- **D-12:** Turn-card = glyph + text: committed arrow-glyph icon set (8 cardinal/turn PNGs, token colors) + ETA/distance/street text — no map bitmap, no renderer dependency. Bitmap snapshots deferred (Workers cannot rasterize).
- **D-13:** "Best-effort" is honest: cron lag, Telegram delivery variance, TTL expiry are documented behaviors; card copy never promises what the mechanism can't.
- **D-14:** Ending a trip deletes the guidance session (KV record gone) and shows "trip ended, data deleted" (en/ms). Arrival = snapped progress ≥ end-of-route threshold; abandon = the PX-03 confirm path. Both delete.
- **D-15:** `watchPosition` high-accuracy, app throttles by distance — guidance work runs on ≥10 m moved or ≥5 s elapsed, whichever first. D-05's gated-streaming law flips its watch ON here.
- **D-16:** Scripted drive replays verify the state machine: recorded JSON position streams (corridor follow, off-route excursion, missed-turn excursion, GPS jitter) replayed through the machine in tests, asserting every transition, the storm guard, and ETA deadband. **Plus one real drive** as the device gate.
### OpenCode's Discretion (research here, recommend)
- Graph build toolchain choice (osmium/planetiler-graph/custom overpass extract), binary layout, A\* variant (bidirectional), alternatives generation via penalty runs
- Deviation/storm/ETA constants' initial values (replay-tuned; named, not magic)
- Cron interval within 60–120 s; "N minutes" repeat cadence; glyph icon production method (hand SVG → PNG)
- Banner/list micro-layout within the Rams system; exact copy wording within the tone law
- Envelope write timing (on guidance start vs first minimize — either honors the contract)
### Deferred Ideas (OUT OF SCOPE)
- Engine B: fully client-side webview router
- Turn restrictions in the graph; contraction hierarchies; CH preprocessing
- Bitmap map snapshots in turn-cards (v1.x)
- Voice guidance, live trip share, toll toggle, speed limits (v1.x)
- Traffic-aware ETAs, incident-aware rerouting (v2)
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| GUID-01 | Prominent next-maneuver banner (distance + arrow + street) + collapsible full maneuver list | D-06 layout; maneuver extraction from route steps (RouteStep model already has `name`/`distanceM`); banner/list as pure builders per the route-card pattern; Telegram safe-area APIs verified (8.0+) |
| GUID-02 | Missed-turn detection + silent auto-recalc, no asking/blaming | D-04 state machine with named constants; industry threshold anchors (OsmAnd deviation-distance setting verified; Mapbox off-route mechanism verified); engine A `/api/route` for recalc; replay tests per D-16 |
| GUID-03 | Honest ETA + remaining distance progressing with each fix, no whiplash | D-07 EMA + deadband; `cumulativeDistanceM` day-one field (05-01) is the progress backbone; formulation + constants researched |
| GUID-04 | Haptic cues at maneuver points | `HapticFeedback.selectionChanged()` / `impactOccurred(style)` Bot API 6.1+ [VERIFIED]; capability-guard pattern matches webapp.ts contract style |
| GUID-05 | Minimized mid-drive → bot pushes chat turn-cards, best-effort | Cron Triggers verified (`triggers.crons`, `scheduled()`, 60 s granularity, local test endpoint); KV `list({prefix:"guidance:"})` verified; `sendPhoto` URL+caption (≤1024 chars) verified; sharp SVG→PNG verified for the 8-glyph set |
| GUID-06 | Re-open mid-trip resumes live guidance at progressed position | Envelope read-back + `cumulativeDistanceM` projection from `startedAtSec`; resume via in-memory store (alive webview) or startParam deep-link from the turn-card; `isActive`/`activated` events (8.0+) verified |
| PX-01 | BackButton walks search → routes → guidance → back to search, never dead-ends | BackButton API verified (6.1+); existing two-state wire (`wireBackButton`) extends to the three-state graph via the same predicate+handler-as-one-change pattern |
| PX-03 | Safe areas respected, vertical-swipe-close disabled, "abandon route?" confirm during guidance | `disableVerticalSwipes()` Bot API 7.7+ [VERIFIED]; `safeAreaInset` 8.0+ [VERIFIED] (already in the webapp.ts contract); `enableClosingConfirmation()` 6.2+ [VERIFIED] as the only native-close hook (header swipe cannot be disabled — official docs) |
</phase_requirements>
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Maneuver banner / list / guidance card UI | Browser | — | DOM surface; pure builders Node-testable per the route-card precedent |
| Route progress + reroute detection (D-04 state machine) | Browser | — | Fixes never leave the device while on-route (GEO-03); pure module, replay-tested |
| ETA smoothing (EMA + deadband) | Browser | — | Recomputes from route model + client progress; no server round-trip per fix |
| Haptics, BackButton, swipe protection, safe areas | Browser | — | Telegram WebApp APIs live in the webview |
| Snapping + A\* route computation (engine A) | API / Backend (Worker) | — | D-05: engine A owns snapping — one authority; `/api/route` is the JSON surface |
| Road-graph binary build + hosting | Build tool (maintainer) → CDN/Storage (R2) | — | D-01: build script committed, forks upload their own graph to their own R2 |
| Envelope lifecycle (mint/write/delete, `guidance:<id>`) | API / Backend (Worker + KV) | Browser (triggers) | TTL-only KV law; chat resolved from the Bearer claim, never the body |
| Turn-card projection + Telegram push | API / Backend (Worker cron) | — | Backgrounded webview cannot push; cron scans + projects + sends |
| Trip end deletion + messaging | Browser (trigger) | API / Backend (delete) | D-14: both arrival and abandon paths delete the KV record |
## Standard Stack
### Core (all already in the repo or the platform — zero new runtime deps)
| Library / Service | Version | Purpose | Why Standard |
|-------------------|---------|---------|--------------|
| grammY | 1.46.0 (installed) | Cron-side Telegram sends (`bot.api.sendPhoto`) | Already the bot stack; `createBot(env)` works identically inside `scheduled()` |
| @araharah/routing | workspace (installed) | polyline6 codec, `cumulativeDistanceM`, `haversineM`, new: CSR decode + snap + A\* + `createHttpRoutingSource` | The D-01 seam — engine A plugs beneath it; bot's first workspace import (precedent: `@araharah/search` already imported by bot) |
| maplibre-gl | 6.15.0 (installed) | Guidance camera (locked follow, pitch 35°) | Existing; follow controller + recenter semantics reused (D-10) |
| Cloudflare R2 | platform | Graph binary hosting | D-01 binding; free tier; ranged reads available |
| Cloudflare Cron Triggers | platform | 60–120 s turn-card projection | D-11; `*/1 * * * *` supported; local test endpoint exists |
| Workers KV `chat_sessions` | platform (existing namespace) | `guidance:<id>` envelopes | Phase 5-frozen contract; TTL-only law |
| sharp | 0.35.5 [VERIFIED: npm registry] | SVG → PNG glyph build (maintainer script only) | libvips `svg` is a first-class input format [VERIFIED: sharp api-input docs]; devDependency only — runtime stays zero-dep |
### Supporting
| Library / Service | Version | Purpose | When to Use |
|-------------------|---------|---------|-------------|
| Overpass API | public instance | KL road-network extract (ways + `out geom`) | Primary build source — pure HTTP, no native deps, matches the `fetch-route-fixtures.mjs` maintainer-tool pattern |
| osm-pbf-parser | 2.3.0 [VERIFIED: npm registry] | Pure-JS OSM PBF decode | Fallback build path if Overpass bbox response proves unwieldy (Geofabrik Malaysia extract) |
| @resvg/resvg-js | 2.6.2 [VERIFIED: npm registry] | Alternative SVG → PNG renderer | Only if sharp's SVG rendering disappoints (native binary; sharp preferred) |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Overpass extract (primary) | Geofabrik .osm.pbf + osmium CLI | PBF is more robust for large regions, but osmium is NOT installed locally [VERIFIED env] and adds a native-toolchain install step for every fork — Overpass is the fork-friendliest $0 path |
| Overpass extract | planetiler / GraphHopper graph-builder | Java toolchains; produce rich graphs but far heavier than D-01's "compact typed arrays" intent and contradict turn-restrictions-deferred scope |
| Isolate memoization of the graph (recommended) | Workers Cache (`"cache": {"enabled": true}`) | Workers Cache now works on workers.dev [VERIFIED: developers.cloudflare.com/workers/cache], BUT requests carrying an `Authorization` header auto-bypass — `/api/route` is Bearer-authenticated, so it would never hit; cron invocations are never cached; and enabling it converts normally-free static-asset requests into billed ones against the 100 k/day free quota. Skip it. |
| Client-side ETA recompute | Server-computed ETA per fix | A server call per fix violates the memory/privacy law and burns the request budget; D-07 explicitly says recompute from the route model |
| KV cursor key per card push | Stateless cadence-only pushes | Cursor key costs KV writes (1,000/day free [VERIFIED]); stateless is simpler; discretion area |
**Installation (new devDependencies only — maintainer build surface):**
```bash
npm install -D -w routing sharp # glyph + graph build scripts only; zero runtime deps added
```
No runtime dependency changes are required: engine A is hand-rolled typed arrays (D-01), the replay fixtures are plain JSON, and cron sends ride grammY.
**Version verification:** `sharp@0.35.5`, `@resvg/resvg-js@2.6.2`, `osm-pbf-parser@2.3.0` verified against the registry this session [VERIFIED: npm registry]. Installed stack verified via `npm ls`: grammy 1.46.0, maplibre-gl 6.15.0, wrangler 4.151.3, @cloudflare/workers-types 5.20261022.1 (includes R2Bucket/Cron types).
## Architecture Patterns
### System Architecture Diagram
```
┌────────────────────────── BUILD TIME (maintainer) ──────────────────────────┐
│ Overpass API ──► build-road-graph.mjs ──► kl-roadgraph.v1.bin (CSR) │
│ hand SVGs ─────► build-glyphs.mjs ──────► 8 arrow PNGs (committed) │
│ wrangler r2 object put ──────────► R2: araharah-graphs/… │
└──────────────────────────────────────────────────────────────────────────────┘
│ R2 binding (ROAD_GRAPH)
▼
┌─── Browser (Mini App webview) ────────────────┐ ┌─── Cloudflare Worker ────────────────────────┐
│ watchPosition (high-accuracy) │ │ fetch(): │
│ └─ distance throttle ≥10 m / ≥5 s (D-15) │ │ POST /api/session, /api/location (existing)│
│ │ Fix │ │ POST /api/route ──► snap origin ─► A* ─► │
│ ▼ │ │ JSON route (+steps) │
│ guidance state machine (PURE, D-04) │ │ POST /api/guidance ──► mint envelope, │
│ ON_ROUTE ► OFF_ROUTE ► REROUTING ► ON_ROUTE │ │ KV guidance:<id> │
│ │ (storm guard, D-04) │ │ TTL 7200 s │
│ ├─ banner / list / card builders │ │ DELETE /api/guidance ─► KV delete (D-14) │
│ ├─ ETA EMA + deadband (D-07) │ │ scheduled(): every 60–120 s │
│ ├─ haptics (D-08) │ │ KV list(prefix "guidance:") │
│ └─ camera locked-follow (D-10) │ │ per envelope: decode polyline6, │
│ BackButton graph (PX-01) · confirm card │ │ project progress (elapsed × speed), │
│ (PX-03) · disableVerticalSwipes · safe areas │ │ maneuver changed? ──► sendPhoto(glyph URL, │
│ │ │ caption text) │
│ minimize (deactivated) ──POST envelope──────┼──►│ │
│ re-open ──GET resume / startParam───────────┼──►│ Telegram Bot API (grammY, cron side) │
└───────────────────────────────────────────────┘ └──────────────────────────────────────────────┘
│
▼
user's chat: turn-cards
(glyph + ETA/distance/street)
```
Trace the primary use case: driving → throttled fixes feed the pure state machine → banner/ETA update on-device → user misses a turn → deviation crosses threshold + grace → REROUTING → `POST /api/route` (one position sent) → engine A snaps + A\* → new route snaps in silently → user minimizes → envelope POSTed → cron projects and pushes cards → re-open resumes from envelope → trip ends → DELETE → "trip ended, data deleted".
### Recommended Project Structure
```
routing/
├── src/
│ ├── graph.ts # CSR binary decode → typed-array graph (pure)
│ ├── snap.ts # point-to-segment snapping + progress tracking (pure)
│ ├── astar.ts # typed-array A* (bidirectional) over the graph (pure)
│ └── http-source.ts # createHttpRoutingSource — web's RoutingSource backend #2
├── scripts/
│ ├── build-road-graph.mjs # Overpass extract → CSR binary (maintainer tool)
│ └── build-glyphs.mjs # 8 SVGs → PNGs (maintainer tool, sharp)
└── test/ # graph/snap/astar suites + fixture mini-graph binary
bot/
├── src/
│ ├── kv/guidance-sessions.ts # RELAX GE4–GE7: gains mint/get/delete (envelope shape frozen)
│ ├── routing/worker-router.ts # env.ROAD_GRAPH load + memoize + route handler (imports @araharah/routing)
│ └── guidance/cron.ts # scheduled() handler + PURE projectTurnCards() (Node-testable)
├── test/guidance-cron.test.mjs # projection/push decision tests + KV write-budget pins
└── wrangler.jsonc # +r2_buckets, +triggers.crons
web/
├── src/guidance/
│ ├── guidance.ts # THE pure state machine (D-04) — replay-tested, no DOM/map imports
│ ├── eta.ts # EMA + deadband (D-07) — pure
│ ├── banner.ts # maneuver banner + collapsible list builders
│ └── watch.ts # watchPosition + ≥10 m/≥5 s throttle (D-15)
└── test/guidance-*.test.mjs # replay suites + structural gates
web/test/fixtures/drives/ # recorded JSON position streams (D-16)
```
### Pattern 1: CSR (Compressed Sparse Row) graph in typed arrays
**What:** the classic adjacency layout — one flat edge array + per-node offset array — so A\* touches zero per-node JS objects.
**When to use:** engine A's entire data plane (D-01 mandates typed arrays).
**Example layout** (all little-endian; zero-copy `DataView`/`TypedArray` views over the R2 `ArrayBuffer`):
```
header: magic u32 | version u32 | nodeCount u32 | edgeCount u32 | bbox 4×f64
nodes: f64 lat[nodeCount] ++ f64 lng[nodeCount] (32 B/node)
offsets: u32 edgeStart[nodeCount + 1] (CSR row pointers)
edges: u32 targetNode[edgeCount] ++ u8 flags[edgeCount] (flags: bit0 oneway-reversed)
weights: f32 seconds[edgeCount] (speed-derived travel time)
```
Size sanity: 500 k nodes / 700 k edges ≈ 16 MB nodes + 2.8 MB offsets + 5 MB edges+weights ≈ **~24 MB** — inside D-01's 30–80 MB envelope with headroom [ASSUMED counts; the build script's committed stats gate pins the real number]. Float32 coordinates (≈0.000 003 ° ≈ 0.3 m resolution — well under GPS noise) halve node storage if the budget tightens.
### Pattern 2: typed-array A\* with a binary heap of indices
**What:** open set = binary heap over `f32 key[]` + `u32 node[]` backing arrays; g-scores `f64[]` indexed by node id; closed set `u8[]`. No `Map`, no objects — allocation-free hot loop.
**Heuristic:** haversine(toGoal) ÷ graphMaxSpeedKmh — admissible, consistent, cheap (precompute max speed at graph build).
**Bidirectional variant** (discretion area): alternate expansion from origin- and goal-rooted searches, stop when a node is settled by both; halves explored nodes typically [ASSUMED: classic result; the replay tests assert correctness, the CPU gate asserts the win].
**Reroute uses ONE best route** (no alternatives) — alternatives' penalty runs happen only at the pre-drive consult, where CPU headroom is not an active-drive concern.
### Pattern 3: deviation state machine as a pure function (D-04/D-16)
**What:** `stepGuidance(state, fix, routeModel, nowMs) → {state, effects[]}` — pure, injected clock, no DOM/map imports (the repo's Node-testability law, RS4/RS8 precedent). Replay fixtures are `Fix`-shaped JSON streams (same six fields as `web/src/location/types.ts`) pushed through the reducer; tests assert every transition, the storm guard, and the ETA deadband.
**Named constants** (initial values, replay-tuned — discretion area):
| Constant | Initial | Anchor |
|----------|---------|--------|
| `OFF_ROUTE_THRESHOLD_M` | 50 (accuracy-aware: `max(35, 2 × accuracyM)` capped 100) | OsmAnd ships exactly this control ("Minimal distance to recalculate route in case of deviation") [VERIFIED: osmand.net navigation-settings]; exact defaults undocumented for Mapbox/Google [ASSUMED value family 30–100 m] |
| `OFF_ROUTE_CONSECUTIVE_FIXES` | 3 | standard GPS-jitter rejection practice [ASSUMED] |
| `OFF_ROUTE_GRACE_MS` | 8000 | grace window before acting (D-04) [ASSUMED] |
| `STORM_GUARD_MS` | 20000 | minimum time between recalculations (D-04) [ASSUMED] |
| `MISSED_TURN_PAST_MANEUVER_M` | 40 | off-route past the maneuver point shortens grace (D-04) [ASSUMED] |
| `ARRIVAL_THRESHOLD_M` | 25 | snapped progress ≥ end-of-route (D-14) [ASSUMED] |
### Pattern 4: incremental snapping (client progress) vs graph snapping (server, D-05)
Two distinct operations — conflating them is the classic architecture error:
- **Client progress** (every throttled fix): project the fix onto the *selected route polyline* — walk forward from the last progress index (local window, point-to-segment in equirectangular space), read remaining distance from `cumulativeDistanceM`. No network, no graph. This drives banner/ETA/reroute-deviation.
- **Server snap** (only when `POST /api/route` fires): engine A projects the origin onto the *graph* (coarse grid index built once per isolate) — one authority for road geometry, per D-05.
This division means on-route fixes never leave the device; a reroute origin is the only in-drive position ever transmitted — the privacy-minimal reading of GEO-03.
### Pattern 5: envelope lifecycle as a structural foreground gate (D-11)
The cron pushes cards **only while an envelope exists** — the minimize→POST creates it, re-open or trip-end DELETE removes it. Foreground pausing is therefore structural (no key, no push), not a flag. Two timing options both honor the contract (D-06 discretion):
- **Write at minimize** (D-11's literal flow): the `deactivated` event (Bot API 8.0+ [VERIFIED]) fires the POST — but a backgrounding webview may freeze before the fetch completes. Honest best-effort (D-13), plus a belt: also attempt on `visibilitychange`-style earlier signals.
- **Write at guidance start, cron gated on a "minimized" marker**: costs extra KV writes; only worth it if the minimize-race proves unacceptable in the real drive.
Research recommends **write at minimize** with the race documented (D-13's honesty law), reassessed at the real-drive gate.
### Pattern 6: BackButton full state graph (PX-01) extends the existing wire
`wireBackButton(wa, hasOverlay, walkBack)` already implements "visible iff pushed state; overlay click → walkBack (never `close()` from a pushed state); root click → close()". The graph grows from 2 to 3+ states by making `walkBack` context-aware: guidance → (abandon confirm per D-09 if exiting guidance) → routes → search. Predicate and handler ship as one change — the Phase 5 header comment explicitly reserved this extension.
### Anti-Patterns to Avoid
- **Per-fix server calls:** any "POST my position each fix" design — violates GEO-03's spirit, burns requests, adds latency to the hot loop.
- **Global nearest-node snapping per fix:** O(n) scans per fix; also unstable (jumps between parallel roads). Track progress incrementally.
- **JS-object graphs** (`{node: {edges: [...]}}`): allocation churn kills the 10 ms budget; typed arrays or nothing (D-01 mandates this anyway).
- **Editing the frozen envelope shape:** a 5th field means drift (SessionClaim law). Any per-session state beyond the 4 fields is either a separate short-TTL key or doesn't exist.
- **`caches.default` for the graph:** non-functional on workers.dev [VERIFIED: developers.cloudflare.com/workers/runtime-apis/cache]; the isolate memoization pattern replaces it.
- **Alternatives generation during REROUTING:** 2–3 A\* runs inside an active-drive storm-guard window is CPU risk with zero UX need — the banner stays calm (D-04) and one good route is the product.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Great-circle distance | Custom lat/lng math per call site | `haversineM` from `@araharah/routing/src/geo.ts` (exists, tested) | One authority; the throttle (D-15) and the heuristic both need it |
| Polyline decode for cron projection | Re-parsing per push | `routing/src/polyline.ts` codec (round-trip-proven) | bot's workspace import gets the identical codec |
| Distance-along-line | Custom accumulation | `deriveCumulativeDistanceM` (exists) | The day-one field is already the contract for progress |
| SVG rasterization | Canvas hacks / external web services | `sharp` (libvips SVG input) in a maintainer script | Deterministic, committed PNGs; $0; no runtime dep |
| Telegram sends from cron | Raw `fetch("https://api.telegram.org…")` | `createBot(env).api.sendPhoto(...)` | grammY handles typing/retries; already the stack |
| Scheduled local testing | Deploy-to-test loops | `curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=*+*+*+*+*"` [VERIFIED: cron docs] | Instant iteration in `wrangler dev` |
| KV scan pagination | Ad-hoc key guessing | `list({prefix:"guidance:"})` + `list_complete` loop [VERIFIED: KV list docs] | Tombstone-aware pagination is documented behavior |
**Key insight:** every "infrastructure-ish" primitive this phase needs (codec, geo math, camera, harness, gates) already exists from Phases 3–5. The genuinely new code is one pure state machine, one typed-array router, and one cron projection — all Node-testable without a browser or a deployed Worker.
## Runtime State Inventory
Omitted — this is a greenfield feature phase (no rename/refactor/migration of existing strings or records).
## Common Pitfalls
### Pitfall 1: Free-plan CPU ceiling vs A\* (the phase's top risk)
**What goes wrong:** consistent >10 ms CPU invocations die with Error 1102 ("Worker exceeded resource limits") [VERIFIED: limits docs]; on the free plan this applies per HTTP request AND per cron tick.
**Why it happens:** A\* over a too-large graph (unfiltered highway=\* includes footways/service roads), per-node object allocation, or recomputing the graph-grid index per request.
**How to avoid:** filter road classes at build (drivable classes only); typed-array everything; build the snap-grid lazily once per isolate; measure early — a dedicated task runs a real route through `wrangler dev` and reads the invocation CPU time; the budget gate belongs in the plan, not in hope.
**Warning signs:** `exceededCpu` in invocation logs; slow route responses in dev (~>200 ms wall on cold isolate).
### Pitfall 2: The minimize race on the envelope POST
**What goes wrong:** the app tries to POST the envelope during `deactivated`, but the webview's JS is frozen before the fetch completes → no envelope → no cards (GUID-05 silently absent).
**Why it happens:** mobile webviews suspend timers/network on backgrounding non-deterministically.
**How to avoid:** fire the POST on the earliest available signal; treat failure as documented best-effort (D-13); optionally also write at guidance start (discretion allows) so the worst case is "cards begin one cadence late".
**Warning signs:** real-drive test shows minimize→no-card gaps; bot-side KV list empty while user reports driving.
### Pitfall 3: Cron cardinality vs the KV write budget
**What goes wrong:** per-session cursor writes each tick (every 60 s) exhaust 1,000 writes/day [VERIFIED] after ~16 session-hours; or same-key writes violate the 1 write/s/key limit.
**How to avoid:** stateless cadence ("push every N minutes when maneuver changed by projection" derived from wall clock, no cursor), or cursor with a coarse write rate; pin the budget with a bot-side gate like PG1.
**Warning signs:** KV write metrics climbing per cron tick; 429-class KV behavior.
### Pitfall 4: `disableVerticalSwipes()` is not close protection
**What goes wrong:** assuming disabling vertical swipes prevents app closure during guidance.
**Why it happens:** official docs state the user can ALWAYS minimize/close by swiping the app's *header* regardless [VERIFIED: core.telegram.org/bots/webapps, `isVerticalSwipesEnabled`].
**How to avoid:** layer defenses — `disableVerticalSwipes()` (7.7+) kills body-swipe minimize; `enableClosingConfirmation()` (6.2+) puts a native confirm on the close path; the D-09 Rams card guards in-app exits. Research recommends enabling the native closing confirmation during guidance as defense-in-depth (planner decision).
**Warning signs:** device test closes the app from the header with no confirm.
### Pitfall 5: Version-gating Telegram APIs
**What goes wrong:** `HapticFeedback`/`disableVerticalSwipes`/`safeAreaInset` calls throw or no-op silently on older clients.
**Why it happens:** features span Bot API 6.1 → 8.0+; Telegram clients in the wild lag.
**How to avoid:** capability-guard exactly like the existing contract: optional members on `TelegramWebAppLike` + `isVersionAtLeast("7.7")` checks + `?.` chains; haptics are graceful no-ops by design (D-08).
**Warning signs:** TS forcing non-optional members onto the fake; exceptions on the Android device matrix.
### Pitfall 6: Reroute storm after one excursion
**What goes wrong:** each fresh fix re-triggers REROUTING (dense urban canyon near parallel roads).
**Why it happens:** missing grace/consecutive-fix/storm-guard constants, or deviation measured against the OLD route after a successful reroute.
**How to avoid:** implement the full D-04 machine (threshold + grace + N fixes + storm guard) with the constants table above; after ON_ROUTE, deviation is measured against the NEW route only; replay tests assert the storm guard explicitly (D-16).
**Warning signs:** >1 `/api/route` call inside STORM_GUARD_MS in replays.
### Pitfall 7: RP1/RP2 and GE4–GE7 gates reddening "unexpectedly"
**What goes wrong:** Phase 6's first bot-side routing import fails `bot:test` because 05-05's RP gates ban `/api/route` and `@araharah/routing` in bot/src; GE4–GE7 ban the envelope service.
**Why it happens:** those gates were designed as negative-space guards with documented Phase 6 handoffs.
**How to avoid:** the plans that add these surfaces re-pin the gates in the same change (the C6/L3/GE handoff precedent) — RP1/RP2 shrink to ban *unauthenticated* or *proxy-shaped* routing; GE5/GE6/GE7 re-pin to "exactly two KV namespaces", "Env has guidance fields", "service exists".
**Warning signs:** RED suites at phase start — expected, and each gate's doc-comment names the owning phase.
### Pitfall 8: Equirectangular math near the equator vs meters
**What goes wrong:** point-to-segment projection using raw degree deltas produces distorted distances in lng.
**How to avoid:** scale lng by `cos(lat)` (KL at ~3.1° N — error negligible) or convert via local-meter approximation; all displayed distances route through `cumulativeDistanceM`/`haversineM`, never raw Δdegrees.
## Code Examples
### Cron config + scheduled handler (wrangler.jsonc + Worker)
```jsonc
// bot/wrangler.jsonc — additions (existing keys unchanged)
{
"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }],
"triggers": { "crons": ["*/1 * * * *"] } // 60 s cadence; app-side logic decides whether a card is due (D-11: 60–120 s)
}
```
```typescript
// webhook.ts — extend the existing ExportedHandler (both handlers on ONE worker)
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> { /* existing + /api/route + /api/guidance */ },
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
// scan → project → push; category-only logs (D-14)
let cursor: string | undefined;
do {
const page = await env.chat_sessions.list({ prefix: "guidance:", cursor });
for (const key of page.keys) { /* get → projectTurnCards → maybe sendPhoto */ }
cursor = page.list_complete ? undefined : page.cursor;
} while (cursor !== undefined);
},
} satisfies ExportedHandler<Env>;
```
Sources: [VERIFIED: developers.cloudflare.com/workers/configuration/cron-triggers, /r2/api/workers/workers-api-reference, /kv/api/list-keys]
### R2 graph load + isolate memoization (the cache that works)
```typescript
// bot/src/routing/worker-router.ts
let graphP: Promise<Graph> | null = null; // module scope = per-isolate memo
function loadGraph(env: Env): Promise<Graph> {
graphP ??= env.ROAD_GRAPH.get(GRAPH_KEY) // 1 R2 GET per cold isolate (I/O, not CPU)
.then(o => o === null ? null : decodeGraph(await o.arrayBuffer()));
return graphP;
}
```
Ranged reads (`get(key, { range: { offset, length } })`) exist if sub-graph streaming is ever needed [VERIFIED: R2 API reference].
### Telegram capability guards (extending the hand-rolled contract)
```typescript
// TelegramWebAppLike gains OPTIONAL members (the contract stays the contract):
HapticFeedback?: { impactOccurred(style: "light" | "medium" | "heavy" | "rigid" | "soft"): unknown;
notificationOccurred(type: "error" | "success" | "warning"): unknown;
selectionChanged(): unknown };
disableVerticalSwipes?: () => void; // Bot API 7.7+
enableVerticalSwipes?: () => void;
enableClosingConfirmation?: () => void; // Bot API 6.2+
isActive?: boolean; // Bot API 8.0+
// events: "activated" | "deactivated" via existing onEvent?(event, cb)
```
All names/versions verified against the official page this session [VERIFIED: core.telegram.org/bots/webapps].
### Distance throttle (D-15) reusing the workspace geo module
```typescript
import { haversineM } from "@araharah/routing/src/geo.ts";
const moved = last === null ? Infinity : haversineM(last, fix);
if (moved >= 10 || fix.ts - lastAcceptedTs >= 5000) accept(fix); // ≥10 m moved OR ≥5 s elapsed
```
The web platform has NO native distance filter — `watchPosition` options are only `enableHighAccuracy/timeout/maximumAge` [VERIFIED: MDN]; the throttle is app-side by design.
### Turn-card send (cron side)
```typescript
await bot.api.sendPhoto(chat_id, `${MINI_APP_URL}/glyphs/${turnGlyph}.png`, {
caption, // ≤ 1024 chars [VERIFIED: Bot API sendPhoto]
parse_mode: "HTML",
});
```
`sendPhoto` accepts a public HTTPS URL (Telegram fetches it) — glyphs ride the Worker's existing static assets [VERIFIED: Bot API via gramio mirror of core.telegram.org/bots/api].
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Cache API on custom domains only | Workers Cache (`"cache": {"enabled": true}`) works on workers.dev, tiered, request-collapsing | Docs current as of Oct 2026 | Not needed this phase (Bearer auto-bypass + cron uncached); isolate memoization remains correct |
| Workers CPU 30 s (paid) / 10 ms (free) | Unchanged, but cron wall time documented at 15 min; CPU free = 10 ms/tick | limits doc updated 2026-10-12 | Confirms the A\* design constraint |
| Bot API ~7.x assumed in training | Bot API 10.1 (June 2026); Mini Apps gained `isActive`, fullscreen, safe areas, LocationManager at 8.0 (Nov 2024) | core.telegram.org changelog | All Phase 6 APIs (6.1/6.2/7.7/8.0) are years-stable; `isActive`/`deactivated` give clean minimize detection |
| `@types/telegram-web-app` dependency | Hand-rolled `TelegramWebAppLike` contract | Project decision (Phase 3) | Grows by optional members only; fakes stay cheap |
**Deprecated/outdated:**
- Cloudflare Pages (already superseded project-wide; nothing here touches it)
- `node-osmium` npm bindings (abandoned upstream — do not use; CLI osmium or pure-JS parsing instead)
- Unbound/Bundled Workers plans (deprecated platform-side; irrelevant to a new free-plan Worker)
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | KL-metro A\* fits the 10 ms free-plan CPU budget with class-filtered CSR + typed arrays | Pitfall 1 / Standard Stack | Reroute calls fail 1102 → mitigations: shrink graph (drop minor classes), bidirectional A\*, or document Workers Paid ($5/mo, breaks $0 law) as an explicit fork decision. CPU-measurement task mandatory before wide rollout. |
| A2 | KL drivable graph ≈ 300–800 k nodes / ≤1 M edges → ~18–50 MB binary | Pattern 1 | If far larger, binary exceeds 30–80 MB envelope → tighten class filter / Float32 coords / split tiles; build script's stats gate catches it immediately |
| A3 | R2 free tier ≈ 10 GB storage, 1 M Class-A + 10 M Class-B ops/month | Standard Stack | If changed, upload/read pattern still $0 at this project's scale; verify at bucket-creation task |
| A4 | Reroute threshold family (50 m / 3 fixes / 8 s grace / 20 s storm) are sane starting constants | Pattern 3 | Constants are replay-tuned by design (D-04); wrong initial values cost iteration time only |
| A5 | EMA α ≈ 0.25/fix smooths adequately with the locked ±1 min/±5% deadband | D-07 | Same replay-tuning story as A4 |
| A6 | Backgrounded webviews stop receiving `watchPosition` callbacks (making cron cards the right mechanism) | Architecture | If fixes continued, cron projection would double-count; harmless (envelope carries no live fix), but the D-11 framing already assumes it |
| A7 | Overpass `out geom` for the KL bbox returns ~50–300 MB JSON within timeout=300 | Don't Hand-Roll / build script | Fallback path documented: Geofabrik extract + osm-pbf-parser (pure JS); worst case split the bbox into 2–4 queries |
| A8 | KV write budget (1,000/day free) suffices with stateless or coarse cursor pushes at v1 scale | Pitfall 3 | Budget gate pin + stateless fallback; if v1 traffic grows, this surfaces as a planned-purchase decision, not a redesign |
| A9 | `watchPosition` fix cadence in Android WebView ≈ 1 Hz under `enableHighAccuracy` | Pattern 4 | Only affects tuning feel; the ≥10 m/≥5 s throttle normalizes any cadence |
## Open Questions (RESOLVED)
1. **Resume path when the webview was KILLED (not just minimized)**
- What we know: alive-minimized webview resumes from its memory-held session; a killed one boots fresh with no guidance id. The frozen envelope is keyed by opaque `guidance:<id>`; KV cannot be queried by chat_id value.
- What's unclear: whether the turn-card's OPEN deep-link should carry `startapp=<guidance-id>` (startParam is already in the contract; Bot API direct-link `startapp` verified) so a fresh boot can GET and resume.
- Recommendation: yes — card button = Mini App URL + `?g=<id>`/startapp param; fresh boot reads it, fetches the envelope, resumes. No extra KV keys; privacy-clean (id is opaque, TTL-bounded). Planner picks the exact knob name.
- RESOLVED: the `?g=` knob ships — 06-07's card button carries `<MINI_APP_URL>/?g=<id>` (web_app url) and 06-06 task 3 reads it at boot (UUID-validate → GET → RESUMING state).
2. **Native closing confirmation during guidance**
- What we know: the header swipe cannot be disabled (official docs); `enableClosingConfirmation()` is the only native hook; D-09's Rams card covers in-app exits.
- Recommendation: enable it during guidance as defense-in-depth; the Rams card remains the designed surface. Flag for UI-SPEC.
- RESOLVED: engaged during guidance as defense-in-depth, version-gated 6.2+ — 06-05 task 4 (close-protection.ts engage/release); the Rams card remains the designed surface (UI-SPEC records the layering).
3. **Cron push state: cursor key vs stateless cadence**
- What we know: 1,000 KV writes/day free; cursor writes cost 1/push; stateless cadence may repeat or skip cards around maneuver boundaries.
- Recommendation: start stateless ("push when projected next-maneuver index differs from previous tick's *computed* index" — derivable without state by re-deriving from wall clock? No — needs state; so: fixed cadence N=3 min). Reassess at the real drive. Planner decides within discretion.
- RESOLVED: stateless cadence — `CARD_CADENCE_S = 180`, `due = nowSec % 180 < 60`, zero KV writes — 06-07 task 1 (cron.ts); reassessment happens at the real drive (06-08 Part B).
4. **Overpass bbox exact extent**
- What we know: must cover the gazetteer's KL-metro region (KL_CENTER [101.6958, 3.1513] + fixture pairs to IOI City Mall in the south).
- Recommendation: derive the bbox from the committed corpus + gazetteer extremes + margin, pinned as a named constant with the build's stats gate; keeps tuning mechanical.
- RESOLVED: bbox computed at build time from the golden corpus pairs + gazetteer extremes + margin, printed by the stats gate — 06-04 task 1 (build-road-graph.mjs).
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node | all build/test | ✓ | 24.18.0 | — |
| npm workspaces | workspace scripts | ✓ | 11.16.0 | — |
| wrangler | R2 bucket, cron config, dev, deploy | ✓ | 4.151.3 | — |
| @cloudflare/workers-types | R2Bucket/ScheduledController types | ✓ | 5.20261022.1 | — |
| playwright | capture legs (device-gate prep) | ✓ | 1.64.1 | — |
| osmium CLI | PBF graph build (fallback path) | ✗ | — | Overpass JSON primary (no install); osm-pbf-parser npm as second fallback |
| inkscape / rsvg-convert | SVG → PNG | ✗ | — | sharp 0.35.5 (npm, libvips SVG input) [VERIFIED] |
| R2 bucket `araharah-graphs` | graph hosting | ✗ (not yet created) | — | `npx wrangler r2 bucket create` — first-time setup task (R2 on the account: verify free-tier entitlement at setup) |
| Cron trigger | turn-cards | ✗ (not yet configured) | — | wrangler.jsonc + deploy; 15 min propagation noted [VERIFIED] |
| Node under nvm | every shell command | ✓ | — | `source ~/.nvm/nvm.sh` first (all 20 prior plans' precedent) |
**Missing dependencies with no fallback:** none — every gap has a documented setup task or fallback.
**Missing dependencies with fallback:** osmium (→ Overpass primary), rasterizers (→ sharp), R2/cron provisioning (→ setup tasks in-plan, fork docs per D-01).
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Hand-rolled zero-dep runners (established Phases 1–5): `web/test/runner.mjs`, `bot/test/runner.mjs`, `routing/test/runner.mjs` over `harness.mjs` registries |
| Config file | none — auto-discovers `*.test.mjs` per workspace |
| Quick run command | `npm test -w web` / `npm test -w bot` / `npm run routing:test` |
| Full suite command | `npm run web:test && npm run bot:test && npm run routing:test` (+ `npm run web:capture` for legs, `npx wrangler dev` + `/cdn-cgi/local/scheduled` for cron integration) |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| GUID-01 | Banner (distance+arrow+street) renders; list collapses/expands | unit (builders, fake DOM) + capture leg | `npm test -w web` (guidance-surface suite) | ❌ Wave 0 (`web/test/guidance-surface.test.mjs`) |
| GUID-02 | OFF_ROUTE→REROUTING→ON_ROUTE transitions; storm guard; no re-route spam | replay (pure state machine, JSON drives) | `npm test -w web` (replay suite) + real-drive device gate | ❌ Wave 0 (`web/test/guidance-replay.test.mjs`, `web/test/fixtures/drives/*.json`) |
| GUID-03 | ETA/remaining progress; EMA + deadband holds under jitter | replay | `npm test -w web` (eta suite inside replay fixtures) | ❌ Wave 0 (`web/test/guidance-eta.test.mjs`) |
| GUID-04 | Haptics fire at approach/maneuver; no-op when unsupported | structural + capability-guard unit | `npm test -w web` | ❌ Wave 0 (inside guidance-surface suite) — real feel only at device gate |
| GUID-05 | Cron projects progress; pushes on maneuver change; pauses foreground (structural: no envelope = no push) | unit (pure `projectTurnCards`) + wrangler-dev cron endpoint + KV budget pins | `npm test -w bot` + `curl "http://localhost:8787/cdn-cgi/local/scheduled"` | ❌ Wave 0 (`bot/test/guidance-cron.test.mjs`) |
| GUID-06 | Resume at progressed position from envelope | replay + unit (projection math) | `npm test -w web` | ❌ Wave 0 (replay fixture: minimize→resume scenario) |
| PX-01 | BackButton walks search→routes→guidance→search; never dead-ends | behavioral (fake WebApp) | `npm test -w web` (backbutton-graph suite) | ❌ Wave 0 (extend existing backbutton coverage) |
| PX-03 | disableVerticalSwipes on guidance; confirm card gates exit; safe-area padding applied | structural + unit | `npm test -w web` | ❌ Wave 0 (inside guidance-surface suite) — device gate covers real swipes |
| Engine A (D-01/D-03) | CSR decode round-trip; snap correctness; A\* equals brute-force Dijkstra on a fixture mini-graph; honest null when graph absent | unit | `npm run routing:test` | ❌ Wave 0 (`routing/test/graph.test.mjs`, `astar.test.mjs`, `snap.test.mjs` + committed mini-graph fixture) |
| Engine A perf | A\* CPU within budget on realistic subgraph; `/api/route` wall time sane | integration (wrangler dev invocation CPU + timing) | `npx wrangler dev` + scripted route calls; read invocation CPU logs | ❌ Wave 0 (script; measurement printed per BA9 "budget gates print their measurement" pattern) |
| D-14 | Trip end deletes envelope; both arrival and abandon paths | unit (KV fake) + bot gates | `npm test -w bot` | ❌ Wave 0 (guidance-envelope service suite; GE4–GE7 re-pins) |
| PX-03/GUID-02 device gates | one real drive: banner legibility, reroute self-heal, resume, trip-end messaging | manual-only (device gate — the phase's declared protocol, per D-16 and the phase 3/4 precedent) | human drive with capture checklist | ❌ device-gate doc (phase 6 exit) |
### Sampling Rate
- **Per task commit:** the touched workspace's quick suite (`npm test -w web|bot` / `npm run routing:test`)
- **Per wave merge:** full three-workspace suite + `npm run web:typecheck` / `bot:typecheck` / `routing:typecheck` (+ theme audit when CSS touched)
- **Phase gate:** all suites green + capture legs (extended with guidance legs) + the one real drive + A\* CPU measurement printed and inside budget, before `/gsd-verify-work`
### Wave 0 Gaps
- [ ] `routing/test/` graph/astar/snap suites + committed mini-graph binary fixture (hand-built tiny CSR — also the replay ground truth)
- [ ] `web/test/fixtures/drives/` — four recorded streams per D-16 (corridor follow, off-route excursion, missed-turn excursion, GPS jitter)
- [ ] `web/test/guidance-{replay,eta,surface}.test.mjs` + `bot/test/guidance-cron.test.mjs`
- [ ] Gate re-pin tasks: RP1/RP2 (bot route surface lands), GE4–GE7 (envelope service lands)
- [ ] Setup tasks: `wrangler r2 bucket create` + upload script + fork docs; `triggers.crons` + glyph assets in static dir; `npm install -D -w routing sharp` for build scripts
- [ ] BA9 budget extension to cover `web/src/guidance/` (+ whatever the bundle law names)
*(Framework install: none needed — the harness family already exists.)*
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | yes (Worker endpoints) | Existing initData→Bearer session gate reused for `/api/route`, `/api/guidance`; cron authenticates to Telegram via BOT_TOKEN secret only |
| V3 Session Management | yes | One-time opaque tokens (existing mintSession rotation); envelope carries NO token — chat_id resolved server-side from the claim |
| V4 Access Control | yes — the phase's core server surface | `/api/guidance` derives chat ONLY from the Bearer claim (the `/api/location` precedent, T-03-01); cron sends ONLY to envelope.chat_id read from KV; no client-supplied chat ids anywhere |
| V5 Input Validation | yes | Origin lat/lng validated + clamped to the graph bbox before A\* (reject non-numeric/out-of-range — 400 category); route/guidance request bodies size-capped (MAX_INITDATA_CHARS pattern); no zod — hand validation per repo pattern |
| V6 Cryptography | no new | No new crypto; session ids remain `crypto.randomUUID()` (T-02-05 precedent) |
| V7/V9 (logging/errors) | yes | D-14 category-only logging extended to cron + routing paths — never coordinates, chat ids, or payloads in logs |
### Known Threat Patterns for {Cloudflare Workers + KV/R2 + Telegram}
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Unauthenticated routing oracle (anyone hammering /api/route for free routing) | DoS / Elevation | Bearer-gate `/api/route` like `/api/location`; CPU budget per request is the natural rate limiter on free plan |
| Envelope injection / cross-chat card push (client posts someone else's chat_id) | Tampering / Spoofing | chat_id from claim only; envelope minted server-side; randomUUID ids |
| Graph binary tampering (fork uploads malicious/absent graph) | Tampering | Graph absence = honest router-pending (D-03, designed); upload script is maintainer-surface; header magic+version+size check on decode, fail-honest |
| Privacy leak via cron logs or card copy | Information Disclosure | Cards carry ETA/distance/street to the USER'S OWN chat only; logs category-only (D-14); envelope TTL 7200 s + D-14 active delete |
| Origin leakage while on-route (continuous tracking) | Information Disclosure | On-route fixes never transmitted (Pattern 4 division of labor); only reroute origins + minimize-time envelope leave the device |
## Sources
### Primary (HIGH confidence)
- developers.cloudflare.com/workers/platform/limits — CPU 10 ms free/5 min paid; cron CPU 10 ms free/30 s paid; 128 MB memory; cron wall 15 min; KV/R2 subrequest accounting (fetched 2026-10-24)
- developers.cloudflare.com/workers/configuration/cron-triggers — triggers.crons config, scheduled() signature, `*/1 * * * *`, `/cdn-cgi/local/scheduled` local testing, 15-min propagation
- developers.cloudflare.com/r2/api/workers/workers-api-reference — r2_buckets binding, get/arrayBuffer, range reads, list
- developers.cloudflare.com/kv/platform/limits — 100 k reads/day, 1,000 writes/day free, 1 write/s/key, 25 MiB values, 1,000 ops/invocation
- developers.cloudflare.com/kv/api/list-keys — list({prefix,cursor}), list_complete, tombstone caveat
- developers.cloudflare.com/workers/cache + /workers/runtime-apis/cache — classic Cache API non-functional on workers.dev; new Workers Cache: workers.dev OK but Authorization auto-bypass, cron never cached
- core.telegram.org/bots/webapps — HapticFeedback (6.1+), BackButton (6.1+), disableVerticalSwipes (7.7+), enableClosingConfirmation (6.2+), safeAreaInset (8.0+), isActive/activated/deactivated (8.0+), header-swipe-cannot-be-disabled note
- developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition — options surface (no distance filter)
- osmand.net/docs/user/navigation/guidance/navigation-settings — "Minimal distance to recalculate route" control
- docs.mapbox.com/android/navigation/guides/turn-by-turn-navigation/rerouting-and-refresh — off-route observer → auto-reroute mechanism
- sharp docs (api-input) — SVG first-class input format; npm registry — sharp 0.35.5, @resvg/resvg-js 2.6.2, osm-pbf-parser 2.3.0
### Secondary (MEDIUM confidence)
- Telegram Bot API sendPhoto semantics (caption ≤1024 chars, photo-by-URL) — core.telegram.org/bots/api content mirrored via gramio.dev + shapeless.ai spec sheets, cross-agreeing
- wiki.openstreetmap.org/Overpass_API + gis.stackexchange — `out geom`, bbox query patterns for road extraction
### Tertiary (LOW confidence — flagged for validation)
- Mapbox/Google specific off-route default values (undocumented publicly; ASSUMED family values only)
- Background webview geolocation suspension specifics per OS (A6)
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — every platform API and version verified against official docs/registry this session
- Architecture: HIGH for boundaries (seams all exist from Phases 3–5; division of labor is forced by privacy law), MEDIUM for cron/envelope lifecycle edge (minimize race, documented best-effort per D-13)
- Pitfalls: HIGH on platform limits and gate re-pins (verified/documentable), MEDIUM on performance constants (A1/A4 — measurement tasks mandated)
- Validation: HIGH — replay design directly extends the established hand-rolled harness; device-gate protocol inherited from phases 3–5
**Research date:** 2026-10-24
**Valid until:** 2026-11-23 (platform docs current as of Oct 2026; re-verify Workers limits + Telegram changelog if planning slips past 30 days)