Files
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

19 KiB
Raw Permalink Blame History

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
05-routing-route-display 04 web-route-surface
route-display
maplibre-layers
route-card
backbutton
fit-bounds
casing-stack
i18n
tdd
rams
phase provides
05-routing-route-display/01 @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 provides
05-routing-route-display/02 the committed 15-pair fixture corpus that makes the fixture backend resolve REAL geometry at runtime (8 pairs carry alternates for the chips)
phase provides
03-mini-app-shell-geolocation 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 provides
04-destination-search-kl-gazetteer the resolved destination (id + verbatim name) the route consults TO and displays
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
05-05 gates (?fixpair= captures + BA budget)
phase-6 guidance (consumes the drawn-route state machine)
added 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)
created modified
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
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)
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
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
RTNG-01
RTNG-02
14min 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).