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

9.2 KiB

Phase 5: Routing + Route Display - Pattern Map

Mapped: 2026-10-24 Files analyzed: 24 new/modified surfaces (grouped into 16 rows below) Analogs found: 15 / 16 rows matched (1 genuinely new: the polyline6 codec — pure algorithm, RESEARCH is the spec)

Source files for this map: 05-CONTEXT.md (D-01..D-12), 05-RESEARCH.md, 05-UI-SPEC.md (binding). The controlling analogs are the search/ workspace (Phase 4) — interface + ladder + loader + corpus gate — and the Phase 3 shell surfaces (map/, ui/cards.ts, telegram/backbutton.ts).

File Classification

New/Modified File Role Data Flow Closest Analog Match Quality
routing/ workspace pkg (package.json, tsconfig.json) package config — search/package.json / search/tsconfig.json (Phase 4, shipped) exact
routing/src/types.ts (RoutingSource + RouteResult + day-one model fields) model (types) — search/src/types.ts exact
routing/src/polyline.ts (polyline6 codec) utility transform (pure) none — new algorithm; 05-RESEARCH Finding 1 is the spec (round-trip law makes it self-verifying) none (algorithm)
routing/src/fixture-source.ts (backend #1) service request-response (in-memory match) search/src/source.ts + search/src/gazetteer.ts (lazy LAW 1/2/3) exact
routing/src/index.ts (public API) package surface — search/src/index.ts (named exports, subpath-with-extension import law) exact
routing/test/runner.mjs + harness.mjs + *.test.mjs test — search/test/* (same harness shape) exact
routing/src/data/route-fixtures.json (committed fixtures) data — search/src/data/kl-gazetteer.json (committed dataset, ODbL header object) exact
routing/test/fixtures/golden-pairs.json (pinned corpus) data (test fixture) — search/test/fixtures/golden-corpus.json exact
routing/scripts/fetch-route-fixtures.mjs script (repo tool, network) file-I/O + network search/scripts/overpass-topup.mjs (UA mandatory, soft-exit, never a runtime dep) exact
routing/scripts/route-corpus.mjs + routing/test/corpus.test.mjs script + Nyquist gate batch search/scripts/gazetteer-corpus.mjs + search/test/corpus.test.mjs (one measurement, two reporters) exact
bot/src/kv/guidance-sessions.ts (envelope TYPES ONLY) model (types) — bot/src/kv/sessions.ts (SessionClaim "the entire persisted claim surface — a 5th property means drift") role-match (types-only, no KV calls)
web/src/route/route-layers.ts (GeoJSON sources + 4 layers) service (map painting) addSource/addLayer/setData web/src/map/map.ts building-3d block (addLayer inside map.on("load"), reuses style-owned values, never re-sets style) exact
web/src/route/route-markers.ts (origin ring + destination pin + name chip) view (DOM markers) Marker DOM web/src/map/marker.ts (createPositionMarker DOM-marker pattern, .pos-* chrome in shell.css) exact
web/src/route/route-card.ts (route card + chips + pending card) view (card builders) DOM web/src/ui/cards.ts (mountHintedCard) + web/src/search/destination-card.ts (createElement + textContent ONLY, strings-module copy) exact
web/src/route/route.ts (controller: consult, selection swap, camera) controller event/state web/src/map/follow.ts (constants + controller closure; CAMERA_PADDING_PX/RECENTER_DURATION_MS precedent) role-match
web/src/main.ts (mod) + web/src/telegram/backbutton.ts (mod: two-state walk) controller wiring — themselves (Phase 3/4; overlayPresent closure becomes the route-state predicate) —
web/src/shell.css (mod: chip/card-body/marker-chrome rules) style — itself (token-only law; machined-geometry exception list) —
web/src/i18n/strings.ts (mod: 10 route keys) config/i18n — itself (COPY1..COPY6 gate) exact
web/index.html (mod: ?fixpair= knob) config (harness page) — itself (?dest= profile block, 04-04) exact
web/src/mock/route-profiles.ts (harness-only pair-origin lookup) test harness — web/src/mock/telegram-mock.ts (harness-only module, BA5-excluded) exact
web/scripts/capture.mjs (mod: 5 route legs + ROUTE signal) script batch itself (SIGNALS array + captureLeg) exact
web/test/build-artifact.test.mjs (mod: BA9) test (gate) — itself (BA1..BA8) exact
web/test/route-surface.test.mjs (new structural suite) test — web/test/destination-preload.test.mjs (DP-gate style) exact

Pattern Assignments

routing/src/types.ts (model, types-only)

Analog: search/src/types.ts — same header-contract comment style, same "the seam every backend implements" law, same empty-value-is-load-bearing semantics ([] → null).

// search/src/types.ts:57-63 — the shape being mirrored:
export interface SearchSource {
  readonly name: string;
  search(q: SearchQuery): Promise<SearchHit[]>;  // EMPTY array = honest refusal
  byId(id: string): GazetteerEntry | null;
}
// becomes: route(req): Promise<RouteResult | null>  — null = router pending (D-03)

routing/src/fixture-source.ts (service)

Analog: search/src/gazetteer.ts LAW 1/2/3 verbatim — lazy memoized index (never module scope), static JSON import (with { type: "json" }), zero logging/storage. The matcher adds one pure function the gazetteer never needed: a haversine distance with a 200 m tolerance constant (05-RESEARCH Finding 3).

routing/scripts/fetch-route-fixtures.mjs (repo tool)

Analog: search/scripts/overpass-topup.mjs — descriptive User-Agent mandatory, inter-request delay, per-item soft diagnostics, writes a committed JSON dataset, imported by NOTHING at runtime. The header contract states the D-12 boundary (fixed public coordinates, maintainer machine, no runtime dependency).

routing/test/corpus.test.mjs (Nyquist gate)

Analog: search/test/corpus.test.mjs — "the suite re-runs the corpus runner's measurements IN-PROCESS (never spawns the script), so npm run routing:test fails whenever the corpus regresses." One measurement core exported from the script, two reporters.

web/src/route/route-layers.ts (map painting)

Analog: web/src/map/map.ts:43-67 — the building-3d block is the only existing addLayer precedent: inside map.on("load"), no beforeId, paint values copied from style-owned constants ("zero invented colors", DSGN-01). UI-SPEC extends it: 4 layers added AFTER the building-3d block, bottom→top route-alt-casing → route-alt-line → route-casing → route-line; sources added empty, filled via setData (data, not style — never a style re-set).

// web/src/map/map.ts:43 — the exact insertion point (AFTER this block, same handler):
map.on("load", () => { /* building-3d … console.log("RAMS-BASEMAP-READY …") */ });

