Files
arah2/.planning/phases/05-routing-route-display/05-RESEARCH.md
T

6.7 KiB
Raw Blame History

Phase 5: Routing + Route Display — Research (light pass)

Researched: 2026-10-24 (light pass — the pivot removed Valhalla; per roadmap note "standard patterns, no research phase needed", this pass covers only the three open questions planning actually faces) Scope: fixture encoding format · OSRM demo build-time script shape · RoutingSource interface analogs. Everything else is decided in 05-CONTEXT.md (D-01..D-12) and 05-UI-SPEC.md (binding).


Finding 1 — Fixture encoding: encoded polyline6 VERBATIM (not GeoJSON)

Decision: store each route as the encoded-polyline string exactly as the OSRM demo returns it (geometries=polyline6), plus distanceM/durationS. A decoder lives in the routing workspace.

  • Verified live (2026-10-24): GET https://router.project-osrm.org/route/v1/driving/101.7117,3.1578;101.6869,3.1343?alternatives=2&overview=full&geometries=polyline6 → code: Ok, 2 routes, route 1 = 6907.9 m / 663.2 s with a 1200-char geometry string; route 2 = 6572.8 m / 696 s / 1252 chars.
  • Size math vs the D-11 budget: ~45 routes (15 pairs × up to 3) × ~1.2–2.5 KB ≈ 40–60 KB raw → ~12–20 KB gzipped. The ≤ 200 KB gz budget leaves an order of magnitude of headroom. GeoJSON coordinate arrays for the same geometry measure ~10–20× larger for zero fidelity gain — rejected.
  • Verbatim storage is the lossless choice: the string is what the demo server returned — no float rounding, no re-serialization. The decoder (standard Google encoded-polyline varint/delta algorithm, precision 1e6) is ~40 lines, pure, and round-trip testable (encode(decode(s)) === s per fixture — a free corpus assertion).
  • Snapped-progress support (D-07, GUID-05/06 upstream obligation): fixtures do NOT precompute cumulative distances — the runtime derives cumulativeDistanceM once at decode/load (deterministic haversine walk, memoized). Keeps fixtures lean; the MODEL still carries the field from day one.

Finding 2 — OSRM demo build-time script shape (verified shape, repo-tool precedent)

  • Endpoint: https://router.project-osrm.org/route/v1/driving/{lng},{lat};{lng},{lat}?alternatives=2&overview=full&geometries=polyline6 — alternatives=2 returns up to 2 alternates (≤ 3 total routes; matches RTNG-02's "up to 3"), overview=full gives the complete geometry, steps=false (turn instructions are Phase 6; fixture steps stay typed-but-empty — never fake).
  • Response contract: code: "Ok", routes[] best-first (preserve order), each with distance (m), duration (s), geometry (polyline6 string). A code !== "Ok" or 0-route response is a script HARD FAILURE for that pair — never a silently missing pair.
  • Script posture — exact analog search/scripts/overpass-topup.mjs (Phase 4 D-06 "repo tools never runtime deps"): runs on the MAINTAINER's machine (npm run routing:fixtures), never in CI, never at vite build, never at Worker request time. Mandatory descriptive User-Agent, polite delay between pair fetches, writes the committed routing/src/data/route-fixtures.json, soft-fails with a per-pair diagnostic. D-12 boundary stated in the script header: it hits FIXED PUBLIC coordinates (gazetteer landmark points) only; no user coordinate can ever enter a fixture; the app itself gains no network dependency.
  • Sustainability honesty: the demo server is a free best-effort service. That is acceptable precisely because its output is COMMITTED — the runtime never touches it. A server policy change breaks the (rare) regeneration script, never the product.

Finding 3 — RoutingSource interface (the SearchSource analog)

Mirror of search/src/types.ts (verbatim shape correspondence, D-01):

SearchSource (Phase 4, shipped) RoutingSource (this phase)
readonly name: string readonly name: string — "fixture" now; "wasm-router" (#2) / "valhalla" (#3) documented, not shipped
search(q): Promise<SearchHit[]> route(req): Promise<RouteResult | null>
EMPTY array = honest refusal (D-13) null = honest "router pending" (D-03) — never a blank map, never a fake route
byId(id): GazetteerEntry | null (sync) not needed this phase (the webview already holds the resolved GazetteerEntry from Phase 4)
  • RouteResult carries options: RouteOption[] — index 0 is the best route (OSRM returns best-first; order preserved), 1..3 entries. Each option: geometry (decoded points + derived cumulative distances), summary (distance/duration), and the day-one steps field (typed, empty in fixture world — D-10 defers instructions to Phase 6; an empty array is honest "not wired", a fabricated step list would not be).
  • Fixture matching law: a pair serves a request when destinationId matches exactly AND the requested origin sits within a fixed public tolerance (200 m haversine) of the pair's pinned origin. Anything else → null (the pending state). The 200 m pad absorbs GPS jitter around a known landmark without ever fabricating a route for an unknown origin.
  • Lazy-index + three-environment laws carry over verbatim from search/src/gazetteer.ts (LAW 1 lazy memoized load; LAW 2 static JSON import inlined by esbuild AND Vite, importable by bare Node 24 with with { type: "json" }; LAW 3 no logging/no storage — the D-15 analog: request coordinates never leave the caller).

Validation Architecture

  • Framework: existing hand-rolled runners; plan 05-01 adds the fifth workspace's identical pair (routing/test/runner.mjs + routing/test/harness.mjs, copied verbatim from search/test/). No new framework (Phase 4 precedent).
  • Primary acceptance instruments: (1) the routing workspace suite (codec vectors, contract gates, lazy/no-logging/no-storage laws); (2) the golden-pair corpus gate (offline: every pair's geometry↔summary consistency recomputed by haversine + round-trip codec proof + alternatives floor) — the D-14 corpus-gate shape, adapted to fixtures; (3) the web suite extensions (route state machine, copy gate, BackButton walk); (4) BA9 bundle-budget gate over the production build; (5) the capture harness legs (?fixpair=) with the ARAHARAH-ROUTE state= token.
  • Offline law: no test, suite, or gate ever touches a network. The OSRM demo is exercised ONLY by the maintainer script. This is what makes the corpus gate deterministic and the "fixture resolves cannot fail at runtime" claim (UI-SPEC Copywriting §Error state) true.
  • Blocking checkpoint: none this phase — no real-device leg is required (route display is fixture-world; the Phase 3/4 device gates already proved map + deep-link on hardware). Capture legs + structural gates carry verification.

RESEARCH COMPLETE

Light pass as directed: three findings, folded into plans 05-01/05-02/05-04/05-05. No open questions remain.