diff --git a/.planning/phases/04-destination-search-kl-gazetteer/04-03-SUMMARY.md b/.planning/phases/04-destination-search-kl-gazetteer/04-03-SUMMARY.md new file mode 100644 index 0000000..7cc1805 --- /dev/null +++ b/.planning/phases/04-destination-search-kl-gazetteer/04-03-SUMMARY.md @@ -0,0 +1,245 @@ +--- +phase: 04-destination-search-kl-gazetteer +plan: 03 +subsystem: bot +tags: [cari, sendvenue, inline-keyboard, callback-query, deep-link, web-app-button, telegram, grammY, i18n, privacy] +requires: + - phase: 04-01 (shared search module) + provides: "@araharah/search — createGazetteerSource(), SearchSource/GazetteerEntry contracts, byId() honest null" + - phase: 04-02 (corpus + bake-off close) + provides: "closed green corpus gate the /cari surface ships against (lever None)" + - phase: 02 (bot skeleton) + provides: "createBot factory + baseFetch seam, strings module + CS gates, register-commands.mjs, category-only logging law" +provides: + - "/cari + /search command surface — one sendVenue venue card per search (D-01/D-02/D-10/SRCH-03)" + - "OPEN IN ARAHARAH web_app button with the corrected deep-link carrier: /?dest= (D-11, RESEARCH Pitfall 1)" + - "alternates callback surface: scoped alt: matcher + always-answer catch-all; one-tap card swap via NEW sendVenue (D-02)" + - "sendVenueCard — the ONE shared card builder (first card and swapped card structurally identical)" + - "MINI_APP_URL env contract (public origin in wrangler vars) + docs/CONFIGURATION.md row" + - "search copy in en+ms (usage / honest refusal / over-cap / OPEN label / OSM attribution) + /cari helpMenu lines" + - "CR1-CR15 payload-shape gate suite (fake-transport + source-introspection) and the four amended CS gates" +affects: + - "plan 04-04 (webview reads ?dest= from location.search; device gate validates the same origin)" + - "phase 5 (origin/destination resolvers plug into the same SearchSource; /pergi_ke reserved there)" +tech-stack: + added: [] + patterns: + - "fake-transport payload capture: createBot(env, { baseFetch }) + synthetic Telegram updates (bot_command entity required) — asserts run on recorded request bodies, never the live API" + - "structural gates count RAW source: comment prose must not carry gate-banned tokens (editMessageText / keyboard / pergi_ke literals)" + - "carrier-swap seam: miniAppUrlFor(origin, id) is the one place the deep link is built" +key-files: + created: + - bot/src/bot/commands/cari.ts + - bot/src/bot/handlers/search-alternate.ts + - bot/test/cari.test.mjs + modified: + - bot/src/i18n/strings.ts + - bot/src/bot/bot.ts + - bot/src/bot/handlers/location-fallback.ts + - bot/src/config.ts + - bot/wrangler.jsonc + - bot/.dev.vars.example + - bot/package.json + - package-lock.json + - bot/scripts/register-commands.mjs + - bot/test/commands.test.mjs + - docs/CONFIGURATION.md +key-decisions: + - "Deep-link carrier corrected per RESEARCH Pitfall 1: WebAppInfo has exactly one field (url), so the OPEN button carries ?dest= read from location.search; initData.start_param stays the secondary direct-link/home-screen path; no crypto (CR3 enforces)" + - "MINI_APP_URL set to the REAL deployed origin https://araharah-bot.araharah-bot.workers.dev (matches the deployed WEBHOOK_URL origin; public var, not a secret) — must equal the @BotFather Mini App URL, verified at 04-04's device gate" + - "Blank MINI_APP_URL omits the OPEN button (never guesses an origin); the card + alternates still send (T-04-30)" + - "CS4 carve documented as the SECOND deliberate exception (inline keyboard in commands/cari.ts output); reply keyboards remain banned in commands/" + - "CS5b future-stub ban narrowed to /pergi_ke only — /cari is live, the Phase-5 navigation command stays unregistered (D-01)" +requirements-completed: [SRCH-03, ONBD-02] +metrics: + duration: "~13 min" + completed: 2026-10-23 + tasks: 3 + filesCreated: 3 + filesModified: 11 +--- + +# Phase 4 Plan 3: Chat Search Surface (/cari + venue card + alternates) Summary + +**One-liner:** `/cari` (+ `/search`) turns a query into ONE native sendVenue card — canonical name, +curated address, inline OPEN IN ARAHARAH `web_app` button carrying `?dest=` from the +configured `MINI_APP_URL`, plus 2–4 alternates as `alt:` callback buttons that swap the card in +one tap — with honest out-of-area refusal, a 128-char pre-search cap, category-only logging, and a +15-case payload gate suite proving the venue shape, the no-crypto deep link, and the always-answer rule. + +## Performance + +- **Duration:** ~13 min +- **Started:** 2026-10-23T17:10:48Z +- **Completed:** 2026-10-23T17:24:00Z +- **Tasks:** 3/3 +- **Files modified:** 14 (3 created, 11 modified) + +## Accomplishments + +- The chat-side search surface is live end-to-end through the real `createBot` factory: search → + ONE venue message → OPEN button deep link → alternates swap, all gated by tests that capture the + exact request bodies Telegram would receive. +- The deep-link mechanism is the research-corrected carrier: `web_app` button URL carries the plain + gazetteer id (`?dest=`), because `WebAppInfo` has exactly one field (`url`) — `start_param` + cannot ride an inline button. D-11's locked intent (plain id, no crypto, one tap) is delivered + unchanged; CR3 structurally bans every crypto token from the deep-link path. +- Privacy law is enforced mechanically: every `console.log` in `cari.ts` must be a category-only + string literal (`[search] rejected: `), the success path logs nothing, refusal copy + never echoes the query, and `callback_data` is a public place id only (≤ 64 bytes). +- The four `commands.test.mjs` landmines were defused in the same plan that landed `/cari` (CS4 + carve, CS5 menu, CS5b narrowed stub ban, CS6 alias set) — the suite went RED on cue after task 2 + and is green again at close. + +## Task Commits + +1. **task 1: the search copy + the /cari command + the venue card + the OPEN deep link** - `a73a388` (feat) +2. **task 2: the alternates callback handler + the bot factory wiring** - `13e5073` (feat) +3. **task 3: the four commands.test.mjs landmines + the new cari.test.mjs gate suite** - `5dcef0e` (test) + +## Files Created/Modified + +- `bot/src/bot/commands/cari.ts` (NEW) — the /cari + /search handler: query cap before any search, + SearchSource call, honest refusals, one `sendVenueCard` call; exports `MAX_QUERY_CHARS`, + `miniAppUrlFor`, the pure text builders, `registerCari` +- `bot/src/bot/handlers/search-alternate.ts` (NEW) — the ONE `sendVenueCard` builder, async + `alternatesFor`, the scoped `alt:` callback matcher (answer → validate → byId → swap), the + always-answer catch-all +- `bot/test/cari.test.mjs` (NEW) — CR1–CR15: fake-transport payload capture through the real + `createBot` + source-introspection gates with the `stripComments` helper +- `bot/src/i18n/strings.ts` — five new fields in en+ms + the `/cari` helpMenu line in both locales +- `bot/src/bot/bot.ts` — ONE `createGazetteerSource()` shared by both registrations; origin from + `env.MINI_APP_URL ?? ""`; header names the second D-02 exception +- `bot/src/bot/handlers/location-fallback.ts` — non-location messages now pass through (`next`) +- `bot/src/config.ts` — `MINI_APP_URL` in the Env contract +- `bot/wrangler.jsonc` — `"vars": { "MINI_APP_URL": ... }` (public origin, not a secret) +- `bot/.dev.vars.example` — commented MINI_APP_URL line +- `bot/package.json` + `package-lock.json` — `@araharah/search` workspace dependency +- `bot/scripts/register-commands.mjs` — menu list gains `cari` (two fetch calls unchanged) +- `bot/test/commands.test.mjs` — commandSources + cari.ts; CS4 carve; CS5 three-command menu; CS5b + narrowed to `/pergi_ke`; CS6 alias set gains cari+search; header documents the carve +- `docs/CONFIGURATION.md` — MINI_APP_URL env-table row + wrangler section + non-secret settings note + +## Verification Results + +- `npm test -w bot` — **65 passed, 0 failed** (PASS CR1…CR15 ×15, PASS CS×9, all prior suites green) +- `npm run bot:typecheck` — green +- `npm test -w web` — 33/33 (unaffected); `npm run search:test` — 57/57 (unaffected) +- `npx wrangler deploy --dry-run` — exit 0; `MINI_APP_URL` bound as an Environment Variable; the + bundle INLINES the gazetteer (`suria-klcc` marker present — completes 04-01's deferred L3 half) +- `grep -rl 'createHmac\|crypto\.subtle' cari.ts search-alternate.ts` → nothing (D-11) +- Non-category log lines in cari.ts → 0; `pergi_ke` registrations → 0 in both cari.ts and + register-commands.mjs (D-01 reservation) + +## Decisions Made + +- **Carrier correction implemented as specified** (``): `miniAppUrlFor(origin, id)` + is the single builder; the swap to another carrier is one line if the device test fails. +- **Real origin, not a placeholder:** `MINI_APP_URL` = `https://araharah-bot.araharah-bot.workers.dev`, + taken from the deployed `WEBHOOK_URL` origin (local gitignored `.dev.vars`; hostname only was read). +- **Alternates layout:** the plan's authoritative code snippet puts all alternates on one row after + the OPEN row (verified fine for 4 buttons in Telegram's inline keyboard shape). +- **`osmAttribution` consumer deferred with reason:** the venue card cannot carry text and the + refusal copy is pinned verbatim, so the attribution string is defined + CR11-pinned but not yet + rendered by a bot surface (see Known Stubs). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] The GEO-02 message handler swallowed every non-location message, starving /cari** +- **Found during:** task 3 (GREEN — the fake-transport suite drove `/cari KLCC` through the real + `createBot` and recorded zero API calls) +- **Issue:** `registerLocationFallback`'s `bot.on("message", …)` matches EVERY message and returned + without calling `next()`. Existing commands worked only because they were registered before it; + `/cari`, registered after, never fired. Same-shape synthetic updates without Telegram's + `bot_command` entity also don't match grammY's command filter (test-side learning, fixed in the + harness). +- **Fix:** the handler now takes grammY's `next` and passes non-location messages through + (`if (!location) return next();`), with the law documented in its doc comment. +- **Files modified:** bot/src/bot/handlers/location-fallback.ts +- **Verification:** CR1–CR8 (command path) green through the real factory; all LP*/WK* suites stay + green (the pin path is untouched). +- **Committed in:** `5dcef0e` (part of the task 3 commit) + +**2. [Rule 1 - Bug] Gate-banned tokens appeared in comment prose (structural gates count raw source)** +- **Found during:** task 2 (editMessageText) and task 3 (GREEN — CS4, CS5b) +- **Issue:** `search-alternate.ts`'s header mentioned `editMessageText` (gate: zero occurrences); + `bot.ts`'s new header said "inline keyboard" (CS4's `/keyboard/i` scan); the + `register-commands.mjs` header named `pergi_ke` (CS5b's ban is a raw-source grep). +- **Fix:** reworded the three comments to carry the same meaning without the banned literal — the + gates' intent (no edit CALLS, no reply keyboards in commands/, no pergi_ke REGISTRATION) is intact. +- **Files modified:** bot/src/bot/handlers/search-alternate.ts (pre-commit, `13e5073`), bot/src/bot/bot.ts, + bot/scripts/register-commands.mjs (in `5dcef0e`) +- **Verification:** CS4/CS5b green; `grep -c editMessageText search-alternate.ts` → 0. + +--- + +**Total deviations:** 2 auto-fixed (2 × Rule 1) +**Impact on plan:** Both fixes were required for the plan's own gates to hold; no scope creep. The +location-fallback pass-through is the one behavioral change outside the plan's file list and is +covered by the pre-existing LP suites. + +## Issues Encountered + +- grammY's `bot.handleUpdate` does NOT auto-initialize (it throws "Bot not initialized") — the + harness calls `await bot.init()` through the fake transport, which also proved `getMe` lands in + the fake (no network anywhere in the suite). +- grammY's command filter requires the `bot_command` entity on synthetic command messages; the + harness now emits it (real Telegram always does). + +## Known Stubs + +- `strings.*.osmAttribution` (bot/src/i18n/strings.ts) — defined and CR11-pinned in both locales but + not yet rendered by any bot surface: `sendVenue` has no text field and the refusal copy is pinned + verbatim, so no honest rendering site exists in v1's chat surface. The webview already carries the + identical string (web/src/i18n/strings.ts line 73) where OSM data is actually displayed. A future + chat surface that displays OSM-derived data (e.g. Phase 5 route summaries) consumes it from here — + the strings module is deliberately the single home for copy. + +## Auth Gates + +None — no secrets touched; the deployed origin was read from local gitignored `.dev.vars` (hostname +only), never echoed beyond the origin, and lives in `wrangler.jsonc` `vars` (public by design). + +## Threat Flags + +None — the built surface is exactly the plan's threat model (T-04-21…T-04-31), and every mitigation +landed with a gate: the 128-char cap (CR7), category-only logs (CR10), `alt:`-only validated +callback ids (CR5/CR15 + slug regex before byId), configured-origin-only deep links (CR2), the +no-crypto law (CR3), the honest refusal (CR6), the always-answer rule (CR9), and no in-place venue +edits (CR14). + +## TDD Gate Compliance + +NOTE — this plan realizes RED→GREEN at the PLAN level, not per-task: the plan itself sequences the +payload-shape gate suite in task 3 ("the four landmines in the SAME change"; "the suite goes RED the +moment cari.ts + the strings land"). RED evidence per task was the failing acceptance probes (task +1: missing file/strings/env greps; task 2: exactly the predicted CS4+CS5 failures, 48/2), and task +3's suite then pinned the implementation (65/65). The git log therefore shows +`feat → feat → test` rather than per-task `test → feat` pairs. No plan-level `type: tdd` frontmatter +exists (type: execute). + +## User Setup Required + +None new — `MINI_APP_URL` ships as a public `vars` entry in `bot/wrangler.jsonc` (already set to the +deployed origin). The device gate in plan 04-04 must confirm it equals the @BotFather Mini App URL +(Bot API 10.2 enforces the match). The deployed Worker needs a `wrangler deploy` to pick up the new +vars + handlers — deliberately NOT done here (04-04 owns deploy + device gate). + +## Next Phase Readiness + +- Plan 04-04 (web) can build the `?dest=` reader against a stable carrier: `?dest=` from + `location.search` (primary), `initData.start_param` (secondary), `/^[A-Za-z0-9_-]{1,64}$/` + validation, unknown id → honest "destination unavailable". +- The alternates/best-hit flow exercises `createGazetteerSource()` in the Worker bundle (verified by + the dry-run marker), so the webview and bot now share one dataset end-to-end. +- `/pergi_ke` remains unregistered, reserved for Phase 5; the SearchSource seam is ready for the + origin/destination resolvers. + +## Self-Check: PASSED + +- All 15 touched/created files exist on disk (verified individually above). +- All 3 task commits present in `git log` (`a73a388`, `13e5073`, `5dcef0e`); zero file deletions in + any task commit (`git diff --diff-filter=D` = 0 after each). +- `npm test -w bot` re-run at close: 65 passed, 0 failed; typecheck green; web 33/33; search 57/57.