28 KiB
Phase 4: Destination Search + KL Gazetteer - Pattern Map
Mapped: 2026-10-23 Files analyzed: 30 new/modified surfaces (grouped into 18 rows below) Analogs found: 16 / 18 rows matched (2 rows are genuinely new: repo-level LICENSE + ODbL data notice)
File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
bot/src/bot/commands/cari.ts |
controller | request-response | bot/src/bot/commands/start.ts |
exact |
bot/src/bot/handlers/search-alternate.ts |
controller | event-driven (callback_query) | bot/src/bot/handlers/location-fallback.ts |
exact |
bot/src/bot/bot.ts (mod) |
controller wiring | request-response | itself (registration list) | — |
bot/src/search/source.ts (SearchSource + GazetteerSource) |
service | request-response (in-memory query) | bot/src/kv/location-pin.ts (typed module + defensive validation) |
role-match |
bot/src/i18n/strings.ts (ext: search copy) |
config/i18n | transform | itself + web/src/i18n/strings.ts |
exact |
bot/src/config.ts (ext: possible MINI_APP_URL) |
config | — | itself (typed Env) |
— |
shared/ workspace pkg (package.json, src/index.ts) |
package config | — | theme/package.json (@araharah/theme) |
exact |
shared/src/types.ts |
model (types) | — | bot/src/kv/location-pin.ts interface block |
role-match |
shared/src/rules.ts (JUPEM table) |
config/data | transform | (data — no code analog; RESEARCH §"Malay normalizer" is the source) | none (data) |
shared/src/normalize.ts |
utility | transform (pure) | bot/src/security/initdata.ts shape (pure parse fn, typed in/out) |
partial |
shared/src/distance.ts (Damerau OSA) |
utility | transform (pure) | none — new algorithm; RESEARCH code example is the spec | none |
shared/src/score.ts (tiered scorer) |
utility | transform (pure) | none — new; RESEARCH Pattern 3 is the spec | none |
shared/data/kl-gazetteer.json |
data | — | theme/basemap/liberty-rams.json (committed JSON consumed cross-workspace) |
role-match |
shared/data/golden-corpus.json |
data (test fixture) | — | bot/test/fixtures/ (new dir) convention |
none (data) |
web/src/search/destination.ts |
service | request-response (id→entry) | web/src/session/store.ts (module-scope, memory-only) |
role-match |
web/src/main.ts (mod: dest preload) |
controller (boot) | event-driven boot | itself (staged boot; Phase 3) | — |
web/src/telegram/webapp.ts (ext: home-screen) |
middleware/adapter | — | itself (TelegramWebAppLike optional members) |
— |
web/src/pwa/homescreen.ts (new guard, placement at planner's discretion) |
hook | event-driven | web/src/telegram/backbutton.ts wire pattern |
role-match |
web/src/mock/telegram-mock.ts (ext) |
test harness | — | itself (MockOptions extension precedent) |
— |
web/index.html (mod: ?dest= profile) |
config (harness page) | — | itself (profile block, lines 20–35) | — |
web/scripts/capture.mjs (ext: dest capture) |
script | batch | itself (profile-driven shots) | — |
bot/scripts/gazetteer-topup.mjs |
script (repo tool) | file-I/O + network | bot/scripts/register-commands.mjs + theme/scripts/audit.mjs |
exact |
bot/scripts/gazetteer-corpus.mjs |
script (test runner) | batch | bot/test/runner.mjs + theme/scripts/audit.mjs (exit-code contract) |
exact |
bot/scripts/gazetteer-to-sqlite.mjs |
script | file-I/O transform | bot/scripts/register-commands.mjs |
role-match |
bot/test/*.test.mjs (8 new suites) |
test | — | bot/test/commands.test.mjs + bot/test/privacy.test.mjs (grep gates) |
exact |
web/test/destination.test.mjs, web/test/homescreen.test.mjs |
test | — | web/test/ suites (same harness shape as bot) |
exact |
bot/test/commands.test.mjs (ext), web/test/no-storage.test.mjs (ext), web/test/build-artifact.test.mjs (ext) |
test (gates) | — | themselves | — |
root package.json + bot/package.json + web/package.json (mod: workspace wiring) |
config | — | existing @araharah/theme wiring (web/package.json:15) |
exact |
LICENSE (GPL-3.0) + shared/data/NOTICE.md (ODbL) |
doc | — | none — repo has no LICENSE today (RESEARCH Finding 4) | none |
bot/wrangler.jsonc / bot/.dev.vars.example (mod: MINI_APP_URL if chosen) |
config | — | themselves (.dev.vars.example:1-5 comment style) |
— |
Pattern Assignments
bot/src/bot/commands/cari.ts (controller, request-response)
Analog: bot/src/bot/commands/start.ts (command registration + pure builder + strings-only copy) — with location-fallback.ts as the markup-exception documentation precedent, because /cari is the second deliberate exception to Phase 2's D-02 "plain text only" law (it sends sendVenue + inline keyboard).
File header + register pattern (bot/src/bot/commands/start.ts:14-32):
import type { Bot, Context } from "grammy";
import { pickLocale, strings } from "../../i18n/strings.ts";
import type { Locale } from "../../i18n/strings.ts";
/** Pure builder — the exact text the /start surface replies with. */
export function startReplyText(locale: Locale): string {
return strings[locale].startIntro;
}
async function handleStart(ctx: Context): Promise<void> {
const locale = pickLocale(ctx.from?.language_code);
await ctx.reply(startReplyText(locale), { link_preview_options: { is_disabled: true } });
}
/** Register the command stack entry: /start and its Malay alias /mula. */
export function registerStart(bot: Bot): void {
bot.command("start", handleStart);
bot.command("mula", handleStart);
}
Copy for /cari: registerCari(bot, source) registers bot.command("cari", ...) (+ its reserved alias if any), a pure exported builder for testability, all copy from strings.ts, query length-bound before any processing (RESEARCH §Security Domain V5: ≤128 chars).
The documented-exception pattern for the venue card's inline keyboard (bot/src/bot/handlers/location-fallback.ts:15-22 header — replicate this documentation style in cari.ts):
* D-02 structural note: this handler is the ONE deliberate exception
* to the Phase-2 "plain text only" command-surface rule. The GEO-02
* reply keyboard is required by the platform; it lives in handlers/
* and is NOT part of commandSources in commands.test.mjs's CS4 gate
⚠️ Consequence for the planner: commands.test.mjs:78-87 (CS4) greps commandSources for /keyboard/i and must FAIL unless the CS4 file list or the gate is amended when cari.ts ships. commands.test.mjs:150-163 (CS6) asserts the exact registered command set ["help","mula","privasi","privacy","start"] — adding /cari breaks this gate by design; extend the expected list. commands.test.mjs:141-145 explicitly greps register-commands for cari|pergi_ke stubs — that assertion must be updated when the command goes live.
Bot API call pattern via bot.api (not bot.chat) (bot/src/bot/handlers/location-fallback.ts:36-44):
export async function requestLocationFallback(bot: Bot, chatId: number, locale: Locale): Promise<void> {
await bot.api.sendMessage(chatId, strings[locale].awaitingPin, {
reply_markup: {
keyboard: [[{ text: strings[locale].shareLocationButton, request_location: true }]],
resize_keyboard: true,
one_time_keyboard: true,
},
});
}
sendVenue follows the same bot.api.* / ctx.replyWithVenue shape; RESEARCH §"Code Examples" gives the exact grammy InlineKeyboard().webApp(...).row().text(alt, \alt:${id}`)` payload.
bot/src/bot/handlers/search-alternate.ts (controller, event-driven)
Analog: bot/src/bot/handlers/location-fallback.ts — the register-with-dependency pattern (handler receives its dependency via createBot, not env).
Registration + dependency threading (bot/src/bot/handlers/location-fallback.ts:62-72, wired at bot/src/bot/bot.ts:35):
export function registerLocationFallback(bot: Bot, kv: LocationPinKv): void {
bot.on("message", async (ctx: Context) => {
const location = ctx.message?.location;
if (!location) return;
const chatId = ctx.chat?.id;
if (chatId === undefined) return;
await recordSharedPin(kv, chatId, location);
const locale = pickLocale(ctx.from?.language_code);
await ctx.reply(strings[locale].pinReceived, { link_preview_options: { is_disabled: true } });
});
}
Copy for the alternates handler: registerSearchAlternate(bot, source) — bot.callbackQuery(/^alt:(.+)$/, ...), regex-validate ctx.match[1] against /^[a-z0-9-]{1,64}$/ before the gazetteer lookup (V5), await ctx.answerCallbackQuery() on every path (grammY's 30-second rule), plus the catch-all bot.on("callback_query:data") that always answers. Note bot.ts threads env.chat_sessions positionally — registerSearchAlternate(bot, source) matches this shape; the SearchSource instance is constructed in createBot from the gazetteer import.
Wiring site (bot/src/bot/bot.ts:28-37) — where the new registrations slot in:
export function createBot(env: Env, opts: { baseFetch?: typeof fetch } = {}): Bot {
const bot = opts.baseFetch
? new Bot(env.BOT_TOKEN, { client: { fetch: opts.baseFetch } })
: new Bot(env.BOT_TOKEN);
registerStart(bot); // /start + /mula (D-04)
registerHelp(bot); // /help
registerPrivacy(bot); // /privacy + /privasi (D-04)
registerLocationFallback(bot, env.chat_sessions); // GEO-02 location reply fallback
return bot;
}
bot/src/search/source.ts (service, in-memory query) + shared/src/types.ts
Analog: bot/src/kv/location-pin.ts — the repo's pattern for a typed data module: narrow exported interface, a named constant, "the entire surface" documentation, and defensive validation on read (a corrupt record can never become a coordinate).
Typed record + named constant + full-surface discipline (bot/src/kv/location-pin.ts:25-32):
/** GEO-03: a shared pin ephemerally relays chat → Mini App (which polls
* GET /api/location); 300 s bounds the exposure, then it is gone. */
export const PIN_TTL_SECONDS = 300;
/** The entire persisted pin surface — a 4th property means drift. */
export interface LocationPin {
latitude: number;
longitude: number;
horizontal_accuracy: number | null;
}
GazetteerEntry (id, name, aliases, lat, lng, category, address?) copies this discipline: the interface comment names D-07/D-08 and states that a field beyond the list means schema drift.
Defensive read — the pattern the gazetteer loader and byId() should mirror (bot/src/kv/location-pin.ts:52-73):
export async function getPin(kv: LocationPinKv, chatId: number): Promise<LocationPin | null> {
const raw = await kv.get(`loc:${chatId}`);
if (raw === null) return null;
try {
const parsed: unknown = JSON.parse(raw);
if (
parsed &&
typeof parsed === "object" &&
"latitude" in parsed &&
...
Math.abs((parsed as LocationPin).latitude) <= 90 &&
Math.abs((parsed as LocationPin).longitude) <= 180
) {
return parsed as LocationPin;
}
return null;
} catch {
return null;
}
}
GazetteerSource.search() / byId() return null/[] on any miss — never a best-guess entry (D-13's honest-refusal contract).
Env contract extension point (bot/src/config.ts:4-12): if the plan adds MINI_APP_URL (RESEARCH A6 — public origin, belongs in wrangler.jsonc vars, not a secret), it extends this same typed Env interface with the same per-line comment style.
shared/ workspace package (NEW — cross-workspace module)
Analog: the theme package — the repo's one proven example of a scoped workspace consumed by another workspace, including a JSON subpath import, which is exactly the gazetteer's consumption shape.
Package identity (theme/package.json:1-3):
{
"name": "@araharah/theme",
"type": "module",
Dependency declaration (web/package.json:13-16):
"dependencies": {
"maplibre-gl": "^6.15.0",
"@araharah/theme": "*"
},
Cross-workspace JSON import — the exact gazetteer pattern (web/src/map/map.ts:20):
import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";
shared/package.json copies the @araharah/ scope + "type": "module"; both bot/package.json and web/package.json gain "@araharah/search": "*"; root package.json:5-9 workspaces array gains "shared". Note: theme has no exports map — subpath imports resolve by file path. shared/ should do the same (no build step, source-imported), OR add an exports map if the planner wants path stability — theme's no-exports shape is the precedent.
Import-extension convention (load-bearing for the shared module): bot and web both import TS with explicit .ts extensions (bot/tsconfig.json:10-11 — verbatimModuleSyntax: true, allowImportingTsExtensions: true; every import in bot/src/webhook.ts and web/src/main.ts ends in .ts). The shared module's own internal imports must use .ts extensions too, and import type for type-only imports (verbatimModuleSyntax enforces it).
Node-24 type-stripping is proven in-repo — .mjs scripts already import .ts directly (bot/test/commands.test.mjs:27, bot/scripts/register-commands.mjs:25):
const { strings, pickLocale } = await import("../src/i18n/strings.ts");
So the corpus runner can await import("@araharah/search/src/index.ts") under Node 24 (research A5 verified by existing passing suites; still assert it in the plan's first task).
shared/src/normalize.ts, shared/src/score.ts, shared/src/distance.ts (utility, pure transform)
No codebase analog — the repo has no pure-computation module yet; the closest style analog is bot/src/security/initdata.ts (pure function, typed input/output, zero IO). RESEARCH is the spec: §"Pattern 2" (rule table + inversion at module load), §"Pattern 3" (disjoint tier bands), §"Code Examples" (full OSA implementation + normalizer + glued-word splitter). Repo conventions to carry in: the file-header comment block documenting decision IDs (every bot/web file opens with a /* name.ts — purpose (D-xx). --- */ banner — see location-pin.ts:1-21), and named exported constants over inline literals (the PIN_TTL_SECONDS / PG1 "named TTL constant" rule at bot/test/privacy.test.mjs:41-45 shows literals are gated against).
web/src/search/destination.ts + web/src/main.ts dest preload (controller, boot)
Analog: web/src/session/store.ts (module-scope in-memory state — the destination lives the same way) + the boot sequence in main.ts.
Memory-only module state — the exact persistence posture for the destination (web/src/session/store.ts:18-26):
export const SESSION_TTL_SECONDS = 900;
let token: string | null = null;
let issuedAtSeconds: number | null = null;
export function setSession(nextToken: string, issuedAtSec: number = Math.floor(Date.now() / 1000)): void {
token = nextToken;
issuedAtSeconds = issuedAtSec;
}
The destination id/entry is in-memory module scope "alongside lastFix, exactly like the existing in-memory-only pattern" (RESEARCH §Security Domain V3). No storage call anywhere — web/test/no-storage.test.mjs greps every web/src file with comments stripped.
Boot-order discipline (web/src/main.ts:55-94): safe-area → Telegram bindings → map mounts FIRST → chrome → locale from RAW initData (main.ts:94: const locale: Locale = wa ? localeFromInitData(wa.initData) : "en";). The destination read slots in beside the locale read — read location.search + initData.start_param once at boot, resolve through the web's gazetteer copy, hold the entry in module scope, null-guard everything (the map !== null guard pattern at main.ts:218-221 is the model for "unknown id → honest unavailable state, never a blank map").
Payload readers — RESEARCH §"Code Examples" gives the two readers (readDestinationId() from location.search, readStartParam(wa) from initData), both validated /^[A-Za-z0-9_-]{1,64}$/ before lookup. initDataUnsafe is untrusted (Telegram's own docs) — safe here only because the value is re-resolved against the local gazetteer, never used as identity/URL/path.
web/src/telegram/webapp.ts extension (home-screen capability, ONBD-03)
Analog: itself — the hand-rolled contract's optional-member pattern. New capabilities arrive as optional methods so existing fakes stay valid.
The optional-member precedent to copy (web/src/telegram/webapp.ts:26-37):
isVersionAtLeast(version: string): boolean;
close(): void;
onEvent?(event: string, cb: () => void): void;
offEvent?(event: string, cb: () => void): void;
BackButton?: { show(): void; hide(): void; onClick(cb: () => void): void; offClick(cb: () => void): void };
LocationManager?: {
...
openSettings?(): void;
};
Extend with addToHomeScreen?(): void and checkHomeScreenStatus?(cb: ...): void + the HomeScreenStatus union — exactly RESEARCH's §"ONBD-03 feasibility" snippet. The capability guard combines method presence + wa.isVersionAtLeast("8.0") (the mock already implements versionAtLeast — web/src/mock/telegram-mock.ts:112-122). Do NOT re-add @types/telegram-web-app (deliberately dropped, per the file header at webapp.ts:4-9).
web/src/mock/telegram-mock.ts extension + web/index.html ?dest= profile (test harness)
Analog: themselves — the MockOptions extension pattern and the URLSearchParams profile block.
MockOptions extension precedent (web/src/mock/telegram-mock.ts:29-43 — every knob is documented with its consumer):
/** Telegram language_code carried in the fake initData's user JSON —
* lets main.ts's localeFromInitData exercise its real parsing path
* and lets the runner page switch locales via ?locale=. */
languageCode?: string;
/** Artificial delivery delay (ms) for LocationManager.init()/getLocation()
* callbacks — the runner page's ?fixdelay= knob. ... */
deliveryDelayMs?: number;
Add startParam?: string (appended into buildMockInitData — currently web/src/mock/telegram-mock.ts:136-140 emits only user/auth_date/query_id/hash, the exact gap RESEARCH flags) and homeScreenStatus?: "unsupported"|"unknown"|"added"|"missed" so the ONBD-03 branches are testable without a device.
Runner-page profile pattern (web/index.html:21-35):
const q = new URLSearchParams(location.search);
const profile = q.get("mock") || "android";
const scheme = q.get("scheme") === "dark" ? "dark" : "light";
const locale = q.get("locale") === "ms" ? "ms" : "en";
...
// Set BEFORE main.ts is imported — this is the marker main.ts's install guard checks.
window.__ARAHARAH_MOCK__ = { profile, scheme, locale };
The ?dest=<id> knob follows this: q.get("dest") inside the same if (__ARAHARAH_HARNESS__) block, plumbed into installMockWebApp({...}) at web/index.html:63-70. The capture harness (web/scripts/capture.mjs:79-116) already builds the harness bundle and asserts installer presence/absence both directions — add a dest-profile shot the same way the existing profile shots are declared in its header (capture.mjs:22-29).
bot/scripts/gazetteer-topup.mjs, gazetteer-corpus.mjs, gazetteer-to-sqlite.mjs (repo tooling)
Analog 1: bot/scripts/register-commands.mjs — bot-side repo script: shebang, decision-ID header, .dev.vars reading, exit(1) on failure.
Script config-reading pattern (bot/scripts/register-commands.mjs:29-54):
function readDevVars() {
const devVarsPath = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..", ".dev.vars");
let raw = "";
try {
raw = readFileSync(devVarsPath, "utf8");
} catch {
return {};
}
...
}
const token = process.env.BOT_TOKEN ?? readDevVars().BOT_TOKEN;
if (!token) {
console.error("BOT_TOKEN missing — copy bot/.dev.vars.example to bot/.dev.vars ...");
process.exit(1);
}
Analog 2: theme/scripts/audit.mjs — the gate-script contract: CLI flags with defaults + documented exit codes, no silent passes (theme/scripts/audit.mjs:11-23):
* Usage:
* node scripts/audit.mjs [--tokens <tokens.css>] [--pages <a.html,b.html>] [css...]
* Exit codes (no silent passes — T-1-07):
* 0 clean — every checked declaration derives from tokens
* 1 violations — each printed as file:line: declaration
* 2 drift/error — token sheet degraded (see message)
The corpus runner copies this contract verbatim: usage header, exit 0 / exit 1 (top-3 < 90% or any negative leaked), per-case MISSED/LEAKED reporting (RESEARCH §"The corpus runner"). The top-up script gets --source=overpass|nominatim flags, reads its Overpass fixture from bot/test/fixtures/overpass-response.json in test mode, and must never be imported by bot/src/ (grep gate — Pitfall 6). Repo scripts import TS via Node-24 type stripping (register-commands.mjs:25 proves the pattern).
bot/test/*.test.mjs (8 new suites) + web test suites
Analog 1: the harness pair — bot/test/harness.mjs:6-10 (registry) and bot/test/runner.mjs:15-30 (auto-discovery; a suite that fails to load becomes a failing case, so new suites can never crash the run silently). New suites are drop-in files; no config.
Analog 2 — source-introspection gate style (bot/test/commands.test.mjs:27-32):
const { strings, pickLocale } = await import("../src/i18n/strings.ts");
const BOT_SRC = path.join(fileURLToPath(new URL(import.meta.url)), "..", "..", "src", "bot");
const commandSources = ["bot.ts", "commands/start.ts", ...].map((rel) =>
path.join(BOT_SRC, ...rel.split("/")),
);
…with regex assertions over the read source (CS4 at lines 78–87, CS6 registration-set scan at 150–163). New structural gates copy this: the D-15 "no query in any log line" grep gate, the "no overpass URL under bot/src" gate, the corpus-tautology check, the id-set parity check (reads both workspace JSON copies).
Analog 3 — the storage grep gate (bot/test/privacy.test.mjs:32-49, PG1): per-line scanning with file:line failure messages, and PG2's forbidden-identifier regex. The D-15 query-leak gate is this exact style applied to log statements. Also note PG1's named-constant rule (line 41–45) — a convention new code must keep.
Shared Patterns
Category-only logging (D-15 — binding)
Source: bot/src/webhook.ts:66, 112, 122, 150, 181, 196
console.log("[webhook] rejected: misconfigured");
console.log(`[initdata] rejected: ${result.reason}`); // D-14: category ONLY — no payload, no ids, no timestamps
console.log("[location-fallback] failed: telegram-send");
Apply to: every log line in cari.ts, search-alternate.ts, source.ts. Shape: [search] rejected: no-match. Never the query text, never results, never user ids. Enforced by a new grep gate in the privacy.test.mjs style.
Strings-module-only copy + en/ms parity gate
Source: bot/src/i18n/strings.ts:21-42 (typed BotStrings interface with per-field decision-ID doc comments; Record<Locale, BotStrings> so a missing ms key is a type error) and the CS1 gate (bot/test/commands.test.mjs:36-41). Web mirror: web/src/i18n/strings.ts:16-47.
Apply to: cari.ts, search-alternate.ts, the web destination-unavailable state, the ONBD-03 affordance. Extend BotStrings/ShellStrings; extend CS1 expectations; "not in v1 area" copy lives in strings, never inline.
register*(bot, deps) factory + env-threaded dependencies
Source: bot/src/bot/bot.ts:32-35, location-fallback.ts:62
Apply to: registerCari(bot, source), registerSearchAlternate(bot, source). Dependencies are constructed in createBot and passed positionally; handlers never import env or singletons.
Explicit .ts import extensions + import type under verbatimModuleSyntax
Source: bot/tsconfig.json:10-11; every import in bot/src/webhook.ts, web/src/main.ts
Apply to: all new TS files in bot/src, web/src, and inside shared/src (internal imports too).
Cross-workspace consumption via @araharah/* + committed JSON subpath import
Source: web/package.json:15, web/src/map/map.ts:20, theme/package.json:1-3, root package.json:5-9
Apply to: shared/ package creation and both consumers' package.json edits. Both the Worker (wrangler/esbuild bundles the import) and Vite inline the JSON at build time — no fs anywhere under bot/src/web/src (Workers have no fs; the anti-pattern list is explicit).
Memory-only client state, never storage
Source: web/src/session/store.ts:1-17 (header documents the invariant), enforced by web/test/no-storage.test.mjs
Apply to: the preloaded destination in the webview — module scope, no localStorage/sessionStorage/cookie/IndexedDB/caches. Extend the no-storage gate for the new web/src/search/ + web/src/pwa/ files.
Source-introspection grep gates for structural claims
Source: bot/test/privacy.test.mjs (PG1/PG2), bot/test/commands.test.mjs (CS4/CS5/CS6)
Apply to: D-15 log gate, no-Overpass-in-Worker gate, callback_data ≤64 bytes + alt:-only gate, corpus-tautology gate, LICENSE/ODbL-notice presence gate, id-set parity gate.
Repo-script contract: header, flags, exit codes, no silent passes
Source: theme/scripts/audit.mjs:11-23, bot/scripts/register-commands.mjs:29-54
Apply to: gazetteer-topup.mjs, gazetteer-corpus.mjs, gazetteer-to-sqlite.mjs. Corpus runner exit 1 = D-14 gate failure; wire as npm run search:corpus (root script delegating with -w bot style used at package.json:18-28).
Harness profile knobs: URLSearchParams → MockOptions → module global
Source: web/index.html:20-35, web/src/mock/telegram-mock.ts:25-43, 136-140
Apply to: ?dest=<id> (ONBD-02 capture evidence), startParam (Carrier B), homeScreenStatus (ONBD-03 branches). Everything inside if (__ARAHARAH_HARNESS__) so production dead-code-eliminates it (the BA5/T-03-21 discipline documented at web/index.html:14-19 and asserted in capture.mjs:99-115).
No Analog Found
| File | Role | Data Flow | Reason |
|---|---|---|---|
LICENSE (GPL-3.0) + shared/data/NOTICE.md (ODbL) |
doc | — | Repo has no LICENSE today (RESEARCH Finding 4). Verbatim GPL-3.0 text + OSMF-style attribution; content is prescribed, not patterned. |
shared/src/distance.ts, shared/src/score.ts, shared/src/rules.ts |
utility (pure) | transform | No computation modules exist in the repo. RESEARCH §"Malay normalizer" + §"Pattern 3" + §"Code Examples" carry the implementations verbatim — the planner should lift them as the spec, adding only the repo's file-header/decision-ID conventions. |
shared/data/kl-gazetteer.json, golden-corpus.json |
data | — | Curated content (D-06/D-08/D-13) — hand-authored, never generated; schema shape comes from RESEARCH Pattern 1. |
Metadata
Analog search scope: bot/src/**, bot/test/**, bot/scripts/**, web/src/**, web/scripts/capture.mjs, web/index.html, web/package.json, theme/scripts/**, theme/package.json, root package.json, bot/tsconfig.json, bot/wrangler.jsonc, bot/.dev.vars.example
Files read in full or in part: 22
Key cross-cutting findings for the planner:
/caribreaks three existing gates by design — CS4 (no-keyboard grep), CS5 (nocaristubs in register-commands), CS6 (exact command set). Plan must amendbot/test/commands.test.mjsin the same task that adds the command.- The shared-module consumption shape is proven end-to-end by
@araharah/theme: scoped workspace name,"*"dependency, no exports map, committed-JSON subpath import, source-imported TS via Node 24 type stripping (proven bycommands.test.mjs:27andregister-commands.mjs:25). - The
web_appinline button on the venue card is the second exception to the D-02 text-only law —location-fallback.tsis the documentation precedent (header note + exclusion from CS4's file list). Pattern extraction date: 2026-10-23