Files
arah2/.planning/phases/06-turn-by-turn-guidance-live-driving/06-01-SUMMARY.md
T
araharah-executor 9456a870fe docs(06-01): complete engine-a data-plane plan
- SUMMARY: CSR codec + snapping + typed-array A* (57/57 routing tests,
  625/625 Dijkstra equivalence), 2 documented deviations (header alignment
  padding, compass turn convention), TDD gate sequence intact
2026-10-24 12:16:37 +08:00

16 KiB
Raw 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 01 routing
engine-a
csr-binary
typed-arrays
astar
snapping
road-graph
mini-graph-fixture
tdd
zero-copy
fail-honest
phase provides
05-routing-route-display/01 the routing workspace (RoutingSource seam, polyline6 codec, geo.ts haversineM, pure-module zero-laws, hand-rolled harness/runner, subpath-with-extension import law)
routing/src/graph.ts — the PINNED CSR binary codec (decodeGraph fail-honest null / encodeGraph) + RoadGraph typed-array model + street-name pool (UNNAMED_NAME_ID sentinel)
routing/src/snap.ts — snapToGraph point-to-segment snapping (cos(lat)-scaled, WeakMap-memoized coarse grid, bbox+margin honest null, SnapResult.nameId)
routing/src/astar.ts — routeAstar typed-array A* (allocation-free hot loop, admissible heuristic, exhaustive Dijkstra-equivalence-proven) + classifyTurn (THE turn authority, plan 07 imports it) + deriveSteps (kind/name/distanceM/polyline6 steps)
routing/test/fixtures/mini-graph.bin + build-mini-graph.mjs — the committed 5x5 ground-truth fixture (25 nodes / 80 edges / 3 names / 1 oneway / 2 speed classes)
RouteStep.kind (additive optional, 8 turn kinds) — the GUID-01 banner's maneuver vocabulary
index.ts ladder note: backend #2 is 'engine-a' (D-01); wasm-router note superseded; D-02 decision trail recorded
06-02 replay fixtures
06-04 road-graph build script
06-05 http-source + worker-router
06-07 cron projection (classifyTurn)
added patterns
PINNED CSR binary layout: little-endian sections (header/nodes/offsets/edges/weights/names), 8-byte-aligned via a 64-byte padded header so zero-copy TypedArray views are legal
fail-honest decode: magic+version+exact-byte-size validated BEFORE any view allocation; any mismatch -> null (never throw, never a half-built graph)
oneway encoding: the blocked mirror direction stays in the CSR flagged bit0 — visible to snapping, never traversed by A*
allocation-free A* hot loop: binary heap of indices over preallocated typed arrays (doubling growth), on-demand cached haversine/maxSpeed heuristic, no Map/no objects
snap grid: ~0.002-deg cells built once per RoadGraph, memoized in a module-scope WeakMap (LAW 1); per-fix cost is O(3x3 neighborhood)
created modified
routing/src/graph.ts
routing/src/snap.ts
routing/src/astar.ts
routing/test/graph.test.mjs
routing/test/snap.test.mjs
routing/test/astar.test.mjs
routing/test/fixtures/build-mini-graph.mjs
routing/test/fixtures/mini-graph.bin
routing/src/types.ts (RouteStep gains optional kind — additive only)
routing/src/index.ts (engine-A named exports + D-01/D-02 ladder note)
Header padded 60 -> 64 bytes (4 alignment pad bytes) and the f32 edgeSeconds section aligned after the u8 flags array — the plan-literal offsets put f64/f32 views at unaligned byte offsets, and unaligned Float64Array/Float32Array constructors THROW RangeError in every engine, making the plan's own zero-copy-view requirement (and its G5 mutation test) impossible on the literal layout. Field order/content/endian unchanged; every consumer inherits the layout through encodeGraph, so plans 04/07 need no awareness of it.
classifyTurn uses the compass convention (bearings clockwise; negative delta = left): the plan's behavior list contains classifyTurn(0, 40) -> 'slight-left' while ALSO pinning (0,-90) -> 'left' and (0,90) -> 'right' — mutually contradictory; 3 of 4 cases fix the convention and the implementation follows the majority + real bearing math. Test A6 pins (0,40) -> 'slight-right'.
edgeFlags bit0 semantics pinned: a oneway segment's blocked mirror direction remains in the CSR (so snapping sees segment geometry + street name) but A* never relaxes it — the only reading under which the plan's 'flag on the reverse edge' + 'respected by A*, ignored by snapping' behaviors coexist; the tests' brute-force Dijkstra shares the traversal law.
deriveSteps throws on a non-traversable consecutive node pair (caller bug — loud) while route-level misses stay null (D-03 fail-honest): a malformed path is a programmer error, not a runtime condition.
gridBuildCount() exported from snap.ts as workspace-internal instrumentation for the memoization gate (S6); deliberately NOT re-exported through index.ts, keeping the public surface exactly the plan's list.
CSR binary codec posture: pinned layout constants as named exports (GRAPH_MAGIC/GRAPH_VERSION/UNNAMED_NAME_ID), encode/decode pair in one module, round-trip-proven, decode validated before allocation
typed-array A* shape: NodeHeap over f64 keys + u32 nodes with lazy deletion; reusable for the plan-04 Worker route handler unchanged
committed binary test fixture: hand-built deterministic grid as routing ground truth (golden-pairs.json analog for graph data)
GUID-02
11min 2026-10-24

