Files

254 lines
15 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: 04-destination-search-kl-gazetteer
plan: 04
subsystem: web
tags: [destination-preload, deep-link, start-param, gazetteer, homescreen, add-to-home-screen, device-gate, capture, tdd]
requires:
- phase: 04-01 (shared search module)
provides: "@araharah/search — getEntryById for the webview's own build-time id lookup"
- phase: 04-03 (chat search surface)
provides: "the corrected carrier — <MINI_APP_URL>/?dest=<slug> on a web_app button; MINI_APP_URL origin"
- phase: 03 (mini app shell)
provides: "the staged boot, card stack, strip, harness mock, capture runner, BA gates"
provides:
- "web/src/search/resolve-destination.ts — the two-carrier reader (URL ?dest= PRIMARY, signed initData start_param SECONDARY, one slug validation, dest_ prefix stripped) + the memory-only holder"
- "web/src/search/destination-card.ts — the destination confirmation + honest-unavailable surfaces (createElement/textContent only)"
- "web/src/telegram/homescreen.ts — wireHomeScreen behind the BOTH-halves Bot API 8.0 guard (added hides / unknown shows / missed shows / unsupported nothing)"
- "TelegramWebAppLike gains startParam, addToHomeScreen?, checkHomeScreenStatus? + HomeScreenStatus; the mock satisfies them in the same change"
- "BA7 (bot/web id-set parity: the shipped bundle carries EVERY gazetteer id) + BA8 (no deep-link crypto in the shipped JS)"
- "ARAHARAH-DEST state-token boot signal + the shell-dest-preload capture leg (8/8)"
- "the deployed Worker (31138f79) with the destination-preload webview live; 04-DEVICE-GATE.md with six UNVERIFIED physical legs"
affects:
- "phase 5 (origin/destination resolvers consume getDestination(); the destination is already seeded at boot)"
- "phase 7 (ONBD-02 device verification fallback venue if legs stay unverified)"
tech-stack:
added: []
patterns:
- "two-carriers-one-validation reader: URLSearchParams for location.search, hand-rolled &-split (never URLSearchParams) for the signed initData string"
- "RED-verified gates first (suite-load failure = RED), per-task test→feat commit pairs"
- "state-token capture signals (ARAHARAH-DEST state=resolved|absent, never the id) paired with a DOM text assertion"
key-files:
created:
- web/src/search/resolve-destination.ts
- web/src/search/destination-card.ts
- web/src/telegram/homescreen.ts
- web/test/destination-preload.test.mjs
- web/test/homescreen.test.mjs
- .planning/phases/04-destination-search-kl-gazetteer/04-DEVICE-GATE.md
modified:
- web/src/telegram/webapp.ts
- web/src/mock/telegram-mock.ts
- web/src/i18n/strings.ts
- web/src/main.ts
- web/index.html
- web/package.json
- package-lock.json
- web/test/build-artifact.test.mjs
- web/test/no-storage.test.mjs
- web/scripts/capture.mjs
decisions:
- "The destination surface mounts into the EXISTING card stack and holds the center register at live (null when no id → Phase 3 boot byte-identical, D-12); the denied/awaiting location registers keep their cards — the destination returns at live"
- "waitMode 'dest' waits on the DOM (page.waitForFunction for 'Suria KLCC') after the state-token assertion — the signal fires at boot but the card mounts at live, so a one-shot text sample would race the chain"
- "homeScreenAlreadyAdded copy is defined in both locales but has NO render site: status 'added' HIDES the affordance (do not nag) — the string is reserved for a future surface that must say 'already added' (T-04-37 law)"
- "Deploy canary drift (menu button unarmed, command list empty on the live bot) fixed by re-running the repo's own one-time registration scripts — Telegram-side state, not source"
metrics:
duration: "~14 min executor time"
completed: 2026-10-23
tasks: 4 (3 automated + 1 blocking device-gate checkpoint)
filesCreated: 6
filesModified: 10
---
# Phase 4 Plan 4: Webview Deep-Link Hand-off + Device Gate Summary
**One-liner:** the webview half of ONBD-02 — a two-carrier destination reader (`?dest=` URL
query first, signed `initData.start_param` second, one `[A-Za-z0-9_-]{1,64}` validation,
`dest_` prefix stripped) resolving against the webview's own build-time copy of the shared
gazetteer into a memory-only holder, rendering the destination card (never a blank map,
never a fabricated coordinate) — plus ONBD-03's minimal Telegram-native home-screen hooks
behind a Bot API 8.0 guard with the iOS "unknown" treated as indeterminate, the BA7/BA8
drift gates proving the shipped bundle carries all 30 ids and zero crypto, the 8/8 capture
including the destination-preload leg, and the deployed origin with its blocking
six-leg device gate recorded honest-UNVERIFIED.
## Performance
- **Duration:** ~14 min
- **Started:** 2026-10-23T17:31:23Z
- **Completed:** 2026-10-23T17:45Z (automated half; device gate pending user)
- **Tasks:** 3/3 automated + the blocking checkpoint escalated
- **Files:** 16 (6 created, 10 modified)
## Accomplishments
- The deep-link hand-off is complete end-to-end in code and live on the deployed Worker:
`/cari` → venue card → OPEN (`?dest=<slug>`) → the webview resolves the id against its
own gazetteer copy → the DESTINATION card renders (verified by the capture leg's
`state=resolved` + "Suria KLCC" DOM assertion). No crypto anywhere on the path.
- ONBD-03's minimal hooks are live: `wireHomeScreen` guards on BOTH halves (method
presence + `isVersionAtLeast("8.0")`), hides on "added", shows the neutral prompt on
"unknown"/"missed", nothing on "unsupported", catches refused installs, and hides on
`homeScreenAdded` — with NO browser install APIs (HS9).
- The honesty laws are mechanical: DP6 rejects traversal/XSS/empty/200-char ids on BOTH
carriers; DP8/D-11 pin the honest null; BA7 proves the shipped bundle carries every
committed gazetteer id (the bot/web parity gate); BA8 proves no deep-link crypto ships.
- Deploy `31138f79` is live with all canaries green after re-arming Telegram-side
registrations (menu button web_app, help/privacy/cari command menus).
## Task Commits
| task | commit | message |
| ---- | ------ | ------- |
| 1 RED | `3e3d191` | test(04-04): RED — the ONBD-02 destination-preload gate suite (DP1-DP12) |
| 1 GREEN | `4f95841` | feat(04-04): the destination resolver — two carriers, one validation, memory only |
| 2 RED | `6e421e6` | test(04-04): RED — the ONBD-03 home-screen gate suite (HS1-HS9) |
| 2 GREEN | `c2d78b0` | feat(04-04): ONBD-03 home-screen hooks + the boot wiring + the ?dest= harness profile |
| 3 | `747eeb7` | test(04-04): the BA7/BA8 drift gates + the search-tree GEO3 pin + the destination-preload capture leg |
| 4 | `7b02c7d` | docs(04-04): the blocking ONBD-02/ONBD-03 device gate — deployed + canaries green, six legs UNVERIFIED pending a real device |
## Verification Results
- `npm test -w web` — **57 passed, 0 failed** (DP1–DP12, HS1–HS9, BA1–BA8, GEO3 incl. the
new explicit `web/src/search` pin, COPY gates over the 5 new strings, all prior suites)
- `npm test -w bot` — **65 passed, 0 failed** (unaffected; the deep-link contract's bot half)
- `npm run search:corpus` — threshold PASS: top-3 68/68 (100%), negatives refused 7/7,
margin 0.643 < 0.72 (D-14 close holds)
- `npm run web:typecheck` / `search:typecheck` — green
- `npm run build -w theme && npm run build -w web` — green; BA7 RED against the stale
pre-gazetteer bundle, GREEN after (the RED evidence for task 3)
- `npm run capture -w web` — **CAPTURE OK — 8/8 non-blank captures**, incl.
`shell-dest-preload.png` (34,499 B) whose leg asserted `ARAHARAH-DEST state=resolved`
AND the rendered card text "Suria KLCC"; GEO-03 runtime probe PASSED (storage empty)
- Live-origin canaries: `getWebhookInfo.url` ends `/telegram/webhook` (last_error is the
STALE 2026-10-22 phase-3-era 500, pending 0); menu button `web_app` → origin ✓;
commands default+ms+all_private_chats list `help/privacy/cari` ✓; unsigned webhook
POST → 401 ✓; the live `main-*.js` chunk carries **30/30 gazetteer ids, zero crypto
tokens**, and the new copy ✓
- `MINI_APP_URL` == deployed origin (binding shown in the deploy log); secrets never echoed
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] The dest capture leg's DOM check raced the boot chain**
- **Found during:** task 3 (design review of the wait mode, pre-commit)
- **Issue:** the plan's `waitMode: 'dest'` snippet sampled `page.textContent('body')` ONCE,
immediately after the ARAHARAH-DEST race. The signal fires at boot stage 13, but the
destination card mounts only when the chain reaches LIVE (the boot stages replace the
card stack transiently) — a one-shot sample could read the body before the card existed.
- **Fix:** kept the plan's `state=resolved` assertion, then waited for the card text with
`page.waitForFunction(...'Suria KLCC', { timeout: 15000, polling: 250 })` instead of a
single sample. Same assertion, race-free. Capture passed 8/8.
- **Files modified:** web/scripts/capture.mjs
**2. [Rule 3 - Blocking] Live-bot registrations were un-armed (menu button + command menu)**
- **Found during:** task 4 (deploy canaries — `getChatMenuButton` returned the default
`commands` type, `getMyCommands` empty)
- **Issue:** Telegram-side bot registrations from earlier phases did not survive on the
live bot; the plan requires the menu button web_app armed and the command list verified
post-deploy.
- **Fix:** re-ran the repo's own one-time registration scripts (`node
bot/scripts/set-menu-button.mjs`, `node bot/scripts/register-commands.mjs`); re-verified
via `getChatMenuButton`/`getMyCommands`. No source changes.
- **Files modified:** none (Telegram-side state)
**3. [Planner-note] Capture-leg comment numbering**
- The plan's leg snippet was commented "/* 8 —" while instructing placement after leg 6;
inserted as leg 7 with gallery-compare renumbered 8 so execution order reads correctly.
Cosmetic only; the `expected` array and the 8/8 line match the plan verbatim.
---
**Total deviations:** 2 auto-fixed (1 × Rule 1, 1 × Rule 3) + 1 cosmetic note
**Impact on plan:** none on scope; both fixes were required for the plan's own gates/
canaries to hold.
## Auth Gates
None — no secrets handled beyond reading the local gitignored `bot/.dev.vars` for deploy
canary Bot API calls; the token was never echoed (only concatenated into API URLs), and
`MINI_APP_URL` is a public var by design.
## Known Stubs
- `strings.*.homeScreenAlreadyAdded` (web/src/i18n/strings.ts) — defined + copy-gated in
both locales but with NO render site: the plan's own behaviour law makes status "added"
HIDE the affordance (do not nag), so no surface can honestly display "Already on your
home screen" in this phase. Reserved for a future surface that must SAY "already added"
(e.g. a settings view); the string lives in the strings module so copy stays centralized.
No other stubs — every export is wired to real behavior; no TODO/FIXME/placeholder in the
new tree.
## Threat Flags
None — the built surface is exactly the plan's threat model (T-04-32…T-04-42), every
mitigation landed WITH a gate: charset validation before any lookup (DP6, T-04-32), the
signed-string-only initData parse with start_param as a pure lookup key (DP2/DP10,
T-04-33), the bounded prank consequence + honest null (DP8, T-04-34), memory-only
destination (DP9/DP12 + GEO3 search-tree pin + runtime probe, T-04-35), the both-halves
capability guard with caught refusals (HS1/HS2/HS7, T-04-36), the never-fail status
handling (HS4/HS5, T-04-37), the browser-install-API ban (HS9, T-04-38), the id-parity
drift gate (BA7, T-04-39), the no-crypto bundle gate (BA8 + DP10, T-04-40), the harness
DCE proof (BA5/BA6 + capture's inverse assertion, T-04-41), and the anti-fake capture
race (state=resolved + DOM text, T-04-42).
## TDD Gate Compliance
PASSED — tasks 1 and 2 each carry a `test(...)` RED commit verified failing (suite-load
failures: 33+1 and 45+1) followed by a `feat(...)` GREEN commit:
`3e3d191→4f95841`, `6e421e6→c2d78b0`. Task 3 is a gates-and-tooling task (no production
source): its RED (`BA7` failing against the stale pre-gazetteer `web/dist`, 56+1) and its
GREEN (BA1–BA8 green after the rebuild — the build output is gitignored, so no source
commit exists for the GREEN half) are recorded here and in the execution log; the git
sequence is therefore `test → feat → feat → test → test → docs` at plan level.
## Device Gate (task 4 — BLOCKING CHECKPOINT)
`.planning/phases/04-destination-search-kl-gazetteer/04-DEVICE-GATE.md` records the
deploy (version `31138f79`), the canary table, the origin prerequisite, and SIX physical
legs — ONBD-02 on Android / iOS / Desktop, the ONBD-03 home-screen add + launch, the
unknown-id honesty check, and the `/cari Kuching` honest refusal — **every leg
UNVERIFIED**: no physical device is attached to this environment and none was faked.
**ORCHESTRATOR ACTION REQUIRED:** per this wave's split of ownership, `.planning/STATE.md`
and `.planning/ROADMAP.md` were deliberately NOT touched. The device-gate blocker
("ONBD-02/ONBD-03 device legs UNVERIFIED — needs a real Android/iOS/Desktop; phase held
open") must be recorded under STATE.md "Blockers/Concerns" by the orchestrator, and
ONBD-02/ONBD-03 must NOT be marked complete in REQUIREMENTS.md until the legs pass (the
plan's resume signal: "device gate passed").
## User Setup Required
Walk the six legs in `04-DEVICE-GATE.md` on a real device (the bot is LIVE at
https://araharah-bot.araharah-bot.workers.dev — `/cari KLCC` → OPEN). Also visually
confirm @BotFather's Mini App URL equals that origin (the Bot API cannot read the
BotFather-side value; the bot-side menu button + `MINI_APP_URL` are confirmed equal).
## Next Phase Readiness
- Phase 5 can consume `getDestination()` — the boot already seeds the memory-only holder
from either carrier; the origin/destination resolvers plug into the SearchSource seam.
- The carrier-swap lever stands ready if device legs 1–3 fail: `miniAppUrlFor(origin, id)`
in `bot/src/bot/commands/cari.ts` is the one place the deep link is built.
- `/pergi_ke` remains registered nowhere (D-01 reservation); the corpus gate and all
suite gates are green for the phase-exit verifier.
## Self-Check: PASSED
- All 6 created files exist on disk (verified below); all 10 modified files committed.
- All 6 task commits present in `git log` (`3e3d191`, `4f95841`, `6e421e6`, `c2d78b0`,
`747eeb7`, `7b02c7d`); zero file deletions in any (`git diff --diff-filter=D` = 0 after
each).
- Suites re-run at close: web 57/57, bot 65/65, corpus threshold PASS; capture 8/8;
deploy live with canaries green.
## Checkpoint resolution (orchestrator, 2026-10-24)
Device gate **PASSED** (user verdict): legs 1–4 + 6 PASS on real hardware; leg 5
user-accepted after the BotFather Main Mini App fix (automated DP coverage stands).
Post-gate rounds shipped: map tilt + extrusion, /start logo, command-scope shadow
law. Known-open cosmetic: client command menu (Telegram non-default setMyCommands
defect; diagnosis in 04-DEVICE-GATE.md). Verification: 04-VERIFICATION.md —
phase 4 PASSED.