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