Files
Arah2/docs/ARCHITECTURE.md
T
2026-10-23 19:18:43 +08:00

26 KiB
Raw Blame History

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.ts owns routing; nothing else dispatches. The Worker is the only place that decides which path runs, so run_worker_first and the secret-token gate apply uniformly.
  • bot/src/security/ and bot/src/kv/ are dependency-light on purpose. initdata.ts imports nothing from grammY and runs in both Workers and bare Node; kv/*.ts take a structural KvLike so the suites pass fakes.
  • bot/src/i18n/strings.ts is the single copy source. The commands register script derives its menu text from the same helpMenu lines, and bot/test/commands.test.mjs gates that the /privacy copy still matches the SessionClaim fields in sessions.ts.
  • web/src/location/ separates sources from the chain. Each source is a small module that normalizes to Fix and answers null for denial; chain.ts owns ordering and knows nothing about any single source's API.
  • web/src/session/ splits network from state. client.ts owns fetch and error vocabulary; store.ts owns the in-memory token and the 900 s staleness rule. Neither ever touches storage.
  • web/src/telegram/ is the only place that touches Telegram.WebApp. Every other module takes the narrow TelegramWebAppLike interface, 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 why web can 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 — every put on chat_sessions must carry an explicit expirationTtl on the same call line. No write path can persist without a TTL.
  • web/test/no-storage.test.mjs — comments stripped, every web/src file scanned; zero occurrences of localStorage, 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+.