# 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:`, 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 | 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) | ## 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:`) | 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:` 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: │ │ │ (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 { /* existing + /api/route + /api/guidance */ }, async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise { // 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; ``` 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 | null = null; // module scope = per-isolate memo function loadGraph(env: Env): Promise { 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:`; KV cannot be queried by chat_id value. - What's unclear: whether the turn-card's OPEN deep-link should carry `startapp=` (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=`/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 `/?g=` (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)