Files
arah2/docs/CONFIGURATION.md
T

406 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- generated-by: gsd-doc-writer -->
# Configuration
arah² is an npm-workspaces monorepo with five workspaces — `theme`, `bot`, `web`, `search`, `routing`
— plus a root `package.json` that composes their scripts. There is **no committed `.env` file**:
secrets live in `bot/.dev.vars` (gitignored) for local work and in Cloudflare Workers secrets for
deployment.
Configuration surfaces, by kind:
| Surface | File | Applies to |
| --- | --- | --- |
| Workspaces + scripts | `package.json` | all |
| Worker deployment + bindings | `bot/wrangler.jsonc` | bot |
| Secret env contract | `bot/.dev.vars` (from `.dev.vars.example`) | bot + the repo gate CLIs |
| Worker env typing | `bot/src/config.ts` | bot |
| Local secrets template | `bot/.dev.vars.example` | bot |
| Static-asset cache policy | `web/public/_headers` | web (served by the Worker's asset binding) |
| Bundler config | `web/vite.config.ts` | web |
| TypeScript config | `bot/tsconfig.json`, `web/tsconfig.json`, `search/tsconfig.json`, `routing/tsconfig.json` | bot, web, search, routing |
| Design tokens (source of truth) | `theme/src/tokens.css` → `theme/src/theme.css` | theme + everything downstream |
| Graph binary + gazetteer data | `routing/data/kl-roadgraph.v1.bin`, `search/src/data/kl-gazetteer.json` | routing, search |
| Ignore rules | `.gitignore`, `bot/.gitignore` | repo |
---
## Environment variables
Three variables reach the Worker (BOT_TOKEN, WEBHOOK_SECRET, MINI_APP_URL) plus two bindings; `WEBHOOK_URL` is script-only (`bot/src/config.ts` declares them as a typed `Env` interface,
plus two *bindings* — a KV namespace and an R2 bucket); `WEBHOOK_URL` is consumed only by the
one-time registration scripts, never by the Worker.
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `BOT_TOKEN` | **Required** | none | Telegram Bot API token from @BotFather. Used by `createBot()` in `bot/src/bot/bot.ts` to construct the grammY `Bot`, and by `validateInitData()` in `bot/src/security/initdata.ts` as the HMAC key for Mini App `initData` verification. |
| `WEBHOOK_SECRET` | **Required** | none | Value Telegram echoes in the `X-Telegram-Bot-Api-Secret-Token` header on every update. `/telegram/webhook` constant-time-compares it **before** any bot processing or KV access. If missing or empty at request time the route fails closed with `500 Server misconfigured` — grammY's secret check would otherwise accept every request. |
| `MINI_APP_URL` | **Required for the deep link** | none (concrete value committed) | Public origin of the deployed Mini App — NOT a secret, so it lives in `bot/wrangler.jsonc`'s `vars`, not in a secret. The `/cari` venue card's OPEN button is built from it: `<MINI_APP_URL>/?dest=<gazetteer id>`. It must equal the Mini App URL registered in @BotFather (Bot API 10.2 enforces the origin). The Worker never derives it from user input — an unvalidated origin would be an open redirect. |
| `WEBHOOK_URL` | **Required for the one-time scripts** | none | Full origin + path of the deployed Worker, used by `bot/scripts/set-webhook.mjs` and `bot/scripts/set-menu-button.mjs`. If the value points at the workers.dev root (no path), `set-webhook.mjs` appends `/telegram/webhook` automatically. The Worker never reads this variable. |
| `chat_sessions` | **Required (binding)** | n/a | Not an env var but a Cloudflare KV namespace binding. Wired in `bot/wrangler.jsonc` under `kv_namespaces`; every `kv.put()` across `bot/src/kv/` (`sessions.ts`, `location-pin.ts`, `guidance-sessions.ts`) writes through it with an explicit `expirationTtl`. |
| `ROAD_GRAPH` | **Required (binding)** | n/a | Not an env var but a Cloudflare **R2 bucket** binding (`r2_buckets` in `bot/wrangler.jsonc`, bucket `araharah-graphs`). `/api/route` decodes the road-graph binary through it; `loadGraph` memoizes one decoded graph per isolate. An absent or corrupt object degrades honestly (null → router-pending), never a crash. The bucket name is public configuration, never a secret — `docs/ROAD-GRAPH.md` owns the fork contract. |
### Maintainer-only credentials (never reach the Worker)
Two more variables are read **only** by the `--live` probes of the repo gate CLIs
(`scripts/lib/cloudflare-probes.mjs`), never by any `src/` tree and never by the Worker:
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `CLOUDFLARE_API_TOKEN` | Required for `--live` probes only | none | Wrangler credential used to run `wrangler kv key list` (KV-expiry probe) and the asset-header probe. Resolved as `process.env` first, then parsed from `bot/.dev.vars`. Never printed, never logged, never written anywhere. Not present in `.dev.vars.example` — add it by hand to `.dev.vars` or export it in the shell. |
| `CLOUDFLARE_ACCOUNT_ID` | Required for `--live` probes only | none | Cloudflare account id paired with the API token above. Same resolution order and same never-echo rule. |
The probes are invoked with `npm run privacy-audit -- --live` and `npm run budget-audit -- --live`.
Without the credentials the probes report `NOT RUN` and the CLI exits `2` — a skipped probe is
never mistaken for a pass. Without `--live`, the probes are listed as `NOT RUN (pass --live)` by
the operator's explicit choice, which is excluded from the exit-2 class.
The gate CLIs also honor one non-secret override:
| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `PRIVACY_AUDIT_ROOT` | Optional | repo root | Repo-root override for the gate CLIs' scanners (used by their test suites and by fork checkouts). The gates are otherwise cwd-independent — a stray subdirectory run must not scan the wrong tree and false-pass. |
<!-- VERIFY: the actual credential VALUES (BOT_TOKEN, WEBHOOK_SECRET, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) and the Cloudflare account they live under cannot be read from the repository — only their names and resolution order are documented here. -->
Local secret template — `bot/.dev.vars.example`:
```bash
# Copy to .dev.vars and fill for local wrangler dev. NEVER commit .dev.vars.
BOT_TOKEN=
WEBHOOK_SECRET=
# Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set.
WEBHOOK_URL=
# Public Mini App origin (not a secret) — lives in bot/wrangler.jsonc "vars" in production.
# The /cari venue card's OPEN button deep link is built from it: <MINI_APP_URL>/?dest=<id>.
MINI_APP_URL=
```
The template does NOT list `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` — those are read
directly from `.dev.vars` (or the shell) by the audit probes when present; they are optional and
only required for the `--live` mode. Values are never echoed anywhere in this documentation.
Script-level precedence: every script in `bot/scripts/` resolves a variable as
`process.env.<NAME> ?? devVars.<NAME>` — shell environment wins over the file, and the file is the
fallback. `set-webhook.mjs` requires all three of `BOT_TOKEN`, `WEBHOOK_URL`, `WEBHOOK_SECRET` and
exits `1` listing whichever are missing. `register-commands.mjs` requires only `BOT_TOKEN`.
`set-menu-button.mjs` requires `BOT_TOKEN` and `WEBHOOK_URL`.
---
## Configuration flow
```mermaid
flowchart TD
subgraph Local["Local development"]
DEVVARS["bot/.dev.vars (gitignored)"]
WRANGLERDEV["wrangler dev --port 8787 --local"]
end
subgraph Scripts["One-time Telegram registration"]
SC1["scripts/set-webhook.mjs"]
SC2["scripts/register-commands.mjs"]
SC3["scripts/set-menu-button.mjs"]
end
subgraph Production["Deployed Worker"]
SECRETS["wrangler secrets: BOT_TOKEN, WEBHOOK_SECRET"]
KV["KV binding: chat_sessions (TTL-only, 4 families)"]
R2["R2 binding: ROAD_GRAPH (araharah-graphs)"]
ASSETS["Static assets: ../web/dist + _headers"]
CRON["Cron trigger: */2 * * * * (stateless scan)"]
WORKER["src/webhook.ts fetch + scheduled handlers"]
end
DEVVARS --> WRANGLERDEV
DEVVARS --> SC1
DEVVARS --> SC2
DEVVARS --> SC3
SECRETS --> WORKER
KV --> WORKER
R2 --> WORKER
ASSETS --> WORKER
CRON --> WORKER
WRANGLERDEV --> WORKER
style Local fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style Scripts fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style Production fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style DEVVARS fill:#EB8008,stroke:#1A1A1A,color:#1A1A1A
style SECRETS fill:#ED3F1C,stroke:#1A1A1A,color:#FFFFFF
```
The Mini App in `web/` has **no build-time or runtime configuration surface**. It calls its own
origin with relative paths (`/api/session`, `/api/location`, `/api/location-fallback`), so the API
base URL is implied by wherever the bundle is served from — which is why the Worker serves
`../web/dist` as its own static assets and reserves `/api/*` and `/telegram/*` for itself. The
same is true of `search/` and `routing/`: neither reads any environment variable at runtime
(`loader-env.test.mjs` in both workspaces pins that the data loads with zero infrastructure —
static imports under Node, esbuild inlining under Vite, and an inlined seed under the wrangler
/esbuild Worker bundle).
The Worker has exactly one scheduled configuration surface: the `*/2 * * * *` cron trigger in
`wrangler.jsonc`. The tick is stateless — zero KV writes (the budget law) — and gates its card
sends on `CARD_CADENCE_S = 240` in code (`bot/src/guidance/cron.ts`): `*/2` is the coarsest
cadence that keeps 2-min ticks × 1 list-op = 720/day inside the free plan's 1,000/day KV-list
ceiling while honoring the 240 s card window.
---
## Required vs optional settings
**Fail loudly (Worker):**
- `WEBHOOK_SECRET` — the only setting with an explicit runtime guard, at `bot/src/webhook.ts`:
`if (!env.WEBHOOK_SECRET)` logs `[webhook] rejected: misconfigured` and returns
`500 Server misconfigured`. The rationale is documented in the file: grammY's
`compareSecretToken` silently accepts every request when the secret is `undefined`, so an
unarmed binding must never reach it.
- `BOT_TOKEN` — declared required in the `Env` type but not guarded at runtime; a missing token
surfaces as a grammY authentication failure on the first request that instantiates the bot.
**Fail loudly (scripts):** `set-webhook.mjs`, `register-commands.mjs`, and `set-menu-button.mjs` each
print a missing-variable list to stderr and `process.exit(1)` before any network call.
**Fail loudly (gate CLIs):** `privacy-audit` and `budget-audit` never pass silently — a required
measurement that cannot run (missing graph binary, missing build, missing `--live` credentials)
yields exit `2` NOT RUN, never a green check.
**Degrade honestly (binding):** a missing or corrupt `ROAD_GRAPH` object does not crash the
Worker — `/api/route` returns the honest router-pending state. This is deliberate: the graph
bucket is fork provisioning, not a startup invariant.
**With defaults / derived:**
- `KV namespace id` — `bot/wrangler.jsonc` already carries a concrete id for `chat_sessions`; the
file's leading comment notes it was filled in after `npx wrangler kv namespace create chat_sessions`.
- `MINI_APP_URL` — a **public** configuration value declared in the Worker `Env` and shipped via
`bot/wrangler.jsonc`'s `vars` block with its concrete value committed
(`https://araharah-bot.araharah-bot.workers.dev`). When it is blank the `/cari` OPEN button is
omitted rather than guessed; the venue card and its alternates still send.
- `WEBHOOK_URL` path segment — auto-derived by `set-webhook.mjs` when the configured origin has no path.
- KV TTLs — constants in source, not configuration. One per family, all pinned by grep gates in
the test suite so no write path can persist without a TTL:
| Constant | Value | File | Key family |
| --- | --- | --- | --- |
| `SESSION_TTL_SECONDS` | `900` | `bot/src/kv/sessions.ts` | `session:<token>`, `chat:<chat_id>` |
| `PIN_TTL_SECONDS` | `300` | `bot/src/kv/location-pin.ts` | `loc:<chat_id>` |
| `GUIDANCE_SESSION_TTL_SECONDS` | `7200` | `bot/src/kv/guidance-sessions.ts` | `guidance:<id>` |
The live KV probe asserts every listed key's family prefix is one of the four TTL-only families
(`session:`, `chat:`, `loc:`, `guidance:`) and carries an `expiration`.
- `CARD_CADENCE_S = 240` (`bot/src/guidance/cron.ts`) and the cron expression `*/2 * * * *`
(`wrangler.jsonc`) — a code/config pair: the cron ticks every 120 s, a card is due on the first
tick of each 240 s window.
---
## Worker configuration file (`bot/wrangler.jsonc`)
```jsonc
{
// chat_sessions namespace created via `npx wrangler kv namespace create chat_sessions` — the id below is live.
"name": "araharah-bot",
"main": "src/webhook.ts",
"compatibility_date": "2026-10-22",
// workers_dev: serve on the <name>.<account>.workers.dev URL — the WEBHOOK_URL
// target for this phase (a custom route/domain may supersede)
"workers_dev": true,
// Workers static assets binding: serves ../web/dist. The Worker fetch still
// owns /api/* and /telegram/* first via run_worker_first (a GLOB ARRAY, not a boolean).
"assets": {
"directory": "../web/dist",
"binding": "ASSETS",
"not_found_handling": "none"
},
"run_worker_first": ["/api/*", "/telegram/*"],
// Public vars (NOT secrets): the deployed Mini App ORIGIN used to build
// the /cari venue card's OPEN deep link (<MINI_APP_URL>/?dest=<id>).
"vars": { "MINI_APP_URL": "https://araharah-bot.araharah-bot.workers.dev" },
// R2 binding: hosts the road-graph binary /api/route loads (loadGraph memoizes
// ONE GET per cold isolate). Forks create + upload their OWN bucket:
// `npx wrangler r2 bucket create <name>` + routing/scripts/upload-road-graph.mjs
// (docs/ROAD-GRAPH.md is the fork contract).
"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }],
// Cron trigger: */2 scan — 2-min ticks × 1 list-op = 720/day, inside the free
// plan's 1,000/day KV-list ceiling; card cadence is 240 s in code (CARD_CADENCE_S).
// The tick is STATELESS (zero KV writes — the budget law).
"triggers": { "crons": ["*/2 * * * *"] },
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
}
```
Key notes:
- `assets.directory: "../web/dist"` — the Worker serves the Mini App bundle itself, so `npm run build`
(theme then web) must run before `wrangler deploy`; a stale or absent `web/dist` ships no Mini App.
Vite copies `web/public/_headers` into the bundle, so the cache policy ships with it (see below).
- `run_worker_first` is a **glob array**, not a boolean: `/api/*` and `/telegram/*` are handled by
`src/webhook.ts` first; everything else falls through to the static asset binding.
- `not_found_handling: "none"` — unmatched paths are not served the SPA `index.html` fallback.
- `workers_dev: true` serves on the `<name>.<account>.workers.dev` URL, which is the `WEBHOOK_URL`
target and the committed `MINI_APP_URL` value.
- `vars: { "MINI_APP_URL": ... }` — a **public** origin shipped as a plain var (never a secret).
It must equal the Mini App URL registered in @BotFather; see the env-var table above.
- `r2_buckets` / `triggers` — the R2 graph bucket and the */2 cron are both free-plan-compatible;
the cron's budget arithmetic (720 KV list-ops/day against a 1,000/day ceiling) is measured and
printed by `npm run budget-audit` (see `docs/BUDGETS.md`).
### Static-asset cache policy (`web/public/_headers`)
Committed under `web/public/`, so Vite copies it into every build output (`web/dist`,
`web/dist-harness`) and the Workers asset binding serves it verbatim:
```text
/assets/*
Cache-Control: public, max-age=31556952, immutable
/
Cache-Control: public, max-age=0, must-revalidate
```
Rationale (from the file's header comment): `/assets/*` filenames are content-HASHED — a hashed
URL's bytes can never change, so they are immutable for a year. The HTML entry is NOT hashed (it
points at the current hashed assets), so it must revalidate on every visit or a stale entry would
point at deleted asset files. The `--live` asset-headers probe (`privacy-audit --live`, re-used by
`budget-audit --live`) **quotes the observed `cache-control` and `cf-cache-status` headers verbatim**
against the deployed origin — it reports, it does not assert the policy.
---
## Build and run scripts
Root `package.json` (workspaces: `theme`, `bot`, `web`, `search`, `routing`; the root and `bot`,
`web`, `search`, `routing` packages set `"private": true` while `@araharah/theme` omits the field,
ESM via `"type": "module"`):
| Command | Description |
| --- | --- |
| `npm run build` | Builds the theme then the web bundle (`theme` → `web`). |
| `npm run deploy` | `build`, then `npm run deploy -w bot` (`wrangler deploy` + `set-webhook.mjs`). |
| `npm run dev` | `build`, then `npm run dev -w bot` (`wrangler dev --port 8787 --local`). |
| `npm run bot:test` / `bot:typecheck` / `bot:dev` / `bot:deploy` / `bot:commands` | Per-workspace passthroughs to the `bot` package. |
| `npm run web:test` / `web:typecheck` / `web:dev` | Passthroughs to the `web` package. |
| `npm run search:test` / `search:typecheck` / `search:corpus` | `search` workpace passthroughs; `search:corpus` runs `node search/scripts/gazetteer-corpus.mjs` (golden corpus measurement). |
| `npm run routing:test` / `routing:typecheck` / `routing:corpus` / `routing:fixtures` | `routing` workspace passthroughs; `routing:corpus` runs `node routing/scripts/route-corpus.mjs`; `routing:fixtures` re-fetches route fixtures from the OSRM demo server (maintainer-time, fixed public coordinates only). |
| `npm run web:build` | Same as `build`; `web:build:harness` runs the harness-mode build; `web:capture` runs the screenshot harness. |
| `npm run sheet` / `gallery` / `audit` / `capture` | Passthroughs to the `theme` design-system tooling. |
| `npm run privacy-audit` | `node scripts/privacy-audit.mjs` — the five static privacy gates (+ `--live` probes). Exits `0` clean, `1` violations, `2` NOT RUN. |
| `npm run budget-audit` | `node scripts/budget-audit.mjs` — the five measured budget gates + the free-tier comparison table (+ `--live` cache-header probe). Same exit-code contract. |
Workspace-level commands worth knowing:
| Command | Where | Notes |
| --- | --- | --- |
| `npm test` | `bot`, `web`, `search`, `routing` | All run `node test/runner.mjs` (a hand-rolled runner, no external test framework). |
| `npm run typecheck` | `bot`, `web`, `search`, `routing` | `tsc --noEmit`, `strict` plus `noUncheckedIndexedAccess`. |
| `npm run dev` | `bot` | `wrangler dev --port 8787 --local` — reads `bot/.dev.vars`. |
| `npm run deploy` | `bot` | `wrangler deploy && node scripts/set-webhook.mjs`. |
| `npm run commands` | `bot` | `node scripts/register-commands.mjs` — `setMyCommands` in default scope and in `language_code: "ms"`; descriptions are derived from `src/i18n/strings.ts` `helpMenu` lines so the menu and `/help` reply cannot drift. |
| `npm run build` | `web` | `vite build` → `web/dist`. |
| `npm run build:harness` | `web` | `vite build --mode harness` → `web/dist-harness`, with mock installer and fetch stubs. |
| `npm run dev` | `web` | `vite` dev server on port 5173 (`strictPort: true`). |
| `npm run capture` | `web` | `node scripts/capture.mjs` — serves `web/dist-harness` and screenshots 22 boot/route/guidance legs. |
| `npm run build` | `theme` | `tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify`. |
| `npm run send` | `theme` | Copies `fonts/geist` + `fonts/space-grotesk` into `dist/fonts/`. |
| `npm run audit` | `theme` | `node ./scripts/audit.mjs ./dist/rams.css` — flags any rendered color/spacing/typography/radius/shadow/blur value not derived from `src/tokens.css`. Exits `0` clean, `1` violations, `2` drift. |
| `npm run basemap` | `theme` | `node basemap/build.mjs` — forks OpenFreeMap `liberty` into `theme/basemap/liberty-rams.json`, deriving every cartographic value from the token file. |
| `npm run gallery` / `sheet` / `capture` | `theme` | Design tooling; `gallery` builds, captures, then serves `http://localhost:8123/gallery.html + /basemap/` via `npm run basemap:preview` (`python3 -m http.server 8123`). |
Routing maintainer scripts with no npm alias (run directly with `node` from the workspace):
`build-road-graph.mjs`, `upload-road-graph.mjs` (fills the R2 bucket), `measure-route-cpu.mjs`
(the A*-CPU ladder the budget gate re-runs), `build-glyphs.mjs` (256×256 PNG render tiles via
`sharp`). Search maintainer scripts: `json-to-sqlite.mjs`, `overpass-topup.mjs` (gazetteer top-up
from Overpass — maintainer-time, never a runtime dependency). `docs/ROAD-GRAPH.md` is the fork
contract for the graph binary and the R2 bucket.
---
## Bundler configuration (`web/vite.config.ts`)
Vite is configured from a function of `mode`, with one switch driving a **dual-build contract**:
- `__ARAHARAH_HARNESS__` is injected via `define` as `true` only in `--mode harness` and `false`
everywhere else. Vite replaces the identifier textually, so the production bundle tree-shakes the
mock installer and `/api/*` fetch stubs out entirely; `web/index.html` carries its mock profile
block inside the same guarded branch. In plain `vite dev` the define keeps `main.ts`'s ambient
reference from throwing while the fallback mock still fires via `import.meta.env.DEV`.
- `build.outDir` is `dist-harness` in harness mode, `dist` otherwise, with `emptyOutDir: true` and
`target: "es2022"`. The two outputs must never share a directory — the file's own comment calls
that out as the reason the split exists (sharing one would make them clobber each other and make
the BA5 gate meaningless).
- `base: "/"`, dev server on port 5173 with `strictPort: true`.
- `__ARAHARAH_HARNESS__` is declared ambient in `web/src/vite-env.d.ts` so `tsc --noEmit` passes in
both modes.
- Harness-only URL parameters (documented in `web/index.html`): `?mock=android|ios|tdesktop|macos|web`,
`?scheme=light|dark`, `?pin=1`, `?dest=<gazetteer id>`, `?fixpair=<pairId>`, `?drive=<model>`,
`?locale=en|ms`, `?sessiondelay=<ms>`, `?fixdelay=<ms>`.
---
## Design token configuration
`theme/src/tokens.css` is the single source of truth for every color, spacing, typography, and radius
value. `theme/src/theme.css` imports Tailwind v4 plus `tokens.css` and `components.css`, declares
self-hosted Geist and Space Grotesk `@font-face` rules from `theme/fonts/` (no Google Fonts CDN),
and scopes class generation with explicit `@source` lines — the four mockup markup files under
`stitch_minimalist_route_navigator/` are the only allowed utility sources, with
`@source not "../scripts"` and `@source not "../basemap"` excluding prose.
Downstream consumers:
- `web/src/shell.css` — `@import "@araharah/theme/dist/rams.css";`
- `web/src/map/map.ts` — `import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";` (inlined
at build time, never re-set at runtime)
- `theme/basemap/liberty-rams.json` — the generated basemap style; its source URL, sprite, and glyph
endpoints point at `https://tiles.openfreemap.org/...`.
---
## Data configuration (search + routing)
`search/` and `routing/` carry their data as committed files rather than configuration:
| File | Purpose |
| --- | --- |
| `search/src/data/kl-gazetteer.json` | The KL gazetteer seed — static-imported, esbuild-inlined for the Worker bundle. Carries the ODbL provenance header. |
| `routing/data/kl-roadgraph.v1.bin` | The built road-graph binary; `/api/route` decodes it via the `ROAD_GRAPH` R2 binding in production. |
| `routing/src/data/route-fixtures.json` | Committed demo-pair routes (polyline6 geometry), precomputed from the OSRM demo server by `routing/scripts/fetch-route-fixtures.mjs` over FIXED PUBLIC coordinates only — never user data. |
Neither workspace reads an environment variable at runtime; the loader-env suites in both pin the
"zero infrastructure" property (bare Node, Vite, and the wrangler/esbuild bundle all serve the same
seed). The only environment knob in this layer is `PRIVACY_AUDIT_ROOT`, honored by the maintainer
gate CLIs, not by the workspaces themselves.
---
## Ignore rules and generated output
`.gitignore` at the repo root TRACKS `.planning/` (the ignore line is commented out), `node_modules/`, `dist/` (which covers both
`theme/dist/` and `web/dist/`), `web/dist-harness/`, `theme/screenshots/`, `web/screenshots/`,
`.env`, `.env.*` (with `!*.example`), and `*:Zone.Identifier`. `bot/.gitignore` adds `.dev.vars` and
`.wrangler/`.
Practical consequence: **`web/dist` and `theme/dist` are never committed.** Both must be rebuilt from
source before a deploy, which is why `npm run deploy` and `npm run dev` both run `build` first.
---
## Per-environment overrides
There are no committed `.env.development`, `.env.production`, or `.env.test` files. The separation
is structural rather than file-based:
| Environment | Configuration source | How it differs |
| --- | --- | --- |
| Local `wrangler dev` | `bot/.dev.vars` | Secrets read from the gitignored file; KV emulated locally under `bot/.wrangler/`; Mini App served from `web/dist`. |
| Local `vite dev` | none | Vite dev server on 5173 with no Worker behind it — `main.ts` falls back to the mock WebApp via `import.meta.env.DEV`. |
| Harness capture | `vite build --mode harness` | `web/dist-harness` plus `web/index.html`'s URL-parameter profile. Never deployed. |
| Production | `wrangler.jsonc` + Cloudflare secrets | Secrets set with `wrangler secret put` rather than a file; assets come from the uploaded `../web/dist` (with `_headers`); KV resolves to the real namespace; `ROAD_GRAPH` resolves to the real R2 bucket; the cron trigger schedules the stateless scan. |
| Gate-CLI `--live` | `process.env` or `bot/.dev.vars` | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` (shell wins over the file); absent credentials → exit `2` NOT RUN, never a pass. |
`@araharah/theme`, `@araharah/web`, `@araharah/search`, and `@araharah/routing` are consumed by
relative path within the workspace, so there is no registry token or publish step to configure.