Phase 6 Plan 1: Engine A Data Plane — CSR Graph Codec + Snapping + Typed-Array A* Summary

Engine A's pure data plane is REAL and proven: a pinned little-endian CSR binary codec that round-trips and fails honest (null) on every malformed-header class, cos(lat)-correct GPS-fix snapping over a WeakMap-memoized grid, and a typed-array A* proven equal to brute-force Dijkstra on all 625 mini-graph pairs — steps carry turn kinds, street names, and polyline6 geometry, 57/57 routing tests green.

Performance

  • Duration: 11 min
  • Started: 2026-10-24T04:03:16Z
  • Completed: 2026-10-24T04:15:09Z
  • Tasks: 3/3 (each RED→GREEN)
  • Files: 10 (8 created, 2 modified)

Accomplishments

  • The CSR codec exists and is pinned (D-01): decodeGraph/encodeGraph over the little-endian layout (header magic/version/counts/bbox/maxSpeed ++ f64 coords ++ CSR offsets ++ edges/flags/weights ++ name table). Round-trip exact (G1); wrong magic/version/truncated/hostile-huge-count headers ALL resolve null — validated before any view allocation (T-06-03/T-06-03b, G2–G4); decoded views are zero-copy over the input buffer (G5 identity + mutation proof); the name pool resolves nameAt(0) === "Jalan Mini 1" and the unnamed sentinel → undefined (G6).
  • The committed ground truth (Wave 0 gap closed): mini-graph.bin (1,668 B) + its builder script — a hand-designed 5×5 grid near KL center (3.1500–3.1520 N, 101.6900–101.6920 E), 25 nodes, 80 directed edges, three named streets, exactly ONE oneway pair, two speed classes (60/30 km/h). G7 pins its invariants; snap and astar suites consume it as the shared fixture.
  • Snapping is the server authority (D-05): snapToGraph returns the closer endpoint of the best projected segment with distanceM (haversine) and that segment's nameId (null for unnamed) — the additive field plan 07's cron street-hop consumes. Bbox+margin misses resolve null (D-03 router-pending); oneway direction ignored; the ~0.002° grid memoizes per RoadGraph in a WeakMap (S6 build-counter + structural greps prove WeakMap + Math.cos).
  • A* is provably optimal (GUID-02's engine): the exhaustive 625-pair sweep matches brute-force Dijkstra cost exactly (A1); the oneway pair routes the long way against the flagged edge and direct with it (A2); unreachable → null (A3). The hot loop is allocation-free — binary heap of indices over preallocated typed arrays, typed-array g/closed/came-from, on-demand cached heuristic, new Map( grep = 0 (A8).
  • Turn steps speak the banner's language (GUID-01 upstream): deriveSteps splits at non-straight bearings or street-name changes, emits depart-first/arrive-last steps with kind/name/distanceM/polyline6 (A4/A5); step distances sum to the route distance. classifyTurn is exported as THE turn authority with pinned thresholds (25/60/150) — plan 07's cron imports this exact classifier.
  • The ladder record moved (D-01/D-02): index.ts now documents backend #2 as "engine-a" (supersedes the phase-5 "wasm-router" note), records the Valhalla-port-infeasible + geojson-path-finder-rejected decisions, and names engine B the post-v1 evolution. createHttpRoutingSource deliberately absent (A7 pins it for plan 04).
  • Suite state: routing 57 passed / 0 failed (was 35 — 22 new cases, all existing untouched-green); routing:typecheck, web:typecheck, bot:typecheck clean; web (76) and bot (74) suites unaffected by the additive RouteStep.kind.

Task Commits

Each task followed RED→GREEN with atomic commits:

  1. task 1: mini-graph fixture + CSR graph codec
    • 5374094 test(06-01): add failing CSR graph codec suite G1-G8 (RED)
    • 1ce3590 feat(06-01): CSR graph codec + committed mini-graph fixture + RouteStep.kind (GREEN)
  2. task 2: GPS-fix snapping
    • 72703d2 test(06-01): add failing GPS-fix snap suite S1-S6 (RED)
    • 2e73068 feat(06-01): point-to-edge GPS-fix snapping with memoized grid index (GREEN)
  3. task 3: typed-array A* + step derivation
    • 6adfd86 test(06-01): add failing A* equivalence + step-derivation suite A1-A8 (RED)
    • e34c229 feat(06-01): typed-array A* + turn classifier + step derivation + public exports (GREEN)

Files Created/Modified

  • routing/src/graph.ts — CSR codec; named layout constants; fail-honest decode; encode with input validation + stable CSR grouping; nameAt over the UTF-8 pool
  • routing/src/snap.ts — snapToGraph; SnapResult {node, distanceM, nameId}; SNAP_BBOX_MARGIN_DEG = 0.01; WeakMap grid; gridBuildCount instrumentation
  • routing/src/astar.ts — routeAstar (NodeHeap, lazy deletion); classifyTurn + TURN_* constants; GuidanceTurnKind = NonNullable<RouteStep["kind"]>; deriveSteps
  • routing/src/types.ts — RouteStep.kind (additive optional, 8 kinds, D-10 note) — no other line touched
  • routing/src/index.ts — engine-A named exports + D-01/D-02 ladder note in the header
  • routing/test/graph.test.mjs — G1–G8; routing/test/snap.test.mjs — S1–S6; routing/test/astar.test.mjs — A1–A8 (incl. the 625-pair equivalence sweep + test-local Dijkstra)
  • routing/test/fixtures/build-mini-graph.mjs + mini-graph.bin — the committed ground truth + its regeneration script

Decisions Made

See key-decisions: header alignment padding, compass turn convention, oneway flag semantics, deriveSteps throw-on-malformed-path, internal grid instrumentation export.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking spec conflict] CSR header padded to 64 bytes; f32 section 4-byte-aligned

  • Found during: task 1 GREEN implementation
  • Issue: the pinned layout's node arrays start at byte 60 (header is 6×u32 + 4×f64 + f32 = 60 B). new Float64Array(buffer, 60, n) throws RangeError in every engine — typed-array views require element-size-aligned offsets. The plan simultaneously mandates "zero-copy TypedArray views" (must_have truth + the G5-style mutation acceptance + a RoadGraph interface typed in Float64Array/Float32Array), which is UNSATISFIABLE on the literal offsets; the f32 edgeSeconds array is likewise unaligned whenever edgeCount ≢ 0 (mod 4) because of the u8 flags array.
  • Fix: 4 pad bytes after maxSpeedKmh (header 60 → 64 B, f64 arrays 8-byte aligned) + align4 padding before edgeSeconds. Field order, content, and endianness are unchanged; the layout is encapsulated behind encodeGraph, so plan 04's build script and every other consumer inherit it automatically. Both test files pin the layout the implementation ships (G5 mutates byte 64).
  • Files modified: routing/src/graph.ts (documented in the module header)
  • Commit: 1ce3590

