Files
araharah-executor a9041fb527 docs(06-02): complete the guidance-brain plan (replays + ETA + throttle)
- SUMMARY: 3/3 tasks, 5 commits, 100/100 web tests, replay-proven D-04
  machine, honest ETA, D-15 throttle; deviations + self-check recorded
- STATE.md / ROADMAP.md deliberately untouched (orchestrator-owned)
2026-10-24 12:53:16 +08:00

179 lines
18 KiB
Markdown
Raw Permalink 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: 02
subsystem: web-guidance
tags: [guidance-brain, d04-state-machine, reroute-storm-guard, replay-fixtures, ema-eta, deadband, distance-throttle, watchposition, haptics, tdd, pure-reducer]
# Dependency graph
requires:
- phase: 06-turn-by-turn-guidance-live-driving/01
provides: "the routing workspace (haversineM/deriveCumulativeDistanceM pure geo, polyline6 codec, classifyTurn turn vocabulary as RouteStep.kind, subpath-with-extension import law, pure-module zero-laws)"
- phase: 05-routing-route-display/01
provides: "route-fixtures.json corpus (the golden pair geometries drives derive from) + the six-field Fix contract (web/src/location/types.ts)"
provides:
- "web/src/guidance/guidance.ts — THE pure D-04 reroute state machine: createGuidanceSession + stepGuidance (idle/on_route/off_route/rerouting/resuming/no_reroute), projectAlongRoute forward-only projection, accuracy-aware threshold fn, ALL named constants (OFF_ROUTE_*, STORM_GUARD_MS, MISSED_TURN_*, ARRIVAL_THRESHOLD_M, APPROACH_HAPTIC_M, GUIDANCE_START_ALONG_M), GuidanceEffect stream (haptic approach/maneuver/arrive, reroute-requested, arrival)"
- "web/src/guidance/eta.ts — honest ETA (D-07): updateEta per-axis EMA + independent ±1 min / ±5% display deadbands, composeEtaClock 24h HH:MM instrument format"
- "web/src/guidance/watch.ts — the D-15 gated stream flipped ON: createGuidanceWatch (high-accuracy watchPosition, maximumAge 0, Fix normalization) + shouldAcceptFix (>=10 m / >=5 s) + idempotent stop()"
- "web/test/fixtures/drives/*.json — the four committed D-16 drive replays (corridor-follow, off-route-excursion, missed-turn-excursion, gps-jitter) derived from golden pair central-market-to-merdeka-118, ODbL attribution in each _notice"
- "web/test/guidance-replay.test.mjs — R1-R12 + T1-T3: every D-04 transition, storm guard, missed-turn grace, haptic edges, start gate, projection, throttle, purity gates"
- "web/test/guidance-eta.test.mjs — E1-E8: seed, jitter-holds-display, real-drop, independent axes, clock format, purity gate"
affects: [06-03 guidance surface (renders snap+effects), 06-06 resume/envelope wiring (stepGuidance loop), 06-07 cron projection (same projection math), 06-08 real-drive gate]
# Tech tracking
tech-stack:
added: [] # zero new dependencies — haversineM/polyline6 come from @araharah/routing, everything else is V8-native
patterns:
- "pure reducer posture: createGuidanceSession/stepGuidance over immutable GuidanceSnapshots with an injected clock — the whole machine replays in Node from Fix-shaped JSON (D-16), no DOM/map/console/storage"
- "accuracy-aware deviation gate: max(35, 2 x accuracyM) capped 100 — exported as offRouteThresholdM, replay-proven by the jitter drive"
- "storm-guard semantics: request-clock (not apply-clock); a re-confirmed excursion inside STORM_GUARD_MS stays silent (no effect, no state flap — banner calm); after expiry, off_route re-requests (retry after a silent controller)"
- "missed-turn detection by PROGRESS PINNING: alongM stuck at a maneuver mark (+/-20 m) while the fix is >=40 m past its point — separates corner overshoot from mid-block excursion"
- "once-per-edge effect arming: approachArmed/arrived flags in the snapshot make every haptic strictly once-per-edge; reroute-applied re-arms"
- "forward-only projection window (60 segments): never a global nearest-point scan, never backward progress; corner-rounding artifacts are absorbed by the 3-fix + 8 s grace by design"
key-files:
created:
- web/src/guidance/guidance.ts
- web/src/guidance/eta.ts
- web/src/guidance/watch.ts
- web/test/guidance-replay.test.mjs
- web/test/guidance-eta.test.mjs
- web/test/fixtures/drives/corridor-follow.json
- web/test/fixtures/drives/off-route-excursion.json
- web/test/fixtures/drives/missed-turn-excursion.json
- web/test/fixtures/drives/gps-jitter.json
modified: []
key-decisions:
- "Drives derive from routing/src/data/route-fixtures.json (the verbatim ODbL geometries the fixture backend serves), keyed by the golden-pairs.json pairId — the plan pointed at golden-pairs.json for 'the real committed route lines', but that file is the pair manifest (ids/origins only); the geometry lives one directory over. Attribution travels in each fixture's _notice."
- "MISSED_TURN_GRACE_MS = 3000 (new named constant, executor discretion): the plan pins MISSED_TURN_PAST_MANEUVER_M=40 as the DETECTOR but no shortened grace VALUE; 3 s is replay-tuned (detection ~4 s after the threshold crossing on a 1 Hz stream) and pinned by R1/R4."
- "off_route re-requests after the storm guard expires (Rule 2 fix): without it, a controller that never answers (reroute-failed/applied never arrives) strands the driver with exactly one request forever; R9 pins the exact retry timing (t+8 s re-confirm, t+20 s fire)."
- "Storm-guard suppression keeps the banner CALM: an excursion re-confirmed inside the guard produces no effect AND no off_route flap — the honest deviation still lives in the snapshot's deviationM for the banner's data."
- "reroute-applied lands directly on_route (the plan's behavior text) with fromIdx=0 on the NEW model; 'resuming' is the self-heal path (off_route -> good fix -> resuming -> on_route) and the no_reroute recovery path."
- "watch.ts timestamps fixes from pos.timestamp (fallback Date.now() when a platform reports 0) — the throttle compares true inter-fix intervals, not wrapper-entry times."
patterns-established:
- "replay-fixture shape: { _notice, pairId, maneuverIdx, kinds, fixes[] } — Fix-shaped streams over committed ODbL geometry; steps synthesized at the recorded maneuver indices with OSRM-style distances (a step runs from its maneuver to the next boundary; the maneuver point rides in step.geometry)"
- "controller-in-replay pattern: the test plays the controller via an effect handler + pendingEvents queue (request -> started -> applied with a merge-model built from the drive's own upcoming fixes) — reusable for 06-06's envelope wiring tests"
- "structural purity gates per module (comment-stripped source scan): no console/document/maplibre/localStorage/fetch + haversineM-presence (RS10 analog) — extend this block, never a new suite, for future guidance modules"
requirements-completed: [GUID-02, GUID-03, GUID-04, GUID-06] # replay/eta halves; UI/cron halves render+wire in plans 03/06/07 (state writes are orchestrator-owned)
# Metrics
duration: 25min
completed: 2026-10-24
---
# Phase 6 Plan 2: The Guidance Brain — D-04 State Machine + Honest ETA + Distance Throttle Summary
**THE pure, Node-replayable guidance brain is REAL: the four committed D-16 drive replays prove every D-04 transition in seconds — corridor to arrival with a once-per-edge haptic stream, excursion to exactly ONE reroute request with the storm guard holding under an immediate second excursion, missed turn firing under a 3 s shortened grace, and ±15 m GPS jitter never leaving on_route — plus the EMA+deadband ETA that never whiplashes and the ≥10 m / ≥5 s distance-throttled watch, 100/100 web tests green with zero browser imports in the module family.**
## Performance
- **Duration:** 25 min
- **Started:** 2026-10-24T04:26:32Z
- **Completed:** 2026-10-24T04:51:41Z
- **Tasks:** 3/3 (tasks 1–2 RED→GREEN, task 3 test+impl atomic)
- **Files:** 9 (all created)
## Accomplishments
- **The D-04 machine is replay-proven** (GUID-02): `createGuidanceSession` + `stepGuidance` reduce Fix events + controller events (reroute-started/failed/applied) over immutable snapshots with an injected clock. R2 proves the corridor: start gate at ≥30 m along, `on_route` throughout, ZERO reroute requests, 6 approach haptics (5 turns + arrival; the final one ~297 m before arrival), 5 maneuver impacts, exactly one arrival at remaining ≤ 25 m. R3 proves the excursion: exactly ONE `reroute-requested` carrying the honest raw-fix origin, `on_route → off_route → rerouting → on_route`, and the immediate second excursion produces NO second request and NO `off_route` flap (Pitfall 6: deviation measured against the NEW model from its first segment). R4 proves the missed turn: past the maneuver point ≥40 m shortens the grace — the request fires 3 s after the first off-threshold fix, far inside the standard 8 s. R5 proves jitter: ±15 m alternating at accuracy 20 (threshold 40) never leaves `on_route`, zero reroutes, still arrives.
- **The storm guard has exact retry semantics** (R9): first request at t+8 s (3 consecutive fixes + grace), a persistent excursion re-confirms and re-fires exactly when the 20 s guard expires (t+34 s) — a controller that never answers cannot strand the driver.
- **The honest degradation and self-heal exist** (R7/R8): `reroute-failed` lands in `no_reroute`; a manual rejoin walks `resuming → on_route`; an excursion that decays on its own heals through `resuming` without any controller action.
- **The honest ETA is whiplash-free** (GUID-03, D-07): `updateEta` consumes numbers only (a Fix cannot enter — E8 structural), seeds on first update, EMA α 0.25 per axis, display refreshes only past ±60 s (time) or ±5% (distance), axes independent — ±40 s jitter holds the display through 60 fixes while a real 5-minute drop moves it after ONE fix and converges within 10% by ~9. `composeEtaClock` renders the 24-hour `HH:MM` arrival instrument (local wall clock, midnight-rollover pinned).
- **The D-15 gated stream is ON** (GUID-06 upstream): `createGuidanceWatch` wires `{ enableHighAccuracy: true, maximumAge: 0 }` (T2 recorded-options gate), normalizes to the six-field Fix (null accuracy/heading, source webview), drops sub-threshold fixes before they touch the machine, and `stop()` clears the watcher id exactly once (T3; T-06-05b's privacy-minimal trip end).
- **Purity is gated, not promised**: R12/R12b/E8 scan the comment-stripped source of all three modules — zero `console.`/`document`/`maplibre`/`localStorage`/`fetch(` — and pin `haversineM` as the only distance authority. No storage, no network, no DOM: the T-06-05 posture holds by construction.
- **Suite state:** web 100 passed / 0 failed (76 pre-existing untouched-green), routing 57, bot 74 — all workspaces typecheck clean. Zero new dependencies.
## Task Commits
1. **task 1: drive fixtures + THE D-04 state machine** (TDD)
- `b879e56` test(06-02): add failing D-16 drive replay suite R1-R12 + four committed drive fixtures (RED)
- `ad1fd30` feat(06-02): THE pure D-04 guidance state machine + forward-only projection + named constants (GREEN)
2. **task 2: honest ETA — EMA + deadband** (TDD)
- `6585294` test(06-02): add failing honest-ETA suite E1-E8 — EMA + deadband + arrival clock (RED)
- `299827a` feat(06-02): honest ETA — per-axis EMA + independent display deadbands + arrival clock (GREEN)
3. **task 3: watchPosition + distance throttle**
- `a45f8a5` feat(06-02): D-15 distance-throttled watchPosition wrapper + throttle truth table
## Files Created/Modified
- `web/src/guidance/guidance.ts` — the pure reducer, projection, constants, effect stream (4 haversineM call sites: deviation, missed-turn distance, projection-external use)
- `web/src/guidance/eta.ts` — updateEta + composeEtaClock + the three D-07 constants
- `web/src/guidance/watch.ts` — shouldAcceptFix + createGuidanceWatch + the two D-15 constants
- `web/test/guidance-replay.test.mjs` — R1–R12, R12b, T1–T3 (replay harness + buildModel/buildRerouteModel + controller-in-replay + structural gates)
- `web/test/guidance-eta.test.mjs` — E1–E8
- `web/test/fixtures/drives/{corridor-follow,off-route-excursion,missed-turn-excursion,gps-jitter}.json` — 234/116/55/234 Fix-shaped fixes over the central-market-to-merdeka-118 line (1943.8 m, 5 recorded turns), each with the ODbL `_notice`
## Decisions Made
See key-decisions: route-fixtures-vs-golden-pairs resolution, MISSED_TURN_GRACE_MS=3000, the off_route retry (Rule 2), calm-banner suppression semantics, apply→on_route vs resuming-as-self-heal, watch timestamps from pos.timestamp.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 2 - Missing critical functionality] `off_route` never re-requested a reroute**
- **Found during:** task 1 GREEN (R9 authored RED-side exposed it)
- **Issue:** the plan's state graph routes re-detection through `on_route`; once `off_route`, a controller that never answers (the reroute call hangs, or `reroute-failed`/`applied` never arrives) left the driver with exactly one request forever — the D-04 storm guard ("minimum time between recalculations") implies retry semantics the state list didn't spell out.
- **Fix:** `off_route` keeps counting (3 consecutive + grace) and re-fires `reroute-requested` once `STORM_GUARD_MS` has elapsed since the last request; within the guard it stays silent (no effect, no state flap). R9 pins the exact timing.
- **Files modified:** web/src/guidance/guidance.ts, web/test/guidance-replay.test.mjs
- **Commit:** ad1fd30
**2. [Rule 1 - Spec pointer] Drives derive from `route-fixtures.json`, not `golden-pairs.json`**
- **Found during:** task 1 RED authoring
- **Issue:** the plan's read_first/key_links call `routing/test/fixtures/golden-pairs.json` "the committed real KL route lines drives are derived from", but that file is the pair manifest (pairId/origin/destinationId only) — the verbatim ODbL route geometry lives in `routing/src/data/route-fixtures.json` (what the fixture backend actually serves).
- **Fix:** the replay harness resolves the pair by `pairId` through route-fixtures.json (decode → cumulative → steps at the fixture's maneuver indices); each fixture's `_notice` carries the ODbL attribution line. Test-side only; no product code affected.
- **Files modified:** web/test/guidance-replay.test.mjs, web/test/fixtures/drives/*.json
- **Commit:** b879e56
### Test-side fixes during GREEN (not deviations — executor test bugs caught by verification)
- R9's storm probe originally offset fixes along the route's own east axis — the "80 m excursion" rode the line (deviation ≈ 2 m) and fired nothing; offsets are pure latitude now (commit ad1fd30).
- R5 asserted per-frame `deviationM ≤ threshold`; the forward-only cursor legally rounds a 90° corner one fix early, reading 40–68 m for the outside fixes — which the 3-fix + 8 s grace then absorbs (state never left `on_route`). Relaxed to the plan's actual truth: never `off_route`, zero reroutes, ≥90% of fixes inside the threshold (commit ad1fd30).
- T1's 10 m boundary case: a rounded meters-per-degree constant left a 3e-8 relative error that flipped the `>= 10` comparison; now uses geo.ts's exact `6371000·π/180` (+0.1 µm margin; the `>=` semantics stay pinned exactly by the 4999/5001 ms cases) (commit a45f8a5).
### Judgment calls within plan laws (not deviations)
- `MISSED_TURN_GRACE_MS = 3000` — the plan names the detector constant but no grace value ("shortens grace", executor discretion for replay-tuned constants).
- E4's display-convergence bound is `≤ DEADBAND_MIN_S` from the smoothed truth (the deadband's own contract), not "within 10% of target" — the display intentionally trails the converged EMA by up to one deadband.
- Haptics process only in `on_route`/`resuming`; `rerouting` is display-only (stale maneuvers never haptic); arrival is the implicit final mark crossed at `remaining ≤ 25 m`.
## Known Stubs
None. All exports are fully wired pure implementations; the drive fixtures are intentional committed test data (the plan's declared D-16 ground truth), not stubs.
## Threat Surface Scan
No new surface beyond the plan's `<threat_model>`: no network endpoints, no auth paths, no storage, no logging — the structural gates (R12/R12b/E8) pin zero `console./document/maplibre/localStorage/fetch(` across all three modules. T-06-05 (on-route fixes never leave the device; the only network-bound position is the `reroute-requested` origin) holds by construction; T-06-05b (the watch stops) is pinned by T3's idempotent `clearWatch`. The DoS disposition (forward-walking O(local) projection, 60-segment window, no unbounded memory) is implemented and replay-bounded. No threat flags.
## Issues Encountered
- Mid-execution, another concurrent process briefly rewrote `web/test/guidance-replay.test.mjs` on the shared tree (a different implementation whose `golden-pairs.json` relative path resolved outside the repo — the source of one transient "suite load failed" run). The file self-reverted to the committed version; content was md5-verified against HEAD before every commit thereafter. No net impact.
- Node lives under nvm; every shell command sourced `~/.nvm/nvm.sh` first (all prior plans' precedent). No project impact.
## User Setup Required
None — pure modules, committed fixtures, zero new dependencies.
## Next Phase Readiness
- **06-03 (guidance surface)**: renders `GuidanceSnapshot` (state/alongM/remainingM/deviationM) and dispatches `GuidanceEffect` (haptics via the capability-guarded WebApp API, banner states) — every behavior is already decided; the surface only reads and renders.
- **06-06 (resume + envelope)**: the controller-in-replay pattern (effect handler + pendingEvents) is the template for the real controller; `reroute-requested.origin` is the one position that may go on the wire.
- **06-07 (cron projection)**: reuses `projectAlongRoute` + the model's cumulative field for the turn-card progress math — one projection authority across foreground and cron.
- **06-08 (real drive)**: replay-verified thresholds get their device gate; the `gps-jitter` drive is the comparison baseline for real urban-canyon noise.
---
*Phase: 06-turn-by-turn-guidance-live-driving*
*Completed: 2026-10-24*
## Self-Check: PASSED
- All 9 created files verified present on disk (3 guidance modules, 2 test suites, 4 drive fixtures).
- All 5 plan commits verified in git log: b879e56, ad1fd30 (task 1 RED→GREEN), 6585294, 299827a (task 2 RED→GREEN), a45f8a5 (task 3) — TDD gate sequence intact for the tdd tasks.
- `npm test -w web`: 100 passed / 0 failed (76 pre-existing untouched-green); routing 57, bot 74; all three workspaces typecheck clean.
- Acceptance greps re-run at close: haversineM present in guidance.ts (4) and watch.ts (2); zero banned tokens (console./document/maplibre/localStorage/fetch() in comment-stripped sources; zero TODO/FIXME/placeholder stub markers.
- STATE.md and ROADMAP.md untouched by this executor (their working-tree diffs are the orchestrator's own concurrent writes); REQUIREMENTS.md likewise left to the orchestrator.
- One concurrent-write incident on the shared tree was md5-verified back to the committed content before every commit.