Files
arah2/.planning/phases/06-turn-by-turn-guidance-live-driving/06-RESEARCH.md
T

56 KiB
Raw Blame History

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):

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".

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)

// 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)
}
// 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)

// 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)

// 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

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)

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)