Files
2026-10-23 22:37:55 +08:00

14 KiB
Raw Permalink Blame History


phase: 03-mini-app-shell-geolocation plan: 03 subsystem: web-shell-chrome epoch_start: 2026-10-22 tags: [d-06, d-08, d-09, d-10, px-02, maplibre-v6, copy-gate, rams-shell] requires:

  • "03-01 web/ workspace (vite build, strict tsconfig, /api half routes)"
  • "03-02 location chain (acquireFix/isFixStale/fixAgeSeconds), session store+client, telegram wrappers, D-11 mock"
  • "theme/dist/rams.css tokens + component classes; theme/basemap/liberty-rams.json" provides:
  • "web/src/i18n/strings.ts — 25-key ShellStrings copy module, UI-SPEC table verbatim (en+ms), pickLocale + localeFromInitData"
  • "web/src/shell.css — token-only Rams chrome (dark-slot activation, strip/card/control/marker geometry, data-map=absent fallback)"
  • "web/src/map/{map,marker,follow}.ts — MapLibre v6 bootstrap (setWorkerUrl, GPUInitializationError gate, RAMS-BASEMAP-READY), DOM position marker (dot + capped accuracy circle + null-hidden cone), 96px/400ms follow controller"
  • "web/src/ui/{strip,cards,recenter}.ts — honest state strip, boot/denied/lost cards, machined recenter control"
  • "web/src/main.ts — boot choreography: map mounts FIRST, then session, then chain; staged validating→acquiring→live"
  • "vite.config.ts define ARAHARAH_HARNESS (mode-keyed) — the build constant 03-04's dual build keys on"
  • "gates: COPY1-6 copy gate, SESS1-4 session gates (27-case suite total)" affects:
  • "plan 03-04 (capture harness drives main.ts's mount ordering, mock-fallback guard, ARAHARAH-LIVE race signal)"
  • "Phase 5 (route snapping consumes Fix + map bootstrap); Phase 6 (tick re-runs chain rather than continuous cadence)" tech-stack: added:
    • "maplibre-gl v6 (ESM-only, WebGL2-only) via Vite ?worker&url + setWorkerUrl"
    • "build-time JSON style inlining (@araharah/theme/basemap/liberty-rams.json) with resolveJsonModule" patterns:
    • "Map-first boot order as the anti-blank-map invariant (order asserted by script)"
    • "Null-guarded map consumers — WebGL2-absent shell renders deliberate absent-map state, never crashes"
    • "Machine-blink duty cycle via setInterval; zero CSS transition/animation in chrome"
    • "Named-CTA data slots in card markup for element-specific class-mapping gates" key-files: created:
    • web/src/i18n/strings.ts
    • web/src/shell.css
    • web/src/map/map.ts
    • web/src/map/marker.ts
    • web/src/map/follow.ts
    • web/src/ui/strip.ts
    • web/src/ui/cards.ts
    • web/src/ui/recenter.ts
    • web/test/copy-gate.test.mjs
    • web/test/session.test.mjs modified:
    • web/src/main.ts (stub → composition root)
    • web/src/map/map.ts (attribution bottom-left)
    • web/src/map/marker.ts (comment reword only)
    • web/src/shell.css (attrib chip scope, strip cluster, recenter z-index)
    • web/vite.config.ts (define harness constant)
    • .planning/phases/03-mini-app-shell-geolocation/03-UI-SPEC.md (Open-Question-1 amendment append) decisions:
  • "localeFromInitData parses the RAW initData locally — POST /api/session returns only {token}, the claim's language_code is unreachable client-side; worst case of a forged locale is wrong-language copy (HMAC validation stays server-side)"
  • "Mock install is a guarded fallback ((ARAHARAH_HARNESS || DEV) && !window.ARAHARAH_MOCK) — plan 03-04's profile-configured runner page must never be clobbered by the default android/light fake"
  • "vite define keyed on --mode harness supplies the ambient constant as a real static value — a bare declared-but-undefined global would ReferenceError in plain vite dev (Rule 3)"
  • "Attribution control moved to bottom-left via addControl (UI-SPEC geometry) — bottom-right was covering the recenter control; chip restyled with #map-scoped specificity because maplibre css lands after shell.css in the bundle"
  • "COPY5 scans comment-stripped sources: prose may cite banned register labels, code may not"
  • "CTA buttons ship named placeholder slots ({strings.ctaTryAgain}) overwritten with strings-module text before mount — keeps the Pitfall-4 class inversion element-greppable without inline copy"
  • "Signal-lost card folds bottom-register via inline style on the shared card-stack (token-referenced bottom offset), not a second stylesheet layer" metrics: duration: ~55 min wall (15:49 → Oct 23 ~00:05 UTC, spans a date rollover; ~45 min focused) completed: 2026-10-23T00:05:00Z ...