web/src/route/route-card.ts (card builders)

Analog: web/src/search/destination-card.ts — createElement + textContent ONLY (an id/name-derived string never reaches innerHTML), composed from theme classes, every string from the strings module (COPY5). Chips add the .toggle-option focus precedent (3px orange bracket) and the <button type="button"> law. Per the UI-SPEC post-approval addendum: NO per-card padding overrides — compose from the existing .card > * padding rules (shell.css:74-80).

web/src/route/route.ts (controller)

Analog: web/src/map/follow.ts — named constants beside the existing ones (ROUTE_CAMERA_PADDING = { top: 96, bottom: 320, left: 64, right: 64 }, ROUTE_FIT_DURATION_MS = 400), a closure controller, no state outside memory (D-07).

bot/src/kv/guidance-sessions.ts (envelope types)

Analog: bot/src/kv/sessions.ts SessionClaim — "the entire persisted claim surface — a 5th property means drift" — but TYPES-ONLY per D-06: no KvLike, no .put(, no wrangler namespace; the TTL exists as a documented constant (GUIDANCE_SESSION_TTL_SECONDS = 7200) awaiting its Phase 6 consumer. The grep gate (bot test) pins "types-only" mechanically.

web/src/telegram/backbutton.ts (two-state walk)

Analog: itself — Phase 3's total-function wire (overlayPresent() => false → close()); the predicate becomes "route or pending state active" and the pushed-state click walks back via the SAME total function SEARCH AGAIN invokes (UI-SPEC §BackButton Contract). The handler and predicate ship as one change.

Data Flow Notes

  • Origin never persists: the request {origin, destinationId} is constructed in main.ts's closure from the live fix + the Phase 4 memory-only destination, passed by argument, and dropped with the frame (D-07/GEO-03 law — the route module takes no storage, keeps no module-scope origin).
  • Fixtures are public data: route-fixtures.json enters the bundle by static import exactly like kl-gazetteer.json (build-time inlining, BA7-style parity gate idea carries to BA9's budget measurement).
  • Signals are state tokens: ARAHARAH-ROUTE state=drawn|pending — never coordinates (D-15 hygiene, the ARAHARAH-DEST state= precedent).

Pattern map: 2026-10-24 — pattern-mapper role run inline (no subagent spawn capability; sanctioned fallback).