docs: full docs-update pass — 9 GSD docs regenerated + verified (434→454 claims; verifier corrections applied)

This commit is contained in:
araharah-executor
2026-10-25 12:51:01 +08:00
parent 809864f5b6
commit 44851c70fe
9 changed files with 2014 additions and 324 deletions
+40 -17
View File
@@ -8,20 +8,21 @@
A privacy-respecting, Telegram-first GPS navigator — a waze-like route navigator delivered through a Telegram bot and Mini App instead of an app-store install, built on OpenStreetMap data, and designed to Dieter Rams' "less, but better." First users are in the Kuala Lumpur metro.
The Telegram bot is **arah2bot**.
The Telegram bot is **arah2bot**. The product is live — a private beta with real drivers in KL: open [@arah2bot](https://t.me/arah2bot), send `/cari KLCC`, open the venue card's Mini App button, and follow live turn-by-turn guidance. The app is served from <https://araharah-bot.araharah-bot.workers.dev>.
---
## Overview
arah² is an npm-workspaces monorepo with four packages:
arah² is an npm-workspaces monorepo with five packages:
| Package | Name | What it does |
|---|---|---|
| `theme/` | `@araharah/theme` | The Telemetry Rams design system as a Tailwind v4 CSS-first token package. Every color, spacing, type, and radius value in the project is defined once in `theme/src/tokens.css` and compiled to `theme/dist/rams.css`. Also ships the MapLibre basemap style `theme/basemap/liberty-rams.json` plus gallery/audit/screenshot tooling. |
| `bot/` | `@araharah/bot` | A [grammY](https://grammy.dev) Telegram bot running on a Cloudflare Worker. It serves the `/telegram/webhook` route, four JSON API routes, keeps TTL-only chat sessions in Workers KV, and serves the Mini App's static assets through a wrangler assets binding. |
| `web/` | `@araharah/web` | The Telegram Mini App: a MapLibre GL navigation shell built with Vite. Owns the three-source location acquisition chain, an in-memory session store, and the Telegram WebApp integrations (theme slots, BackButton, safe-area insets). |
| `bot/` | `@araharah/bot` | A [grammY](https://grammy.dev) Telegram bot running on a Cloudflare Worker. It serves the `/telegram/webhook` route, five JSON API routes, keeps TTL-only chat sessions in Workers KV, loads the road graph from an R2 bucket, and pushes turn cards to chat from a scheduled cron tick. It also serves the Mini App's static assets through a wrangler assets binding. |
| `web/` | `@araharah/web` | The Telegram Mini App: a MapLibre GL navigation shell built with Vite. Owns the three-source location acquisition chain, the in-app destination search UI, route rendering, turn-card guidance replay, an in-memory session store, and the Telegram WebApp integrations (theme slots, BackButton, safe-area insets). |
| `search/` | `@araharah/search` | The shared, dependency-free destination-search module: the JUPEM-derived Malay abbreviation table as editable data, a symmetric normalizer, a Damerau (OSA) residual scorer with disjoint tiers, and the curated KL gazetteer with locals' aliases — imported by the bot, the Mini App, and the search-quality corpus runner. |
| `routing/` | `@araharah/routing` | The shared, dependency-free routing data plane: a CSR road graph with snap-to-graph, A* search, and polyline6 codecs. Engine A runs it Worker-side over the R2-hosted graph (`/api/route`); the fixture source (15 committed KL golden pairs) and the route-quality corpus runner live alongside it. |
The Worker is the single origin. It answers `/api/*` and `/telegram/*` itself (`run_worker_first`) and serves everything else out of `web/dist`.
@@ -29,7 +30,8 @@ The Worker is the single origin. It answers `/api/*` and `/telegram/*` itself (`
graph TD
U["Telegram client — arah2bot chat + Mini App webview"]
W["Cloudflare Worker — araharah-bot"]
KV[("Workers KV — chat_sessions, TTL 900s")]
KV[("Workers KV — TTL-only records: sessions 900s, pins 300s, guidance 7200s")]
R2[("R2 — road-graph binary, one GET per cold isolate")]
TG["Telegram Bot API"]
APP["Mini App assets — web/dist via ASSETS binding"]
MAP["MapLibre GL — Rams basemap, OpenFreeMap tiles"]
@@ -39,10 +41,12 @@ graph TD
TG --> W
U -->|"GET Mini App"| APP
APP --> W
W -->|"POST /api/session, /api/location-fallback"| TG
W -->|"venue card, pin keyboard, turn cards"| TG
APP -->|"POST /api/session — initData HMAC"| W
APP -->|"GET /api/location — Bearer token"| W
APP -->|"POST /api/route — Bearer token"| R2
W --> KV
W -->|"cron */2 → turn cards"| TG
APP --> MAP
THEME -.->|"inlined at build time"| MAP
THEME -.->|"dist/rams.css"| APP
@@ -53,7 +57,7 @@ graph TD
classDef web fill:#F5EDE0,stroke:#736B1E,stroke-width:2px,color:#1E1B14
class U,TG telegram
class W worker
class KV storage
class KV,R2 storage
class APP,MAP,THEME web
```
@@ -69,7 +73,7 @@ cd arah2
npm install
```
Requires Node.js and npm (no version is pinned in the repo — use a current LTS). Wrangler handles Cloudflare authentication on first use via `wrangler dev` / `wrangler deploy`.
Requires Node.js and npm (no version is pinned in the repo — use a current LTS; the corpus runners rely on Node's native TypeScript type-stripping). Wrangler handles Cloudflare authentication on first use via `wrangler dev` / `wrangler deploy`.
<!-- VERIFY: Node.js minimum version — no `engines` field or `.nvmrc` exists in the repository; confirm the supported floor with the maintainers. -->
@@ -114,12 +118,21 @@ The bot answers in English and Bahasa Melayu. Commands are registered into Teleg
| Command | Alias | Behaviour |
|---|---|---|
| `/start` | `/mula` | One-sentence introduction to arah² and its privacy posture. |
| `/help` | — | Lists the commands that exist in this phase. |
| `/help` | — | Lists the commands that exist and points at the known-limits record. |
| `/privacy` | `/privasi` | Names exactly what is stored (user id, chat id, language, ~15 min) and exactly what is never stored (location, routes, trip history, profile). |
| `/cari` | `/search` | Searches the curated KL gazetteer (`/cari KLCC`) and replies with **one** native venue card (`sendVenue`) carrying an OPEN IN ARAHARAH Mini App button plus 2–4 alternates as callback buttons. Out-of-area queries get an honest refusal that names the v1 area, never the query text. |
### The live flow, end to end
1. **Search.** `/cari <place>` normalizes and scores the query against the KL gazetteer (`@araharah/search`); the best hit ships as a venue card with deep link `MINI_APP_URL/?dest=<gazetteer-id>`.
2. **Open the Mini App.** The webview reads `?dest=` from `location.search`, preloads the destination, and mints a session by posting the verbatim `Telegram.WebApp.initData` string to `/api/session`.
3. **Acquire a starting point.** The three-link location chain (`web/src/location/chain.ts`) tries **Telegram → WebView → chat pin**, in that locked order.
4. **Route.** The app posts the origin and destination to `/api/route`; the Worker's router (`bot/src/routing/worker-router.ts`) loads the R2-hosted CSR graph once per cold isolate, snaps, and runs A* (`@araharah/routing`). Every miss class answers `200 null` — router-pending, never a wrong line.
5. **Guidance.** Accepting a route mints a guidance envelope (`POST /api/guidance`, 7200 s TTL); a scheduled cron (`*/2 * * * *`) projects progress along the frozen route snapshot and pushes due turn cards — a maneuver glyph photo with a live caption — into the chat, with a button back into the Mini App at `?g=<envelope-id>`.
### HTTP surface
The Worker's `fetch` handler routes exactly four paths and returns `404` for anything else. All rejections carry a category only — never a payload, a user id, or a timestamp.
The Worker's `fetch` handler routes exactly six paths and returns `404` for anything else. All rejections carry a category only — never a payload, a user id, or a timestamp.
| Method | Path | Purpose |
|---|---|---|
@@ -127,6 +140,8 @@ The Worker's `fetch` handler routes exactly four paths and returns `404` for any
| `POST` | `/api/session` | The security gate. The client posts its verbatim `Telegram.WebApp.initData` string (capped at 4 KB, over-cap bodies are rejected `413` before parsing). An HMAC-validated, fresh identity receives a one-time opaque session token. |
| `GET` | `/api/location` | Polled by the Mini App with its in-memory `Bearer` token. The KV key derives from the resolved claim's `chat_id`, never from a client-supplied id, so one user's token can never read another user's pin. Returns honest nulls; a missing or expired token is `401`. |
| `POST` | `/api/location-fallback` | The "ask via chat" path when geolocation is denied. The bot sends a `request_location` reply keyboard into the user's own chat. Target chat id and locale come from the resolved claim — the request body is never read. A rejected Telegram send is a category-only `502`. |
| `POST` `GET` `DELETE` | `/api/guidance` | Guidance envelopes. `POST` mints a 7200 s session from a validated six-field route snapshot (a body `chat_id` is structurally ignored — identity is server-owned). `GET`/`DELETE` resolve the record and compare it to the claim, so a foreign and an absent session are indistinguishable `404`s. |
| `POST` | `/api/route` | Engine A's arm. Bearer-gated exactly like `/api/location`; returns a route summary, steps, and polyline6 geometry, or `200 null` on every miss class (honest degradation). Backed by the R2 road-graph bucket; a missing or corrupt graph degrades to router-pending, never a crash. |
```bash
# Mint a session from a Mini App initData string
@@ -189,11 +204,16 @@ Run from the repo root, or with `-w <workspace>`.
| `npm run build` | Build the theme, then the Mini App |
| `npm run dev` | Build, then run the Worker locally on port `8787` |
| `npm run deploy` | Build, then `wrangler deploy` + `set-webhook.mjs` |
| `npm run bot:test` / `npm run web:test` / `npm run search:test` | Run each package's zero-dependency test suite |
| `npm run bot:typecheck` / `npm run web:typecheck` / `npm run search:typecheck` | `tsc --noEmit` |
| `npm run bot:test` / `npm run web:test` / `npm run search:test` / `npm run routing:test` | Run each package's zero-dependency test suite |
| `npm run bot:typecheck` / `npm run web:typecheck` / `npm run search:typecheck` / `npm run routing:typecheck` | `tsc --noEmit` |
| `npm run bot:commands` | Register the persistent Telegram command menu (EN + MS) |
| `npm run web:capture` | Capture Mini App screenshots |
| `npm run web:build:harness` | Dual build into `dist-harness/` with the mock installer and fetch stubs |
| `npm run privacy-audit` | Repo-wide privacy audit: storage gate, KV-TTL gate, category-only log gate, runtime-host allowlist, production-bundle telemetry scan (`--live` probes add KV expiry + cache headers) |
| `npm run budget-audit` | Free-tier budget audit: re-measures Worker CPU, bundle sizes, KV write classes, subrequests, and re-runs the golden corpora |
| `npm run search:corpus` | Search-quality gate: golden corpus against the shipped search module |
| `npm run routing:corpus` | Route-quality gate: fixture corpus (RT1–RT10) against the shipped routing module |
| `npm run routing:fixtures` | Refresh the committed route fixtures from the fixture HTTP source |
| `npm run sheet` / `gallery` / `audit` / `capture` | Design-token tooling (see above) |
The two build outputs must never share an `outDir` — the harness build emits mock fetch stubs that production must not carry.
@@ -205,17 +225,20 @@ The two build outputs must never share an `outDir` — the harness build emits m
Privacy is structural, not a policy page:
- Every write to the `chat_sessions` KV namespace carries an explicit `expirationTtl` on its call line; the storage test suite greps for this.
- The persisted claim is exactly four fields — user id, chat id, issued-at, language code — under a 900-second TTL. Minting rotates any prior token for the chat.
- Three TTL-only record families, nothing else: sessions — exactly four fields (user id, chat id, issued-at, language code) under a 900-second TTL; location pins — 300 seconds; guidance envelopes — 7200 seconds. Minting rotates any prior token for the chat.
- The turn-card cron is **stateless**: it reads KV, sends cards, and writes nothing.
- The Mini App holds its session token in a module-scope closure. There is no `localStorage`, `sessionStorage`, cookie, IndexedDB, or Cache API anywhere in `web/src` — the `no-storage` test suite pins this.
- Nothing durable, nothing reconstructable. A token cannot outlive the 15-minute initData freshness proof that minted it.
- Two audits back all of this with evidence: `npm run privacy-audit` (storage, TTL, category-only logs, runtime-host allowlist, shipped-bundle telemetry scan) and `npm run budget-audit` (the free-tier ceilings, measured — see `docs/BUDGETS.md`). v1 carries **zero telemetry**; there is nothing to opt into yet.
## Repository layout
```
theme/ @araharah/theme — tokens.css, components.css, theme.css, fonts/, basemap/, scripts/
bot/ @araharah/bot — src/webhook.ts entry, bot/, kv/, security/, i18n/, scripts/, wrangler.jsonc
web/ @araharah/web — src/main.ts entry, location/, map/, session/, telegram/, ui/, mock/
search/ @araharah/search — src/types.ts, src/normalize.ts, src/score.ts, src/gazetteer.ts, src/data/kl-gazetteer.json
bot/ @araharah/bot — src/webhook.ts entry, bot/, kv/, security/, routing/, guidance/, i18n/, scripts/, wrangler.jsonc
web/ @araharah/web — src/main.ts entry, location/, route/, guidance/, search/, map/, session/, telegram/, ui/, mock/
search/ @araharah/search — src/types.ts, normalize.ts, distance.ts, score.ts, gazetteer.ts, source.ts, data/kl-gazetteer.json
routing/ @araharah/routing — src/graph.ts, snap.ts, astar.ts, polyline.ts, geo.ts, fixture-source.ts, http-source.ts, data/kl-roadgraph.v1.bin
stitch_minimalist_route_navigator/ read-only mockup screens (class-vocabulary source for the theme build)
DESIGN.md Rams philosophy — superseded for any specific value
```
@@ -225,4 +248,4 @@ DESIGN.md Rams philosophy — superseded for any specific value
Code is licensed under **GPL-3.0** — the full text is in [LICENSE](./LICENSE). The curated KL
gazetteer (`search/src/data/kl-gazetteer.json`) is **data**, licensed separately under the Open
Database License (ODbL) v1.0 with attribution to © OpenStreetMap contributors; the dual-licence
note and the share-alike scope are in [LICENSES.md](./LICENSES.md).
note and the share-alike scope are in [LICENSES.md](./LICENSES.md).