Files
arah2/.planning/phases/03-mini-app-shell-geolocation/03-02-SUMMARY.md
araharah-executor 4e4b75da00 docs(03-02): complete location chain core + Mock-Telegram harness plan — SUMMARY
4 tasks, 17 test cases green (GEO-01/02/03, PX-02), typecheck clean; 3 auto-fix
deviations documented; no STATE/ROADMAP writes (orchestrator-owned)
2026-10-22 23:47:47 +08:00

130 lines
9.0 KiB
Markdown
Raw Permalink 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: 03-mini-app-shell-geolocation
plan: 02
subsystem: web-location-core
epoch_start: 2026-10-22
tags: [geo-01, geo-02, geo-03, px-02, location-chain, telegram-mock, d04, d11]
requires:
- "03-01 web/ workspace (runner/harness, tsconfig strict+allowImportingTsExtensions)"
- "03-01 Worker routes: GET /api/location, POST /api/location-fallback, POST /api/session contract"
provides:
- "web/src/location/* — the D-04 three-source acquisition core: Fix type, telegram/webview/chat-pin adapters, acquireFix state machine (GEO-01 root for Phases 5/6)"
- "Android stale-fix recovery: looksStale + nudgeAndRetryTelegram (issue #56 nudge, result discarded)"
- "web/src/session/* — memory-only token holder + /api/session client (D-03/T-03-08)"
- "web/src/telegram/* — four narrow platform wrappers: TelegramWebAppLike, theme slot switch (PX-02), BackButton total wire, safe-area union"
- "web/src/mock/telegram-mock.ts — D-11 harness (fake WebApp, Web-Crypto dev-signed initData, controllable location stream)"
- "17-case hand-rolled suite: location-chain (10), no-storage (3), theme (4)"
affects:
- "plan 03-03 (main.ts boot choreography + map shell mount the chain/store/wrappers)"
- "plan 03-04 (capture harness drives this mock; Desktop chat-pin leg proven here)"
- "Phase 5 (route snapping consumes Fix); Phase 6 (pluggable cadence — no setInterval/watchPosition exists in chain.ts by design)"
tech-stack:
added: []
patterns:
- "Capability probe → nudge → fall-through chain with staleness as a separate axis"
- "Comment-stripped source-introspection gates (GEO-03, Pitfall-7) anchored on bot/test precedents"
- "Node type-stripped .ts imports from .mjs test suites (Node 24), stub-and-restore discipline for navigator/fetch/document globals"
key-files:
created:
- web/src/location/types.ts
- web/src/location/telegram.ts
- web/src/location/webview.ts
- web/src/location/chat-pin.ts
- web/src/location/chain.ts
- web/src/session/store.ts
- web/src/session/client.ts
- web/src/telegram/webapp.ts
- web/src/telegram/theme.ts
- web/src/telegram/backbutton.ts
- web/src/telegram/safe-area.ts
- web/src/mock/telegram-mock.ts
- web/test/location-chain.test.mjs
- web/test/no-storage.test.mjs
- web/test/theme.test.mjs
modified: []
decisions:
- "telegram/webapp.ts (task 3's file) created in task 1 — task 1's code imports TelegramWebAppLike and the per-task typecheck gate needs the module to exist (Rule 3)"
- "looksStale normalizes an omitted/undefined lastAccepted to null — acquireFix(deps) is the documented first-fix call (Rule 1, found by GEO1 case 1)"
- "client.ts header avoids the literal initDataUnsafe token so its own acceptance grep stays honest; prose says re-serialized form"
- "Mock signer is async Web Crypto (makeDevInitData) — node:crypto provably absent from the browser-bundled module path; tests use their own test-side signer"
- "Mock getLocation delivers cb(fix) on setTimeout(0) and cb(null) on denial — synchronous null would hide async-chain bugs"
metrics:
duration: ~10 min (15:36 → 15:46 UTC)
completed: 2026-10-22T15:46:36Z
...
# Phase 3 Plan 02: location chain core + Mock-Telegram harness Summary
**One-liner:** the D-04 three-source acquisition chain (telegram → webview → chat-pin) with the Android stale-fix nudge, a memory-only session layer, four narrow Telegram wrappers, and the D-11 fake-WebApp harness — 17 automated cases proving GEO-01, GEO-02 (client), GEO-03, and PX-02 with no device touched.
## Tasks
| task | Name | Commit | Files |
| ---- | ---- | ------ | ----- |
| 1 | Fix type + three source adapters (telegram/webview/chat-pin) | 9552e7e | web/src/location/{types,telegram,webview,chat-pin}.ts, web/src/telegram/webapp.ts (Rule 3 early) |
| 2 | chain.ts state machine + memory-only session store + /api/session client | faab77a | web/src/location/chain.ts, web/src/session/{store,client}.ts |
| 3 | Telegram platform wrappers (theme/backbutton/safe-area) | 0a35bce | web/src/telegram/{theme,backbutton,safe-area}.ts |
| 4 | Mock-Telegram harness + the four gates | 2659b7f | web/src/mock/telegram-mock.ts, web/test/{location-chain,no-storage,theme}.test.mjs, web/src/location/telegram.ts (Rule 1 fix) |
## Verification Results
- `npm test -w web` → `[runner] 17 passed, 0 failed` (10 GEO-01/02 + 3 GEO-03 + 4 PX-02; ≥ 3 runs across tasks)
- `npm run typecheck -w web` → clean (ran after every task)
- GEO-03 gate green: comment-stripped scan of 15 web/src .ts files finds zero banned storage tokens; no service-worker surface; store.ts assigns to nothing
- Telegram-side invariants proven: `telegramLocationSupported(null)` → false (no TypeError); `getLocation()` silence → null at 60ms in test; byte-identical pair → exactly ONE nudge read, result discarded; bounded chat-pin poll returns null (60 s production bound asserted by name); the Desktop no-geolocation leg reaches `denied` with the same `timeout` reason key a mobile run produces
- Pitfall-7 invariant greppable: `setStyle` absent from theme.ts source; the attribute set is the only observable mutation (deepStrictEqual on the stub's dataset keys)
- No file deletions in any commit; no untracked residue (only orchestrator-owned .planning/ROADMAP.md pre-existing modification)
## Threat model compliance
| Threat | Where implemented | Test |
| ------ | ----------------- | ---- |
| T-03-07 (coords/token to any store) | store.ts module-scope `let`; zero persistence calls under web/src | GEO3 gate (comment-stripped, all src) + store.ts assignment probe |
| T-03-08 (client-side gate bypass) | createSession posts verbatim initData body, validates token shape, maps 401 categories | client.ts source; category vocabulary mirrors bot/src/webhook.ts |
| T-03-09 (stale shown as LIVE) | looksStale (<0.01 m successive pair) → nudge; isFixStale 45s; fixAgeSeconds | GEO1 stale + nudge cases; 45000ms threshold grep-gated by plan criteria |
| T-03-10 (wedged getLocation) | hard `setTimeout` resolves null in telegram + webview wrappers | GEO1 timeout case (55ms ≤ wait < 2s) |
| T-03-11 (themeParams consumed as design values) | resolveBranch reads header_bg_color as 0–1 luminance signal only | PX02 luminance cases (dark #121212 / light #f0ede8) |
| T-03-12 (cross-origin WebApp calls) | mock injects into the SAME window; no iframe anywhere | mock module design; capture gating lands in 03-04 |
| T-03-29 (wa:null TypeError) | telegramLocationSupported null-guard `if (!wa) return false` | GEO1 null-webapp case + source-grep criterion |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] web/src/telegram/webapp.ts created in task 1, not task 3**
- **Found during:** task 1 (typecheck)
- **Issue:** the plan's task-1 telegram.ts imports `TelegramWebAppLike` from `../telegram/webapp.ts`, a task-3 file — per-task typecheck cannot exit 0 without the module existing.
- **Fix:** wrote task-3's verbatim `webapp.ts` content in task 1; task 3 then ships theme/backbutton/safe-area only. No content difference from the plan.
- **Commit:** 9552e7e
**2. [Rule 1 - Bug] looksStale/nudgeAndRetryTelegram crash when the caller omits lastAccepted**
- **Found during:** task 4 (GEO1 case 1)
- **Issue:** `acquireFix(deps)` without a second argument is the documented first-fix call, but `looksStale(undefined, first)` dereferenced `prev.source` → TypeError.
- **Fix:** signature widened to `Fix | null | undefined` with undefined normalized to null at both entry points.
- **Commit:** 2659b7f
**3. [Rule 2 - Gate safety] client.ts header comment tripped its own acceptance grep**
- **Found during:** task 2 (acceptance run)
- **Issue:** the planned "never `initDataUnsafe`" prose made `grep -q 'initDataUnsafe' client.ts` succeed — grep doesn't strip comments.
- **Fix:** reworded to "never a re-serialization of its parsed fields"; the behavioral invariant (verbatim body) is unchanged.
### Structural note (non-fix)
- The Desktop GEO2 test drives the real `fetchChatPin` + `acquireFix` rather than a `pollPin` shortcut, so the bound lands on the same reason key (`timeout`) a mobile run would produce — the Pitfall-4 "identical copy keys" check is end-to-end.
## Known Stubs
- `web/src/mock/telegram-mock.ts` `close()` and BackButton `show()/hide()` are no-ops — the runner page (plan 03-03) drives the harness; no production behavior rides on them.
- `makeDevInitData` is exported but unconsumed until the 03-03 runner page wires it (its consumer is D-11's browser loop, not this plan's tests — they use the test-side signer).
- Neither is a data-flow stub: production modules (location/session/telegram) are fully wired to their contracts.
## Auth Gates
None — no live Telegram or network secrets touched. The mock signs against a caller-provided bot token; nothing real is committed.
## Self-Check: PASSED
- 15 created / 0 modified files all present on disk (15 FOUND in the sweep just run)
- 4 task commits (`9552e7e`, `faab77a`, `0a35bce`, `2659b7f`) verified in `git log --oneline`; final runner output `17 passed, 0 failed`
- STATE.md and ROADMAP.md untouched by this executor (ROADMAP's pre-existing change belongs to the orchestrator)