--- phase: 05-routing-route-display plan: 05 type: execute wave: 3 depends_on: ["02", "04"] files_modified: - web/test/build-artifact.test.mjs - web/index.html - web/src/mock/route-profiles.ts - web/scripts/capture.mjs - bot/test/route-privacy.test.mjs autonomous: true requirements: [RTNG-01, RTNG-02] must_haves: decisions_implemented: - "D-11: the BA build-artifact bundle-budget gate — fixture geometry + route-model code ≤ ~200KB gzipped measured over production-shaped artifacts, printed with every run" - "D-05: the Worker gains no /api/route — a structural gate proves bot/src never registers a route proxy and never imports the routing workspace" - "D-15: capture signals are state tokens only — ARAHARAH-ROUTE state=drawn|pending, never coordinates; the capture legs pair the token with DOM text checks" - "D-12: the capture harness's ?fixpair knob delivers a PINNED PUBLIC origin from the committed fixture corpus — the harness introduces no user coordinate" - "DSGN-02: the route-gallery fidelity artifact extends the Phase 1/3 gallery pattern beside navigate_map_overview/screen.png" truths: - "BA9 measures the gzipped size of the fixture dataset + routing workspace sources + web route modules against a 204800-byte budget and PRINTS the measurement — the budget is visible, never discovered" - "BA10 proves every committed pairId rides the shipped production bundle (the BA7 id-parity analog — a stale bundle without the fixtures is unshippable)" - "The harness-only route-profiles module is provably excluded from web/dist (a named sentinel token the BA5-family scan greps for)" - "Five new capture legs exist and pass: shell-route-fixture, shell-route-alt (post-chip-tap), shell-route-pending, shell-route-dark, route-gallery-compare — all non-blank, all state-token-raced" - "bot/test/route-privacy.test.mjs proves the Worker stayed out of the routing path: zero /api/route tokens, zero @araharah/routing imports in bot/src" artifacts: - path: "web/test/build-artifact.test.mjs" provides: "BA9 (bundle budget, printed) + BA10 (pairId parity) — the production-build gates" contains: "BA9" - path: "web/src/mock/route-profiles.ts" provides: "the harness-only ?fixpair origin lookup (imports getPairOrigin from @araharah/routing; sentinel-guarded)" contains: "ROUTE_PROFILE_HARNESS_ONLY" - path: "web/index.html" provides: "the ?fixpair= knob — the mock's telegramFix becomes the pair's pinned public origin" contains: "fixpair" - path: "web/scripts/capture.mjs" provides: "the five route capture legs + the ARAHARAH-ROUTE signal race + DOM assertions" contains: "ARAHARAH-ROUTE" - path: "bot/test/route-privacy.test.mjs" provides: "RP1/RP2 — the Worker-side no-route-proxy gates (D-05)" contains: "RP1" key_links: - from: "web/index.html" to: "web/src/mock/route-profiles.ts" via: "the harness block resolves ?fixpair to a pinned origin and threads it as the mock's telegramFix" pattern: "getRouteProfileOrigin" - from: "web/src/mock/route-profiles.ts" to: "@araharah/routing/src/index.ts" via: "getPairOrigin(pairId) — the committed public fixture corpus is the ONLY origin source the harness can deliver" pattern: "getPairOrigin" - from: "web/scripts/capture.mjs" to: "web/src/route/route.ts" via: "the capture race waits on the ARAHARAH-ROUTE state token the controller emits" pattern: "ARAHARAH-ROUTE" - from: "bot/test/route-privacy.test.mjs" to: "bot/src" via: "the D-05 structural scan (no route proxy, no routing import) — the privacy-gate family pattern" pattern: "/api/route" --- Close the phase with its verification gates: the BA9 bundle-budget + BA10 fixture-parity build-artifact gates over the production build, the five route capture legs driven by the new `?fixpair=` harness knob, and the Worker-side no-route-proxy privacy gates — the mechanical proof that the amended SC3 holds ("bundle-size and type gates holding"). Purpose: D-11's bundle budget is a build-artifact gate, not a hope; the UI-SPEC's five capture artifacts are the phase's visual evidence (and the DSGN-02 fidelity obligation); and D-05/D-12's privacy posture stays verifiable by reading gate output. The route legs run against the production-shaped harness bundle (the Phase 3 capture discipline — never against dev-mode). Output: BA9/BA10 in `build-artifact.test.mjs`, `web/src/mock/route-profiles.ts` + the `?fixpair` knob in `web/index.html`, five new capture legs in `capture.mjs` (expected list 8 → 13), and `bot/test/route-privacy.test.mjs` (RP1/RP2). @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md @AGENTS.md @.planning/phases/05-routing-route-display/05-CONTEXT.md @.planning/phases/05-routing-route-display/05-UI-SPEC.md @.planning/phases/05-routing-route-display/05-02-PLAN.md @.planning/phases/05-routing-route-display/05-04-PLAN.md @web/test/build-artifact.test.mjs @web/scripts/capture.mjs @web/index.html @web/src/mock/telegram-mock.ts @bot/test/privacy.test.mjs @routing/src/index.ts @routing/src/data/route-fixtures.json The five new legs (UI-SPEC §Capture Profiles — ALL existing legs unchanged; the expected list grows 8 → 13). The demo pair is selected AT RUNTIME from the committed corpus — never hardcoded: `const routePairs = JSON.parse(readFileSync(resolve(REPOROOT, "routing/src/data/route-fixtures.json", "utf8"))).pairs;` then `const demoPair = routePairs.find((p) => p.routes.length >= 2) ?? routePairs[0];` (RT10 guarantees ≥ 4 multi-route pairs, so the find succeeds; the fallback keeps the legs runnable in the degenerate case). Queries are then built from `demoPair.destinationId` / `demoPair.pairId`: | Artifact | Query | Race + assertions | |----------|-------|-------------------| | `shell-route-fixture.png` | `mock=android&dest=${demoPair.destinationId}&fixpair=${demoPair.pairId}` (light) | race `ARAHARAH-ROUTE`; assert the line reads `state=drawn`; DOM: `waitForFunction` body text contains the destination's NAME (from the gazetteer entry read at capture time) AND `MIN` (the card's telemetry); 2000ms tile settle; shot | | `shell-route-alt.png` | same profile | after the drawn race: if `.route-chip` exists (multi-route pair — the RT10-guaranteed case), `page.click(".route-chip")`, wait 800ms (instant swap + 400ms refit + margin), assert body text matches `/ROUTE \d\/\d/`; ELSE (degenerate single-route fallback) assert the no-chip law: the page contains NO `.route-chip` element; shot | | `shell-route-pending.png` | `mock=android&dest=national-museum&fixpair=${demoPair.pairId}` | race `ARAHARAH-ROUTE`; assert `state=pending`; DOM: body contains `not wired up yet` (the pending headline, en); shot | | `shell-route-dark.png` | route-fixture profile + `scheme=dark` | same race + the PX-02 honesty check (`data-tg-theme` === "dark" before shooting); shot | | `route-gallery-compare.png` | — | the DSGN-02 composer (gallery-compare precedent): `shell-route-fixture.png` beside `stitch_minimalist_route_navigator/navigate_map_overview/screen.png`, columns labeled with source paths | Harness mechanics: - `SIGNALS` gains `'ARAHARAH-ROUTE'` (the seen-map + diagnostics strings too). - New `waitMode: 'route'` — race the ARAHARAH-ROUTE console event (30s timeout, the SIGNAL_TIMEOUT_MS shape), then parse the captured line for `state=` and let the leg assert its expected state + DOM. - `web/index.html` harness block: `const fixpair = q.get("fixpair");` — when present, import the profile module and resolve the pinned origin; pass `telegramFix: { lat: , lng: , accuracyM: 12, headingDeg: null }` (fall back to the current default fix when the pairId is unknown — an unknown pairId must not wedge the boot). - `web/src/mock/route-profiles.ts` (HARNESS-ONLY, imported ONLY from the index.html harness block): imports `getPairOrigin` from `@araharah/routing/src/index.ts`; exports `getRouteProfileOrigin(pairId: string): { lat: number; lng: number } | null` and the sentinel `export const ROUTE_PROFILE_HARNESS_ONLY = true;`. Header comment: D-12 (the pinned origin is committed public fixture data — the harness delivers no user coordinate) + the BA-exclusion note. **BA9 — the bundle budget (D-11).** Appended to `build-artifact.test.mjs`: - Inputs (all committed files, production-shaped by construction): `routing/src/data/route-fixtures.json`, every `routing/src/**/*.ts`, every `web/src/route/**/*.ts`. - Measurement: sum of `gzipSync(readFileSync(f)).length` over the inputs; assert `<= 204800` (200 KiB); the failure message and a `console.log` line BOTH print the measured bytes + budget (`BA9 route bundle budget: {n} / 204800 gz bytes`) — visible every run (D-11: "enforced as a BA build-artifact gate"). - Comment in the gate: the measurement bounds the phase's ADDED payload (fixtures dominate; the minified route code's gz size is approximated by its source gz size — the approximation is conservative in the gate's favor at these sizes and the 200 KiB ceiling carries an order-of-magnitude headroom per 05-RESEARCH Finding 1). - The gate skips with a build-first message when `web/dist` is absent (the BA-suite convention) — run after `npm run build -w theme && npm run build -w web`. **BA10 — fixture parity (the BA7 analog).** Every `pairId` from the committed `route-fixtures.json` must appear in the concatenated `web/dist/assets/*.js` text — a stale bundle that lost the fixtures could never serve a route (assert `pairs.length >= 10` first, the "scanned a real dataset" guard pattern). **BA5-family extension.** Add `"ROUTE_PROFILE_HARNESS_ONLY"` to BA5's token list — the harness-only route-profiles module must be dead-code-eliminated from production (its import lives inside `if (__ARAHARAH_HARNESS__)` in index.html; the define-replaces-with-false mechanism strips the branch exactly like installMockWebApp). Verified in this session — do not re-derive: - `capture.mjs` structure: `SIGNALS` array at line 166, `captureLeg({name, query, colorScheme, grantGeo, waitMode})`, the waitMode dispatch chain (`live`/`dest`/`denied`/`settle400`/`immediate`), and the final `expected` list of 8 artifacts with the missing/undersized/failures gate — the extension points are exactly these four. - `web/index.html`'s harness block already reads `?dest=` and threads it as `startParam`; `telegramFix` is hard-coded `{lat:3.1513,lng:101.6958}` today — the fixpair knob replaces its VALUE (never the shape). - `build-artifact.test.mjs` reads `search/src/data/kl-gazetteer.json` via `import … with { type: "json" }` (line 47) — the fixtures JSON imports identically; BA7's per-id loop is the BA10 template. - The mock's `telegramFix` delivery maps `{lat,lng,accuracyM,headingDeg}` → the Telegram wire shape (`telegram-mock.ts` lines 190–199) — the profile module returns the authoring shape. - Plan 05-02 pinned `national-museum` as the pending-probe destination (RT4: never a corpus destination) — the pending leg's carrier is valid by construction. - Plan 05-04's controller emits exactly two console tokens: `ARAHARAH-ROUTE state=drawn` / `ARAHARAH-ROUTE state=pending` (RS8-pinned). - `bot/test/privacy.test.mjs` is the bot-side grep-gate style reference (comment-stripping + banned-token scans). ## Trust Boundaries | Boundary | Description | |----------|-------------| | Harness query params → mock fix | `?fixpair=` selects which PUBLIC pinned origin the mock delivers. It must never accept arbitrary coordinates (a `?lat=&lng=` knob would reintroduce the coordinate-in-URL surface the fixture world exists to avoid). | | Committed artifacts → the measured budget | The budget gate reads repo files; a silent fixture bloat (someone re-fetching with `overview=full&steps=true`) must fail the build gate, not surface as a slow webview. | | Production build → users | Harness-only modules leaking into `web/dist` would ship mock wiring — the BA5-family sentinel scan is the tripwire. | | Worker surface | A route proxy appearing in bot/src would violate D-05's "the Worker gains no /api/route" — the structural gate keeps the pivot honest. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-12 | Denial of Service (availability budget) | Fixture/code growth pushing the webview bundle past the D-11 budget | mitigate | BA9 fails the suite above 204800 gz bytes and prints the measurement every run; RT9 (plan 05-02) is the fixture-level early-warning tier of the same law. | | T-05-14 | Information Disclosure | A coordinate-parameter harness knob or a coordinate-bearing capture signal reintroducing user-coordinate surfaces | mitigate | The knob accepts ONLY a pairId (resolved from the committed public corpus via `getPairOrigin` — arbitrary lat/lng params are never read); the capture legs assert the `state=` token form (no coordinates — RS8 pins the source; the legs pin the runtime); the pending leg proves an out-of-corpus pair renders the honest card, never a fabricated geometry. | | T-05-16 | Tampering (D-05 regression) | A route proxy growing into the Worker | mitigate | RP1: zero `/api/route` tokens in comment-stripped `bot/src`; RP2: zero `@araharah/routing` imports in `bot/src` — the Worker cannot route even accidentally. | | T-05-15 | Tampering (harness leakage) | The route-profiles harness module shipping in production | mitigate | The `ROUTE_PROFILE_HARNESS_ONLY` sentinel joins BA5's token scan — the module is provably dead-code-eliminated from `web/dist`. | task 1: BA9 bundle-budget + BA10 fixture-parity + BA5 sentinel extension web/test/build-artifact.test.mjs - `web/test/build-artifact.test.mjs` (BA1–BA8 — the gate file this task extends; BA7's loop is the BA10 template) - `routing/src/data/route-fixtures.json` (the committed pair set BA10 asserts) - `.planning/phases/05-routing-route-display/05-RESEARCH.md` (Finding 1 — the size math behind the budget) Extend `web/test/build-artifact.test.mjs`: - Import the fixtures dataset (`import routeFixtures from "../../routing/src/data/route-fixtures.json" with { type: "json"; }`) and `gzipSync` from `node:zlib`. - Define the input file list: `routing/src/data/route-fixtures.json` + a recursive walk of `routing/src` (`*.ts`) + `web/src/route` (`*.ts`) (resolve paths relative to the test file like `WEB`/`DIST` are). - `BA9` test: skip-with-build-first-message when `web/dist` is absent (the suite convention); compute the summed gz size; `console.log("BA9 route bundle budget: " + total + " / 204800 gz bytes")`; assert `<= 204800` with the measurement in the failure message. - `BA10` test: assert `routeFixtures.pairs.length >= 10` (a real dataset guard); concatenate `web/dist/assets/*.js`; assert EVERY `pairId` appears in the bundle text (failure names the missing pairId — the T-04-39 phrasing pattern). - BA5: add `"ROUTE_PROFILE_HARNESS_ONLY"` to the scanned token list (both the JS-assets and the HTML scans). - Update the file-header doc-comment inventory (BA9/BA10 lines). Build first, then run: `npm run build -w theme && npm run build -w web && npm test -w web` — ALL BA gates green including the new ones (04-04's BA7/BA8 still pass — the route family adds no crypto, no CDN, and the gazetteer ids remain). cd /home/C-Fu/araharah && npm run build -w theme >/dev/null 2>&1 && npm run build -w web >/dev/null 2>&1 ; npm test -w web 2>&1 | tail -15 - `npm test -w web` exits 0 and the output contains `PASS BA9` and `PASS BA10` - `grep -q 'BA9 route bundle budget' web/test/build-artifact.test.mjs` exits 0 (the printed measurement) - `grep -q '204800' web/test/build-artifact.test.mjs` exits 0 - `grep -q 'ROUTE_PROFILE_HARNESS_ONLY' web/test/build-artifact.test.mjs` exits 0 (BA5 extension) - `grep -q 'pairId' web/test/build-artifact.test.mjs` exits 0 (BA10 parity loop) - `node -e "const{gzipSync}=require('node:zlib');const{readFileSync,readdirSync}=require('node:fs');const{join}=require('node:path');let t=0;const w=(d)=>{for(const f of readdirSync(d,{withFileTypes:true})){const p=join(d,f.name);if(f.isDirectory())w(p);else if(f.name.endsWith('.ts'))t+=gzipSync(readFileSync(p)).length}};w('routing/src');t+=gzipSync(readFileSync('routing/src/data/route-fixtures.json')).length;w('web/src/route');console.log('measured:',t);if(t>204800)throw new Error('over budget');console.log('BA9 preview OK')"` exits 0 The production build passes the budget gate (measured and printed), carries every committed pairId, and provably excludes the harness-only profile module. task 2: ?fixpair harness knob + five route capture legs web/src/mock/route-profiles.ts, web/index.html, web/scripts/capture.mjs - `web/index.html` (the harness block — where the knob threads) - `web/src/mock/telegram-mock.ts` (MockOptions.telegramFix — the authoring shape the profile feeds) - `web/scripts/capture.mjs` (SIGNALS, captureLeg waitModes, the expected list, the gallery composer) - `.planning/phases/05-routing-route-display/05-UI-SPEC.md` §Capture Profiles (the binding artifact table) **`web/src/mock/route-profiles.ts`** — per the `` block: sentinel export + `getRouteProfileOrigin` wrapping `getPairOrigin` from `@araharah/routing/src/index.ts`; header comment citing D-12 and the BA5 sentinel. **`web/index.html`** — inside the harness block: read `const fixpair = q.get("fixpair");`; after the existing mock-import, when `fixpair` is truthy dynamically import the profile module (`await import("/src/mock/route-profiles.ts")` — the `/src/` absolute form is the PROVEN harness-import path), resolve the origin, and use it as the mock's `telegramFix` lat/lng (accuracy 12, heading null); unknown pairId → keep the current default fix (log nothing). Add `fixpair` to the profile comment line listing the knobs. **`web/scripts/capture.mjs`**: - `SIGNALS` gains `'ARAHARAH-ROUTE'`; the seen-map + `signalDiagnostics` gain the same key. - At the top of the leg block, load the committed corpus and pick the demo pair (`routePairs` / `demoPair` per the `` block — the legs are data-driven from the committed dataset, never hardcoded pair literals). - New `waitMode: "route"` branch: race the `ARAHARAH-ROUTE` console event (`SIGNAL_TIMEOUT_MS`); on timeout → hard failure with diagnostics; parse `state=` from the captured line; the leg's expected state + DOM assertions are leg parameters (`expectState`, `expectText: […]` — `waitForFunction` over `document.body.textContent`, 15s, the dest-leg polling shape); 2000ms settle before the shot (tile render). - Add the five legs per the `` table (fixture → alt (chip click when present) → pending → dark → gallery-compare, reusing the existing composer with `shell-route-fixture.png` in the implemented column). - Extend `expected` to the 13-artifact list. Update the header doc-comment's Outputs inventory. The alt leg's chip click: `page.click(".route-chip")` (first chip), then assert `/ROUTE \d\/\d/.test(bodyText)` — the `{n}/{N}` register the swapped card renders; the degenerate no-chip fallback asserts the single-route rendering law instead (UI-SPEC: single-route draws render NO chip row). **Acceptance:** `npm run web:capture` exits 0 with 13/13 non-blank captures (`CAPTURE OK — 13/13`), including the five new legs; no existing leg regressed. cd /home/C-Fu/araharah && npm run web:capture 2>&1 | tail -20 - `npm run web:capture` exits 0 and its output contains `CAPTURE OK — 13/13` - `test -f web/screenshots/shell-route-fixture.png && test -f web/screenshots/shell-route-alt.png && test -f web/screenshots/shell-route-pending.png && test -f web/screenshots/shell-route-dark.png && test -f web/screenshots/route-gallery-compare.png` exits 0 - `grep -q 'ARAHARAH-ROUTE' web/scripts/capture.mjs` exits 0 AND `grep -q 'fixpair' web/index.html` exits 0 - `grep -q 'ROUTE_PROFILE_HARNESS_ONLY' web/src/mock/route-profiles.ts` exits 0 - `grep -q 'getPairOrigin' web/src/mock/route-profiles.ts` exits 0 (the pinned-corpus-only origin law) - the runner-page profile comment lists the fixpair knob: `grep -q '?fixpair' web/index.html` exits 0 Five route capture legs pass against the production-shaped harness bundle — drawn, alt-swapped, pending, dark, and the DSGN-02 gallery artifact — all raced on state tokens with DOM text assertions. task 3: Worker-side no-route-proxy gates (D-05) bot/test/route-privacy.test.mjs - `bot/test/privacy.test.mjs` (the bot-side grep-gate style: comment stripping, banned-token scans, named cases) - `bot/src` tree (the scanned surface) - `.planning/phases/05-routing-route-display/05-CONTEXT.md` D-05 (the law being gated) **`bot/test/route-privacy.test.mjs`** — RED-first-then-green (pure gates over the unmodified Worker tree): - `RP1 no route proxy` — strip comments from every `bot/src/**/*.ts` (the privacy.test.mjs pattern) and assert ZERO occurrences of `/api/route` and `route` handler registrations matching `/app\.route\(|on\("message:url"|\.route\("route/`… keep it PRECISE: assert zero `/api/route` tokens AND zero `registerRoute|routeProxy` tokens (D-05: the Worker gains no route endpoint this phase). - `RP2 no routing import` — zero `@araharah/routing` specifiers anywhere in `bot/src` (routing lives in the webview bundle only; the Worker imports only `@araharah/search`). Header comment: the D-05 citation + "the pivot's Worker boundary, gated". **Acceptance:** `npm test -w bot` exits 0 with `PASS RP1` and `PASS RP2` (all existing bot suites + GE gates still green). cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -10 - `npm test -w bot` exits 0 and the output contains `PASS RP1` and `PASS RP2` - `grep -rc '/api/route' bot/src --include='*.ts' | grep -v ':0' | wc -l | tr -d ' '` returns 0 (the gate's premise holds in the tree itself) - `grep -rc '@araharah/routing' bot/src --include='*.ts' | grep -v ':0' | wc -l | tr -d ' '` returns 0 The Worker's routing abstinence is mechanically gated: no route endpoint, no routing-workspace import — the pivot's D-05 boundary holds by test, not by memory. 1. `npm run build -w theme && npm run build -w web && npm test -w web` — BA1–BA10 all green (BA9 prints the measurement). 2. `npm run web:capture` — CAPTURE OK — 13/13 (the five new legs non-blank, state-raced, DOM-asserted). 3. `npm test -w bot` — all suites green including GE1–GE7 and RP1/RP2. 4. Full suite: `npm run routing:test && npm run routing:corpus && npm test -w web && npm test -w bot` — green. 5. The amended SC3 evidence chain: RT1–RT10 (corpus routes cleanly) + BA9/BA10 (bundle-size gate) + the 05-01 C2 type gate + the pending capture (deliberate, designed) — all four clauses have a green instrument. - BA9/BA10 pass over the production build with the measurement printed. - 13/13 captures pass including the five route legs driven by ?fixpair. - RP1/RP2 gate the Worker out of the routing path. - The phase's full verification story is green across all four workspaces.