Files
arah2/.planning/phases/05-routing-route-display/05-05-PLAN.md
T

24 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
05-routing-route-display 05 execute 3
02
04
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
true
RTNG-01
RTNG-02
decisions_implemented truths artifacts key_links
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
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
path provides contains
web/test/build-artifact.test.mjs BA9 (bundle budget, printed) + BA10 (pairId parity) — the production-build gates BA9
path provides contains
web/src/mock/route-profiles.ts the harness-only ?fixpair origin lookup (imports getPairOrigin from @araharah/routing; sentinel-guarded) ROUTE_PROFILE_HARNESS_ONLY
path provides contains
web/index.html the ?fixpair=<pair-id> knob — the mock's telegramFix becomes the pair's pinned public origin fixpair
path provides contains
web/scripts/capture.mjs the five route capture legs + the ARAHARAH-ROUTE signal race + DOM assertions ARAHARAH-ROUTE
path provides contains
bot/test/route-privacy.test.mjs RP1/RP2 — the Worker-side no-route-proxy gates (D-05) RP1
from to via pattern
web/index.html web/src/mock/route-profiles.ts the harness block resolves ?fixpair to a pinned origin and threads it as the mock's telegramFix getRouteProfileOrigin
from to via pattern
web/src/mock/route-profiles.ts @araharah/routing/src/index.ts getPairOrigin(pairId) — the committed public fixture corpus is the ONLY origin source the harness can deliver getPairOrigin
from to via pattern
web/scripts/capture.mjs web/src/route/route.ts the capture race waits on the ARAHARAH-ROUTE state token the controller emits ARAHARAH-ROUTE
from to via pattern
bot/test/route-privacy.test.mjs bot/src the D-05 structural scan (no route proxy, no routing import) — the privacy-gate family 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).

<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@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

<capture_contract> 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: <origin.lat>, lng: <origin.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. </capture_contract>

<ba_gate_contract> 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). </ba_gate_contract>

<environment_facts> 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). </environment_facts>

<threat_model>

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.
</threat_model>
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 <acceptance_criteria> - 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 </acceptance_criteria> 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 <capture_contract> 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 <capture_contract> 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 <acceptance_criteria> - 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 </acceptance_criteria> 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 <acceptance_criteria> - 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 </acceptance_criteria> 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.

<success_criteria>

  • 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. </success_criteria>