Files

100 lines
9.2 KiB
Markdown

# 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`).
```typescript
// 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).
```typescript
// 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).*