Files
arah2/.planning/phases/07-hardening-private-launch/07-RESEARCH.md
T
araharah-executor a7d8dbf345 docs(07): revise phase 07 plans — wire the safe-area watch, fold the corpora into budget-audit, add the subrequests row, resolve the research open questions
Checker-issue round 1 on the Phase 7 plan set:

BLOCKER (07-01) — watchSafeAreaChanges and watchFullscreenEvents were created
and unit-tested but never called in production: main.ts:74 was the only
safe-area site and was not in files_modified, and the fullscreen events were
never registered. Insets never re-resolved at runtime on a real device while
every gate stayed green.

  - web/src/main.ts joins 07-01's files_modified; task 4 step 8 wires
    watchSafeAreaChanges(getWebApp()) directly after the boot site's
    applySafeArea(resolveSafeInsets(getWebApp())) line.
  - task 4 step 6 registers watchFullscreenEvents once at the guidance-start
    edge in route.ts, with a deliberately no-op callback (events are
    observability, never correctness).
  - New grep acceptance criteria pin BOTH production call sites; two new
    must_haves key_links name them; T-07-06b now names the boot-site call.

WARNING (07-04 / VALIDATION.md) — the golden corpora were attributed to 07-04
t3, which never ran them. Folded into the budget audit: runGoldenCorpusGate()
re-runs `npm run routing:corpus` and `npm run search:corpus` as child
processes (the same shape runCpuLadderGate already uses), so the ONE
budget-audit command enforces SC4.a/SC4.b. VALIDATION.md rows now name 07-04
t2 and the budget-audit command.

WARNING (07-02 t3) — the landscape-leg snippet omitted the required setup.
The action now says to copy the existing corridor guidance leg VERBATIM
(setup stubGuidanceRoute(page, corridorModel) + guideQuery carrying dest +
fixpair + waitMode/expectGuide) and change ONLY the name, the scheme, and the
viewport; two grep criteria pin the setup and the query.

WARNING (07-04) — D-06/SC4 name subrequests but nothing measured beside the
50/request ceiling. Added runSubrequestClassGate(): a per-request-class
inventory derived from the Worker's route table (engine-A /api/route = 0
external subrequests; the cron tick's Telegram send is the only recurring
class; KV ops tracked separately against their own ceiling). Unmeasurable
counts read "not measured", never 0. docs/BUDGETS.md gains a
"## Subrequests per request class" section and the record states that the
inventory is source-derived, not a live Workers Logs metric.

WARNING (07-RESEARCH.md) — "## Open Questions" is now
"## Open Questions (RESOLVED)" and each of the five items carries its adopted
disposition; Q2/Q3 resolve to "answered by measurement at the launch gate per
D-04/D-06".

INFO (07-VALIDATION.md) — nyquist_compliant flipped to true and the sign-off
is approved 2026-10-25 after the map-row correction.

Waves, dependencies, autonomous flags, and the other three plans are unchanged.
2026-10-25 02:44:30 +08:00