Phase 3 Plan 03: Rams shell — copy, chrome, MapLibre bootstrap, boot choreography Summary

One-liner: the visible Rams Mini App shell — verbatim en+ms copy module with a 6-case copy gate, token-only stylesheet, MapLibre v6 basemap with WebGL2-absent fallback, dot/cone/accuracy marker, machined recenter control, six-state honest strip, denied card with the Desktop-first CTA inversion, and the map-first staged boot that never shows a blank screen.

Tasks

task Name Commit Files
1 i18n/strings.ts copy module (25 keys, en+ms) + COPY1-6 gate 83985c4 web/src/i18n/strings.ts, web/test/copy-gate.test.mjs
2 shell.css + map bootstrap / marker / follow c437d31 web/src/shell.css, web/src/map/{map,marker,follow}.ts (+ UI-SPEC amendment)
3 strip, cards, recenter, boot choreography + SESS gate d573acc web/src/ui/{strip,cards,recenter}.ts, web/src/main.ts, web/test/session.test.mjs, (task-3 wiring edits: map.ts, shell.css, marker.ts, vite.config.ts)

Verification Results

  • npm test -w web → [runner] 27 passed, 0 failed (10 GEO + 3 GEO3 + 4 PX02 + 6 COPY + 4 SESS; ≥24 required)
  • npm run typecheck -w web → clean after every task; npm run build -w web → clean, 3 assets (js + css + worker) + hashed woff2 fonts in dist/fonts
  • Boot-order script: bootMap( precedes acquireFix( in main.ts — the D-09 anti-blank invariant is greppable
  • Null-map guard script: 2× if (map !== null), follow = createFollowController(map mapped, follow?.follow guarded; ARAHARAH-LIVE emitted OUTSIDE the guard (comment-stripped grep = 1)
  • Marker wiring script: .addTo(map) inside the first guard and before the first marker.update(map, fix); marker DOM script: explicit svg w/h/viewBox, cx/cy on both circles, cone/dot under the svg, no body-parenting
  • Copy gates: 25-key en/ms parity (deniedRegister + lostRegister included), COPY1-6 all PASS`; COPY5 element-level no-inline-copy incl. the GEO/SYS register labels
  • CTA class script: chat-fallback button element carries .btn-primary, retry carries .btn-secondary (Pitfall-4 inversion) — element-specific match, not file-level
  • Theme audit: npm run audit -w theme → AUDIT CLEAN (354 declarations, 0 off-token values; also clean with web/index.html added via --pages)
  • shell.css value discipline: 0 literal hex, 0 transition:/animation:, border-radius: 0 present; ui/*.ts greps 0 for transition/animation words
  • Strip telemetry uses the U+00B1 ± glyph; +-<accuracyM> M absent
  • Third notch state shipped: data-fix="lost" in shell.css + recenter.setFixState("lost") in main.ts + setFixState in recenter.ts
  • UI-SPEC amendments: Pitfall-4 callout pre-present (verified, not duplicated); Open-Question-1 sentence appended exactly once under Boot Choreography
  • Real-browser smoke (headless Playwright, vite dev): map tiles render, strip lamp+word clustered left, D-06 denied card visible with inverted CTA classes, recenter visible and top-most at bottom-right, attribution chip bottom-left, webfonts loaded (dist/fonts hashed copies)

Threat model compliance

Threat Where implemented Evidence
T-03-13 (style JSON swapped at runtime) build-time ramsStyle import; no map.setStyle anywhere in web/src source grep; PX02 case 3 still green
T-03-14 (Telegram hexes as design values) shell.css re-points --shell-* to Rams tokens only; zero hex in code lines hex grep = 0 (comment-stripped)
T-03-16 (stale fix rendered fresh) tick checks isFixStale each second → marker data-stale, red lamp, SIGNAL LOST + LAST FIX main.ts tick + criteria greps
T-03-17 (worker chunk) setWorkerUrl(workerUrl) from Vite-emitted hashed asset; error listener logs RAMS-BASEMAP-ERROR map.ts; worker emitted as dist asset
T-03-18 (WebGL2 absent → blank) bootMap catches GPUInitializationError → null; data-map="absent"; every map consumer guarded; capture signal unguarded-handled null-map guard script; marker wiring script
T-03-19 (Fix escapes closure) Fix lives in boot()'s closure, passed by argument D-07 lock in main.ts; GEO3 gate still green

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] vite.config.ts gained define: __ARAHARAH_HARNESS__ (mode-keyed)

  • Found during: task 3 (typecheck/dev wiring)
  • Issue: the plan mandates the literal condition (__ARAHARAH_HARNESS__ || import.meta.env.DEV) && !window.__ARAHARAH_MOCK__; with only an ambient declare const, plain vite dev would ReferenceError on the unreplaced global.
  • Fix: define: { __ARAHARAH_HARNESS__: JSON.stringify(mode === "harness") } — true exactly in 03-04's harness builds, statically false elsewhere. Consistent with 03-04 task 2's dual-build intent, not a fork of it.
  • Commit: d573acc

2. [Rule 3 - Blocking] ramsStyle needs a typed cast + error listener listener-shape fix

  • Found during: task 2 (typecheck)
  • Issue: the plan asserted resolveJsonModule alone suffices; in practice TS2732-adjacent narrowing failed: the JSON infers version: number vs the style spec's version: 8 literal (TS2322), and the v6 error event's ErrorEvent & Object shape rejects { error?: Error } (TS2769).
  • Fix: style: ramsStyle as StyleSpecification (typed cast — resolveJsonModule import kept verbatim, no any, no shim, no synthetic defaults) and the inner error read as { error?: { message?: string } }. Behavior unchanged; RAMS-BASEMAP-ERROR/READY prefixes verbatim.
  • Commit: c437d31

3. [Rule 1 - Bug] Attribution sat bottom-right and covered the recenter control; chip rendered MapLibre-white

  • Found during: task 3 (real-browser render inspection)
  • Issue: default attribution position conflicts with the floating control; and because maplibre's css lands after shell.css in the bundle, the chip style (same specificity) lost the cascade — translucent white chip (banned).
  • Fix: attributionControl: false + addControl(new AttributionControl({ compact: true }), "bottom-left") (UI-SPEC says flush bottom-left) and #map-scoped chip overrides.
  • Commit: d573acc

4. [Rule 2 - Critical] ARAHARAH-LIVE console signal was missing from the implementation

  • Found during: task 3 (acceptance script)
  • Issue: the comment referenced it but the emit did not exist — a WebGL2-absent device would hang the 03-04 capture race.
  • Fix: console.log("ARAHARAH-LIVE fix=… acc=…") in handleLive, outside the map guard.
  • Commit: d573acc

5. [Rule 3 - Blocking] COPY5 resilient to execution order

  • Found during: task 1 (first test run)
  • Issue: COPY5 hard-requires web/src/ui/ to exist; task 1 precedes task 3, so the suite failed ENOENT mid-plan.
  • Fix: scan [main.ts] always + ui/*.ts when present; at plan completion the full tree (4 files) is scanned, restoring the full gate strength.
  • Commit: 83985c4

6. [Rule 1 - Bug] Comments tripped several greps (grep does not strip comments)

  • Found during: tasks 2-3 (acceptance runs)
  • Issue: marker.ts's prose cited document.body.appendChild(group), cards.ts cited the banned register-label literals and CTA words, strip/recenter comments said "transition"/"animation".
  • Fix: reworded comments (e.g. "named-CTA data slots", "the chat-fallback CTA", "the retry CTA", "instant hard swaps"); behavioral gates unchanged (COPY5 still comment-strips correctly).
  • Commits: c437d31, d573acc

Plan-text variances (documented, non-fixes)

  • pickLocale is NOT imported by main.ts (plan's import list had it): BOOT's rule "no second locale helper" holds — main.ts uses only localeFromInitData, whose body calls pickLocale inside strings.ts. Unused import avoided.
  • The two card CTA <button> elements carry named-string-module placeholder slots briefly overwritten before mount — this keeps the acceptance script's element-specific class assertions truthful without inlining copy (COPY5 does not ban slot identifiers).
  • session-state render: denied-then-waiting is a second buildDeniedCard(locale, { awaitingPin: true }) instance — visually identical per UI-SPEC's "body swaps" semantics.

Known Stubs

  • The card checkout slot {strings.ctaTryAgain}/{strings.ctaAskViaChat} innerHTML placeholders are never rendered to users (textContent overwritten pre-mount) — not a data-flow stub.
  • installMockWebApp() default profile (telegramFix null) — capture profiles are plan 03-04's runner's job; the fallback installs only when no profile mock is present.
  • Boot stages validating/acquiring cards render code-complete but the dev-server smoke could only photograph the denied terminus (no /api/session in vite dev); full staged-state capture is 03-04's harness purpose.

Threat Flags

None — no new network endpoints (tiles were the one pre-declared runtime fetch), no auth-path changes, no schema changes. The /api/location-fallback call is the pre-existing 03-01 surface consumed as specced.

Auth Gates

None — no live Telegram, no secrets; dev-signed mocks and real runs were both untouched.

Self-Check: PASSED

  • 10 created + 5 modified files present on disk (sweep live at write time); 3 asset + 6 font artifacts in web/dist
  • 3 task commits verified in git log --oneline (83985c4, c437d31, d573acc); zero file deletions across all three
  • Suite: 27 passed / 0 failed; typecheck + build clean; theme audit clean
  • STATE.md and ROADMAP.md untouched by this executor (ROADMAP.md pre-existing modification belongs to the orchestrator)