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

15 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, metrics
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions requirements-completed metrics
04-destination-search-kl-gazetteer 03 bot
cari
sendvenue
inline-keyboard
callback-query
deep-link
web-app-button
telegram
grammY
i18n
privacy
phase provides
04-01 (shared search module) @araharah/search — createGazetteerSource(), SearchSource/GazetteerEntry contracts, byId() honest null
phase provides
04-02 (corpus + bake-off close) closed green corpus gate the /cari surface ships against (lever None)
phase provides
02 (bot skeleton) createBot factory + baseFetch seam, strings module + CS gates, register-commands.mjs, category-only logging law
/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
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)
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
created modified
bot/src/bot/commands/cari.ts
bot/src/bot/handlers/search-alternate.ts
bot/test/cari.test.mjs
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
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)
SRCH-03
ONBD-02
duration completed tasks filesCreated filesModified
~13 min 2026-10-23 3 3 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.