forked from c-fu/arah2
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)
130 lines
9.0 KiB
Markdown
130 lines
9.0 KiB
Markdown
---
|
||
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)
|