Files
arah2/.planning/phases/05-routing-route-display/05-04-SUMMARY.md
T
araharah-executor 7aa1113a57 docs(05-04): complete web route surface plan
- 4/4 tasks, 5 commits (f710913, d0751c1, 01a75f1, 65952fb, 52c9320)
- 73 web tests green incl. RS1-RS10; typecheck + production build + theme audit clean
- deviations: live-branch routePhase guard (spec state-table precedence),
  controller dep growth for Node-testability, pin-tail 3x8/20 synthesis
2026-10-24 05:40:35 +08:00

189 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 05-routing-route-display
plan: 04
subsystem: web-route-surface
tags: [route-display, maplibre-layers, route-card, backbutton, fit-bounds, casing-stack, i18n, tdd, rams]
# Dependency graph
requires:
- phase: 05-routing-route-display/01
provides: "@araharah/routing — the RoutingSource seam + createFixtureRoutingSource (imported via the explicit subpath @araharah/routing/src/index.ts) and the RoutePoint/RouteOption/RouteSummary model the layers/cards/controller consume"
- phase: 05-routing-route-display/02
provides: the committed 15-pair fixture corpus that makes the fixture backend resolve REAL geometry at runtime (8 pairs carry alternates for the chips)
- phase: 03-mini-app-shell-geolocation
provides: the live shell — map.ts load handler (the layer insertion point), follow controller (the walk-back camera return), card-stack pattern (mirrored as .route-stack), strings module, BackButton wire, mock harness
- phase: 04-destination-search-kl-gazetteer
provides: the resolved destination (id + verbatim name) the route consults TO and displays
provides:
- "web/src/route/ module family — route-layers (2 empty GeoJSON sources + 4-layer D-09 casing stack, MapLike-parameterized), route-markers (origin ring + destination pin + name chip), route-card (route + pending builders), route controller (idle|route|pending state machine)"
- "map.ts opts.onLoad hook — route layers install inside the ONE load handler, after building-3d, before the ready log; the style is never re-set"
- "main.ts boot wiring — auto-consult exactly once per boot after live+destination, .route-stack second register holder, null-tolerant map/follow/endpoints (WebGL2-absent degrades to cards-only)"
- "BackButton two-state walk — predicate 'route or pending active' + walkBack as ONE change; the click walks to the root via the SAME total function SEARCH AGAIN binds, never close() from a pushed state"
- "10 new ShellStrings keys (en+ms verbatim from the UI-SPEC copy table) + COPY7 copy gate + COPY5 scope over web/src/route"
- "RS1–RS10 structural + fake-map behavioral suite (15 cases) pinning the UI-SPEC laws without a browser"
affects: [05-05 gates (?fixpair= captures + BA budget), phase-6 guidance (consumes the drawn-route state machine)]
# Tech tracking
tech-stack:
added: [] # zero new npm dependencies — @araharah/routing is a workspace dep (lockfile-settled), maplibre-gl already present
patterns:
- "MapLike structural seam (method syntax, parameter bivariance): route layers Node-testable through a recording fake; the MapLibreMap bridge is a typed cast at the call site (the map.ts:32 precedent), never any"
- "DOM/maplibre concerns INJECTED into the controller (card builders, show callbacks, endpoint attach/remove) so the RS gates run in bare Node while the webview gets the real implementations"
- "Second register holder (.route-stack) beside the byte-identical Phase 3 stack — pending (center) + signal-lost (bottom) coexistence falls out of two independent stacks"
- "Paint constants as verbatim tokens.css hex (DSGN-01: MapLibre paint cannot consume CSS variables); sources are data (setData), style additions only (addLayer once, idempotence-guarded)"
key-files:
created:
- web/src/route/route-layers.ts
- web/src/route/route-markers.ts
- web/src/route/route-card.ts
- web/src/route/route.ts
- web/test/route-surface.test.mjs
modified:
- web/package.json (+@araharah/routing)
- package-lock.json
- web/src/i18n/strings.ts (10 route keys en+ms)
- web/test/copy-gate.test.mjs (COPY1 floor 40, COPY5 src/route scope, COPY7 verbatim)
- web/src/map/map.ts (opts.onLoad hook)
- web/src/shell.css (route chrome: stack registers, card rows, chips, marker rules)
- web/src/main.ts (boot wiring)
- web/src/telegram/backbutton.ts (two-state walk)
key-decisions:
- "Controller deps grew by locale/attachEndpoints/removeEndpoints beyond the plan's sketch — the Node-testability law (RS4 imports route.ts in bare Node) forbids maplibre value imports, so the DOM/SVG marker concern is injected like the card builders"
- "main.ts passes a no-op MapLike + no-op endpoints when bootMap() returned null — the WebGL2-absent shell still consults and renders route/pending CARDS (cards-only degradation, D-09 anti-blank); on a real map the typed alias bridges MapLibreMap → MapLike at the call site"
- "renderState's live branch gained a routePhase guard (plan said 'untouched' — but the untouched branch re-shows the destination card after staleness recovery, violating the BINDING UI-SPEC state table 'staleness never evicts the route card'; spec wins, fix inline)"
- "showRouteCard vacates the Phase 3 stack when a route/pending card shows (the map is the center surface per the state table); walkBack restores the destination card through showCard — 'center empty' realized with two independent stacks"
- "Pin tail synthesized as 3px wide × 8px tall (20px total glyph): the UI-SPEC's pin-table prose says 3px while its BINDING Spacing Scale table computes 12+8=20px total — both numbers satisfied, spacing arithmetic wins"
- "RS4's fake map installs the route sources before consult (mirroring production boot order) — the first RED run proved the gates catch a controller whose sources are missing"
patterns-established:
- "State-token console discipline: the ONLY console lines a feature module may emit are ARAHARAH-<DOMAIN> state= tokens — RS8 makes it mechanical (count, content, no interpolation)"
- "Absence-law gates: RS6 proves a design decision (D-10) by grepping COMMENT-STRIPPED source for the furniture that must never exist"
- "always-refit selection: alternatives swap = setData + card rebuild + fitBounds in one function (drawSelection) — chip tap and initial draw share the identical code path"
requirements-completed: [RTNG-01, RTNG-02]
# Metrics
duration: 14min
completed: 2026-10-23
---
# Phase 5 Plan 4: Web Route Surface — Layers + Cards + Controller + Two-State BackButton Summary
**The route experience renders on the live shell: the D-09 orange casing stack (addLayer once, setData forever), origin ring + destination pin, the bottom route card with N−1 alternates chips, the designed ROUTE // ROUTER pending card, the 96/320/64/64 fit-bounds camera law, and the first real BackButton walk — 73 web tests green (15 new RS gates), typecheck + production build clean, theme audit clean.**
## Performance
- **Duration:** 14 min
- **Started:** 2026-10-23T21:24:06Z
- **Completed:** 2026-10-23T21:39:03Z
- **Tasks:** 4/4
- **Files touched:** 13 (5 created, 8 modified)
## Accomplishments
- **The D-09 paint stack exists** (T-05-09): `route-layers.ts` adds sources `route` + `route-alt` EMPTY and four line layers bottom→top (`route-alt-casing` 5px matte-black / `route-alt-line` 3px soft-beige / `route-casing` 9px matte-black / `route-line` 5px signal-orange), opacity 1.0, cap square, join bevel, no beforeId — through the new `opts.onLoad` hook inside map.ts's ONE load handler, after building-3d, before `RAMS-BASEMAP-READY`. Selection changes are `setData` only; RS2 proves zero `setStyle` in `web/src/route/` and the install position textually.
- **The card is the route's semantic statement** (D-08): `route-card.ts` builds the bottom card — `ROUTE` / `ROUTE {n}/{N}` register, verbatim destination-name headline, telemetry-num minutes + MIN/KM units, one-line difference vs best, up to 2 chips (`{nn} // {min} MIN · {km} KM`, aria-labels from templates, 44px hit slop, instant `:active` invert, 3px focus bracket, chip count invariant at N−1, single-route draws render NO chip row) — and the center pending card (D-03): register `ROUTE // ROUTER`, clinical copy with the privacy clause, standard ink, no red. createElement + textContent ONLY (RS5).
- **The controller is a total state machine** (D-03/D-10): consult once per boot (main.ts's `routeConsulted` guard; only when live AND destination resolved), `.catch(() => null)` → pending (fail-honest, T-05-04, fitBounds never called), draw = setData + endpoints + bottom card + fitBounds(96/320/64/64 @ 400ms, pitch 35 explicit, bearing from getBearing) in one tick, chip select = swap + rebuild + ALWAYS refit, `walkBack` = the single total function shared by BackButton and SEARCH AGAIN. The module's ONLY console lines are the two `ARAHARAH-ROUTE state=` tokens (RS8).
- **The BackButton walks** (T-05-11): predicate and handler shipped as ONE change — visible exactly when route/pending is active, overlay click invokes the injected walkBack (never `close()` from a pushed state), root click keeps Telegram semantics. RS7 proves both arms.
- **Copy discipline held**: 10 keys verbatim en+ms (COPY7 pins the pending headline/body/CTA, the diff templates with literal `{min}`/`{n}` placeholders, the aria templates, the bilingual-identical `ROUTE // ROUTER`); COPY5 now scans `web/src/route/`; RS9 proves the route modules carry zero inline copy.
- **Suite state:** `npm test -w web` 73 passed / 0 failed (58 pre-existing + 15 RS cases); `npm run web:typecheck` exit 0; `npm run build -w theme && npm run build -w web` succeed with the route family verified inside the production bundle; theme audit CLEAN (0 off-token values — the three MapLibre hexes are the sanctioned verbatim-copy exception).
## Task Commits
Each task committed atomically (task 1 RED→GREEN; tasks 2–3 are the implementation tasks whose RS gates the plan deliberately lands in task 4):
1. **task 1: route strings + copy-gate extension + workspace dependency**
- `f710913` test(05-04): extend copy gate for route copy — COPY7 verbatim + floor 40 + COPY5 src/route scope (RED)
- `d0751c1` feat(05-04): route strings (10 keys en+ms) + @araharah/routing workspace dep (GREEN)
2. **task 2: route layers + endpoint markers + shell.css chrome + map.ts load hook**
- `01a75f1` feat(05-04): route paint stack + endpoint markers + load-hook install + shell chrome
3. **task 3: route cards + controller + main.ts wiring + BackButton two-state walk**
- `65952fb` feat(05-04): route controller + cards + boot wiring + two-state BackButton walk
4. **task 4: the RS structural + fake-map behavioral suite**
- `52c9320` test(05-04): RS1-RS10 structural + fake-map behavioral suite for the route surface
## Files Created/Modified
- `web/src/route/route-layers.ts` — MapLike seam, `ROUTE_PAINT` (verbatim hex + DSGN-01 note), `ROUTE_LAYER_IDS`, idempotent `installRouteLayers`, `setRouteDrawn`, `clearRouteLayers`
- `web/src/route/route-markers.ts` — origin ring (24px, 2px stroke, aria-hidden), destination pin (12px square + 3px×8px tail, white inset rule, never orange), 160px ellipsis name chip (real text); WeakMap-tracked attach/remove
- `web/src/route/route-card.ts` — `buildRouteCard` + `buildRoutePendingCard` + the chip/secondary-button/template helpers (createElement + textContent only)
- `web/src/route/route.ts` — `ROUTE_CAMERA_PADDING`/`ROUTE_FIT_DURATION_MS`/`ROUTE_CAMERA_PITCH`, `RouteState`, `createRouteController` with ALL DOM/map concerns injected
- `web/test/route-surface.test.mjs` — RS1–RS10 (15 cases: behavioral RS3/RS4/RS7 drive real modules through recording fakes; structural RS1/RS2/RS5/RS6/RS8/RS9/RS10 are comment-stripped source introspection)
- `web/src/map/map.ts` — `bootMap(container, opts)` + the guarded `opts.onLoad` slot (its own try/catch — a route failure degrades to "no route drawn", never a broken boot)
- `web/src/main.ts` — `routePhase`/`routeWalkBack` assignable closures, `.route-stack` + `showRouteCard`, the controller wired with null-tolerant map/follow/endpoints, consult-once guard in `handleLive`, phase-aware stale/live branches
- `web/src/shell.css` — `.route-stack` registers (64px control-clearance composite), `.route-telemetry`/`.route-diff`/`.route-chips` rows composing the `.card` child rules (NO padding overrides), `.route-chip` contract, marker SVG chrome; header machined-literal inventory extended
- `web/src/i18n/strings.ts` — 10 route keys (interface + en + ms) with template-placeholder documentation; header inventory note updated
- `web/test/copy-gate.test.mjs` — COPY1 floor 40 + 3 route-key pins, COPY5 route-dir scope + route copy in the banned regex, COPY7 verbatim case
- `web/package.json` + `package-lock.json` — `@araharah/routing` workspace dependency
- `web/src/telegram/backbutton.ts` — the two-state wire (predicate + handler as one change), header updated for Phase 6
## Decisions Made
- **Controller deps grew by `locale`, `attachEndpoints`, `removeEndpoints`** beyond the plan's sketch — RS4 imports `route.ts` in bare Node, and a maplibre value import (route-markers.ts's `Marker`) would break that; injecting the endpoint concern matches the plan's own "all DOM/map concerns injected → Node-testable" law. main.ts binds the real implementations.
- **Null-tolerant wrappers at the composition root** — a no-op MapLike + no-op endpoints when `bootMap()` returned null keep the controller's types total (the plan's own null-tolerant `follow` wrapper pattern, extended to the map seam): the WebGL2-absent shell still consults and shows route/pending CARDS.
- **`renderState`'s live branch gained a `routePhase() === "idle"` guard** — see Deviations.
- **`showRouteCard` vacates the Phase 3 stack** — the UI-SPEC state table's "center register: (empty — the map is the surface)" is realized by hiding the destination card when a route/pending card takes over; `walkBack → showCard(destinationCard)` restores it. Without this, the destination card and the route card would coexist (two stacks, same visual register).
- **Pin tail = 3px wide × 8px tall (20px total)** — the UI-SPEC carries both "3px triangular tail" (pin-table prose) and "8px triangular tail (20px total height)" (the binding Spacing Scale table, whose arithmetic only works with 8). Synthesized both readings; the needle tail reads orthogonal/machined per the glyph family note.
- **RS4's fake installs sources before consult** — production guarantees sources exist at load before any consult (the load hook precedes the location chain); the fake mirrors that boot order. The first RED run of the suite demonstrated exactly this failure mode (setData no-ops on missing sources), proving the gates have teeth.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Spec conflict] renderState's live branch needed the routePhase guard**
- **Found during:** task 3 (main.ts step 6 said the live branch "stays untouched")
- **Issue:** the untouched branch calls `showCard(destinationCard)` on EVERY live render — including the staleness-recovery path (`stale → runChain → handleLive`). With a route drawn, recovery would re-show the destination card BESIDE the persistent route card; with pending, it would collide with the pending card in the center. The BINDING UI-SPEC state table demands the opposite ("staleness never evicts the route card"; pending persists until walked back).
- **Fix:** `if (routePhase() === "idle") showCard(destinationCard);` — Phase 3 behavior byte-identical when idle (the plan's own law), the route surface keeps its registers after recovery.
- **Files modified:** web/src/main.ts
- **Commit:** 65952fb
### Judgment calls within plan laws (not deviations)
- Controller dep growth (locale/endpoints) per the Node-testability law — documented above.
- Pin-tail geometry synthesis (3px×8px/20px) — documented above.
- `showRouteCard` hiding the Phase 3 stack — the mechanism for the spec's "center empty" row; the plan's showRouteCard sketch ("replaces children + sets data-register + display") is implemented verbatim, the showCard(null) call is the addition that makes the state table true.
## TDD Gate Compliance
- **task 1** followed strict RED→GREEN: `f710913` (COPY1/COPY7 failing against the un-extended strings.ts) → `d0751c1` (green).
- **tasks 2–3** carry `tdd="true"` but the plan explicitly defers their gates to task 4 ("the RS gates for this task land in task 4"); they are implementation tasks verified by typecheck + the existing 58-test suite, with task 4 landing the 15 RS gates afterward.
- **task 4**'s first run was genuinely RED (3 failures) — two test-harness gaps (source-spec shape assertion; RS4's fake missing the production install step) — then green. The gates are proven to fail against real deviations, not just pass vacuously. No plan-level `type: tdd` RED/GREEN commit ladder applies (plan frontmatter is `type: execute`).
## Known Stubs
None. Every surface is wired to real data: the fixture RoutingSource serves the committed 15-pair corpus, the pending card is the DESIGNED D-03 deferral surface (not a stub), and the WebGL2-absent no-op MapLike is the documented cards-only degradation, not a placeholder.
## Threat Surface Scan
No new surface beyond the plan's `<threat_model>`: no network endpoints (the fixture backend rides the bundle), no auth paths, no storage, no schema. All five register entries have implemented mitigations with mechanical gates: T-05-09 (zero `setStyle`, idempotence guard, install position — RS2/RS3), T-05-10 (createElement + textContent only — RS5), T-05-11 (overlay walks / root closes — RS7), T-05-13 (two state tokens only, no interpolation, no coordinates — RS8), T-05-04 (`.catch(() => null)` → pending, no fitBounds — RS4 exception path). No threat flags.
## Issues Encountered
- Node lives under nvm in this environment; non-interactive shells start without it — every verification command sourced `~/.nvm/nvm.sh` first (05-01/05-02 precedent). No project impact.
- The production bundle check initially looked like the route layers were missing: the minifier rewrites double-quoted string literals as template literals, so a `grep "route-alt-casing"` over `dist/assets/*.js` misses. Grepping for `casing`/`ARAHARAH-ROUTE` confirmed the full route family is bundled. No action needed — noted for 05-05's BA gate authors (grep minified bundles carefully).
## User Setup Required
None — no external service configuration. The route surface runs entirely against the committed fixture corpus.
## Next Phase Readiness
- **05-05 (gates + captures)**: everything the capture legs need is live — the `ARAHARAH-ROUTE state=drawn|pending` console tokens, the rendered route card DOM, the `?dest=` carrier (the `?fixpair=` knob itself is 05-05's harness extension), and the production build carrying the route family. BA gates measure the bundle from `web/dist` (mind the backtick-literal grep note above).
- **Phase 6 (guidance)**: the route controller is the state machine guidance extends — the drawn route's geometry (with `cumulativeDistanceM`), the walkBack total function, and the routePhase closure are the exact seams PX-01's state graph grows from.
- **Verifier hooks**: RS1–RS10 in `web/test/route-surface.test.mjs` pin the UI-SPEC laws mechanically; the capture legs (05-05) remain the pixel-level proof.
---
*Phase: 05-routing-route-display*
*Completed: 2026-10-23*
## Self-Check: PASSED
- All 5 created files verified present on disk (route-layers/route-markers/route-card/route.ts + route-surface.test.mjs); all 8 modified files verified.
- All 5 plan commits verified in git log: f710913, d0751c1, 01a75f1, 65952fb, 52c9320 (task 1 RED→GREEN; tasks 2–4 per the plan's deferred-gate structure).
- `npm test -w web`: 73 passed, 0 failed (RS1–RS10 among them). `npm run web:typecheck`: exit 0.
- `npm run build -w theme && npm run build -w web`: succeed; route family confirmed inside the production bundle.
- Theme audit: CLEAN (0 off-token values).
- STATE.md and ROADMAP.md untouched (orchestrator-owned writes); only 05-04-SUMMARY.md added (force-add, .planning/ ignored).