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

309 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 06-turn-by-turn-guidance-live-driving
plan: 05
type: execute
wave: 2
depends_on: [06-02]
files_modified:
- web/src/guidance/glyphs/depart.svg
- web/src/guidance/glyphs/straight.svg
- web/src/guidance/glyphs/left.svg
- web/src/guidance/glyphs/right.svg
- web/src/guidance/glyphs/slight-left.svg
- web/src/guidance/glyphs/slight-right.svg
- web/src/guidance/glyphs/uturn.svg
- web/src/guidance/glyphs/arrive.svg
- web/src/guidance/glyphs.ts
- web/src/guidance/banner.ts
- web/src/guidance/haptics.ts
- web/src/guidance/close-protection.ts
- web/src/telegram/webapp.ts
- web/src/i18n/strings.ts
- web/src/shell.css
- web/test/guidance-surface.test.mjs
- web/test/route-surface.test.mjs
- web/test/build-artifact.test.mjs
- web/test/copy-gate.test.mjs
autonomous: true
requirements: [GUID-01, GUID-04, PX-03]
must_haves:
truths:
- "The maneuver banner renders distance + arrow + street per the UI-SPEC anatomy, with REROUTING/RESUMING/NO REROUTE calm states and the machine-blink gantry (GUID-01, D-04/D-06)"
- "The maneuver list collapses/expands over the map with NEXT/FUTURE/PAST row states and tap-out collapse (GUID-01)"
- "The guidance card shows ETA + remaining + progress bar + END TRIP — the route card evolved (D-06)"
- "The abandon-route confirm card guards every guidance exit with the exact en/ms copy (D-09/PX-03)"
- "Haptics fire through capability-guarded calls and no-op cleanly on old clients (GUID-04/D-08)"
- "Close protection engages on guidance start and releases at trip end (disableVerticalSwipes 7.7+, closing confirmation 6.2+) (PX-03)"
- "All guidance copy lives in the strings module in en + ms — never inline (the copy gates)"
artifacts:
- path: "web/src/guidance/glyphs.ts"
provides: "the 8-glyph committed set as raw SVG (single source; PNGs rasterize from these files in plan 07)"
- path: "web/src/guidance/banner.ts"
provides: "banner + maneuver list + guidance card + confirm card + trip-end card builders (createElement + textContent)"
- path: "web/src/guidance/haptics.ts"
provides: "capability-guarded haptic dispatch (approach/maneuver/arrive)"
exports: ["fireHaptic"]
- path: "web/src/guidance/close-protection.ts"
provides: "engage/release close protection (version-gated)"
exports: ["engageCloseProtection", "releaseCloseProtection"]
- path: "web/src/i18n/strings.ts"
provides: "the 18 new ShellStrings keys, en + ms, exact UI-SPEC copy"
key_links:
- from: "web/src/guidance/banner.ts"
to: "web/src/i18n/strings.ts"
via: "every label via strings[locale] — zero inline copy"
pattern: "strings\\[locale\\]"
- from: "web/src/guidance/banner.ts"
to: "web/src/guidance/glyphs.ts"
via: "inline SVG from the committed glyph sources"
pattern: "GLYPHS"
- from: "web/src/guidance/close-protection.ts"
to: "web/src/telegram/webapp.ts"
via: "optional members + isVersionAtLeast guards"
pattern: "isVersionAtLeast"
---
<objective>
The guidance cockpit surface per 06-UI-SPEC: the 8-glyph committed set, the maneuver banner + collapsible list + guidance card + abandon-confirm + trip-end builders, the close-protection and haptics capability guards, and all copy in the strings module — every element token-only, radius-0, no shadows, machine-instant state swaps.
Purpose: GUID-01/PX-03's visible behavior and GUID-04's haptic channel. Builders are pure DOM composition (Node-testable, the route-card law); plan 06 wires them to the state machine.
Output: glyphs + 5 builders + 2 guard modules + webapp contract members + strings keys + shell.css guidance rules + the guidance-surface suite + RS10/BA9/copy-gate bumps.
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.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
<interfaces>
<!-- Builder law to copy (web/src/route/route-card.ts): createElement + textContent ONLY —
"a name-derived string never reaches innerHTML" (RS5). Theme classes only, never local
restyle. aria attributes on interactive groups. Instrument formats composed in code
(fillTemplate), never locale strings. -->
<!-- State machine contract consumed (web/src/guidance/guidance.ts, plan 06-02):
GuidanceState = "idle" | "on_route" | "off_route" | "rerouting" | "resuming" | "no_reroute"
Effects carry haptic cues "approach" | "maneuver" | "arrive". -->
<!-- webapp.ts gains OPTIONAL members only (the contract-stays-the-contract law):
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+
disableClosingConfirmation?: () => void;
isActive?: boolean; // Bot API 8.0+
onEvent?(event: string, cb: () => void): void; // accepts "activated" | "deactivated" -->
<!-- EXACT COPY (06-UI-SPEC copywriting contract — values verbatim, en + ms):
guideRegister GUIDE/GUIDE · guideStreetUnnamed "Unnamed road"/"Jalan tanpa nama"
guideReroutingLine "Finding a new route from where you are."/"Mencari laluan baharu dari kedudukan anda."
guideResumingLine "Resuming from your last known progress."/"Sambung semula dari kemajuan terakhir anda."
guideNoRerouteLine "Rerouting unavailable on this server."/"Pengiraan semula tiada pada pelayan ini."
guideNoRerouteBody "No road graph here — follow the drawn route; recalculation is unavailable."/"Tiada graf jalan di sini — ikut laluan yang dilukis; pengiraan semula tidak tersedia."
ctaBannerList LIST/SENARAI · ctaBannerClose CLOSE/TUTUP · ctaEndTrip "END TRIP"/"TAMAT PERJALANAN"
guideConfirmHeadline "Abandon route?"/"Tinggalkan laluan?"
guideConfirmBody "Guidance stops and this trip's record is deleted. Nothing is stored."/"Panduan berhenti dan rekod perjalanan ini dipadam. Tiada apa-apa disimpan."
ctaConfirmAbandon "YES, ABANDON"/"YA, TINGGALKAN" · ctaConfirmContinue "CONTINUE"/"TERUSKAN"
guideEndHeadline "Trip ended"/"Perjalanan tamat"
guideEndBody "Route guidance stopped and the trip record is deleted — nothing about this drive remains."/"Panduan laluan dihentikan dan rekod perjalanan dipadam — tiada apa-apa tentang pemanduan ini kekal."
ariaManeuverList "Full maneuver list"/"Senarai penuh manuver" · ariaBannerToggle "Next maneuver"/"Manuver seterusnya" · ariaTripProgress "Trip progress"/"Kemajuan perjalanan"
Registers ("GUIDE // NEXT", "GUIDE // REROUTING", "GUIDE // RESUMING", "GUIDE // NO REROUTE",
"GUIDE // CONFIRM", "GUIDE // END") are instrument constants composed in code — en under both locales. -->
<!-- UI-SPEC geometry (machined literals, the sanctioned exception classes):
banner min-height 96px, padding space-sm space-md, arrow plate 48×48 (glyph 32×32),
compact strip min-height 24px padding-block space-xs, banner→strip separator 1px
chassis-subtle, list panel max-width 320px max-height 480px, row min-height 48px
glyph 24×24, next-row lamp 8×8, progress bar 4px. Ink hierarchy: register chassis-subtle,
street soft-beige, distance pure-white, arrow/lamps/bar-fill signal-orange. NO red anywhere
in guidance chrome (abandoning is deliberate, not an error). Past-maneuver rows: outline ink. -->
<!-- Gates this plan bumps (PATTERNS cautions 3–5): RS10 floor 16 → 24 in route-surface.test.mjs;
BA9 input list += web/src/guidance/*.ts in build-artifact.test.mjs (budget ≤ 204800 gz, printed);
copy-gate.test.mjs extends if it enumerates modules/keys. RS6 is NOT touched here (plan 06 owns it). -->
</interfaces>
</context>
<tasks>
<task type="auto">
<name>task 1: The 8-glyph committed set + glyphs.ts + strings keys</name>
<files>web/src/guidance/glyphs/*.svg, web/src/guidance/glyphs.ts, web/src/i18n/strings.ts, web/test/copy-gate.test.mjs</files>
<read_first>
- .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md "The 8-glyph set" table (the EXACT geometry per glyph) + "Maneuver Banner" ink hierarchy
- web/src/i18n/strings.ts (the ShellStrings interface + en/ms records — append, never reorder)
- web/test/copy-gate.test.mjs (what it enumerates — extend in the same change)
- theme/src/tokens.css (verify --color-signal-orange #EB8008 — SVGs use currentColor; PNG rasterization in plan 07 injects the literals)
</read_first>
<behavior>
- glyphs.ts exports GLYPHS: Record<GlyphKind, string> for exactly ["depart","straight","left","right","slight-left","slight-right","uturn","arrive"], each value the raw SVG string of the committed file (?raw import — the committed SVGs are the single source)
- every SVG: viewBox "0 0 32 32", stroke currentColor, stroke-width 3, square linecaps, orthogonal/45° segments only (the geometry table verbatim: depart = 4×4 filled square + vertical shaft; uturn = machined staple, no arc; arrive = shaft ending on a 12×12 filled square)
- every new ShellStrings key exists in BOTH en and ms with the exact UI-SPEC values (the interface gains the 18 keys with doc comments)
</behavior>
<action>
1. Create the 8 SVG files in `web/src/guidance/glyphs/` per the UI-SPEC geometry table — hand-authored, 32×32 grid, `fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"`, filled squares as `<rect fill="currentColor" stroke="none">`. Keep every segment orthogonal or 45° (the orthogonal law — the uturn staple is the proof case).
2. `web/src/guidance/glyphs.ts`: `import depart from "./glyphs/depart.svg?raw"; …` (Vite ?raw), `export type GlyphKind = "depart" | "straight" | "left" | "right" | "slight-left" | "slight-right" | "uturn" | "arrive";`, `export const GLYPH_KINDS: readonly GlyphKind[]` (the 8 in order), `export const GLYPHS: Record<GlyphKind, string>`, and `export function svgHost(kind: GlyphKind): SVGSVGElement` — a `<template>` whose innerHTML is set from the STATIC `GLYPHS[kind]` constant (the ONE sanctioned innerHTML site: a static import value, never a name-derived or runtime string — the surface gate whitelists exactly this line), returning the parsed `<svg>` element for sizing by the caller (banner 32×32, list 24×24).
3. `web/src/i18n/strings.ts`: append the 18 keys from the context table to `ShellStrings` + both locale records, values VERBATIM, doc comments naming D-06/D-09/D-14/UI-SPEC. Extend `web/test/copy-gate.test.mjs` in the same change if it enumerates expected keys/modules so it stays green AND enforces the new keys' presence.
</action>
<verify>
<automated>npm test -w web && npm run web:typecheck</automated>
</verify>
<acceptance_criteria>
- all 8 SVG files exist under web/src/guidance/glyphs/; each contains `viewBox="0 0 32 32"` and `stroke-width="3"` and `stroke-linecap="square"`
- glyphs.ts exports GLYPHS with exactly 8 kinds (test asserts GLYPH_KINDS.length === 8 and each raw string non-empty)
- grep "Unnamed road" and "Tinggalkan laluan\?" and "YA, TINGGALKAN" all match web/src/i18n/strings.ts (en+ms pairs)
- copy-gate.test.mjs green and asserting the new keys exist in both locales
- typecheck clean (the 18 keys are non-optional interface members — a missing locale entry fails the build)
</acceptance_criteria>
<done>The committed glyph set exists once (SVG source of truth, PNGs come later from the same files), and every guidance word the app will ever show is in the strings module in both locales</done>
</task>
<task type="auto" tdd="true">
<name>task 2: Banner + maneuver-list builders + gantry blink</name>
<files>web/src/guidance/banner.ts, web/test/guidance-surface.test.mjs</files>
<read_first>
- web/src/route/route-card.ts (THE builder law: actions interface first, createElement+textContent only, aria on interactive groups, instrument formats composed in code)
- .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md sections: Maneuver Banner, Maneuver List, State Coexistence (the banner's four registers + calm-state slot swaps)
- web/test/route-surface.test.mjs RS5/RS9 (the structural-gate style guidance-surface mirrors)
</read_first>
<behavior>
- buildGuidanceBanner renders all five slots for EVERY state (never blanks mid-swap): register line (GUIDE // NEXT | GUIDE // REROUTING | GUIDE // RESUMING | GUIDE // NO REROUTE), 48×48 arrow plate with the glyph, distance (telemetry-num + label unit; "—" when unknown during REROUTING/RESUMING/NO REROUTE), street line (uppercase, ellipsis; streetUnnamed fallback), toggle word LIST/CLOSE with aria-expanded
- the banner root is a <button> (full-width, the list toggle) with aria-controls → the list panel's id
- buildManeuverList: card panel (max-width 320px, role="region", aria-label=ariaManeuverList) + transparent tap-out layer; rows in trip order with glyph 24×24 + street + right-aligned distance; NEXT row = orange glyph + 8×8 orange lamp; FUTURE = matte-black glyph; PAST = outline ink glyph + outline text; rows are real text, glyphs aria-hidden
- startGantryBlink(el)/stopGantryBlink(el): JS-timed two-state toggle ~500 ms (the boot-card precedent — an indicator duty cycle, never CSS animation)
- structural: zero inline copy in the built surface (RS9-analog scan); the svgHost innerHTML whitelist is the ONLY sanctioned site
</behavior>
<action>
1. `web/src/guidance/banner.ts` — create the module with the banner-surface exports, each `(locale, view, actions) => HTMLElement` with a view interface per the behavior list (copy route-card.ts's actions-interface-first structure):
- `buildGuidanceBanner(locale, view: { state: GuidanceState; glyph: GlyphKind; distanceM: number | null; street: string | null; listOpen: boolean }, actions: { onToggle(): void })`
- `buildManeuverList(locale, rows: Array<{ kind: GlyphKind; street: string; distanceM: number; rowState: "next" | "future" | "past" }>, actions: { onDismiss(): void })` (the tap-out layer dismisses)
- `startGantryBlink(el: HTMLElement): void` / `stopGantryBlink(el: HTMLElement): void` (setInterval 500 ms toggling a data attribute; one interval per element, stop clears)
All copy via `strings[locale]`; registers composed in code (`\`${strings[locale].guideRegister} // NEXT\`` style instrument constants); formats via a local `fillTemplate`-style composer (route-card precedent).
2. RED→GREEN `web/test/guidance-surface.test.mjs` (the RS style: Node-24 dynamic .ts imports, minimal document fake — reuse the route-surface DOM-stub approach): behavioral tests per the behavior list (five-slot-every-state banner, list row states, gantry blink start/stop, banner/list aria) + structural gates — innerHTML ban with the svgHost whitelist (exactly one sanctioned site), inline-copy ban (RS9-analog grep of banner.ts for English/Malay literals outside strings imports — allowed: the instrument register words NEXT/REROUTING/RESUMING/NO REROUTE/CONFIRM/END and unit formats).
</action>
<verify>
<automated>npm test -w web</automated>
</verify>
<acceptance_criteria>
- guidance-surface.test.mjs green with: five-slot-every-state banner test, list row-states test, gantry blink start/stop test, aria assertions (banner button aria-expanded/controls, list region label)
- structural: banner.ts innerHTML appears ONLY in svgHost against GLYPHS[kind]
- typecheck clean
</acceptance_criteria>
<done>The banner and list render: five-slot discipline with the calm REROUTING/RESUMING/NO REROUTE states and the honest collapsible list — Node-tested DOM with the single sanctioned innerHTML site</done>
</task>
<task type="auto" tdd="true">
<name>task 3: Guidance/confirm/trip-end card builders + shell.css guidance block</name>
<files>web/src/guidance/banner.ts, web/src/shell.css, web/test/guidance-surface.test.mjs</files>
<read_first>
- web/src/guidance/banner.ts (task 2's module — the card builders APPEND to it; banner/list builders untouched)
- web/src/ui/strip.ts + the strip section of web/src/shell.css (what [data-guidance="active"] compacts — layout only, strip.ts logic untouched)
- .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md sections: Shell Layout, Guidance Card, Abandon-Route Confirm Card, Trip-Ended Card
</read_first>
<behavior>
- buildGuidanceCard: GUIDE register, destination headline, telemetry row (minutes telemetry-num + MIN + {km} KM + ETA HH:MM), 4px progress bar (role="progressbar", aria-valuemin 0 / max 100 / now pct, aria-label ariaTripProgress), state line ONLY in no_reroute (guideNoRerouteBody), actions = ONE .btn-secondary END TRIP — no .btn-primary anywhere in guidance chrome except the confirm card
- buildConfirmCard: GUIDE // CONFIRM + exact en/ms headline/body + .btn-primary "YES, ABANDON" + .btn-secondary "CONTINUE"; bottom register placement class
- buildTripEndCard: GUIDE // END + exact headline/body, no actions
- distance/ETA formatting instrument constants in code: "{n} M" under 1000 (integer), "{n.n} KM" at/over (one decimal), 24-hour ETA clock — identical under en/ms
- structural: zero "transition"/"animation" CSS in the new shell.css block; zero hex literals (token vars only); zero border-radius; zero box-shadow
</behavior>
<action>
1. `web/src/guidance/banner.ts` — APPEND the three card builders (signatures per the behavior list; route-card.ts actions-interface-first law):
- `buildGuidanceCard(locale, view: { destinationName: string; minutesRemaining: number; kmRemaining: number; etaClock: string; progressPct: number; showNoReroute: boolean }, actions: { onEndTrip(): void })`
- `buildConfirmCard(locale, actions: { onAbandon(): void; onContinue(): void })` and `buildTripEndCard(locale): HTMLElement`
All copy via `strings[locale]`; registers composed in code; instrument formats via the fillTemplate-style composer from task 2.
2. `web/src/shell.css` — append the guidance block, token vars ONLY: banner band (matte-black chassis, min-height 96px, padding space-sm space-md, full-bleed, below safe-top + space-sm), 1px chassis-subtle banner→strip separator, `[data-guidance="active"]` strip compaction (min-height 24px, padding-block space-xs, telemetry cluster display:none — layout only), list panel + row anatomy + separators + lamp, guidance-card telemetry row + 4px progress bar (chassis-subtle track, 1px matte-black border, signal-orange fill, width set by inline style from the pct), ink colors per the hierarchy (register chassis-subtle, street soft-beige, distance pure-white). NO transition/animation/box-shadow/border-radius/hex anywhere in the block.
3. RED→GREEN: extend `web/test/guidance-surface.test.mjs` with the card tests (confirm/trip-end copy asserting the exact strings-module keys, END TRIP is btn-secondary and the only card action, progressbar aria) + the CSS block scan (no transition|animation|box-shadow|border-radius|#[0-9a-f]{3,6} in the appended guidance section) + the strip.ts-untouched structural check.
</action>
<verify>
<automated>npm test -w web</automated>
</verify>
<acceptance_criteria>
- guidance-surface.test.mjs green with: confirm/trip-end copy tests asserting the exact strings-module keys, END TRIP is btn-secondary and the only card action, progressbar aria valueattrs
- structural: grep "btn-primary" in banner.ts matches ONLY the confirm card builder
- shell.css guidance block: grep `transition|animation|box-shadow|border-radius` → 0 matches in the appended section; grep `#[0-9a-fA-F]{3,6}` → 0 matches (token vars only)
- strip.ts untouched (grep confirms no changes; compaction is pure CSS under [data-guidance="active"])
- typecheck clean
</acceptance_criteria>
<done>The cockpit's instruments complete: evolved guidance card with progress bar, guarded confirm surface, trip-end card, and the token-pure shell.css block that dresses the whole cockpit</done>
</task>
<task type="auto" tdd="true">
<name>task 4: Capability guards — haptics + close protection + webapp members + gate bumps</name>
<files>web/src/guidance/haptics.ts, web/src/guidance/close-protection.ts, web/src/telegram/webapp.ts, web/test/guidance-surface.test.mjs, web/test/route-surface.test.mjs, web/test/build-artifact.test.mjs</files>
<read_first>
- web/src/telegram/webapp.ts lines 31–65 (the contract: optional members only — grow it, never break existing fakes)
- web/src/telegram/safe-area.ts lines 35–50 (the isVersionAtLeast-gate shape to copy)
- web/test/build-artifact.test.mjs lines 185–230 (BA9's input list + printed budget — extend in the same change)
- web/test/route-surface.test.mjs RS10 (the floor bump — PATTERNS caution 4)
</read_first>
<behavior>
- fireHaptic(wa, "approach") → wa.HapticFeedback.selectionChanged(); "maneuver" → impactOccurred("medium"); "arrive" → notificationOccurred("success")
- fireHaptic on a wa WITHOUT HapticFeedback → no-op, no throw (D-08 graceful)
- engageCloseProtection(wa): calls disableVerticalSwipes() only when isVersionAtLeast("7.7"), enableClosingConfirmation() only when isVersionAtLeast("6.2"); releaseCloseProtection inverts both; capability-missing → silent no-op (Pitfall 4's layered defense: body-swipe off + native confirm net; the header swipe cannot be disabled — documented in the module header)
- webapp.ts fakes in EXISTING tests still typecheck/run (optional members only)
- BA9: input list includes the guidance modules, budget printed, ≤ 204800 gz bytes
- RS10: floor raised to 24 src files
</behavior>
<action>
1. `web/src/telegram/webapp.ts`: add the optional members from the context box to TelegramWebAppLike (each with the Bot-API-version doc comment). No existing member changes shape.
2. `web/src/guidance/haptics.ts`: `fireHaptic(wa: TelegramWebAppLike | null, cue: "approach" | "maneuver" | "arrive"): void` — `?.` chains only; module header cites D-08 + the no-haptic-on-reroute/confirm law (UI-SPEC: a reroute haptic would announce blame).
3. `web/src/guidance/close-protection.ts`: `engageCloseProtection(wa)` / `releaseCloseProtection(wa)` — version gates `typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("7.7")` / `("6.2")` before each call, `?.` on the member; header documents the Pitfall-4 layering (the Rams card is the designed surface; the native dialog is the net; header swipe undisableable per official docs).
4. RED→GREEN in `web/test/guidance-surface.test.mjs`: recording-fake tests for both modules (calls recorded, version gates respected, no-throw on capability-less fakes).
5. Gate bumps in the same change: `web/test/route-surface.test.mjs` RS10 floor `>= 16` → `>= 24`; `web/test/build-artifact.test.mjs` BA9 input list += the web/src/guidance .ts modules (glyphs, banner, haptics, close-protection, guidance, eta, watch — camera lands in plan 06 and rides the same list then). Run the full web suite; if the bundle budget busts, the guidance modules are DOM strings — investigate before touching the budget (the gate is not negotiable without a recorded call).
</action>
<verify>
<automated>npm test -w web && npm run web:typecheck</automated>
</verify>
<acceptance_criteria>
- webapp.ts gains ONLY optional members (existing interface members unchanged; existing suites green)
- guidance-surface.test.mjs asserts all haptic/close-protection behaviors incl. version gates (7.7 / 6.2) and no-throw no-ops
- route-surface.test.mjs RS10 asserts >= 24 files and passes
- build-artifact.test.mjs BA9 lists the guidance modules, prints its measurement, passes ≤ 204800
- grep `isVersionAtLeast("7.7")` and `isVersionAtLeast("6.2")` match web/src/guidance/close-protection.ts
- typecheck clean
</acceptance_criteria>
<done>The eyes-free channel and the three-layer close protection exist behind version guards that degrade to honest no-ops; the storage and bundle gates grew with the family and stayed green</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Telegram client → WebApp APIs | Client capabilities vary across Bot API 6.1–8.0+; every new call is capability-guarded |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06-06 | Tampering (crash/doS on old clients) | haptics.ts, close-protection.ts | mitigate | Version gates BEFORE every call + `?.` optional-member chains + no-throw fakes proven in tests (RESEARCH Pitfall 5). Graceful degradation is the D-08 law. |
| — | Spoofing (UI overlay confusion) | confirm card | accept | The confirm card is plain DOM in the app's own register band; no privileged surface to spoof; Telegram chrome is out of scope. |
| — | Information Disclosure | banner/list/card | mitigate-by-design | Builders render only route-model data the user is already looking at; no coordinates are ever logged (no console in builders — structural gate). |
| — | DoS | gantry blink interval | mitigate | startGantryBlink stores one interval per element and stopGantryBlink clears it; state swaps always stop the previous blink (wired in plan 06; the API makes a leak visible in review). |
</threat_model>
<verification>
- `npm test -w web && npm run web:typecheck` green (existing suites + guidance-surface + copy gate)
- RS10 (≥24) and BA9 (budget printed, ≤204800) green with the guidance family counted
- Zero inline copy, zero off-token CSS values, zero red in guidance chrome
</verification>
<success_criteria>
The guidance cockpit exists as pure, tested builders + guards: banner five-slot discipline with the calm REROUTING/RESUMING/NO REROUTE states, collapsible honest list, evolved guidance card with progress bar, ONE confirm surface with the exact privacy copy, trip-end card, capability-safe haptics and close protection — every word in strings.ts, every pixel from tokens.
</success_criteria>
<output>
After completion, create `.planning/phases/06-turn-by-turn-guidance-live-driving/06-05-SUMMARY.md`
</output>