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.
This commit is contained in:
araharah-executor
2026-10-25 02:44:30 +08:00
parent 27b340fa6b
commit a7d8dbf345
6 changed files with 1048 additions and 78 deletions
@@ -0,0 +1,748 @@
# 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)