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