Files
arah2/.planning/phases/04-destination-search-kl-gazetteer/04-03-SUMMARY.md
T

246 lines
15 KiB
Markdown
Raw 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: 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: <MINI_APP_URL>/?dest=<plain slug id> (D-11, RESEARCH Pitfall 1)"
- "alternates callback surface: scoped alt:<id> 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=<plain slug id> 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=<plain slug id>` from the
configured `MINI_APP_URL`, plus 2–4 alternates as `alt:<id>` 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=<slug>`), 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: <category>`), 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** (`<mechanism_correction>`): `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=<slug>` 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.