Files

24 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-turn-by-turn-guidance-live-driving 06 execute 3
06-02
06-03
06-04
06-05
web/src/telegram/backbutton.ts
web/src/map/follow.ts
web/src/guidance/camera.ts
web/src/route/route.ts
web/src/main.ts
web/test/route-surface.test.mjs
true
GUID-02
GUID-05
GUID-06
PX-01
PX-03
truths artifacts key_links
Guidance activates when the route is drawn and the first accepted fix is ≥ GUIDANCE_START_ALONG_M along it — never without a route (PX-03's precondition)
The Telegram BackButton walks search → routes → guidance → (confirm) → routes → search and never dead-ends; close() fires only at the true root (PX-01)
BackButton from guidance opens the abandon confirm card; back while the confirm is open means CONTINUE (D-09)
The camera rides the SNAPPED line (locked follow, padding {top:320,bottom:64,left:64,right:64}, pitch 35, north-up); user pan unlocks → the recenter control returns (D-10)
A missed turn flows to a silent /api/route reroute and the new line snaps in — banner calm, no blame (GUID-02 wired)
Minimize POSTs the envelope (keepalive, best-effort D-13); re-open DELETEs it (cron pauses — structural); a fresh boot with ?g=<id> resumes at the progressed position (GUID-05/GUID-06)
Arrival AND abandon both: delete the envelope (idempotent), stop the watch, release close protection, show the trip-ended card (D-14)
path provides exports
web/src/guidance/camera.ts locked-follow guidance camera over the snapped line
GUIDANCE_CAMERA_PADDING
GUIDANCE_ZOOM
GUIDANCE_EASE_MS
createGuidanceCamera
path provides
web/src/route/route.ts the guidance controller wiring: pump, effects dispatch, chrome updates, envelope flows
path provides
web/src/telegram/backbutton.ts the context-aware full-state-graph wire (predicate + handler as one change)
from to via pattern
web/src/route/route.ts web/src/guidance/guidance.ts watch → shouldAcceptFix → stepGuidance → effects stepGuidance
from to via pattern
web/src/route/route.ts web/src/guidance/banner.ts chrome re-render per accepted fix buildGuidanceBanner
from to via pattern
web/src/route/route.ts /api/guidance POST on deactivated (keepalive), DELETE on activated + trip end /api/guidance
from to via pattern
web/src/main.ts routing/src/http-source.ts engine-a first, fixture fallback — the composed ladder, code-constructed createHttpRoutingSource
from to via pattern
web/test/route-surface.test.mjs web/src/route/route.ts RS6 re-pinned (guidance IMPLEMENTATION lives in web/src/guidance/) + the PX-01 full-walk test RS6|walkBack
Wire the guidance brain (06-02) into the live shell through the chrome (06-05) and the server (06-04/03): the full PX-01 BackButton state graph, the D-10 locked-follow camera, the fix pump dispatching banner/list/card/haptics/reroute, and the minimize/resume/trip-end envelope flows — completing GUID-02/05/06's client half and D-14's in-app half.

Purpose: this is where navigation becomes behavior. The BackButton walk (PX-01) only exists now that the full state graph does; the envelope lifecycle (D-11's structural foreground gate) only works when the app POSTs on minimize and DELETEs on return. Output: camera + pump + BackButton graph + envelope flows wired in route.ts/main.ts, RS6 re-pinned, PX-01 walk proven with a recording fake.

<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md @.planning/phases/06-turn-by-turn-guidance-live-driving/06-CONTEXT.md @.planning/phases/06-turn-by-turn-guidance-live-driving/06-PATTERNS.md @.planning/phases/06-turn-by-turn-guidance-live-driving/06-RESEARCH.md

RS6 re-pin (PATTERNS caution 3): the gate currently bans progress/maneuver/btn-primary tokens in web/src/route/. This plan's route.ts gains guidance REFERENCES — re-pin RS6 in the same change: the ban becomes "guidance IMPLEMENTATION lives in web/src/guidance/ — route/ files may import from ../guidance/ and call it, but must not define maneuver/progress rendering inline" (structural: the tokens are allowed only on import … from "../guidance/…" lines and direct call sites).

task 1: Guidance camera (locked follow) + BackButton full state graph web/src/guidance/camera.ts, web/src/map/follow.ts, web/src/telegram/backbutton.ts, web/test/route-surface.test.mjs - web/src/map/follow.ts (the mode machine + programmatic-move lock + CAMERA_PADDING_PX — extend, don't reinvent) - web/src/telegram/backbutton.ts (wireBackButton — the predicate + handler-as-one-change law; the Phase 5 header comment reserved this extension) - web/src/ui/recenter.ts (data-attribute state swaps; the control returns unlocked on pan) - web/test/route-surface.test.mjs RS7 (the recording fake-wa walk test — extend for the full graph) - .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md Camera Behavior + BackButton Contract tables (BINDING parameters) - createGuidanceCamera.easeToSnapped(point): fakeMap.easeTo recorded with center=snapped point, padding {top:320,bottom:64,left:64,right:64}, pitch 35, zoom 16, duration 400 — bearing NEVER passed (north-up) - programmatic moves (originalEvent undefined) do NOT unlock follow; a movestart WITH originalEvent unlocks → recenter control's follow notch clears (Phase 3 semantics) - follow.ts's existing follow(fix) behavior is unchanged when called without opts (Phase 3/5 routes keep their camera law) - PX-01 walk with the recording fake: root → hidden; route state → shown, click → Phase 5 walk back; guidance → shown, click → confirm card opens (NOT teardown); confirm open → click → confirm dismissed (CONTINUE), guidance alive; YES ABANDON → chrome clears → route card state → click → Phase 5 walk → destination card (root) → hidden; trip-ended card → click → search root, hidden; close() recorded ZERO times anywhere except an explicit root close path 1. `web/src/map/follow.ts`: extend `follow(fix, opts?: { padding?: {top,bottom,left,right}; pitch?: number; zoom?: number; durationMs?: number })` — opts spread over the CURRENT defaults so existing callers are byte-identical in behavior. Keep the lock/originalEvent law untouched. 2. `web/src/guidance/camera.ts`: named exports `GUIDANCE_CAMERA_PADDING = { top: 320, bottom: 64, left: 64, right: 64 }`, `GUIDANCE_ZOOM = 16`, `GUIDANCE_EASE_MS = 400`, `GUIDANCE_CAMERA_PITCH = 35`; `createGuidanceCamera(map, followController)` returning `{ easeToSnapped(point: RoutePoint): void; onRerouteApplied(): void }` — easeToSnapped calls followController.follow(point, { padding: GUIDANCE_CAMERA_PADDING, pitch: GUIDANCE_CAMERA_PITCH, zoom: GUIDANCE_ZOOM, durationMs: GUIDANCE_EASE_MS }); onRerouteApplied is a documented no-op camera-wise (setData happens in the route layer — UI-SPEC: no camera jump on reroute). 3. `web/src/telegram/backbutton.ts`: grow the wire's doc-header to the 3+ state graph and extend the predicate/handler so `walkBack` is CONTEXT-AWARE via an injected `resolveAction(): BackAction` where `BackAction = { kind: "hidden" } | { kind: "close" } | { kind: "callback"; fn: () => void }` — keep the old two-argument call shape working (hasOverlay + walkBack compose into the resolver internally) so Phase 5 call sites and RS7 stay valid; the visible-iff-pushed-state and never-close-from-pushed laws hold for every new state. 4. RED→GREEN in `web/test/route-surface.test.mjs`: extend RS7's recording-fake block with the full-walk scenario per the behavior list (the confirm-card steps assert the CALLBACKS the fake receives — the confirm surface itself is 06-05's, wired in task 2), plus camera tests with the route-surface fakeMap pattern (easeTo args recorded; unlock law). npm test -w web && npm run web:typecheck - camera.ts exports the three constants with EXACT UI-SPEC values (test pins them, RS4 style) - fakeMap easeTo assertions: padding/pitch/zoom/duration exact; no bearing key ever present - follow.ts without opts behaves byte-identically (existing follow tests untouched-green) - RS7 extended: full walk recorded, close() count === 0 across all pushed-state clicks - typecheck clean The camera rides the snapped line at the UI-SPEC pose and unlocks only on user gesture; the BackButton resolver walks every state with zero dead ends and zero premature closes task 2: The guidance pump in route.ts — start, effects, chrome, reroute web/src/route/route.ts, web/test/route-surface.test.mjs - web/src/route/route.ts IN FULL (the controller you are evolving: injected deps, setState funnel, fail-honest catch, console law, D-10 reserved note) - web/src/guidance/guidance.ts + watch.ts + eta.ts (06-02 contract — pump against it, never re-implement it) - web/src/guidance/banner.ts + haptics.ts + close-protection.ts (06-05 chrome + guards) - web/src/route/route-card.ts (the card the guidance card evolves from) - .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md State Coexistence table (BINDING: OFF_ROUTE shows no premature signal; REROUTING holds last honest numbers; NO REROUTE state line; list re-render on reroute-apply) - route drawn + fix < GUIDANCE_START_ALONG_M along → still Phase 5 route state (no guidance chrome) - route drawn + fix ≥ 30 m along → guidance activates: engageCloseProtection, disableVerticalSwipes path via engage, [data-guidance="active"] on the shell root, banner + guidance card + collapsed list mount, camera easeToSnapped, ARAHARAH-GUIDE state=on_route token logged (category-only) - accepted fixes pump stepGuidance: banner distance/street update, ETA via updateEta + composeEtaClock, progress bar pct = alongM/totalM, haptic effects → fireHaptic, OFF_ROUTE → banner UNCHANGED (no premature signal), REROUTING → register swap + reroute copy + distance "—" + gantry blink start - reroute-requested effect → source.route({origin: fix, destinationId}) (the composed ladder from task 3); success → reroute-applied event → new line setData, list re-render (scroll top), gantry stop, camera NO jump; failure/throw → reroute-failed → NO REROUTE banner state + card state line (fail-honest, calm) - arrival effect → trip-end flow (task 3's teardown: DELETE envelope idempotent, stop watch, release protection, trip-ended card, chrome clears) - END TRIP tap → confirm card (bottom); YES, ABANDON → same teardown; CONTINUE → dismiss 1. Extend `RouteControllerDeps` with injected guidance concerns: `wa` (TelegramWebAppLike | null), `shellRoot` (the element receiving data-guidance), `register` (the register shower), banner/card/list mount points, `console` stays the module's own (state tokens only: `ARAHARAH-GUIDE state=` — RS8 family; never coordinates). 2. Implement the guidance controller INSIDE route.ts's injected-deps shape: a `startGuidance(model)` / `pumpFix(fix)` / `teardownGuidance(reason: "arrival" | "abandon")` triple held in the controller closure, using ONLY 06-02's machine + 06-05's builders (zero maneuver math inline — RS6's re-pinned ban enforces it). Pump: `shouldAcceptFix` gate → `stepGuidance` → effects dispatch (haptics via fireHaptic; reroute via the injected source; arrival → teardown) → chrome updates (banner five slots, ETA via updateEta/composeEtaClock, progress pct, list rows via the model's steps + projectAlongRoute). REROUTING: startGantryBlink on mount, stopGantryBlink on resolve. The confirm card + trip-end card mount into the injected bottom/center registers per UI-SPEC placement. 3. RED→GREEN in `web/test/route-surface.test.mjs` with the fakeDeps pattern: guidance activation gate (stationary vs moving first fix), pump updates (banner distance from a known fix, ETA state, progress pct), OFF_ROUTE no-premature-signal, REROUTING calm state + exactly one source.route call, reroute failure → NO REROUTE + card state line, reroute success → list re-rendered + no camera jump, END TRIP → confirm → YES ABANDON → teardown effects recorded (close-protection release + watch stop + DELETE fetch recorded via stubFetch), console tokens category-only. 4. RE-PIN RS6 in the same change: tokens `progress`/`maneuver`/`btn-primary` banned in web/src/route/ EXCEPT on `from "../guidance/…"` import lines and direct call expressions of imported names (structural scan per the context note). npm test -w web && npm run web:typecheck - route-surface.test.mjs green including: activation gate, pump updates, no-premature-signal, exactly-one-reroute-call, NO REROUTE degradation, teardown-on-abandon (release + stop + DELETE recorded), ARAHARAH-GUIDE tokens category-only - RS6 re-pinned and green (guidance implementation lives in web/src/guidance/) - grep `ARAHARAH-GUIDE state=` matches web/src/route/route.ts; grep `console\.` in route.ts shows only state-token lines (no `${lat` / coordinate interpolation) - typecheck clean Driving works end-to-end in Node fakes: motion starts guidance, fixes drive the cockpit, excursions self-heal silently through the real /api/route, and both exit paths tear down cleanly task 3: Envelope flows — minimize POST, resume GET, trip-end DELETE + composed ladder web/src/main.ts, web/src/route/route.ts, web/test/route-surface.test.mjs - web/src/main.ts lines 40–70 and 340–470 (the composition root: fixture source construction, route controller creation, session/boot choreography, BackButton wiring) - web/src/session/store.ts (getSession — the Bearer getter for http-source) - bot/src/kv/guidance-sessions.ts (the frozen snapshot fields the POST body must carry EXACTLY) - .planning/phases/06-turn-by-turn-guidance-live-driving/06-RESEARCH.md Pattern 5 (envelope lifecycle as structural foreground gate; write-at-minimize + earliest-signal belt) + Pitfall 2 (the minimize race — fetch keepalive) - web/test/destination-preload.test.mjs (the ?param reader pattern — ?g= follows it) - deactivated event during guidance → POST /api/guidance with the frozen snapshot shape (geometry re-encoded via encodePolyline6 from the live model; startedAtSec = trip start) using fetch(…, { keepalive: true }) — recorded by stubFetch even when the harness simulates immediate freeze - activated event during guidance → DELETE /api/guidance?id= (cron pauses — structural); the id is retained for trip-end idempotent DELETE - fresh boot with ?g= → GET /api/guidance?id= → envelope → model rebuilt (decodePolyline6 + deriveCumulativeDistanceM + destination fields) → RESUMING state (projected numbers from startedAtSec + avg speed; GUIDE // RESUMING banner + gantry) → first accepted fix → live ON_ROUTE; a failed/foreign GET → honest destination-less boot (Phase 4 flow), never a fake route - composed ladder in main.ts: engine-a first, fixture fallback on null — `route: (req) => (await http.route(req)) ?? (await fixture.route(req))` code-constructed (no env reads) - trip end (arrival or abandon) → DELETE envelope (404-tolerant), watch stop, close-protection release, data-guidance removed, trip-ended card; BackButton from trip-end walks to search root 1. In `main.ts`: build the composed RoutingSource ladder beside the existing fixture construction (code-constructed per the types.ts no-registry law): `const httpSource = createHttpRoutingSource({ endpoint: "/api/route", bearer: () => getSession() });` then the wrapper `{ name: "ladder", route: async (req) => (await httpSource.route(req)) ?? (await fixtureSource.route(req)) }` handed to createRouteController. Keep the golden-demo path: with no graph/no token, engine-a nulls fall through to the fixture corpus (captures stay stable). 2. Envelope lifecycle in the controller closure (exposed to main.ts via the controller): `onDeactivated` → POST (keepalive fetch, body = frozen snapshot only — id/chat_id/created_at are SERVER-minted; store the returned id); `onActivated` → DELETE ?id; `teardownGuidance` → DELETE ?id (tolerate 404) + watch stop + release + trip-end card. Wire `wa.onEvent?.("deactivated"|"activated", …)` with capability guards. 3. `?g=` resume in main.ts's boot choreography (destination-preload pattern): read `new URLSearchParams(location.search).get("g")`, UUID-validate, GET envelope, rebuild the model, create the guidance session in RESUMING state (projection: `remainingM = distanceM × (1 − min(1, (nowSec − startedAtSec) × avgSpeed / distanceM))`, avgSpeed = distanceM/durationS), render RESUMING chrome (banner register + gantry + projected ETA card numbers), flip to live on first accepted fix. 4. RED→GREEN in `web/test/route-surface.test.mjs`: stubFetch records POST (keepalive true, snapshot fields exactly the frozen six), DELETE on activated + on teardown (idempotent second DELETE tolerated), ?g= boot → RESUMING → first fix → ON_ROUTE (fake wa events + URL stub), failed GET → Phase 4 boot flow (no guidance chrome). npm test -w web && npm run web:typecheck - stubFetch assertions: POST /api/guidance body has EXACTLY destinationId/destinationName/distanceM/durationS/geometry/startedAtSec (no id/chat_id/created_at — server-minted), keepalive true; DELETE called with the stored id on activated AND teardown - ?g= resume test: RESUMING state rendered (GUIDE // RESUMING register + projected numbers), first fix flips to live ON_ROUTE - main.ts grep `createHttpRoutingSource` and `?? (await` (the composed ladder) present; grep for env reads in the backend selection → 0 (code-constructed law) - full web suite green; typecheck clean The envelope lifecycle is structural (no key, no push), the minimize race is belt-and-braces (keepalive + earliest signal), resume works from a killed webview via ?g=, and both trip ends execute the deletion + honest messaging

<threat_model>

Trust Boundaries

Boundary Description
Minimize/freeze → envelope POST A backgrounding webview may freeze mid-fetch (Pitfall 2) — best-effort honesty, not a security boundary
?g= deep link → resume GET The guidance id is an opaque UUID traveling in the URL; ownership enforced server-side by the claim check
User gestures → camera Only originalEvent-bearing moves unlock follow — synthetic moves cannot hijack the driving camera

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-05 Information Disclosure envelope POST + reroute calls mitigate-by-design Exactly three positions ever leave the device: reroute origins (real excursions only), the minimize envelope (user-initiated handoff), nothing else. On-route fixes stay local (06-02's structural gate). The envelope carries the frozen route snapshot only — no live coordinate stream.
T-06-04 Information Disclosure console + state tokens mitigate ARAHARAH-GUIDE state tokens only — tested to contain no coordinates (RS8 family); no payload/claim values in any log line.
T-06-06 Tampering (old-client crash) onEvent("deactivated"/"activated") wiring mitigate wa.onEvent?.() optional chains; event names ignored by older clients by contract; capability guards proven in 06-05's fakes.
— Spoofing (?g= probe) resume GET mitigate (server, 06-03) Foreign/absent id → 404 → honest Phase 4 boot; the client renders no guidance chrome without a validated envelope.
— DoS (self) reroute requests accept The D-04 storm guard (20 s) bounds reroute rate client-side; replay tests prove exactly-one-call under excursion storms.
</threat_model>
- `npm test -w web && npm run web:typecheck` green (RS6 re-pinned; PX-01 full walk; envelope flows; camera laws) - Zero dead ends in the BackButton graph (RS7 extended walk, close() only at true root) - Console carries only ARAHARAH-GUIDE/ARAHARAH-ROUTE state tokens

<success_criteria> Guidance is ALIVE in the shell: motion activates it, fixes drive the cockpit, missed turns self-heal through the real router with a calm banner, minimizing hands off to the cron (envelope live), re-opening resumes at the progressed position, the BackButton walks the full graph through the abandon confirmation, and both trip ends delete the record and say so. PX-01/PX-03's observable behaviors are claimed and tested. </success_criteria>

After completion, create `.planning/phases/06-turn-by-turn-guidance-live-driving/06-06-SUMMARY.md`