749 lines
59 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.
# Phase 7: Hardening + Private Launch - Research
**Researched:** 2026-10-25
**Domain:** WebView fullscreen/landscape hardening, failure-state QA, privacy/budget audit, free-tier launch gates
**Confidence:** HIGH for repository structure and documented platform limits; MEDIUM for per-client fullscreen behavior until the real-device walk; MEDIUM for battery result because it must be measured
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
### Privacy audit (SC3)
- **D-01:** A **consolidated automated audit script** (`npm run privacy-audit`, repo tool): grep gates over ALL src trees (no storage-API writes outside the platform allowlist — the phase-3 gate's law extended repo-wide; no coordinate/payload logging — the category-only vocabulary verified by schema scan), a KV listing probe (only TTL-keyed namespaces), a bundle scan (**zero telemetry: no analytics, no beacons — the ONLY runtime fetches are OpenFreeMap tiles, the Telegram API, and the app's own Worker endpoints**), plus a **signed written audit record** citing the evidence (PRIV-02/GEO-03 re-verified end-to-end). Reusable by forks (GPL audit tool).
- **D-02:** **Zero telemetry in v1** — opt-in telemetry stays an IDEAS.md aspiration post-validation; nothing to opt into yet, nothing to audit but absence.
### Device matrix + battery (SC1, PX-04)
- **D-03:** **Fullscreen on guidance start** via `requestFullscreen()` behind the 8.0 capability guard (phase-6 guard precedent), with orientation-lock used where available; landscape usability per device in the QA pass; landscape layout fixes are CSS-only media queries (tokens binding; the cockpit/banner/card geometry adapts, the map stays Rams-constant).
- **D-04:** The **battery target is a RECORDED real measurement**: one Android guidance run ≥30 min screen-on, battery % read at start/end (method + caveats written into the gate file); the D-15 distance-throttle is the software half. The <10%/hr target is recorded as measured, never synthesized (no bench harness — over-engineering for one phone).
- **D-05:** The QA pass is **one consolidated walk**: Android + iOS + Desktop + browser × (boot, search→route, guidance, minimize/resume, trip-end, theme flip, landscape/fullscreen) — a single checklist in the phase gate file; user walks once; NOT-RUN legs recorded honestly (the phase 3/4 protocol; iOS legs if the device is available — Phase 4 verified iOS present).
### Budgets + deferred debt (SC4)
- **D-06:** A **consolidated budget record** (`npm run budget-audit` output + written record checked into verification): Worker CPU/subrequests (06-04's measured ladder re-run against the live graph), bundle sizes (BA9 et al.), KV write counts per request class (cron = 0 writes — the free-plan law), asset cache headers. Current numbers vs free-tier limits, in one document.
- **D-07:** **Debts paid here**: (1) 02-REVIEW's 3 deferred INFOs — IN-01 (dead `ParsedInitData` export + unreachable branch + read-helper intent comments), IN-02 (stale wrangler.jsonc comment), IN-03 (exact-900s boundary test — the security-critical comparator pin); (2) the /cari menu: re-run `register-commands.mjs` (scopes were re-armed in phase 4) + record the Telegram non-default-scope defect honestly in the limitations doc; (3) phase 3's stale-fix assumption notes (A1/A3) reviewed and closed or re-pointed. **NOT here**: ALT landmarks (c2) — committed to the v1.x milestone, not this phase.
### Known-limitations doc (SC4)
- **D-08:** `docs/KNOWN-LIMITATIONS.md` in-repo, written for **drivers AND forks**: OSM data gaps (no traffic confidence, no lane guidance, speed limits where mapped), cross-metro CPU degradation (the c3 class), the /cari menu defect + its workaround, beta quality statement, and the roadmap pointers (WASM-router track, ALT, engine B). Surfaced from the bot: `/help` gains one line pointing at the doc (strings-module copy).
### Launch definition
- **D-09:** **Private launch = checklist-signed private beta**: the user's chosen first drivers (friends/family/testers in KL) receive the bot link; word-of-mouth distribution. No app store, no marketing, no landing page (post-v1 idea). The launch checklist: known-limitations shipped · privacy audit signed · budget record green · full device matrix walked · `/help` + `/privacy` accurate · repo pushed public under GPL-3.0. **The milestone completes when the checklist is signed** — the launch act itself is the user's, not the workflow's.
### OpenCode's Discretion
- Audit script file layout (scripts/ + test/ split), gate vocabulary, exit codes
- Battery recording method details; landscape media-query breakpoints within tokens
- INFO-fix mechanics; the boundary test's fixture form
- Known-limitations doc structure/tone; the /help one-liner copy (en/ms)
### Deferred Ideas (OUT OF SCOPE)
- ALT landmark preprocessing (c2) — v1.x milestone lever (measured decision, phase 6)
- Engine B (fully client-side webview router) — the static-hosted endgame track
- WASM-router OSS project — backend #2's standalone evolution
- Opt-in telemetry — post-validation (IDEAS.md)
- Voice guidance, live trip share, toll toggle, speed-limit display, traffic ETA — v1.x table
- Bitmap map snapshots in turn-cards — v1.x
- Public launch (landing page, marketing, app-store presence) — post-v1
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| PX-04 | Guidance supports fullscreen and the screen remains usable in landscape (driving posture) | Telegram Bot API 8.0 offers `requestFullscreen`, `exitFullscreen`, `isFullscreen`, and `fullscreenFailed`; browser Fullscreen API is gesture-gated and can reject; capability wrapper, CSS-only landscape rules, mock/browser tests, and consolidated real-device gate are specified below. |
| PRIV-02 (re-verify) | Backend holds no persistent per-user location history/profile; only TTL-expiring state, verifiable by architecture audit | Existing source gates plus a repository-wide audit tool, live KV key/expiry listing probe, log-vocabulary scan, bundle fetch-host allowlist, and signed audit record are specified below. |
| GEO-03 (re-verify) | User location is never written to persistent storage; only ephemeral TTL server state | Current client storage gate, Worker KV TTL gate, live key/expiry inspection, and end-to-end boot/guidance/trip-end probe are specified below. |
</phase_requirements>
## Summary
Phase 7 is best planned as **four independently verifiable gates joined by one honest launch checklist**, not as a broad feature phase. The code already contains most of the mechanisms that need proving: a narrow Telegram WebApp contract with version guards, memory-only client state, TTL-only KV writes, route/guidance replay fixtures, a deterministic A* CPU measurement, and a capture harness. The work is to consolidate these into reproducible gates, close the explicit UX gap (fullscreen/landscape), exercise every failure class, and preserve an evidence trail that a fork or reviewer can rerun.
The platform answer is now clear. Telegram Bot API 8.0 introduced native Mini App fullscreen plus `requestFullscreen`/`exitFullscreen`, `isFullscreen`, `fullscreenChanged`, `fullscreenFailed`, and `lockOrientation`/`unlockOrientation`; the official guidance says Mini Apps can use portrait or landscape fullscreen and that content must respect safe/content safe areas. Therefore the primary path should call **Telegram's Mini App fullscreen API** at guidance start, not the DOM `Element.requestFullscreen()` API: the DOM API is only “Limited availability,” requires transient user activation, and can reject. A DOM fallback is optional and must be silent. Orientation should be locked only through Telegram's `lockOrientation()` when available, because generic `screen.orientation.lock()` is not supported in Android WebView and may reject elsewhere.
**Primary recommendation:** implement a single `fullscreen.ts` Telegram capability wrapper that (1) requires Bot API >= 8.0 and method presence, (2) calls `Telegram.WebApp.requestFullscreen()` at the guidance-start edge, (3) observes `fullscreenChanged`/`fullscreenFailed`, and (4) treats failure as a non-error fallback to a fully usable portrait/landscape layout; then prove it through one automated matrix plus one honest four-client walk.
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Fullscreen request / orientation lock | Browser / Client | Telegram client | The Mini App owns the capability guard and calls Telegram's Bot API 8.0 methods from the active gesture/start boundary. |
| Landscape cockpit geometry | Browser / Client | CDN / Static | CSS media queries and safe-area variables live in the web app; the map cartography remains unchanged. |
| Failure-state rendering | Browser / Client | API / Backend | The client must render honest states; backend null/error semantics supply stable categories. |
| Version-gated old-client fallback | Browser / Client | Telegram client | Method absence and version checks occur in the web app; the Telegram client decides support. |
| Source privacy gates | API / Backend | Browser / Client | One repo tool scans all `src` trees and production bundles; failures must be deterministic exit codes. |
| Live KV expiry/absence proof | Database / Storage | API / Backend | Wrangler lists actual namespace keys and expiration metadata; source gates prove every writer carries TTL. |
| Coordinate-free logging proof | API / Backend | CDN / Static | Worker logs are category-only; the audit scans log call sites and runtime output. |
| Runtime network allowlist proof | Browser / Client | CDN / Static | Production bundle scan proves only approved tile, Telegram, and own-Worker hosts exist. |
| Worker CPU/subrequest accounting | API / Backend | Database / Storage | Worker invocation metrics, R2/KV/Telegram subrequests, and cron behavior determine free-tier headroom. |
| Golden-route regression | API / Backend | Browser / Client | Routing/search corpus CLIs and in-suite assertions already exist and should be invoked together. |
| Static-asset cache policy | CDN / Static | — | `_headers` controls browser cache behavior for fingerprinted Vite assets and HTML. |
| Battery measurement | Browser / Client | Human checkpoint | The app controls GPS cadence; the human records phone battery before/after a real run. |
| Launch completion | Browser / Client | API / Backend | The human signs the checklist after all automated and physical evidence exists. |
## Project Constraints (from AGENTS.md)
- Telegram bot + Telegram Mini App is the product; no independent mobile app in v1. [VERIFIED: AGENTS.md]
- One Cloudflare Worker serves static assets, `/telegram/webhook`, and `/api/*`; Workers superseded Pages. [VERIFIED: AGENTS.md; bot/wrangler.jsonc]
- Every service must be open-source or genuinely free-tier; no paid APIs. [VERIFIED: AGENTS.md]
- Privacy: no location history/profiles without explicit opt-in; v1 has zero telemetry. [VERIFIED: AGENTS.md; D-02]
- KL metro is the v1 region. [VERIFIED: AGENTS.md]
- Telemetry Rams values are binding: exact palette, Space Grotesk/Geist typography, 8px grid, zero border radius, no shadows/blur/glass. [VERIFIED: AGENTS.md]
- Telemetry Rams tokens are the single styling authority; the `design-taste-frontend` skill's existing-design-system special case applies, but its defaults do not override Rams tokens. This phase's UI surface is limited to landscape media queries and failure-state rendering. [VERIFIED: AGENTS.md; project skill note]
- Use npm workspaces and the repo's custom Node test harnesses; no third-party test framework is currently established. [VERIFIED: AGENTS.md]
- Git hygiene: generated output/local secrets are ignored; public GPL-3.0 publication is part of the launch checklist. [VERIFIED: AGENTS.md; D-09]
- Before direct file changes, use the GSD workflow; planning artifacts are the intended output here. [VERIFIED: AGENTS.md]
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Telegram Mini Apps WebApp JS | 8.0 API floor | Native Mini App fullscreen, orientation lock, safe-area fields/events | Official platform API; D-03 already locks the 8.0 guard. [CITED: https://core.telegram.org/bots/webapps] |
| Browser Fullscreen API | Platform feature-detect | Optional DOM fallback outside Telegram | Standard web API but limited support and transient-user-activation requirement. [CITED: https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen] |
| Screen Orientation API | Platform feature-detect | Diagnostics only; do not make it the primary lock | Not supported in Android WebView; rejects for several reasons. [CITED: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock; Chromium issue 40939281] |
| Cloudflare Workers + KV + R2 | Free plan | Runtime, ephemeral state, road graph | Existing settled stack; official current limits documented below. [VERIFIED: repo; CITED: Cloudflare limits/pricing/KV docs] |
| Wrangler | 4.151.3 declared | KV listing, deploy/canary, bundle inspection | Already installed in `bot`; `kv key list` supports namespace id and prefix. [VERIFIED: bot/package.json; CITED: https://developers.cloudflare.com/kv/reference/kv-commands/] |
| Node.js test harnesses | Repo-local | Deterministic source/route/guidance gates | Existing `node:test`-style runners avoid a new test dependency. [VERIFIED: repo package scripts] |
| Playwright | 1.50.0 declared | Browser viewport/orientation/failure-state captures | Already present in `web`; can exercise landscape media queries. [VERIFIED: web/package.json] |
| Vite | 8.3.5 declared | Production bundle and harness bundle | Already the Mini App bundler; output feeds Workers static assets. [VERIFIED: web/package.json] |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| Android Battery Historian / `adb` | Platform tool | Optional corroboration of a recorded phone run | Use only if already available; D-04 requires one simple recorded measurement, not a lab harness. [CITED: https://developer.android.com/topic/performance/power/battery-historian] |
| `node:zlib` | Node builtin | Gzip size measurements for budget audit | Use in `budget-audit`; no new dependency. [VERIFIED: routing/scripts/route-corpus.mjs precedent] |
| `process.hrtime.bigint()` | Node builtin | Deterministic A* CPU measurement | Reuse existing `routing/scripts/measure-route-cpu.mjs`. [VERIFIED: repo] |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Telegram `WebApp.requestFullscreen()` | DOM `documentElement.requestFullscreen()` | DOM API may reject without transient activation and has limited iOS/WebView support; use only as optional fallback. [CITED: MDN] |
| Telegram `lockOrientation()` | `screen.orientation.lock("landscape")` | Generic lock is unavailable in Android WebView and can reject; Telegram API locks current orientation, not an arbitrary target. [CITED: MDN; Telegram docs] |
| A single new audit framework | Existing custom Node runners plus one orchestration script | Fits the repo, GPL-friendly, and avoids a dependency for a finite gate vocabulary. [VERIFIED: repo conventions] |
| Full Android battery instrumentation | One human-recorded start/end run | D-04 explicitly forbids over-engineering a bench harness. [VERIFIED: D-04] |
**Installation:**
```bash
# No new runtime dependency is required for the recommended implementation.
# Existing workspace installs should already provide:
npm install
npm run bot:test
npm run web:test
npm run routing:test
npm run search:test
```
**Version verification:** The environment used for this research could not run `node`/`npm` (`node: command not found`), so current registry versions were not freshly queried. Declared repository versions above were read from package manifests and are current **for this checkout**, not independently registry-verified. [VERIFIED: repo manifests]
## Architecture Patterns
### System Architecture Diagram
```text
[Driver starts guidance]
|
v
[web/src/telegram/fullscreen.ts capability wrapper]
| | |
| | +-- absent/failure --> [portrait + landscape CSS remains usable]
| +-- Telegram >= 8.0 --> requestFullscreen() --> fullscreenChanged/failed
| |
| +-- lockOrientation() where available
v
[Guidance cockpit + CSS landscape media queries]
| | |
| | +-- [map cartography stays Rams-constant]
| +-- [safe-left/right/top/bottom vars]
+-- [route/guidance failure state] --> [designed empty/error/off-road/no-geometry card]
[Local automated gates]
|
+--> npm run privacy-audit
| +-- source grep: storage APIs / coordinate logs / runtime fetch hosts
| +-- production bundle scan: zero analytics/beacons/unapproved hosts
| +-- live KV listing: key families + expirations only
|
+--> npm run budget-audit
| +-- route corpus + search corpus + CPU measurement
| +-- bundle bytes + KV write classes + asset cache headers
|
+--> [one human device matrix + Android battery run]
|
v
[07-LAUNCH-GATE.md signed]
|
v
[private beta links distributed by human]
```
### Recommended Project Structure
```text
scripts/
├── privacy-audit.mjs # orchestration and human-readable evidence report
├── budget-audit.mjs # orchestration and current-vs-limit report
└── lib/
├── source-gates.mjs # comment-stripped source scans
├── bundle-gates.mjs # production bundle scans
└── cloudflare-probes.mjs # KV list + live headers/metrics probes
web/src/telegram/
└── fullscreen.ts # Telegram 8.0 capability wrapper + fallback
web/test/
└── fullscreen.test.mjs # capability/event/failure vectors
web/scripts/
└── capture.mjs # extend existing harness with landscape/failure legs
docs/
├── KNOWN-LIMITATIONS.md
└── BUDGETS.md # checked-in current-vs-limit record
.planning/phases/07-hardening-private-launch/
└── 07-LAUNCH-GATE.md # signed checklist, battery method/result, device verdicts
```
### Pattern 1: Capability-Guarded Native Fullscreen with Honest Fallback
**What:** At the guidance-start edge, request Telegram Mini App fullscreen only when Bot API 8.0 and the method are present. Listen to `fullscreenChanged` and `fullscreenFailed`. A rejected request is not an app error: guidance continues, and the layout remains usable in either orientation. Use `lockOrientation()` only to lock the **current** orientation; do not attempt to force landscape.
**When to use:** Guidance has just started and the driver has initiated the action. This is the only phase where occupying the whole screen is appropriate.
**Example:**
```typescript
// Source: https://core.telegram.org/bots/webapps
export interface TelegramWebAppLike {
// existing members...
requestFullscreen?(): void;
exitFullscreen?(): void;
isFullscreen?: boolean;
lockOrientation?(): void;
unlockOrientation?(): void;
onEvent?(event: string, cb: (...args: unknown[]) => void): void;
offEvent?(event: string, cb: (...args: unknown[]) => void): void;
}
export function requestGuidanceFullscreen(wa: TelegramWebAppLike | null): boolean {
if (
wa === null ||
typeof wa.requestFullscreen !== "function" ||
typeof wa.isVersionAtLeast !== "function" ||
!wa.isVersionAtLeast("8.0")
) return false;
try {
wa.requestFullscreen();
return true;
} catch {
return false; // absence/failure is a fallback, never a broken guidance state
}
}
```
**Required behavior:**
- Extend the existing narrow `TelegramWebAppLike` contract and Mock-Telegram fake in the same change, following the repo's established “contract + fake + test together” law. [VERIFIED: web/src/telegram/webapp.ts]
- Treat `fullscreenFailed` as a category event, not a user-facing error.
- Do not log coordinates, user identifiers, or route payloads in the fallback path. [VERIFIED: D-01; existing D-14 category-only law]
- Do not block guidance start on fullscreen success; the call is best effort.
### Pattern 2: Landscape as CSS Geometry Adaptation, Not a New Map
**What:** Keep the map Rams-constant. Adapt only the cockpit/banner/card geometry with media queries and safe-area variables. The existing HTML already has `viewport-fit=cover`, and `safe-area.ts` writes `--safe-top/right/bottom/left` from CSS env and Telegram 8.0 insets, taking the larger per edge. [VERIFIED: web/index.html; web/src/telegram/safe-area.ts]
**When to use:** Always. Portrait remains the default; landscape is an additional constraint, not a separate screen.
**Example:**
```css
/* Source: https://webkit.org/blog/7929/designing-websites-for-iphone-x/
Tokens remain binding; no new colors, radii, or shadows. */
@media (orientation: landscape) and (max-height: 560px) {
.guide-banner {
min-height: 72px;
padding-left: max(var(--spacing-space-md), var(--safe-left));
padding-right: max(var(--spacing-space-md), var(--safe-right));
}
.guide-list {
max-width: 420px;
max-height: calc(100dvh - 104px - var(--safe-top) - var(--safe-bottom));
}
}
```
**Cautions:**
- `env(safe-area-inset-*)` is not a replacement for margins; combine it with normal token spacing using `max()` and provide fallback declarations. [CITED: WebKit safe-area guidance]
- The current shell uses `height: 100%` and `position: fixed`; test whether `100dvh`/`100svh` is needed when Telegram chrome and orientation changes occur. Do not change the sizing model without a failing capture. [VERIFIED: web/src/shell.css]
### Pattern 3: Failure-State Matrix as Data
**What:** Represent each failure class as a named vector with its expected DOM/state token and action. The repo already uses named vectors and custom runners. The phase should enumerate empty, error, timeout, off-road, no-geometry, and older-client fallback rather than ad-hoc screenshots.
**When to use:** Every failure-state change and every client-matrix rerun.
**Example:**
```javascript
const FAILURE_VECTORS = [
{ id: "empty-route", trigger: "route options=[]", expect: "pending-card" },
{ id: "route-error", trigger: "route source throws", expect: "pending-card" },
{ id: "no-geometry", trigger: "polyline decodes to <2 points", expect: "pending-card" },
{ id: "off-road", trigger: "3 fixes beyond accuracy-aware threshold", expect: "rerouting-or-no-reroute" },
{ id: "location-timeout", trigger: "LocationManager returns null after timeout", expect: "denied-or-awaiting-card" },
{ id: "old-client", trigger: "WebApp version < 8.0 / methods absent", expect: "guidance-usable-without-fullscreen" },
];
```
**Required behavior:**
- No blank screen, broken layout, fabricated route, or crash. [VERIFIED: SC2; Phase 3/5/6 conventions]
- Off-road is already a calm state machine with grace, storm guard, and `no_reroute` degradation; harden its rendering rather than redesigning it. [VERIFIED: web/src/guidance/guidance.ts]
- Route `null`/throw already renders the designed pending card; audit coverage, not behavior. [VERIFIED: web/src/route/route.ts]
### Pattern 4: Consolidated Audit as Deterministic Exit-Code Tool
**What:** One orchestration command runs source scans, bundle scans, live probes, and existing corpus gates. It prints evidence and exits 0 only when every required gate passes; 1 for a gate failure; 2 for missing credentials/environment so “not run” is not mistaken for “pass.”
**When to use:** Before every phase wave merge and at the launch gate.
**Example:**
```javascript
// Pattern, not copied from an upstream source.
const results = [
await runStorageGate(),
await runCoordinateLogGate(),
await runRuntimeHostGate(),
await runBundleTelemetryGate(),
await runKvExpiryProbe(),
];
for (const result of results) {
console.log(`${result.id}: ${result.pass ? "PASS" : "FAIL"} — ${result.evidence}`);
}
process.exit(results.every((r) => r.pass) ? 0 : 1);
```
### Pattern 5: Reproducible Budget Record
**What:** Reuse the existing corpus and CPU measurements, add bundle/KV/cache measurements, and compare every current number with a documented free-plan ceiling. Store both the machine-readable output and a human-written record.
**When to use:** Phase close and launch checklist.
**Required free-plan reference values:** [CITED: https://developers.cloudflare.com/workers/platform/limits/; https://developers.cloudflare.com/kv/platform/limits/; https://developers.cloudflare.com/workers/platform/pricing/]
| Resource | Free-plan ceiling |
|----------|-------------------|
| Worker HTTP CPU | 10 ms/invocation |
| Cron CPU | 10 ms/invocation |
| Worker subrequests | 50/request |
| Worker requests | 100,000/day |
| Worker memory | 128 MB/isolate |
| Worker bundle | 64 MiB uncompressed |
| Static assets | free and unlimited requests; 20,000 files/version; 25 MiB/file |
| KV reads | 100,000/day |
| KV writes to different keys | 1,000/day |
| KV writes to same key | 1/second |
| KV list requests | 1,000/day |
| KV storage | 1 GB/account and namespace |
| KV operations/invocation | 1,000 |
| Workers Logs Free | 200,000 events/day; 3-day retention |
### Anti-Patterns to Avoid
- **DOM fullscreen as the primary path:** it is gesture-gated and limited availability; the Telegram-native API is the supported product surface. [CITED: MDN]
- **Forcing landscape through `screen.orientation.lock("landscape")`:** unsupported in Android WebView and rejects in several states; Telegram `lockOrientation()` locks the current orientation. [CITED: MDN; Chromium issue]
- **Treating a failed fullscreen request as guidance failure:** the fallback is a deliberate supported state.
- **Grep-only privacy audit:** source scans do not prove production bundles contain no analytics or unapproved hosts; include a bundle scan. [CITED: D-01]
- **Listing KV keys but ignoring `expiration`:** key presence alone does not prove TTL-only; inspect the expiry metadata returned by the list API. [CITED: Cloudflare KV list-keys docs]
- **Calling the cron “zero writes” without a runtime probe:** the cron lists/reads and sends to Telegram; D-06 requires measured write counts by request class.
- **Synthesizing battery results:** D-04 requires a recorded real run and caveats.
- **Marking unavailable device legs PASS:** follow the Phase 3/4/6 honesty protocol; NOT RUN is not PASS. [VERIFIED: existing device gates]
- **Redesigning the visual system during hardening:** Rams tokens remain binding; this phase adapts geometry only.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Mini App fullscreen capability detection | A version-string parser or client-name heuristic | Existing `isVersionAtLeast("8.0")` plus method-presence guard | Official version/floor contract; method absence still matters. [CITED: Telegram docs] |
| Orientation locking | A custom native-bridge protocol | Telegram `lockOrientation()`/`unlockOrientation()` with fallback | Native WebView orientation lock is unavailable; Telegram exposes the supported product API. [CITED: MDN; Telegram docs] |
| Test framework | A new assertion/lint framework | Repo-local Node runners and named vectors | Existing custom harness is the established convention. [VERIFIED: repo] |
| CSS safe-area probing | New per-component inset logic | Existing `safe-area.ts` + CSS `env()` + Telegram `safeAreaInset` union | One authority already exists; extend it rather than duplicating. [VERIFIED: web/src/telegram/safe-area.ts] |
| Cloudflare KV inventory | A bespoke dashboard scraper | `wrangler kv key list --namespace-id ... --prefix ...` | Official CLI supports namespace/binding/prefix. [CITED: Cloudflare docs] |
| Static cache policy | Worker code that special-cases asset URLs | `web/public/_headers` rules for hashed assets and HTML | Workers static assets natively consume `_headers`; default is `public, max-age=0, must-revalidate`. [CITED: Cloudflare static-assets headers docs] |
| Route CPU benchmark | A new profiler | `routing/scripts/measure-route-cpu.mjs` | Deterministic 25-route sample, warm-up, p95<=8 ms gate already exists. [VERIFIED: repo] |
| Golden search/route acceptance | New ad-hoc queries | `search:corpus`, `routing:corpus`, and their in-suite gates | Same measurement is already shared by CLI and test suite. [VERIFIED: repo] |
| Battery lab harness | A synthetic bench | One recorded human run | D-04 explicitly prohibits over-engineering. |
| Launch tracking system | A task tracker or landing page | Signed Markdown gate checklist | D-09 defines the gate file as the completion artifact. |
**Key insight:** Nearly every risky part of this phase already has a local authority: source gates, a typed Telegram facade, a safe-area resolver, route/search corpus runners, and a CPU measurement. Consolidation and evidence are lower-risk than new infrastructure.
## Common Pitfalls
### Pitfall 1: Requesting Fullscreen Outside the User-Activation Window
**What goes wrong:** A DOM `requestFullscreen()` call rejects with a type/permission error if the document lacks transient user activation or permission policy permits it. [CITED: MDN]
**Why it happens:** Guidance activation may happen after asynchronous location/routing work, losing the initiating gesture.
**How to avoid:** Prefer Telegram's `requestFullscreen()` at the guidance-start transition. If a DOM fallback is retained, invoke it only in the direct click handler and catch rejection.
**Warning signs:** `fullscreenFailed`, `fullscreenerror`, or no `fullscreenChanged` event after guidance starts.
### Pitfall 2: Assuming Orientation Lock Exists Because the Method Name Does
**What goes wrong:** `screen.orientation.lock()` is absent or nonfunctional in Android WebView; elsewhere it can reject with `InvalidStateError`, `SecurityError`, `NotSupportedError`, or `AbortError`. [CITED: MDN; Chromium issue 40939281]
**Why it happens:** Presence of an API is treated as support.
**How to avoid:** Use Telegram `lockOrientation()` only after the Bot API 8.0/method guard. It locks the current orientation; never assume it can force landscape. Treat rejection as non-error.
### Pitfall 3: Ignoring Telegram's `safeAreaInset` in Fullscreen
**What goes wrong:** Fullscreen content overlaps Telegram controls or the device notch/status area. Telegram documents separate `safeAreaInset` and `contentSafeAreaInset` fields and events. [CITED: Telegram docs]
**Why it happens:** Portrait CSS works, but fullscreen/landscape changes the insets.
**How to avoid:** Re-resolve safe insets on `safeAreaChanged` and `contentSafeAreaChanged`; current code computes them once at boot. Add an update path and test a changed-inset event.
### Pitfall 4: Using `100vh` as if It Were Stable
**What goes wrong:** Rotating or entering fullscreen changes visible viewport size; fixed elements can be clipped or overlap.
**Why it happens:** CSS assumes a static viewport.
**How to avoid:** Verify `100dvh`/stable-height behavior in Telegram Android/iOS captures. Keep the current fixed-shell model unless a failing leg proves a change.
### Pitfall 5: Source Privacy Greps Produce False Confidence
**What goes wrong:** A source tree is clean, but a production dependency or built bundle contains analytics, a beacon, or a coordinate-bearing payload.
**Why it happens:** The scan stops at first-party source.
**How to avoid:** Scan production `web/dist` and dependency manifests for analytics/beacon/network hosts, and verify the runtime fetch allowlist: OpenFreeMap tiles, Telegram API, and the app's own Worker. [CITED: D-01]
### Pitfall 6: Naive Comment Stripping or String Matching
**What goes wrong:** A prose mention triggers a ban, or a dynamic URL/host evades a literal string scan.
**Why it happens:** Grep gates are treated as parsers.
**How to avoid:** Reuse the repo's comment-stripped source scan, but parse manifests/imports and enumerate network call sites. Prefer positive allowlists for runtime hosts over negative token lists.
### Pitfall 7: KV Listing Without Expiry or Pagination
**What goes wrong:** Keys appear TTL-less because the probe ignores the `expiration` field or stops before `list_complete`. [CITED: Cloudflare KV list docs]
**Why it happens:** The command output is treated as a simple key array.
**How to avoid:** Parse key name, expiration, pagination cursor, and `list_complete`; assert every live key family has an expiry and report the oldest/newest expiry.
### Pitfall 8: CPU Measurement That Does Not Match the Live Request
**What goes wrong:** A local benchmark passes but the deployed `/api/route` still exceeds 10 ms, or vice versa.
**Why it happens:** Process wall/CPU time, Worker CPU accounting, and graph warm state differ.
**How to avoid:** Re-run the existing deterministic ladder and compare against live Workers invocation CPU metrics. Report both; do not substitute one for the other. The existing script's p95 budget is 8 ms under the 10 ms free ceiling. [VERIFIED: routing/scripts/measure-route-cpu.mjs; CITED: Cloudflare limits]
### Pitfall 9: Double-Counting Static Assets as Worker Requests
**What goes wrong:** The budget report inflates Worker usage or expects a hit rate on requests that never invoke the Worker.
**Why it happens:** Asset traffic and dynamic traffic are conflated.
**How to avoid:** State that static asset requests are free and unlimited; measure Worker invocations separately. The repo's `run_worker_first` array limits Worker execution to `/api/*` and `/telegram/*`. [CITED: Cloudflare pricing; VERIFIED: bot/wrangler.jsonc]
### Pitfall 10: Assuming a Cache Hit Rate Without Cache Headers
**What goes wrong:** The budget record claims good cache behavior, but hashed assets retain the default `max-age=0, must-revalidate` and no `_headers` policy exists. [CITED: Cloudflare static-assets headers docs]
**Why it happens:** Cache headers are inferred rather than inspected.
**How to avoid:** Add/verify `web/public/_headers`: long immutable caching for Vite hashed `/assets/*`, no-cache/revalidation for HTML, then inspect live response headers and `CF-Cache-Status`. Treat `CF-Cache-Status` as probabilistic per Cloudflare's own footnote.
### Pitfall 11: Turning a Measured Battery Percentage into a Synthetic Claim
**What goes wrong:** A single short or interrupted run is extrapolated into “<10%/hr.”
**Why it happens:** The target feels like a unit test rather than an observation.
**How to avoid:** Record device/OS, brightness, network, screen-on interval, start/end percentages, route length, and caveats. Calculate elapsed-hour rate from actual minutes. If the run is under 30 minutes or interrupted, mark it NOT RUN and keep the gate open. [CITED: D-04]
### Pitfall 12: Closing the Phase With Unverified Hardware Legs
**What goes wrong:** A browser replay is treated as proof of an iPhone or Desktop WebView behavior.
**Why it happens:** The launch is time-boxed and the checklist needs a signature.
**How to avoid:** Preserve the Phase 4/6 gate honesty protocol. Record PASS/FAIL/NOT RUN with client version and observations; the milestone completes only when the human signs. [VERIFIED: existing gate files; D-09]
## Code Examples
Verified patterns from official sources:
### Telegram Native Fullscreen and Orientation Guard
```typescript
// Source: https://core.telegram.org/bots/webapps
const supported =
wa !== null &&
typeof wa.isVersionAtLeast === "function" &&
wa.isVersionAtLeast("8.0") &&
typeof wa.requestFullscreen === "function";
if (supported) {
wa.requestFullscreen?.();
// lockOrientation locks the CURRENT mode; it is not a force-landscape call.
wa.lockOrientation?.();
}
```
### Safe-Area-Aware Edge Padding
```css
/* Source: https://webkit.org/blog/7929/designing-websites-for-iphone-x/ */
.guide-banner {
padding-left: 12px;
padding-left: max(12px, env(safe-area-inset-left, 0px));
padding-right: 12px;
padding-right: max(12px, env(safe-area-inset-right, 0px));
}
```
### Wrangler KV Listing Probe
```bash
# Source: https://developers.cloudflare.com/kv/reference/kv-commands/
npx wrangler kv key list \
--namespace-id=59f58a54bc44483e89872b20a005a5eb \
--prefix="guidance:"
```
The list response includes each key's `name`, optional absolute `expiration`, optional metadata, `list_complete`, and cursor. [CITED: https://developers.cloudflare.com/kv/api/list-keys]
### Static Asset Cache Rules
```text
# Source: https://developers.cloudflare.com/workers/static-assets/headers/
/assets/*
Cache-Control: public, max-age=31556952, immutable
/
Cache-Control: public, max-age=0, must-revalidate
```
### Existing Offline Golden-Route Gate
```bash
# Source: repository routing/scripts/route-corpus.mjs
npm run routing:corpus
npm run routing:test
npm run search:corpus
npm run search:test
node routing/scripts/measure-route-cpu.mjs
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Telegram Mini App expanded height only | Native Mini App fullscreen, safe-area fields/events, orientation lock | Bot API 8.0, 2024-11-17 | PX-04 can use a supported product API; older clients need fallback. [CITED: Telegram docs] |
| Assume `Element.requestFullscreen()` is universally available | Capability detection plus Telegram-native path; DOM API is limited availability | Ongoing platform divergence | Avoids iOS/WebView activation and policy failures. [CITED: MDN] |
| One boot-time safe-area read | Dynamic safe/content safe-area events and re-resolution | Bot API 8.0 | Fullscreen and orientation changes can update insets. [CITED: Telegram docs] |
| Manual asset cache guesses | Workers static assets default `max-age=0, must-revalidate`, overridable with `_headers` | Current Workers static assets | Budget audit should inspect real headers, not assume caching. [CITED: Cloudflare docs] |
| Privacy proof via unit tests only | Source + bundle + live KV/expiry + signed written record | Phase 7 decision D-01 | End-to-end PRIV-02/GEO-03 evidence for forks. |
**Deprecated/outdated:**
- `wrangler kv:key list` colon syntax is deprecated; current docs use `wrangler kv key list`. [CITED: Cloudflare KV commands]
- Battery Historian is no longer actively maintained; it can still corroborate an Android run, but simple phone battery readings are the required D-04 evidence. [CITED: Android Developers]
- `screen.orientation.lock()` should not be treated as the Android WebView orientation solution; Chromium reports WebView does not support it. [CITED: Chromium issue 40939281]
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | Telegram's native `requestFullscreen()` is preferable to the DOM Fullscreen API for every matrix client. | Architecture Patterns / Standard Stack | MEDIUM: iOS/Telegram client behavior is not fully described by official docs. Validate in the real-device walk; DOM fallback remains harmless. |
| A2 | Telegram `lockOrientation()` is useful but locks the current orientation rather than forcing landscape. | Pitfalls / Architecture | MEDIUM: The official text says “current mode”; keep CSS landscape support as the source of truth regardless. |
| A3 | The current shell can adapt to landscape with CSS-only changes without changing the fixed-position model. | Landscape Pattern | MEDIUM: Telegram viewport/fullscreen behavior may require `dvh`; capture first, change only on a proven failure. |
| A4 | The production bundle's only runtime fetch hosts are OpenFreeMap tiles, Telegram API, and the app's own Worker. | Privacy Audit | MEDIUM: MapLibre/telemetry endpoints and Telegram redirect URLs can vary. Produce the actual host inventory before signing. |
| A5 | The live KV namespace contains only `session:`, `chat:`, `loc:`, and `guidance:` key families, all with expiry. | Privacy Audit / KV Probe | MEDIUM: stale/manual keys may exist. The probe must report unexpected families rather than assume absence. |
| A6 | The current A* measurement can pass the 8 ms p95 gate against the checked-in graph. | Budget Audit | MEDIUM: the Phase 6 file records historical failures and mitigation; the current live graph may differ. Re-run and report, never assume. |
| A7 | A single Android run can produce a defensible <10%/hr observation. | Battery Method | MEDIUM: battery percentage granularity, background activity, and route conditions can dominate. Record caveats; do not synthesize. |
| A8 | Adding `_headers` for hashed assets and HTML will preserve Worker-free static delivery. | Budget / Cache Headers | LOW: documented Workers static-assets behavior; verify live headers. |
| A9 | The existing custom test runners can host the new audit and landscape vectors without a new framework. | Validation Architecture | LOW: established repo convention. |
| A10 | Phase 3 A1/A3 stale-fix assumption notes are locatable and closable/re-pointable in this phase. | Deferred Debt | LOW: locate during implementation; D-07 is explicit. |
## Open Questions (RESOLVED)
*All five questions below were CLOSED by the Phase 7 plans. Each item carries
its adopted disposition. The recommendations were adopted verbatim — the
plans implement them; none was silently dropped.*
1. **Does Telegram iOS 8.0+ reliably emit fullscreen success/failure events to the Mini App?**
- What we know: The official API exposes `requestFullscreen`, `isFullscreen`, `fullscreenChanged`, and `fullscreenFailed`; the exact per-client reliability is not specified. [CITED: Telegram docs]
- What's unclear: iOS/Telegram Desktop event delivery and whether WebView-native fullscreen is used.
- Recommendation: Treat events as observability, not correctness. The UI must be usable without receiving either event; record each client's observed behavior in the gate.
- RESOLVED: **Adopted — events are observability, never correctness.** `web/src/telegram/fullscreen.ts` exposes `watchFullscreenEvents(wa, onChange)` with a no-op-safe, try/catch-wrapped handler that logs NOTHING; it is REGISTERED once at the guidance-start edge in `web/src/route/route.ts` (07-01 task 4) with a deliberately no-op callback. The UI never depends on either event. Per-client observed behaviour is recorded in the 07-06 device matrix (`07-LAUNCH-GATE.md` Part A).
2. **What battery rate did the chosen Android phone actually record?**
- What we know: D-04 requires one >=30-minute screen-on run and no synthesis.
- What's unclear: Phone model, brightness, cellular/Wi-Fi, route conditions, and background apps.
- Recommendation: Use the exact checklist fields in `07-LAUNCH-GATE.md`; if unavailable, mark NOT RUN and hold the launch gate.
- RESOLVED: **Answered by measurement at the launch gate per D-04/D-06.** `07-LAUNCH-GATE.md` Part B carries the exact checklist field list (device/model, OS + client version, brightness, network, ≥30-minute screen-on interval, start %, end %, route description, elapsed minutes, computed %/hr, caveats); a shorter or interrupted run is recorded NOT RUN and the gate stays open (07-06 t1 + t2). No number is synthesized.
3. **What is the live route CPU p95 now?**
- What we know: The script measures deterministic p95 with an 8 ms gate; Phase 6 recorded historical failures/mitigations. [VERIFIED: repo]
- What's unclear: Current live graph and Worker CPU metrics.
- Recommendation: Re-run the script and collect Workers Logs CPU data for the same route class; report both and do not label the budget green if either exceeds its budget.
- RESOLVED: **Answered by measurement at the launch gate per D-04/D-06.** The local ladder is RE-RUN as a child process by the `route-cpu` budget gate (07-04 t2) and its p95 is compared to the 8 ms budget against the 10 ms free-plan ceiling; the live Workers Logs CPU number is a SEPARATE measurement recorded in `docs/BUDGETS.md` §Statement — neither substitutes for the other, and the budget is not labelled green if either exceeds its budget (07-04 t2/t3, 07-06 Part C).
4. **Which static assets are actually long-cacheable?**
- What we know: Vite emits hashed `/assets/*`; Workers defaults to revalidation and supports `_headers`.
- What's unclear: Whether the deployed build has a stable `_headers` file and whether live `CF-Cache-Status` is meaningful.
- Recommendation: Enumerate `web/dist`, add explicit rules, and verify live response headers. Treat `CF-Cache-Status` as probabilistic.
- RESOLVED: **Adopted — explicit rules + a live header probe that quotes, never interprets.** `web/public/_headers` carries exactly two rules (`/assets/*` immutable for 31556952 s, `/` `max-age=0, must-revalidate`) and is verified to reach `web/dist/` (07-04 t1); the `cache-headers` gate prints the OBSERVED live headers and quotes `CF-Cache-Status` as probabilistic (07-04 t2).
5. **Does `/help` need an absolute GitHub URL or a repository-relative limitations link?**
- What we know: D-08 says `/help` gains one line pointing at the doc; D-09 requires the public GPL repository.
- What's unclear: Final public repository URL/name.
- Recommendation: Put the URL in deployment configuration or a single strings-module constant rather than scattering it; defer final copy to implementation.
- RESOLVED: **Adopted — a SINGLE strings-module constant per locale.** `bot/src/i18n/strings.ts` carries exactly one limitations pointer line per locale (`Known limits:` / `Had panduan:`) — the same URL in both, resolved at implementation from the repo's actual HTTPS remote (`git remote -v`) and never scattered across files (07-05 t2).
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js + npm | All repo scripts, corpus gates, audit tools | ✗ in research shell | — | Use the maintainer's normal Node environment; version must be confirmed at execution start |
| Wrangler | Live KV probe, deploy/canary, Worker metrics | Unverified (`node` absent) | Manifest declares 4.151.3 | Install/use workspace dependency once Node is available |
| Cloudflare account/API credentials | Live KV and Worker audit | Unverified | — | Run source/bundle gates locally; mark live-probe gates NOT RUN and keep launch gate open |
| Real Android device | Fullscreen/landscape/battery | Human-owned per prior phases | — | NOT RUN; no synthetic pass |
| Real iOS device | Fullscreen/landscape | Phase 4 records iOS present | — | NOT RUN if unavailable |
| Telegram Desktop | Device matrix | Human-owned per prior phases | — | NOT RUN if unavailable |
| Plain browser | Device matrix + Playwright | Playwright declared in web workspace | 1.50.0 | Use browser leg and capture harness |
| ADB / Battery Historian | Optional battery corroboration | Unverified | — | D-04 only requires phone start/end readings; no fallback needed for the gate |
**Missing dependencies with no fallback:**
- A Node/npm execution environment is required to run any repository gate. The planning environment currently reports `node: command not found`; execution must occur in the project’s configured Node environment.
- Human access to the selected Android phone and the real clients is required to sign SC1/SC4; the phase cannot substitute simulated evidence.
**Missing dependencies with fallback:**
- Cloudflare live probes require credentials. Source/bundle gates and corpus gates can still run; live KV/expiry and cache-header checks remain mandatory before signing and cannot be replaced by local-only checks.
- Battery Historian is optional; D-04’s start/end phone readings are sufficient.
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Repo-local Node test harnesses (`node:test`-style): bot/web/routing/search `test/runner.mjs`; Playwright for browser captures in `web` |
| Config file | None global — each workspace has `test/runner.mjs`; Web `vite.config.ts` |
| Quick run command | `npm run web:test && npm run bot:test` |
| Full suite command | `npm run bot:test && npm run web:test && npm run routing:test && npm run search:test && npm run search:corpus && npm run routing:corpus && node routing/scripts/measure-route-cpu.mjs` |
| Phase commands | `npm run privacy-audit && npm run budget-audit` (new) |
| Physical gate | `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` — one Android+iOS+Desktop+browser walk plus one Android battery run |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| PX-04.a | Bot API <8.0 or absent method: guidance starts without fullscreen and remains usable | unit | `npm test -w web` | ❌ Wave 0 |
| PX-04.b | Bot API 8.0: `requestFullscreen()` called once at guidance start; failure is non-error | unit | `npm test -w web` | ❌ Wave 0 |
| PX-04.c | `fullscreenChanged` / `fullscreenFailed` update state without throwing | unit | `npm test -w web` | ❌ Wave 0 |
| PX-04.d | Safe-area inset changes after fullscreen/rotation are re-applied | unit | `npm test -w web` | ❌ Wave 0 |
| PX-04.e | Landscape viewport keeps banner, list, guidance card, and map usable without overlap/clipping | browser capture | `npm run capture -w web` | Existing harness; ❌ new landscape legs |
| PX-04.f | Fullscreen and landscape on Android/iOS/Desktop/browser | manual device | `07-LAUNCH-GATE.md` device matrix | ❌ Wave 0 gate file |
| PX-04.g | One Android guidance run >=30 min records battery and drains <10%/hr | manual measurement | `07-LAUNCH-GATE.md` battery record | ❌ Wave 0 gate file |
| SC2.a | Empty route options render designed pending card | unit/integration | `npm test -w web` | Existing route surface gates; confirm coverage |
| SC2.b | Route source error/timeout renders designed pending/no-route state | unit/integration | `npm test -w web` | ❌ explicit timeout/error vectors |
| SC2.c | Off-road grace/reroute/no-reroute states render calmly | unit/replay | `npm test -w web` | Existing guidance replay; confirm DOM coverage |
| SC2.d | No-geometry route is rejected/degraded, never half-drawn | unit | `npm test -w routing && npm test -w web` | Existing codec/route gates; confirm end-to-end vector |
| SC2.e | Older Telegram client without 8.0 methods behaves sanely | unit + manual | `npm test -w web`; gate file | ❌ new old-client vector |
| SC3.a | No banned persistence API in any production `src` tree | source gate | `npm run privacy-audit` | ❌ new tool; extends `web/test/no-storage.test.mjs` and bot privacy gates |
| SC3.b | Every Worker KV write carries TTL; cron has zero writes | source + runtime | `npm run privacy-audit` | ❌ new tool; extends bot privacy/cron gates |
| SC3.c | Live KV keys are TTL-expiring and only expected families exist | live probe | `npm run privacy-audit -- --live` | ❌ new probe |
| SC3.d | Logs contain category-only vocabulary; no coordinates/chat ids/payloads | source + bundle | `npm run privacy-audit` | ❌ new tool; extends category-only law |
| SC3.e | Production bundle contains no analytics/beacon and only approved fetch hosts | bundle gate | `npm run privacy-audit` | ❌ new tool |
| SC4.a | Golden route corpus green | CLI + suite | `npm run routing:corpus && npm test -w routing` | ✅ existing |
| SC4.b | Search golden corpus green | CLI + suite | `npm run search:corpus && npm test -w search` | ✅ existing |
| SC4.c | A* CPU p95 <=8 ms local; live Worker CPU <=10 ms | measurement | `node routing/scripts/measure-route-cpu.mjs`; live logs probe | ✅ local script; ❌ live probe |
| SC4.d | Production bundle and Worker bundle within recorded budgets | measurement | `npm run budget-audit` | ❌ new tool; BA9 precedent exists |
| SC4.e | KV writes by request class recorded; cron=0 | source + runtime | `npm run budget-audit` | ❌ new tool |
| SC4.f | Static assets have intended cache headers | live header probe | `npm run budget-audit -- --live` | ❌ new probe; no `_headers` found in repo |
| SC4.g | Known-limitations doc shipped and linked from `/help` en/ms | unit/doc gate | `npm test -w bot`; link check in audit | ❌ doc + one-liner |
| SC4.h | Deferred INFO debts fixed and exact-900s boundary pinned | unit | `npm test -w bot` | ❌ new boundary vector; IN-01/02 mechanical |
### Sampling Rate
- **Per task commit:** `npm test -w web` or `npm test -w bot` for the touched workspace; run `npm run privacy-audit` when touching storage/log/network code.
- **Per wave merge:** full automated suite plus `npm run privacy-audit && npm run budget-audit`.
- **Phase gate:** Full suite green, both audits green, live probes green, physical device matrix complete, battery run recorded, limitations doc shipped, checklist signed.
### Wave 0 Gaps
- [ ] `web/test/fullscreen.test.mjs` — covers PX-04.a–d and old-client fallback.
- [ ] `web/test/failure-state-matrix.test.mjs` — covers SC2 named vectors and DOM state tokens.
- [ ] `scripts/privacy-audit.mjs` + `scripts/lib/*` — consolidated source/bundle/live audit.
- [ ] `scripts/budget-audit.mjs` + `scripts/lib/*` — consolidated budget record and live probes.
- [ ] `web/public/_headers` — hashed-asset immutable caching + HTML revalidation policy, if verified by audit.
- [ ] `bot/test/initdata.test.mjs` exact-900s vector — D-07 IN-03.
- [ ] `docs/KNOWN-LIMITATIONS.md` — D-08.
- [ ] `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` — device matrix, battery method/result, checklist signature.
- [ ] Framework install: none — use the existing custom runners.
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | yes | Existing HMAC initData validation and one-time opaque session tokens; re-pin exact boundary. [VERIFIED: bot/src/security/initdata.ts] |
| V3 Session Management | yes | 900-second session TTL and rotation; live expiry probe. [VERIFIED: bot/src/kv/sessions.ts] |
| V4 Access Control | yes | Bearer claim-derived identity; chat/user ids never accepted from client bodies. [VERIFIED: bot/src/webhook.ts] |
| V5 Input Validation | yes | UUID/slug/body caps and typed snapshot validation; preserve during hardening. [VERIFIED: bot/src/webhook.ts] |
| V6 Cryptography | yes | Web Crypto HMAC; no new cryptography; exact-900 comparator boundary pinned. [VERIFIED: bot/src/security/initdata.ts] |
### Known Threat Patterns for This Stack
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Coordinate leakage through logs | Information Disclosure | Category-only log vocabulary; source scan + runtime log review. |
| Persistent location/profile storage | Privacy / Information Disclosure | Memory-only client; TTL-only Worker KV; live key/expiry probe. |
| Telemetry beacon smuggled into bundle | Information Disclosure | Production bundle host/beacon allowlist scan. |
| Unapproved external runtime fetch | Information Disclosure / Tampering | Positive host allowlist: OpenFreeMap tiles, Telegram API, own Worker. |
| Cross-user envelope access | Elevation of Privilege | Claim-derived ownership and indistinguishable 404 responses. |
| Fullscreen request used as a crash path | Denial of Service | Capability guard, try/catch, `fullscreenFailed` fallback. |
| Orientation lock abuse/DoS | Denial of Service | Telegram-only lock; no forced orientation; reject is non-error. |
| Stale initData at exact boundary | Authentication bypass | Exact-900s inclusive-boundary test (D-07 IN-03). |
| Asset cache policy leak/staleness | Information Disclosure | Explicit `_headers`; HTML revalidation; hashed assets immutable. |
## Sources
### Primary (HIGH confidence)
- `https://core.telegram.org/bots/webapps` — Bot API 8.0 fullscreen/orientation/safe-area methods and events; version guard; current Bot API history.
- `https://telegram.org/blog/fullscreen-miniapps-and-more` — Mini Apps can use full screen in portrait or landscape; orientation locking announcement.
- `https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullscreen` — promise/rejection behavior, transient activation, permissions policy, limited availability.
- `https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock` — lock rejection types and limited availability.
- `https://webkit.org/blog/7929/designing-websites-for-iphone-x/` — `viewport-fit=cover`, `env(safe-area-inset-*)`, `max()` guidance.
- `https://developers.cloudflare.com/workers/platform/limits/` — current Free limits, CPU, subrequests, assets, logs.
- `https://developers.cloudflare.com/workers/platform/pricing/` — static assets free/unlimited; KV and Workers Free quotas.
- `https://developers.cloudflare.com/kv/platform/limits/` — KV Free reads/writes/list/storage and per-invocation operations.
- `https://developers.cloudflare.com/kv/reference/kv-commands/` — current Wrangler KV syntax and flags.
- `https://developers.cloudflare.com/kv/api/list-keys` — key expiration and pagination semantics.
- `https://developers.cloudflare.com/workers/static-assets/headers/` — default cache headers, `_headers`, `CF-Cache-Status` caveat.
- `https://developers.cloudflare.com/workers/observability/logs/workers-logs/` — invocation CPU/wall data and observability configuration.
- `https://developer.android.com/topic/performance/power/battery-historian` — battery measurement context; tool no longer actively maintained.
- Repository files listed in the phase context, especially `web/src/telegram/webapp.ts`, `web/src/telegram/safe-area.ts`, `web/src/shell.css`, `web/src/route/route.ts`, `web/src/guidance/guidance.ts`, `web/src/guidance/watch.ts`, `bot/src/webhook.ts`, `bot/test/route-privacy.test.mjs`, `web/test/no-storage.test.mjs`, `routing/scripts/measure-route-cpu.mjs`, routing/search corpus runners, and Phase 4/6 device gates.
### Secondary (MEDIUM confidence)
- Chromium issue 40939281 — Android WebView does not support `screen.orientation.lock()` and now permits feature detection to report absence.
- MDN browser-compatibility data / Can I Use search results — iOS Safari fullscreen partial support and WKWebView divergence; still requires real-device verification in Telegram.
- Telegram bug report 66082 — Android 10 fullscreen Mini App safe-area/inset issue; use as a QA warning, not as a universal current limitation.
### Tertiary (LOW confidence)
- General open-source navigation “known limitations” conventions — only section structure was synthesized from OSM/OsmAnd/GraphHopper documentation. The exact document wording remains a project choice.
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — repository manifests and official platform docs; registry freshness could not be queried because Node was unavailable in the research shell.
- Architecture: MEDIUM-HIGH — current code boundaries are verified; per-client fullscreen behavior still needs the physical walk.
- Pitfalls: HIGH for official platform constraints and repository conventions; MEDIUM for Telegram iOS/Desktop event reliability and battery interpretation.
**Research date:** 2026-10-25
**Valid until:** 2026-11-24 (platform APIs and Worker limits are current as of research; re-check Telegram client notes and Cloudflare limits before signing the launch gate)