2. [Rule 1 - Spec inconsistency] classifyTurn(0, 40) resolves "slight-right", not the plan's "slight-left"

  • Found during: task 3 RED authoring
  • Issue: the plan's behavior list pins classifyTurn(0, -90) → "left" and classifyTurn(0, 90) → "right" (compass convention: negative delta = left), but also classifyTurn(0, 40) → "slight-left" — contradictory under ANY sign convention.
  • Fix: implemented the convention 3 of the plan's own 4 cases establish (compass bearings clockwise; negative = left) and pinned (0, 40) → "slight-right" + (0, -40) → "slight-left" in A6 with a comment naming the conflict.
  • Files modified: routing/src/astar.ts, routing/test/astar.test.mjs
  • Commit: e34c229

Test-side fixes during GREEN (not deviations — executor test bugs caught by verification)

  • S4 asserted "row-4 node" over ids 10..14 (that's row 2); the implementation had snapped correctly to node 21 (r4c1). Fixed the assertion range to 20..24 (commit 2e73068).
  • A4 expected ["depart","right","left","arrive"] from a mislabeled geography (increasing row = increasing lat = NORTH, so east→north is a LEFT). The implemented bearings were correct; fixed the expectation to ["depart","left","right","arrive"] (commit e34c229).
  • build-mini-graph.mjs initially accumulated 3.15 + r*0.0005 (float error vs the 3.152 literal); switched to the 1e-6 integer grid ((3150000 + r*500)/1e6), the codec's own grid law (commit 1ce3590).

Judgment calls within plan laws (not deviations)

  • Oneway flag semantics (see key-decisions) — the coherent reading of "respected by A*, ignored by snapping".
  • Float64 heap keys instead of the plan's "f32 key[]" hint — numerical safety at zero cost; no gate keys on it.
  • snap.ts documents its 3×3-neighborhood candidacy limit honestly (a segment whose endpoints both sit >1 cell away is not a candidate; miss → null, never a wrong snap) — plan 04's KL build densifies at intersections, well inside the window.

Known Stubs

None. All exports are fully wired pure implementations; the mini-graph fixture is intentional committed test data (the plan's declared ground truth), not a stub.

Threat Surface Scan

No new surface beyond the plan's <threat_model>: no network endpoints, no auth paths, no storage, no logging — the C4 banned-token gate (which now scans the three new modules automatically) stays green, and the task-level grep gates (zero expirationTtl|console.|fetch( in graph.ts) pass. T-06-03 (tampering → fail-honest null) is implemented and gated by G2–G4; T-06-03b (size check before allocation) is implemented in decodeGraph and gated by G4's hostile-huge-count case; T-06-05 (pure modules) holds by construction. No threat flags.

Issues Encountered

  • 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 fixture, zero new dependencies.

Next Phase Readiness

  • 06-02 (replays): route snapshots can embed deriveSteps output directly — banner/list consume kind/name/distanceM/geometry verbatim.
  • 06-04 (graph build): imports encodeGraph (the layout's single authority — the alignment padding travels with it); the mini-graph builder doubles as its shape reference.
  • 06-05 (http-source + worker-router): decodeGraph + snapToGraph + routeAstar are the entire server route path; R2 ArrayBuffer → decodeGraph → (null = router-pending per D-03) → snap origin → A* → JSON steps.
  • 06-07 (cron): imports classifyTurn + TURN_* constants — one turn authority across foreground and cron surfaces.

Phase: 06-turn-by-turn-guidance-live-driving Completed: 2026-10-24

Self-Check: PASSED

  • All 8 created files verified present on disk (graph/snap/astar.ts, 3 test suites, builder + binary).
  • All 6 plan commits verified in git log: 5374094, 1ce3590, 72703d2, 2e73068, 6adfd86, e34c229 (RED→GREEN per task; TDD gate sequence intact).
  • npm run routing:test: 57 passed, 0 failed; npm run routing:typecheck exit 0; web + bot typechecks and suites unaffected.
  • A* ≡ Dijkstra verified exhaustively (625/625 pairs, A1).
  • STATE.md and ROADMAP.md untouched (orchestrator-owned writes); only this SUMMARY force-added.