26 KiB
arah² — Architecture
System Overview
arah² is a Telegram-first GPS navigator: a grammY bot (arah2bot) running as a single
Cloudflare Worker serves both the Telegram webhook and the static Mini App bundle
served from the same origin. The Mini App (@araharah/web, a Vite + MapLibre GL
application) authenticates with the Worker by posting its verbatim
Telegram.WebApp.initData to POST /api/session, receives a short-lived opaque bearer
token, and then acquires a position through a three-link chain (Telegram
LocationManager → WebView navigator.geolocation → chat-relayed pin). All server state
lives in a TTL-only Cloudflare KV namespace, so the system has no database, no persistent
storage, and no analytics. @araharah/theme is the shared design-token package: it
compiles Tailwind v4 CSS consumed by the web app and is the derivation source for the
Rams basemap style the map renders.
Architecturally this is a three-workspace npm monorepo in a layered/hexagonal arrangement: a stateless edge worker with a KV-backed repository layer, a framework-free TypeScript client with its own repository + adapter layers, and a pure build-time token/cartography package that both consume.
graph TD
classDef client fill:#EB8008,stroke:#8A4C00,stroke-width:2px,color:#1A1A1A
classDef worker fill:#D9D2C6,stroke:#736B1E,stroke-width:2px,color:#1A1A1A
classDef store fill:#ED3F1C,stroke:#8C1F0A,stroke-width:2px,color:#FFFFFF
classDef build fill:#1A1A1A,stroke:#000000,stroke-width:2px,color:#FFF8F0
A["Telegram client<br/>chat + Mini App webview"]:::client
B["@araharah/bot Worker<br/>src/webhook.ts"]:::worker
C["grammY handlers<br/>commands + location-fallback"]:::worker
D["Security<br/>initdata.ts HMAC"]:::worker
E["KV chat_sessions<br/>TTL-only"]:::store
F["@araharah/web Mini App<br/>src/main.ts"]:::client
G["Session client + store<br/>src/session/"]:::client
H["Location chain<br/>src/location/"]:::client
I["MapLibre map + UI<br/>src/map/, src/ui/"]:::client
J["@araharah/theme<br/>dist/rams.css + basemap"]:::build
A -->|"POST /telegram/webhook"| B
A -->|"loads Mini App"| B
B --> C
B --> D
B --> E
C --> E
C -->|"sendMessage reply keyboard"| A
F -->|"POST /api/session initData"| B
F -->|"GET /api/location Bearer"| B
F -->|"POST /api/location-fallback"| B
G --> F
H --> G
F --> H
H --> I
J -->|"compiled CSS + inlined style JSON"| F
Component Diagram
graph LR
classDef ws fill:#EB8008,stroke:#8A4C00,stroke-width:2px,color:#1A1A1A
classDef wsfill fill:#FFF8F0,stroke:#EB8008,stroke-width:2px,color:#1A1A1A
classDef botfill fill:#F5EDE0,stroke:#736B1E,stroke-width:2px,color:#1A1A1A
classDef store fill:#ED3F1C,stroke:#8C1F0A,stroke-width:2px,color:#FFFFFF
classDef ext fill:#E0D9CD,stroke:#564335,stroke-width:2px,color:#1A1A1A
subgraph ROOT["root — npm workspaces"]
direction TB
subgraph THEME["theme · @araharah/theme"]
direction TB
T1["src/tokens.css<br/>@theme static, 55 colors"]:::wsfill
T2["src/components.css"]:::wsfill
T3["basemap/build.mjs"]:::wsfill
T4["dist/rams.css"]:::wsfill
T5["basemap/liberty-rams.json"]:::wsfill
T1 --> T4
T2 --> T4
T1 -->|"derives roles"| T3
T3 --> T5
end
subgraph WEB["web · @araharah/web"]
direction TB
W1["src/main.ts"]:::wsfill
W2["src/session/"]:::wsfill
W3["src/location/"]:::wsfill
W4["src/map/"]:::wsfill
W5["src/ui/ + src/telegram/"]:::wsfill
W6["src/shell.css"]:::wsfill
W7["dist/ (served)"]:::wsfill
W1 --> W2
W1 --> W3
W1 --> W4
W1 --> W5
W1 --> W7
W4 --> W7
W5 --> W7
end
subgraph BOT["bot · @araharah/bot"]
direction TB
B1["src/webhook.ts"]:::botfill
B2["src/security/initdata.ts"]:::botfill
B3["src/bot/"]:::botfill
B4["src/kv/"]:::botfill
B5["src/config.ts Env"]:::botfill
B1 --> B2
B1 --> B3
B1 --> B4
B1 --> B5
end
KV[("KV namespace chat_sessions<br/>session:* 900s · chat:* · loc:* 300s")]:::store
TG["Telegram Bot API"]:::ext
OFM["OpenFreeMap tiles"]:::ext
end
WEB -->|"workspace dep @araharah/theme"| THEME
BOT -->|"assets binding ../web/dist"| WEB
B1 --> KV
B4 --> KV
B3 <-->|"webhookCallback / sendMessage"| TG
W3 <-->|"chat pin"| TG
W4 --> OFM
Workspaces Dependency Graph
graph TD
classDef pkg fill:#EB8008,stroke:#8A4C00,stroke-width:2px,color:#1A1A1A
classDef dep fill:#736B1E,stroke:#4A440F,stroke-width:2px,color:#FFF8F0
classDef out fill:#D9D2C6,stroke:#736B1E,stroke-width:2px,color:#1A1A1A
ROOT["araharah (private root)"]:::out
TH["@araharah/theme"]:::pkg
BT["@araharah/bot"]:::pkg
WB["@araharah/web"]:::pkg
KVS[("KV chat_sessions")]:::dep
TGAPI["Telegram Bot API"]:::dep
ROOT -->|"workspaces"| TH
ROOT -->|"workspaces"| BT
ROOT -->|"workspaces"| WB
WB -->|"dependencies: @araharah/theme *"| TH
BT -->|"static assets ../web/dist"| WB
BT --> KVS
BT --> TGAPI
WB -->|"api routes"| BT
@araharah/web declares "@araharah/theme": "*" — the only workspace-to-workspace
runtime import in the repo (@araharah/theme/dist/rams.css in web/src/shell.css,
@araharah/theme/basemap/liberty-rams.json in web/src/map/map.ts). The bot has no
workspace dependency; its coupling to web is a build-order coupling only — the
root build script builds theme then web, and the Worker's [assets] binding points
at ../web/dist.
Data Flow
1. Telegram command surface (bot → chat)
Telegram delivers an update to POST /telegram/webhook. The Worker first fails closed
if WEBHOOK_SECRET is unset, then hands the request to grammY's
webhookCallback(bot, "cloudflare-mod", …), which constant-time-compares the
X-Telegram-Bot-Api-Secret-Token header. createBot(env) is instantiated per
request (the token only arrives with env), registers the command stack, and grammY
dispatches:
POST /telegram/webhook → webhookCallback (secret check) → createBot(env)
→ registerStart / registerHelp / registerPrivacy → pickLocale(language_code)
→ ctx.reply(plain text, link_previews disabled)
All user-facing copy comes from bot/src/i18n/strings.ts; bot/scripts/register-commands.mjs
derives the persistent /setMyCommands menu descriptions from the same helpMenu lines,
so menu and reply cannot drift.
2. Mini App boot (session handshake)
sequenceDiagram
participant WA as WebApp webview
participant MAIN as web/src/main.ts
participant SESS as session/client.ts
participant WK as Worker /api/session
participant ID as security/initdata.ts
participant KV as KV chat_sessions
MAIN->>MAIN: applySafeArea(resolveSafeInsets(wa))
MAIN->>MAIN: bootMap() mounts MapLibre FIRST
MAIN->>MAIN: register strip, cards, recenter
MAIN->>SESS: createSession(wa.initData) [verbatim string]
SESS->>WK: POST /api/session, body = initData (≤ 4 KB)
WK->>ID: validateInitData(raw, BOT_TOKEN)
ID-->>WK: ok | malformed | tampered | stale
WK->>KV: mintSession(chat_sessions, claim)
KV-->>WK: opaque UUID token
WK-->>SESS: 200 { token }
SESS->>SESS: setSession(token) [module scope only]
The HMAC path: dataCheckString = all initData fields minus hash only, sorted by
field name, joined with \n; secret key = HMAC-SHA256(key="WebAppData", message=botToken); check = HMAC-SHA256(key=secretKey, message=dataCheckString)
compared in constant time against hash. signature participates in the check.
auth_date older than 900 s is rejected as stale.
3. Location acquisition chain
flowchart TD
classDef s fill:#EB8008,stroke:#8A4C00,stroke-width:2px,color:#1A1A1A
classDef ok fill:#F5EDE0,stroke:#736B1E,stroke-width:2px,color:#1A1A1A
classDef no fill:#ED3F1C,stroke:#8C1F0A,stroke-width:2px,color:#FFFFFF
START["acquireFix(deps, lastAccepted)"]:::ok
L1{"telegramLocationSupported<br/>isVersionAtLeast 8.0"}:::ok
NUDGE{"looksStale(prev, fix)<br/>haversine < 0.01 m"}:::ok
L2{"webviewLocationSupported"}:::ok
L3{"getToken() !== null"}:::ok
LIVE["return { fix, state: live }"]:::s
DEN1["denied / timeout"]:::no
DEN2["denied / no-permission"]:::no
START --> L1
L1 -->|"yes"| NUDGE
NUDGE -->|"no"| LIVE
NUDGE -->|"yes"||"discarded webview read"| L1
L1 -->|"no"| L2
L2 -->|"yes + fix"| LIVE
L2 -->|"no fix"| L3
L3 -->|"yes: bounded 60s poll"| LIVE
L3 -->|"poll expired"| DEN1
L3 -->|"no token"| DEN2
Link 1 (telegram.ts) calls LocationManager.init() then getLocation(), with a hard
8 s wrapper timeout because the API documents no timeout. A byte-identical fix to the
last accepted one routes to the nudge: one discarded webview read refreshes the
Android native cache, then telegram is retried. Link 2 (webview.ts) calls
navigator.geolocation.getCurrentPosition with maximumAge: 0. Link 3
(chat-pin.ts) polls GET /api/location every 2 s for at most 60 s.
4. Chat-pin relay (the Desktop-Telegram path)
sequenceDiagram
participant U as User (chat)
participant B as Bot handler
participant KV as KV loc:chat_id
participant MA as Mini App
participant W as Worker
MA->>W: POST /api/location-fallback (Bearer token)
W->>W: getClaimByToken → claim.chat_id
W->>B: requestLocationFallback(bot, chat_id, locale)
B->>U: reply keyboard, request_location: true
U->>B: taps button, native location picker
B->>KV: writePin(kv, chat_id, { lat, lng, acc }) expirationTtl 300
loop every 2 s, bounded 60 s
MA->>W: GET /api/location (Bearer token)
W->>KV: getPin(chat_sessions, claim.chat_id)
KV-->>W: LocationPin | null
W-->>MA: { pin } (200) or { pin: null }
end
Both /api/location and /api/location-fallback derive the target chat_id from the
resolved session claim, never from client input, and derive locale from
claim.language_code. Telegram has no push channel from chat to webview, so this poll
is server-mediated by necessity.
5. Worker routing table
| Method | Path | Handler in bot/src/webhook.ts |
Purpose |
|---|---|---|---|
| POST | /telegram/webhook |
inline in fetch() |
grammY webhook adapter + secret-token compare |
| POST | /api/session |
handleSession |
initData → one-time opaque session token |
| GET | /api/location |
handleLocation |
Bearer-authenticated chat-pin relay |
| POST | /api/location-fallback |
handleLocationFallback |
Ask the bot to send the reply keyboard |
| * | anything else | — | 404 |
run_worker_first: ["/api/*", "/telegram/*"] guarantees the Worker's fetch owns these
paths before the static asset fallback runs; every other path resolves to the web/dist
bundle.
Build & Deploy Topology
graph LR
classDef step fill:#1A1A1A,stroke:#000000,stroke-width:2px,color:#FFF8F0
classDef art fill:#D9D2C6,stroke:#736B1E,stroke-width:2px,color:#1A1A1A
classDef cloud fill:#EB8008,stroke:#8A4C00,stroke-width:2px,color:#1A1A1A
classDef store fill:#ED3F1C,stroke:#8C1F0A,stroke-width:2px,color:#FFFFFF
T1["theme: tailwindcss -i src/theme.css -o dist/rams.css --minify"]:::step
T2["theme: basemap/build.mjs<br/>fetch liberty → derive roles → liberty-rams.json"]:::step
W1["web: vite build → dist/"]:::step
W2["web: vite build --mode harness → dist-harness/"]:::step
B1["bot: wrangler deploy"]:::step
B2["bot: scripts/set-webhook.mjs"]:::step
AR1["theme/dist/rams.css + fonts"]:::art
AR2["web/dist (index.html, hashed assets,<br/>inlined liberty-rams style)"]:::art
AR3["web/dist-harness (never deployed)"]:::art
EDGE["Cloudflare Worker araharah-bot<br/>[assets] ../web/dist"]:::cloud
KVS[("KV chat_sessions")]:::store
T1 --> W1
T2 --> W1
T1 --> AR1
W1 --> AR2
W2 --> AR3
AR2 --> EDGE
B1 --> EDGE
B1 --> B2
EDGE --> KVS
The root scripts encode the ordering: npm run build = build -w theme && build -w web;
npm run deploy = npm run build && npm run deploy -w bot (which is
wrangler deploy && node scripts/set-webhook.mjs). bot/wrangler.jsonc binds
assets.directory to ../web/dist, so web/dist must exist before a bot deploy.
The two web builds never share an outDir: vite.config.ts sets
outDir: "dist-harness" only under --mode harness and defines
__ARAHARAH_HARNESS__ to true there and false everywhere else, so the mock
installer and the fetch stubs in web/index.html are dead-code-eliminated from
production. web/test/build-artifact.test.mjs (BA1–BA6) proves the shipped bundle
references no third-party CDN, self-hosts the woff2 fonts, contains the inlined Rams
style, and carries neither installMockWebApp nor __ARAHARAH_MOCK__.
Key Abstractions
@araharah/bot
| Abstraction | File | Description |
|---|---|---|
Env |
bot/src/config.ts |
Typed contract: BOT_TOKEN, WEBHOOK_SECRET (must be non-empty — the webhook fails closed without it), chat_sessions: KVNamespace. |
default { fetch } |
bot/src/webhook.ts |
The ExportedHandler<Env> — four routes, 404 otherwise. Bot is constructed per request. |
validateInitData(raw, botToken, opts) |
bot/src/security/initdata.ts |
Pure HMAC-SHA256 validation of Telegram WebApp initData. Depends only on crypto.subtle, so it runs unchanged in Workers and bare Node. Returns InitDataResult. |
SessionClaim / mintSession / getClaimByToken / getTokenForChat |
bot/src/kv/sessions.ts |
TTL-only session layer. SESSION_TTL_SECONDS = 900. Key scheme session:<token> → claim, chat:<chat_id> → current token. Rotate-on-new with a best-effort reconciliation sweep. |
LocationPin / writePin / getPin / clearPin |
bot/src/kv/location-pin.ts |
TTL-only pin layer. PIN_TTL_SECONDS = 300. Key loc:<chat_id>. getPin range-validates on read. |
createBot(env, opts) |
bot/src/bot/bot.ts |
grammY factory; wires /start + /mula, /help, /privacy + /privasi, and the location-fallback message handler. |
requestLocationFallback(bot, chatId, locale) |
bot/src/bot/handlers/location-fallback.ts |
Sends the request_location reply keyboard. Single caller: handleLocationFallback. |
strings / pickLocale |
bot/src/i18n/strings.ts |
The bot's entire copy surface (en, ms). pickLocale maps an ms/ms-* language code to Malay, everything else to English. |
KvLike |
bot/src/kv/*.ts |
Structural KV interface — lets tests pass a fake without depending on Workers types. |
@araharah/web
| Abstraction | File | Description |
|---|---|---|
Fix |
web/src/location/types.ts |
The single normalized position shape every source reduces to: lat, lng, accuracyM, headingDeg, ts, source. Freshness lives in the fix. |
LocationState |
web/src/location/types.ts |
Discriminated union: idle / validating / acquiring / awaiting-pin / live / stale / denied. |
acquireFix(deps, lastAccepted) |
web/src/location/chain.ts |
The three-link state machine. Pluggable cadence — no setInterval, no watchPosition. |
isFixStale(fix, nowMs) / fixAgeSeconds |
web/src/location/chain.ts |
The 45 s signal-lost threshold and the FIX Ns AGO telemetry helper. |
TelegramWebAppLike |
web/src/telegram/webapp.ts |
Hand-rolled ~12-member contract for window.Telegram.WebApp. getWebApp() may honestly return null off-Telegram. |
createSession(initData) |
web/src/session/client.ts |
POSTs the verbatim initData string; returns { ok: true, token } or a malformed/tampered/stale/network reason. |
setSession / getSession / isSessionStale / clearSession |
web/src/session/store.ts |
Module-scope token holder. No persistence call anywhere in the file. |
bootMap(container) |
web/src/map/map.ts |
MapLibre v6 bootstrap with the build-time-inlined Rams style. Returns null on GPUInitializationError. |
createPositionMarker() |
web/src/map/marker.ts |
SVG accuracy circle + 20 px dot + heading cone. Cone hidden entirely when heading is null. |
createFollowController(map, onModeChange) |
web/src/map/follow.ts |
following/free camera. Only a user gesture (movestart with originalEvent) flips to free. |
createStatusStrip(host) |
web/src/ui/strip.ts |
The status band — state lamp + state word on the left, telemetry on the right. |
buildBootCard / buildDeniedCard / buildSignalLostCard |
web/src/ui/cards.ts |
Rams card surfaces composed from theme component classes; zero inline copy. |
createRecenterControl(host, locale, onRecenter) |
web/src/ui/recenter.ts |
40×40 follow plate with three instant notch states. |
watchTheme(wa) / resolveBranch |
web/src/telegram/theme.ts |
Telegram light/dark signal → data-tg-theme slot switch. MapLibre is deliberately absent. |
resolveSafeInsets / applySafeArea |
web/src/telegram/safe-area.ts |
Union of CSS env() and WebApp.safeAreaInset, max per edge, emitted as CSS custom properties. |
wireBackButton(wa, hasOverlay) |
web/src/telegram/backbutton.ts |
Total function — at the root state a click closes the app; no no-op handler. |
installMockWebApp(options) |
web/src/mock/telegram-mock.ts |
Harness-only fake window.Telegram.WebApp with a dev-signed initData. Excluded from web/dist. |
localeFromInitData(raw) |
web/src/i18n/strings.ts |
Parses the locale out of the raw initData string — the session response carries only { token }. |
@araharah/theme
| Abstraction | File | Description |
|---|---|---|
@theme static block |
theme/src/tokens.css |
55 --color-* tokens plus spacing and typography. static forces every token into the compiled CSS so the package is consumable as plain custom properties. |
theme/src/theme.css |
theme/src/theme.css |
Entry stylesheet — @imports tailwindcss, tokens, components; declares eight self-hosted @font-face rules; defines the --shell-bg / --shell-on-surface theme slots. |
| Component classes | theme/src/components.css |
.card, .btn-primary, .btn-secondary, .chip-*, .maneuver-*, .text-*, .toggle*, .input-field, .checkbox. |
build.mjs |
theme/basemap/build.mjs |
Forks OpenFreeMap liberty, deriving every cartographic color from tokens.css via desaturate(tokenHex, satMultiplier, lightnessPerRole) with saturation clamped to [0.06, 0.12]. |
liberty-rams.json |
theme/basemap/liberty-rams.json |
The generated style. Accent guard fails the build if #EB8008 / #ED3F1C / #736B1E leak into any layer. building-3d is removed (faux 3D). |
Directory Structure Rationale
araharah/
├── package.json # private root; workspaces + build ordering only
├── bot/ # @araharah/bot — the Cloudflare Worker
│ ├── src/
│ │ ├── webhook.ts # the single fetch handler; four routes
│ │ ├── config.ts # Env contract
│ │ ├── bot/ # grammY factory, commands/, handlers/
│ │ ├── security/ # initdata.ts — pure, isolation-free crypto
│ │ ├── kv/ # sessions.ts, location-pin.ts — TTL-only repos
│ │ └── i18n/strings.ts # every user-facing bot string
│ ├── scripts/ # set-webhook, register-commands, set-menu-button
│ ├── test/ # hand-rolled zero-dependency runner + suites
│ └── wrangler.jsonc # worker name, assets binding, run_worker_first, KV
├── web/ # @araharah/web — the Mini App
│ ├── src/
│ │ ├── main.ts # boot choreography (order is load-bearing)
│ │ ├── session/ # client.ts (network), store.ts (memory)
│ │ ├── location/ # chain.ts + telegram/webview/chat-pin sources
│ │ ├── telegram/ # webapp, theme, backbutton, safe-area
│ │ ├── map/ # map.ts, marker.ts, follow.ts
│ │ ├── ui/ # strip.ts, cards.ts, recenter.ts
│ │ ├── mock/ # harness-only Telegram fake
│ │ ├── i18n/strings.ts # every user-facing shell string
│ │ └── shell.css # Rams chrome; imports @araharah/theme/dist/rams.css
│ ├── test/ # runner + gate suites (copy, storage, artifact, chain)
│ ├── scripts/capture.mjs # screenshot capture
│ └── vite.config.ts # dual-build dist/ vs dist-harness/
├── theme/ # @araharah/theme — design tokens + cartography
│ ├── src/ # tokens.css, components.css, theme.css, sheet.html
│ ├── fonts/ # self-hosted Geist + Space Grotesk woff2
│ ├── basemap/ # build.mjs + generated liberty-rams.json
│ ├── scripts/ # audit.mjs (token audit), screenshot.mjs
│ └── dist/rams.css # compiled output
├── stitch_minimalist_route_navigator/ # read-only design reference (mockups)
└── docs/
Rationale for the boundaries:
bot/src/webhook.tsowns routing; nothing else dispatches. The Worker is the only place that decides which path runs, sorun_worker_firstand the secret-token gate apply uniformly.bot/src/security/andbot/src/kv/are dependency-light on purpose.initdata.tsimports nothing from grammY and runs in both Workers and bare Node;kv/*.tstake a structuralKvLikeso the suites pass fakes.bot/src/i18n/strings.tsis the single copy source. The commands register script derives its menu text from the samehelpMenulines, andbot/test/commands.test.mjsgates that the/privacycopy still matches theSessionClaimfields insessions.ts.web/src/location/separates sources from the chain. Each source is a small module that normalizes toFixand answersnullfor denial;chain.tsowns ordering and knows nothing about any single source's API.web/src/session/splits network from state.client.tsowns fetch and error vocabulary;store.tsowns the in-memory token and the 900 s staleness rule. Neither ever touches storage.web/src/telegram/is the only place that touchesTelegram.WebApp. Every other module takes the narrowTelegramWebAppLikeinterface, which is what lets the mock, the test fakes, and real clients satisfy the same contract.theme/is pure build-time. It exports no runtime code — a CSS bundle and a JSON style. That is whywebcan depend on it without pulling a bundler into the token package.stitch_minimalist_route_navigator/is@source-scanned, never edited. The theme compiles every class the four mockups use so the design vocabulary stays provable against the shipped CSS.
Cross-Cutting Concerns
Privacy by construction
Two grep gates pin this rather than documenting it:
bot/test/privacy.test.mjs— everyputonchat_sessionsmust carry an explicitexpirationTtlon the same call line. No write path can persist without a TTL.web/test/no-storage.test.mjs— comments stripped, everyweb/srcfile scanned; zero occurrences oflocalStorage,sessionStorage,indexedDB,caches,document.cookie,WebApp.DeviceStorage,WebApp.SecureStorage,WebApp.CloudStorage.
The persisted surface is exactly SessionClaim (user_id, chat_id, issued_at,
language_code) for 900 s and LocationPin (latitude, longitude, horizontal_accuracy)
for 300 s. The client holds the token and the last fix in module scope only.
Access control
The chat_id used by /api/location and /api/location-fallback is always resolved
from the session claim, and the locale from claim.language_code. The request body of
the fallback route is never read — no chat id or language can arrive from the client.
Honest degradation
Every degradation is a labeled state, never a silent one or a fabricated value:
bootMap() returning null renders the shell over --color-surface-dim with
data-map="absent"; a null heading hides the marker cone rather than guessing an
orientation; getPin range-validates so a corrupt record cannot fly the camera to null
island; the poll is bounded at 60 s so AWAITING PIN cannot persist; every source
timeout resolves null so the chain always advances.
Logging
The Worker's only log statements are category-only — malformed, tampered, stale,
unauthorized, misconfigured, telegram-send. Never raw payloads, never user ids,
never timestamps. Rejection responses carry the category only.
Testing
Both bot/test and web/test use the same hand-rolled zero-dependency runner
(runner.mjs + harness.mjs), auto-discovering *.test.mjs. theme runs
scripts/audit.test.mjs in the same shape. Gates are grep/introspection assertions
against source and build artifacts, not just behavioral assertions.
What Is Not Built Yet
The following appear in IDEAS.md as roadmap items and are not in the codebase:
routing engines (Valhalla/OSRM), Neon/Postgres, RabbitMQ, Prometheus/Grafana, Resend,
Cloudflare R2, Photon geocoding. The current system is exactly the two implemented
phases: the bot skeleton with initData-gated sessions and TTL-only KV, and the Mini App
shell with the three-link location chain and the Rams basemap. Routing/guidance is
Phase 5+.