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

18 KiB
Raw Permalink Blame History

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
06-turn-by-turn-guidance-live-driving 02 web-guidance
guidance-brain
d04-state-machine
reroute-storm-guard
replay-fixtures
ema-eta
deadband
distance-throttle
watchposition
haptics
tdd
pure-reducer
phase provides
06-turn-by-turn-guidance-live-driving/01 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 provides
05-routing-route-display/01 route-fixtures.json corpus (the golden pair geometries drives derive from) + the six-field Fix contract (web/src/location/types.ts)
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
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
added 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
created modified
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
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.
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
GUID-02
GUID-03
GUID-04
GUID-06
25min 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.