diff --git a/.planning/phases/05-routing-route-display/05-04-SUMMARY.md b/.planning/phases/05-routing-route-display/05-04-SUMMARY.md new file mode 100644 index 0000000..402f4c4 --- /dev/null +++ b/.planning/phases/05-routing-route-display/05-04-SUMMARY.md @@ -0,0 +1,188 @@ +--- +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- 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 ``: 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).