Files
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

59 KiB
Raw Permalink Blame History

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:

# 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

[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]
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:

// 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:

/* 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:

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:

// 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

// 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

/* 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

# 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

# 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

# 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)