From 9456a870fe4905efa09b66176c50df11dc203c78 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sat, 24 Oct 2026 12:16:37 +0800 Subject: [PATCH] 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 --- .../06-01-SUMMARY.md | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 .planning/phases/06-turn-by-turn-guidance-live-driving/06-01-SUMMARY.md diff --git a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-01-SUMMARY.md b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-01-SUMMARY.md new file mode 100644 index 0000000..f9aa1d0 --- /dev/null +++ b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-01-SUMMARY.md @@ -0,0 +1,176 @@ +--- +phase: 06-turn-by-turn-guidance-live-driving +plan: 01 +subsystem: routing +tags: [engine-a, csr-binary, typed-arrays, astar, snapping, road-graph, mini-graph-fixture, tdd, zero-copy, fail-honest] + +# Dependency graph +requires: + - phase: 05-routing-route-display/01 + provides: "the routing workspace (RoutingSource seam, polyline6 codec, geo.ts haversineM, pure-module zero-laws, hand-rolled harness/runner, subpath-with-extension import law)" +provides: + - "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" +affects: [06-02 replay fixtures, 06-04 road-graph build script, 06-05 http-source + worker-router, 06-07 cron projection (classifyTurn)] + +# Tech tracking +tech-stack: + added: [] # zero new dependencies — DataView/TypedArrays/TextDecoder are all V8-native + 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)" + +key-files: + created: + - 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 + modified: + - routing/src/types.ts (RouteStep gains optional kind — additive only) + - routing/src/index.ts (engine-A named exports + D-01/D-02 ladder note) + +key-decisions: + - "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." + +patterns-established: + - "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)" + +requirements-completed: [GUID-02] # engine side: the real router this phase's GUID-02 rerouting calls (state/requirements writes are orchestrator-owned) + +# Metrics +duration: 11min +completed: 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; 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 ``: 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.