- golden-corpus.json: 75 hand-written pinned cases (D-13) — landmark 17 /
abbreviation 15 / glued 7 / typo 14 / alias 15 / negative 7, incl. two
near-miss negatives (Penang 0.625, Kampung Cina ~0.58) below the 0.72
admission line; threshold.top3_hit_rate pinned at 0.9
- gazetteer-corpus.mjs: the D-14 runner — imports the SAME src/index.ts the
bot uses, measures top-1/top-3 + per-category rates + avg ms/query, prints
the refusal margin and the fixed escalation ladder on failure, exports
measureCorpus() for the in-process suite, exits 0 only on threshold + zero leaks
- corpus.test.mjs goes green: CO1-CO8 (the Nyquist gate inside npm run search:test)
- delegates: search:corpus (root) + corpus (search workspace)
Measured: top-3 68/68 (100.0%) >= 90%, negatives refused 7/7, margin 0.643 < 0.72 —
threshold met on the first run (D-14 lever: None).
- CO1 top-3 >= corpus threshold (D-14), measured in-process via the runner's measureCorpus
- CO2 zero negative leaks (honest refusal, D-13)
- CO3 tautology floor: >= 15 non-negative queries not string-equal to any alias/name
- CO4/CO5 every expectId exists in the gazetteer + start_param-safe charset
- CO6 size >= 60 + per-category floors (landmark 12 / abbreviation 15 / glued 6 / typo 12 / alias 12 / negative 6)
- CO7 per-category rates printed so suite and runner output agree
- CO8 refusal margin pinned: every negative's best two-level fuzzy sim < 0.72, one near-miss in [0.55, 0.72)
All 8 cases verified failing (ENOENT corpus fixture) before GREEN.
- kl-gazetteer.json: 30 hand-curated KL-metro landmarks (10 required + 20
more) with locals' aliases as native data (D-08) — canonical OSM names
as name, shorthand ('KLCC', 'DUKE', 'Sg Wang') as aliases — plus the
ODbL-1.0 attribution header traveling inside the JSON (D-06/D-07)
- gazetteer.ts: the three laws — lazy memoized index never at module
scope (Workers startup, T-04-04), static JSON import surviving
wrangler/Vite/bare-Node (D-07), zero logging/storage (D-15); aliases
normalized once at index-build (D-09 symmetry)
- source.ts: the D-03 SearchSource — async search() + synchronous byId()
(slug-validated, honest null); tiered admission (fuzzy only at sim
>= 0.72) so 'Kuching'/'Penang' refuse as [] — never a fabricated pin
- index.ts: the named-export public API (no default, subpath+extension
imports per the @araharah/theme precedent)
- LICENSE: the verbatim GPL-3.0 text (D-04); LICENSES.md: the dual-licence
note (code GPL-3.0, data ODbL-1.0 + OSM attribution); README: corrected
licence section + search/ in the layout, four-package table, commands
G1-G11 + L1-L3 green (39/39); typecheck clean; esbuild bundle carries the
suria-klcc marker; wrangler dry-run exits 0; bot + web suites unaffected.
- G1-G6: dataset invariants — schema, slug-id uniqueness, canonical-name
law ('KLCC' is an alias, never a name), DUKE-as-alias, KL bbox, the
ODbL header traveling inside the JSON
- G7-G9: the licensing gate — verbatim GPL-3.0 LICENSE, LICENSES.md,
the README no longer claiming 'all rights reserved'
- G10-G11: no logging/storage anywhere in the module (D-15); the loader
builds its index inside a function, never at module top level
- L1-L3: the phase's top mechanical risk — bare-Node loading with the
SRCH-01/negative probes, the static JSON import law, esbuild INLINING
the dataset + the wrangler dry-run build guard
13 cases fail while the dataset, loader, LICENSE files, and README fix
are absent — the RED gate.
- distance.ts: hand-rolled 3-band OSA, adjacent transposition = 1 edit,
zero imports (D-07); the row rotation is pinned by the D1-D9 vectors
- score.ts: TIER bands strictly disjoint descending — EXACT 1.00 /
ALIAS_EXACT 0.90 / PREFIX 0.75 / TOKEN_SET 0.65 / FUZZY as the 0.60
CEILING (a fuzzy score is fuzzySim * 0.60, never a 0.00 multiplier);
FUZZY_ADMIT_SIMILARITY = 0.72 exported for source.ts + the corpus
runner; token-level residual lets 'Sentrl' reach 'KL Sentral' while
'Jln Tun Razak' cannot reach 'Jalan Ampang'
- scoreCandidate returns the RAW matched alias (corpus provenance);
an exact ALIAS hit rides ALIAS_EXACT so a name-exact outranks it
- score.test.mjs S7 corrected during GREEN: admission belongs to
source.ts — scoreCandidate asserts the sub-floor similarity instead
D1-D9 + S1-S7 green; typecheck clean.
Phase 4 research: in-Worker SearchSource over a curated KL gazetteer.
Two critical platform findings:
- Telegram start_param is NEVER delivered to inline web_app buttons
(direct links + attachment menu only). The destination id must ride
as a URL query param on the button's URL; D-11's substance preserved.
- beforeinstallprompt never fires in a WebView. ONBD-03 must use
Telegram's native WebApp.addToHomeScreen(); iOS status is always
"unknown" by platform design.
Benchmarked the CPU budget: naive full-corpus Damerau is 52ms at
D-07's max scale vs the 10ms Workers Free limit. Exact-hash-first +
token-prefiltered residual is 0.0003ms.
Also: Phase 2 CS3/CS4/CS5 gates need declared amendments; ODbL
attribution obligation for the OSM-derived gazetteer.
- theme.ts: colorScheme → data-tg-theme on <html>; header_bg_color 0-1 luminance fallback
(T-03-11 — Telegram hex is a signal, never a consumed CSS value); applyTheme's ONLY
statement is the attribute set (Pitfall 7 / D-10, greppable invariant)
- backbutton.ts: hide() at init (one shell state), onClick is total — close() at the root,
no dead-ends (UI-SPEC §BackButton)
- safe-area.ts: env(safe-area-inset-*) probe ∪ WebApp.safeAreaInset, per-edge max,
version-gated at 8.0, applied as --safe-* custom properties on :root
- webapp.ts + backbutton/safe-area never read node_modules types;
@types/telegram-web-app stays dropped (web/package.json untouched)