Files
Arah2/.planning/phases/04-destination-search-kl-gazetteer/04-CONTEXT.md
T
2026-10-23 22:37:55 +08:00

115 lines
9.1 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 4: Destination Search + KL Gazetteer - Context
**Gathered:** 2026-10-23
**Status:** Ready for planning
<domain>
## Phase Boundary
Users type `/cari <query>` in chat (bilingual aliases already reserved), the bot answers from an in-Worker curated KL gazetteer with the best result as a native venue card (sendVenue) plus inline alternates, and tapping OPEN IN ARAHARAH launches the Mini App with the destination preloaded via a `startapp` deep link. The search-quality experiment CLOSES this phase: a pinned golden corpus with thresholds decides gazetteer sufficiency vs escalation (normalizer edits → WASM geocoder backend #2 → Photon-on-VPS, in that order). The Malaysia-only WASM geocoder is a documented future backend, not this phase's deliverable. VPS provisioning defers to Phase 5 (Valhalla). License: GPL-3.0 — forkability is a design law.
</domain>
<decisions>
## Implementation Decisions
### Search interaction surface
- **D-01:** Search entry is the **`/cari` command** in chat (with `/pergi_ke` reserved for Phase 5 semantics; bilingual alias scheme per Phase 2 D-05). Free-text-to-bot stays reserved; no inline mode; no in-webview search UI this phase (a shared search module keeps that door open).
- **D-02:** Shortlist presentation: the **best result ships immediately** as the venue card with its OPEN button; **2–4 alternates ride as inline buttons on the same message**. One tap to proceed, one tap to switch — no second message round.
- **D-03:** Search backend is a **pluggable `SearchSource` interface implemented in-Worker**: curated KL gazetteer queried in-Worker, $0 infrastructure. **VPS/Photon provisioning defers to Phase 5** (Valhalla's road graph needs the VPS anyway). Photon remains a documented escalation path *behind the interface*.
### Forkability + license (user-surfaced design law)
- **D-04:** **GPL-3.0** is the project license. Forkability laws are binding from this phase: no hardcoded secrets (env only — already true), data as plain committed files, one-command deploy, one free-tier Cloudflare account per fork.
- **D-05:** The user's **Malaysia-only WASM geocoder** vision (tantivy/sqlite-trigram index, statically hosted, client-side webview search — a genuine privacy win) is captured as **SearchSource backend #2**, an OSS-track artifact *after* v1. The JSON↔SQLite converter script keeps its dataset format reachable from day one. **Backend #3 = Photon-on-VPS.** Neither ships in this phase.
### Gazetteer + Malay normalizer
- **D-06:** Entries are **hand-curated seed + a repo-script Overpass one-shot** that proposes top-up candidates for maintainer approval into the curated file. The script is a repo tool, not a runtime dependency.
- **D-07:** Canonical format is **typed JSON in the repo** (V8-native parse, no WASM in the Worker bundle, diff-friendly PRs). Rationale: at ~200–500 rows, SQLite/sql.js-in-Worker is ~1.5MB + cold-start overhead of pure cost.
- **D-08:** **Aliases live natively inside each entry** (`"KLCC"` → Suria KLCC, KLCC Park; IJN, PWTC, Stesen/Station…) — locals' shorthand is the main corpus quality lever.
- **D-09:** The Malay normalizer is a **rule table + folding + fuzzy-residual** module: bidirectional abbreviation table (Jln↔Jalan, Tmn↔Taman, Kg↔Kampung, vowel-drop-aware Presint/Prsn forms), case/whitespace folding, glued-word split — applied to BOTH the query and entry aliases at index time. Fuzzy scoring (Damerau) is only the residual net for genuine typos on longer words. Per-rule test vectors; extends by table edit, not code change.
### Venue card + deep-link hand-off
- **D-10:** The venue card is **Bot API `sendVenue`** (native pin/name/address semantics SRCH-03 names) with inline buttons: OPEN IN ARAHARAH (`web_app` button carrying `startapp`) + alternates.
- **D-11:** The deep link carries a **plain gazetteer entry id** (`?startapp=dest_<id>`). The webview maps id → destination from its own copy of the same gazetteer module (shared module, build-time). No crypto: the payload's only threat is a user pranking themselves; stale-webview datasets re-map via the shared module version.
- **D-12:** ONBD-03 scope: **minimal PWA hooks** — `addToHomeScreen` prompt handling + a startapp-less boot landing in the search-ready map (existing Phase 3 staged boot covers it). Full PWA manifest/offline story is documented-deferred.
### Acceptance corpus + thresholds (the bake-off close)
- **D-13:** A **pinned golden corpus** in the repo (~60–100 queries): golden landmarks (KLCC, Sunway Pyramid, DUKE), abbreviation forms, typo set (transpositions, missing keys), glued words, aliases, kampung-style names, plus **deliberate negatives** (out-of-KL: "Kuching", "Penang") that must get an honest "not in v1 area" answer, never a fabricated pin. Each positive pins its expected entry id. Reviewers extend by editing one file.
- **D-14:** The bake-off closes on: **top-3 contains the expected id for ≥90% of positives AND negatives are refused honestly**. Below threshold, escalation order is fixed: (1) alias/normalizer table edits → re-run; (2) SearchSource backend #2 (WASM geocoder); (3) Photon-on-VPS. The phase records which lever got pulled and re-runs the corpus — the experiment ends with a documented decision, not an open question.
### Privacy (carried + made specific)
- **D-15:** Search queries are user payload: **category-only logging** (Phase 2 law) — no query text persisted. KV caching of search results, if any, is TTL-only with short TTL and keyed by normalized query (content is public place data, never user identity). Venue choices are not stored.
### OpenCode's Discretion
- Scoring function shape (weighted trigram/Jaro vs Damerau) within the rule-table architecture — corpus decides empirically
- Alternates ranking + tie-breaking details; "not in v1 area" copy (en/ms, clinical tone)
- Overpass query shape for the top-up script; gazetteer entry schema fields beyond (id, name, aliases, lat, lng, category)
- D1/KV TTL result-cache mechanics or none (perf is likely fine without at this scale)
</decisions>
<specifics>
## Specific Ideas
- The venue card message should read like the bot's clinical voice: one line confirming the destination, instruments-grade terseness.
- The search module is shared by design: bot handler AND webview import the same gazetteer + normalizer (web copy of the module is build-time; id mapping stays in sync by construction).
- Corpus file doubles as documentation of what "locals' shorthand" means — keep it curated, not generated.
</specifics>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Phase scope + decisions
- `.planning/ROADMAP.md` §Phase 4 — goal, 5 success criteria, bake-off-closes mandate
- `.planning/REQUIREMENTS.md` §SRCH-01/02/03, ONBD-02/03
- `.planning/phases/03-mini-app-shell-geolocation/03-CONTEXT.md` — boot choreography (startapp lands in the staged flow), session contract, strings/i18n precedent
- `.planning/phases/02-bot-skeleton-initdata-security/02-CONTEXT.md` — command registration, aliases, category-only logging, KV TTL-only law
### Codebase
- `bot/src/bot/bot.ts`, `bot/src/bot/handlers/` — handler patterns, command wiring
- `bot/src/i18n/strings.ts` — bilingual strings module (search copy follows)
- `web/src/session/`, `web/src/main.ts` — boot flow the deep link lands in; `web/src/mock/` harness (destination-preload profiles for captures)
- `bot/wrangler.jsonc`, `bot/src/config.ts` — the Worker env the SearchSource lives in
- `AGENTS.md` — repo conventions; GPL-3.0 LICENSE file to be added this phase
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `web/src/i18n/strings.ts` pattern → `bot/src/i18n/strings.ts` (search copy en/ms)
- Hand-rolled test runners (bot/web/theme) — corpus runner is a new suite in the same shape
- Mock-Telegram harness (web) — add a `?dest=<id>` boot profile for ONBD-02 capture evidence
- `bot/src/webhook.ts` route table — `/cari` handler slots beside location-fallback
### Established Patterns
-npm workspaces + delegate scripts; TDD RED→GREEN; category-only logs; TTL-only KV; strings-module copy gates
### Integration Points
- `startapp` param arrives inside `Telegram.WebApp.initData` (`start_param` field) — session flow untouched; webview reads it post-validation
- `sendVenue` needs no new scopes; web_app inline button URL = deployed origin (already armed in Phase 3)
- The SearchSource interface is the seam Phases 5 (origin/destination), 6, and backend #2/#3 plug into
</code_context>
<deferred>
## Deferred Ideas
- Malaysia-only WASM geocoder (tantivy/sqlite-trigram), statically hosted, client-side webview search — SearchSource backend #2, OSS track after v1
- Photon-on-VPS — SearchSource backend #3; VPS provisioning happens in Phase 5 for Valhalla regardless
- In-webview search UI — needs a UI-SPEC round; shared module already keeps it cheap
- Full PWA (manifest, offline shell) — documented-deferred beyond minimal ONBD-03 hooks
- Free-text bot search + inline mode — reserved interaction surfaces
- Log-derived corpus (real queries) — becomes possible only post-launch (Phase 7+)
</deferred>
---
*Phase: 04-destination-search-kl-gazetteer*
*Context gathered: 2026-10-23*