chore(planning): phase 5/6/7 planning artifacts + milestone-completion state sync

This commit is contained in:
araharah-executor
2026-10-25 12:51:27 +08:00
parent bb587ab537
commit db27aa8440
34 changed files with 3949 additions and 214 deletions
@@ -23,6 +23,7 @@ must_haves:
- "A tampered/truncated/mismatched graph binary decodes to null — never a throw, never a half-built graph (T-06-03)"
- "A GPS fix snaps to the nearest graph node via edge-segment projection with cos(lat)-scaled equirectangular math (D-05)"
- "A* finds the same optimal path as brute-force Dijkstra on the fixture mini-graph for every node pair (GUID-02 engine)"
- "The backend ladder records the engine decision trail in index.ts: fixture #1, engine A #2 (D-01); the Valhalla-to-JS port ruled infeasible and geojson-path-finder rejected (D-02); Engine B documented as the post-v1 evolution of A (D-02)"
- "One-way edges are respected by A* and ignored by snapping"
- "Route steps carry turn kind, street name, distance, and polyline6 geometry derived from the node path"
artifacts:
@@ -161,20 +162,21 @@ Street names are NOT optional: GUID-01's banner renders `RouteStep.name` verbati
<behavior>
- snapToGraph at a node's exact coordinates → that node, distanceM ≈ 0
- snapToGraph mid-edge → the CLOSER endpoint node, distanceM = haversine distance to it (never a fabricated mid-node)
- SnapResult carries `nameId` = the MATCHED segment's edgeNameId (null when UNNAMED_NAME_ID) — ADDITIVE field plan 07's cron street hop consumes (edges carry names; nodes don't)
- snapToGraph outside bbox + SNAP_BBOX_MARGIN_DEG margin → null (honest miss — router-pending upstream, D-03)
- oneway edges: snapping IGNORES direction (a fix near a oneway segment still snaps to its endpoints)
- two snaps for the same RoadGraph instance reuse the memoized grid (WeakMap — observable via a build-counter or timing assertion kept loose)
</behavior>
<action>
1. RED: `routing/test/snap.test.mjs` loads `mini-graph.bin` via decodeGraph and asserts the behaviors above.
2. GREEN: `routing/src/snap.ts` — `snapToGraph(graph: RoadGraph, point: RoutePoint): SnapResult | null` where `SnapResult = { node: number; distanceM: number }`. Implementation: coarse grid index (cell ~0.002°) mapping cell-key → node ids, built ONCE per graph and memoized in a module-scope `WeakMap<RoadGraph, Grid>` (LAW 1 lazy memoize — PATTERNS shared pattern); candidate nodes from the 3×3 cell neighborhood; final distance via point-to-SEGMENT equirectangular projection (lng scaled by cos(lat)) over each candidate's edges, returning the closer endpoint of the best segment. `SNAP_BBOX_MARGIN_DEG = 0.01` named export. All distances through haversineM / cos-scaled math — never raw Δdegrees (Pitfall 8).
2. GREEN: `routing/src/snap.ts` — `snapToGraph(graph: RoadGraph, point: RoutePoint): SnapResult | null` where `SnapResult = { node: number; distanceM: number; nameId: number | null }` — `nameId` is the best segment's `edgeNameId` found by the sweep (null when UNNAMED_NAME_ID); an ADDITIVE field, so `{ node, distanceM }` destructuring consumers keep working. Implementation: coarse grid index (cell ~0.002°) mapping cell-key → node ids, built ONCE per graph and memoized in a module-scope `WeakMap<RoadGraph, Grid>` (LAW 1 lazy memoize — PATTERNS shared pattern); candidate nodes from the 3×3 cell neighborhood; final distance via point-to-SEGMENT equirectangular projection (lng scaled by cos(lat)) over each candidate's edges, returning the closer endpoint of the best segment TOGETHER WITH that segment's `nameId`. `SNAP_BBOX_MARGIN_DEG = 0.01` named export. All distances through haversineM / cos-scaled math — never raw Δdegrees (Pitfall 8).
</action>
<verify>
<automated>npm run routing:test</automated>
</verify>
<acceptance_criteria>
- routing/src/snap.ts exports snapToGraph and SNAP_BBOX_MARGIN_DEG
- snap.test.mjs asserts: exact-node hit, mid-edge closer-endpoint, outside-bbox → null, oneway-direction ignored
- snap.test.mjs asserts: exact-node hit, mid-edge closer-endpoint, outside-bbox → null, oneway-direction ignored, nameId = the best segment's edgeNameId (null for the unnamed segment)
- grep `cos(` matches routing/src/snap.ts (the Pitfall-8 lng scaling)
- grep `WeakMap` matches routing/src/snap.ts (the per-graph memo)
</acceptance_criteria>