24 KiB
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 |
|
|
true |
|
|
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:
SIGNALSgains'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 forstate=and let the leg assert its expected state + DOM. web/index.htmlharness block:const fixpair = q.get("fixpair");— when present, import the profile module and resolve the pinned origin; passtelegramFix: { 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): importsgetPairOriginfrom@araharah/routing/src/index.ts; exportsgetRouteProfileOrigin(pairId: string): { lat: number; lng: number } | nulland the sentinelexport 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, everyrouting/src/**/*.ts, everyweb/src/route/**/*.ts. - Measurement: sum of
gzipSync(readFileSync(f)).lengthover the inputs; assert<= 204800(200 KiB); the failure message and aconsole.logline 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/distis absent (the BA-suite convention) — run afternpm 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.mjsstructure:SIGNALSarray at line 166,captureLeg({name, query, colorScheme, grantGeo, waitMode}), the waitMode dispatch chain (live/dest/denied/settle400/immediate), and the finalexpectedlist 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 asstartParam;telegramFixis hard-coded{lat:3.1513,lng:101.6958}today — the fixpair knob replaces its VALUE (never the shape).build-artifact.test.mjsreadssearch/src/data/kl-gazetteer.jsonviaimport … with { type: "json" }(line 47) — the fixtures JSON imports identically; BA7's per-id loop is the BA10 template.- The mock's
telegramFixdelivery maps{lat,lng,accuracyM,headingDeg}→ the Telegram wire shape (telegram-mock.tslines 190–199) — the profile module returns the authoring shape. - Plan 05-02 pinned
national-museumas 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.mjsis 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> |
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.
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:
SIGNALSgains'ARAHARAH-ROUTE'; the seen-map +signalDiagnosticsgain the same key.- At the top of the leg block, load the committed corpus and pick the demo pair (
routePairs/demoPairper the<capture_contract>block — the legs are data-driven from the committed dataset, never hardcoded pair literals). - New
waitMode: "route"branch: race theARAHARAH-ROUTEconsole event (SIGNAL_TIMEOUT_MS); on timeout → hard failure with diagnostics; parsestate=from the captured line; the leg's expected state + DOM assertions are leg parameters (expectState,expectText: […]—waitForFunctionoverdocument.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 withshell-route-fixture.pngin the implemented column). - Extend
expectedto 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.
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.
<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>