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:
+28
-12
@@ -26,8 +26,8 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
- [x] **Phase 3: Mini App Shell + Geolocation** - Telegram WebApp shell that boots, authenticates, and reads a reliable live location across the Android/iOS/Desktop client matrix (completed 2026-10-23)
|
||||
- [x] **Phase 4: Destination Search + KL Gazetteer** - Free-text KL search that resolves landmarks despite Malay abbreviations and typos, venue cards, and Mini App launch with destination preloaded (completed 2026-10-23)
|
||||
- [x] **Phase 5: Routing + Route Display** - Valhalla-backed route calculation shown as a Rams route line with quiet alternative cards; the full search→route→render loop lands (completed 2026-10-23)
|
||||
- [ ] **Phase 6: Turn-by-Turn Guidance (Live Driving)** - Client guidance state machine: maneuver banner, missed-turn auto-recalculation, honest ETA, haptics, seamless resume, and backgrounded chat turn-cards
|
||||
- [ ] **Phase 7: Hardening + Private Launch** - Device-matrix QA, graceful failure states, privacy-by-construction audit, and golden-route regression before real drivers
|
||||
- [x] **Phase 6: Turn-by-Turn Guidance (Live Driving)** - Client guidance state machine: maneuver banner, missed-turn auto-recalculation, honest ETA, haptics, seamless resume, and backgrounded chat turn-cards (completed 2026-10-24)
|
||||
- [ ] **Phase 7: Hardening + Private Launch** - Device-matrix QA, graceful failure states, privacy-by-construction audit, and golden-route regression before real drivers (planned 2026-10-25)
|
||||
|
||||
## Phase Details
|
||||
|
||||
@@ -198,20 +198,26 @@ Notes:
|
||||
Plans:
|
||||
|
||||
**Wave 1** *(parallel foundations — the pure cores + the envelope going live)*:
|
||||
- [ ] 06-01-PLAN.md — Engine A pure core: pinned CSR binary codec (with street-name table), GPS-fix snapping, typed-array A* + step/turn derivation, proven against a committed mini-graph by A*≡Dijkstra equivalence (D-01/D-05, GUID-02)
|
||||
- [ ] 06-02-PLAN.md — Guidance pure core: the D-04 reroute state machine with named constants, D-07 EMA+deadband ETA, D-15 distance throttle, four committed D-16 drive fixtures + replay/eta suites (GUID-02/03/04/06)
|
||||
- [ ] 06-03-PLAN.md — The frozen envelope goes LIVE: mint/get/delete TTL-only service + Bearer-gated /api/guidance POST/GET/DELETE (claim-only chat), GE4–GE7 re-pinned in the same change (GUID-05, D-14 server half)
|
||||
- [x] 06-01-PLAN.md — Engine A pure core: pinned CSR binary codec (with street-name table), GPS-fix snapping, typed-array A* + step/turn derivation, proven against a committed mini-graph by A*≡Dijkstra equivalence (D-01/D-05, GUID-02)
|
||||
- [x] 06-02-PLAN.md — Guidance pure core: the D-04 reroute state machine with named constants, D-07 EMA+deadband ETA, D-15 distance throttle, four committed D-16 drive fixtures + replay/eta suites (GUID-02/03/04/06)
|
||||
- [x] 06-03-PLAN.md — The frozen envelope goes LIVE: mint/get/delete TTL-only service + Bearer-gated /api/guidance POST/GET/DELETE (claim-only chat), GE4–GE7 re-pinned in the same change (GUID-05, D-14 server half)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 — the real graph + the cockpit chrome, in parallel)*:
|
||||
- [ ] 06-04-PLAN.md — Engine A server surface: Overpass→CSR build tool + real KL graph, R2 bucket/upload/fork docs, isolate-memoized worker-router behind Bearer-gated /api/route, client http-source backend #2, RP1/RP2 re-pins, A*-CPU measurement gate (D-01/D-03, GUID-02)
|
||||
- [ ] 06-05-PLAN.md — Guidance chrome per 06-UI-SPEC: committed 8-glyph set, banner/list/guidance-card/confirm/trip-end builders, close-protection + haptics capability guards, strings keys en+ms, shell.css guidance rules, guidance-surface suite + RS10/BA9 bumps (GUID-01/04, PX-03)
|
||||
- [x] 06-04-PLAN.md — Engine A server surface: Overpass→CSR build tool + real KL graph, R2 bucket/upload/fork docs, isolate-memoized worker-router behind Bearer-gated /api/route, client http-source backend #2, RP1/RP2 re-pins, A*-CPU measurement gate (D-01/D-03, GUID-02)
|
||||
- [x] 06-05-PLAN.md — Guidance chrome per 06-UI-SPEC: committed 8-glyph set, banner/list/guidance-card/confirm/trip-end builders, close-protection + haptics capability guards, strings keys en+ms, shell.css guidance rules, guidance-surface suite + RS10/BA9 bumps (GUID-01/04, PX-03)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 — integration)*:
|
||||
- [ ] 06-06-PLAN.md — Guidance integration: D-10 locked-follow camera, the fix pump (banner/ETA/haptics/reroute), PX-01 full BackButton state graph, minimize-POST/resume-GET/trip-end-DELETE envelope flows, RS6 re-pin (GUID-02/05/06, PX-01/03, D-14 web half)
|
||||
- [ ] 06-07-PLAN.md — Cron turn-cards: pure projectTurnCards + the repo's first scheduled() handler + triggers.crons, honest street-less caption copy, KV-write-budget pin, committed 8-glyph PNG set via sharp (GUID-05, D-11/12/13)
|
||||
- [x] 06-06-PLAN.md — Guidance integration: D-10 locked-follow camera, the fix pump (banner/ETA/haptics/reroute), PX-01 full BackButton state graph, minimize-POST/resume-GET/trip-end-DELETE envelope flows, RS6 re-pin (GUID-02/05/06, PX-01/03, D-14 web half)
|
||||
- [x] 06-07-PLAN.md — Cron turn-cards: pure projectTurnCards + the repo's first scheduled() handler + triggers.crons, street-bearing cards by default via graph reconstruction (street-less only as graph-absent degradation per D-03; D-12 with 06-08 Part C ratification), KV-write-budget pin, committed 8-glyph PNG set via sharp (GUID-05, D-11/12/13)
|
||||
|
||||
**Wave 4** *(the phase's closing gates)*:
|
||||
- [ ] 06-08-PLAN.md — Verification close: ?drive= replay knob + seven guidance capture legs + DSGN-02 gallery, then the ONE REAL DRIVE device gate (blocking checkpoint) (D-16, all reqs)
|
||||
- [x] 06-08-PLAN.md — Verification close: ?drive= replay knob + seven guidance capture legs + DSGN-02 gallery, then the ONE REAL DRIVE device gate (blocking checkpoint) (D-16, all reqs)
|
||||
|
||||
**Cross-cutting constraints** *(shared invariants enforced in multiple plans; no verbatim-duplicated truths)*:
|
||||
- **Claim-only chat identity (T-06-02):** envelope chat_id derives from the Bearer claim server-side — never a request body — in 06-03 (mint/read/delete), 06-06 (POST composition), 06-07 (cron push target)
|
||||
- **Fail-honest degradation (D-03):** absent/tampered graph → null → router-pending (06-01 decode, 06-04 `/api/route`, 06-07 street reconstruction); never a throw, never a wrong answer
|
||||
- **TTL-only KV (GE1):** every envelope write carries `expirationTtl: 7200`; active DELETE on trip end (06-03 service, 06-06 flows, 06-07 zero-write cadence)
|
||||
- **Privacy logging:** category-only logs, never coordinates/chat ids in output (06-03, 06-06, 06-07, 06-08 capture assertions)
|
||||
|
||||
Notes:
|
||||
- Highest behavioral uncertainty (reroute triggers, ETA smoothing, battery) — research recommends `/gsd-research-phase` (backgrounded turn-card "best-effort projection" framing + static map snapshot generation; product decision needed for what "good enough" means minimized).
|
||||
@@ -227,10 +233,20 @@ Notes:
|
||||
2. Every empty/error/timeout/off-road/no-geometry state renders gracefully (no blank screens, no broken layouts); version-gating fallbacks behave sanely on older Telegram clients
|
||||
3. Privacy verification passes: an architecture audit shows no persistent per-user location history or profile anywhere — coordinates exist only in TTL-expiring KV, and logs carry no coordinate data (re-verifies PRIV-02, GEO-03 end-to-end)
|
||||
4. The golden-route regression pass is green, Worker budgets audit clean (subrequests/CPU, cache hit-rate), and the "known limitations" doc (OSM data gaps, no traffic confidence, no lane guidance) ships before the first real drivers
|
||||
**Plans**: TBD
|
||||
**Plans**: 6 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 07-01-PLAN.md — Fullscreen capability layer: the Telegram 8.0-guarded `requestFullscreen()` wrapper, the grown `TelegramWebAppLike` contract, the safe-area re-resolution on inset events (WIRED at the `web/src/main.ts` boot site), the route controller's guidance-start/trip-end wiring + the fullscreen-event watcher, and the FS1–FS9 suite (PX-04, D-03)
|
||||
- [ ] 07-02-PLAN.md — Landscape geometry as CSS-only media queries (tokens binding), the FM1–FM7 failure-state matrix (empty/error/timeout/no-geometry/off-road/old-client/location-timeout), and the landscape capture legs (PX-04, SC2)
|
||||
- [ ] 07-03-PLAN.md — The consolidated `npm run privacy-audit` tool: source gates over ALL src trees, the KV TTL gate, the category-only log gate, the positive fetch-host allowlist, the production-bundle scan, the live KV expiry probe, and the signed audit record (D-01, D-02)
|
||||
- [ ] 07-04-PLAN.md — The consolidated `npm run budget-audit` tool: the re-run A* CPU ladder, the bundle-byte budgets, the KV write classes with the cron's zero-write law, the per-request-class subrequest inventory, the re-run golden corpora, the static-asset `_headers` policy, and the checked-in current-vs-free-tier budget record (D-06)
|
||||
- [ ] 07-05-PLAN.md — The parked debts paid: IN-01's dead export + intent comments, IN-02's stale wrangler comment, IN-03's exact-900s boundary pin, the `/cari` re-arm attempt, the A1/A3 closure — plus `docs/KNOWN-LIMITATIONS.md` and the `/help` pointer line (D-07, D-08)
|
||||
- [ ] 07-06-PLAN.md — The launch gate: the consolidated 4-client × 7-interaction walk, the recorded Android battery measurement, the six-item D-09 checklist, and the milestone completion record (D-04, D-05, D-09)
|
||||
|
||||
Notes:
|
||||
- No exclusive feature requirements beyond PX-04 — this phase hardens the outcomes of Phases 1–6 and is the last chance to catch the "looks done but isn't" failures for a navigation tool whose empty states ARE the polish surface.
|
||||
- **Wave structure**: Wave 1 = 07-01, 07-02, 07-03, 07-05 (four parallel plans, no file overlap) · Wave 2 = 07-04 (shares `package.json` with 07-03 and imports 07-03's live-probe library) · Wave 3 = 07-06 (the blocking launch gate — depends on all five).
|
||||
- The two manual behaviors (the physical device walk and the battery run) live in the 07-06 blocking LAUNCH-GATE checkpoint — never synthesized. NOT-RUN legs are recorded honestly and keep the checklist unsigned.
|
||||
**UI hint**: no
|
||||
|
||||
## Requirements Coverage
|
||||
@@ -264,7 +280,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
|
||||
| 4. Destination Search + KL Gazetteer | 0/4 | Ready to execute | - |
|
||||
| 5. Routing + Route Display | 0/TBD | Not started | - |
|
||||
| 6. Turn-by-Turn Guidance (Live Driving) | 0/TBD | Not started | - |
|
||||
| 7. Hardening + Private Launch | 0/TBD | Not started | - |
|
||||
| 7. Hardening + Private Launch | 0/6 | Ready to execute | - |
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-10-22*
|
||||
|
||||
@@ -10,6 +10,7 @@ files_modified:
|
||||
- web/src/telegram/safe-area.ts
|
||||
- web/src/mock/telegram-mock.ts
|
||||
- web/src/route/route.ts
|
||||
- web/src/main.ts
|
||||
- web/test/fullscreen.test.mjs
|
||||
autonomous: true
|
||||
requirements:
|
||||
@@ -28,8 +29,11 @@ must_haves:
|
||||
provides: "TelegramWebAppLike contract grown with the 8.0-optional fullscreen/orientation members"
|
||||
contains: "requestFullscreen?"
|
||||
- path: "web/src/telegram/safe-area.ts"
|
||||
provides: "Re-resolution on safeAreaChanged / contentSafeAreaChanged (applySafeAreaOnChange)"
|
||||
provides: "Re-resolution on safeAreaChanged / contentSafeAreaChanged (watchSafeAreaChanges — the exported name, never a second alias)"
|
||||
contains: "safeAreaChanged"
|
||||
- path: "web/src/main.ts"
|
||||
provides: "The boot-site call site for watchSafeAreaChanges — the insets re-resolve at runtime, not only once at boot"
|
||||
contains: "watchSafeAreaChanges"
|
||||
- path: "web/test/fullscreen.test.mjs"
|
||||
provides: "FS1..FS9 + old-client fallback vectors (PX-04.a-d, SC2.e)"
|
||||
contains: "FS1"
|
||||
@@ -50,6 +54,14 @@ must_haves:
|
||||
to: "web/src/telegram/webapp.ts"
|
||||
via: "the mock satisfies every new optional member (contract + fake + test together law)"
|
||||
pattern: "requestFullscreen"
|
||||
- from: "web/src/main.ts"
|
||||
to: "web/src/telegram/safe-area.ts"
|
||||
via: "watchSafeAreaChanges(deps.wa ?? null) is CALLED at the boot site, directly after applySafeArea(resolveSafeInsets(getWebApp())) — the insets re-resolve at runtime, not only once at boot"
|
||||
pattern: "watchSafeAreaChanges"
|
||||
- from: "web/src/route/route.ts"
|
||||
to: "web/src/telegram/fullscreen.ts"
|
||||
via: "watchFullscreenEvents(deps.wa ?? null, …) is REGISTERED at the guidance-start edge (the same edge as engageCloseProtection) — the events are observability, the callback is a no-op-safe category handler"
|
||||
pattern: "watchFullscreenEvents"
|
||||
---
|
||||
|
||||
<objective>
|
||||
@@ -64,7 +76,8 @@ guard, orientation lock only where available). The telemetry-Rams tokens stay
|
||||
binding; this plan adds no art direction and no new UI surface.
|
||||
Output: `web/src/telegram/fullscreen.ts` (new), the grown `TelegramWebAppLike`
|
||||
contract, the re-resolving safe-area module, the mock's matching members, the
|
||||
route controller's guidance-start/trip-end wiring, and
|
||||
route controller's guidance-start/trip-end wiring, the `web/src/main.ts` boot
|
||||
wiring for the safe-area watch, and
|
||||
`web/test/fullscreen.test.mjs` (new — PX-04.a–d + the old-client fallback).
|
||||
</objective>
|
||||
|
||||
@@ -161,6 +174,28 @@ Pitfall 1):
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
From `web/src/main.ts` (the BOOT site — the ONLY place the safe-area insets
|
||||
are resolved today; `watchSafeAreaChanges` must be called immediately after
|
||||
this line, or the insets never re-resolve on a real device):
|
||||
|
||||
```typescript
|
||||
async function boot(): Promise<void> {
|
||||
// 1 — safe-area vars are set before any layout reads them (PX-03 groundwork).
|
||||
applySafeArea(resolveSafeInsets(getWebApp()));
|
||||
// ← watchSafeAreaChanges(wa ?? null) belongs HERE (task 4, step 5)
|
||||
// …
|
||||
const wa = getWebApp();
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
NOTE the ORDER: `applySafeArea(resolveSafeInsets(getWebApp()))` is the FIRST
|
||||
statement of `boot()` and `const wa = getWebApp();` appears LATER in the
|
||||
same function. The watch call MUST use the same one-shot `getWebApp()` value
|
||||
as the apply call (`getWebApp()` is idempotent — it returns the same WebApp
|
||||
instance or null), so it does NOT introduce a second `getWebApp()` call and
|
||||
does NOT depend on the later `const wa` binding's position.
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
@@ -418,18 +453,20 @@ await import("../src/telegram/fullscreen.ts");`).
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>task 4: wire the fullscreen call into the guidance-start and trip-end edges</name>
|
||||
<files>web/src/route/route.ts</files>
|
||||
<name>task 4: wire the fullscreen call into the guidance-start and trip-end edges + the safe-area watch at the boot site</name>
|
||||
<files>web/src/route/route.ts, web/src/main.ts</files>
|
||||
<read_first>
|
||||
- web/src/route/route.ts (the activateGuidance / teardownGuidance edges — lines ~337 and ~474)
|
||||
- web/src/main.ts (the boot site — the `applySafeArea(resolveSafeInsets(getWebApp()))` line at the TOP of `boot()`, around line 74)
|
||||
- web/src/telegram/fullscreen.ts (the wrapper, as built in task 1)
|
||||
- web/src/telegram/safe-area.ts (the watch, as built in task 2)
|
||||
- .planning/phases/07-hardening-private-launch/07-RESEARCH.md §Pitfall 1 (the activation window)
|
||||
</read_first>
|
||||
<action>
|
||||
In `web/src/route/route.ts`:
|
||||
|
||||
1. Add the import next to the existing `close-protection.ts` import:
|
||||
`import { requestGuidanceFullscreen, exitGuidanceFullscreen } from "../telegram/fullscreen.ts";`
|
||||
`import { requestGuidanceFullscreen, exitGuidanceFullscreen, watchFullscreenEvents } from "../telegram/fullscreen.ts";`
|
||||
|
||||
2. In `activateGuidance()` — the SYNCHRONOUS guidance-start transition, the
|
||||
same edge that already calls `engageCloseProtection(deps.wa ?? null);` —
|
||||
@@ -456,29 +493,77 @@ In `web/src/route/route.ts`:
|
||||
`releaseCloseProtection(deps.wa ?? null);` — add ONE line directly after
|
||||
it: `exitGuidanceFullscreen(deps.wa ?? null);`
|
||||
|
||||
6. Add a one-line comment above each new call naming D-03 + PX-04 and the
|
||||
6. **REGISTER the fullscreen EVENT watcher at the guidance-start edge.** In
|
||||
`activateGuidance()` — the SAME edge as step 2 — add ONE line directly
|
||||
after the `requestGuidanceFullscreen(...)` line:
|
||||
`watchFullscreenEvents(deps.wa ?? null, () => { /* observability only — no state, no logging */ });`
|
||||
The callback is a deliberate NO-OP: per RESEARCH Open Question 1 the
|
||||
fullscreen events are OBSERVABILITY, never correctness — the UI must be
|
||||
fully usable without receiving either event. The registration MUST NOT
|
||||
log anything (the D-14 category-only law), MUST NOT change any state, and
|
||||
MUST NOT write any storage surface. `watchFullscreenEvents` is internally
|
||||
guarded (null WebApp / missing `onEvent` → returns immediately), so this
|
||||
call is safe in a plain browser and on an old client.
|
||||
Do NOT register the watcher in `resume(trip)` — `activateGuidance()` is the
|
||||
single guidance-start edge and the watcher must be registered exactly
|
||||
once per guidance session.
|
||||
|
||||
7. Add a one-line comment above each new call naming D-03 + PX-04 and the
|
||||
best-effort law, in the file's existing voice (e.g.
|
||||
`// PX-04/D-03: the native fullscreen request is BEST EFFORT — a refusal is a supported fallback, never a guidance failure.`).
|
||||
|
||||
In `web/src/main.ts`:
|
||||
|
||||
8. **WIRE the safe-area watch at the BOOT site.** The ONLY place the insets
|
||||
are resolved today is the FIRST statement of `boot()`:
|
||||
`applySafeArea(resolveSafeInsets(getWebApp()));`
|
||||
Add ONE line DIRECTLY after it (before the `// 3 (declared ahead…)` comment
|
||||
block):
|
||||
`watchSafeAreaChanges(getWebApp());`
|
||||
and extend the existing import to include it:
|
||||
`import { resolveSafeInsets, applySafeArea, watchSafeAreaChanges } from "./telegram/safe-area.ts";`
|
||||
|
||||
WHY THIS SITE: without this call the insets are resolved ONCE at boot and
|
||||
never again — a device rotation or a fullscreen transition would leave the
|
||||
cockpit under a notch or a Telegram control while every gate stays green.
|
||||
`watchSafeAreaChanges` re-resolves through the SAME union authority
|
||||
(`resolveSafeInsets`) on `safeAreaChanged` / `contentSafeAreaChanged`, so
|
||||
the runtime value and the boot value can never disagree.
|
||||
|
||||
Use `getWebApp()` — the SAME one-shot call the adjacent apply line uses —
|
||||
rather than the later `const wa = getWebApp();` binding (which is declared
|
||||
further down in `boot()` and would be a forward reference at this point).
|
||||
`getWebApp()` is idempotent, so this adds no second client lookup.
|
||||
`watchSafeAreaChanges` is internally guarded (null WebApp / missing
|
||||
`onEvent` → returns immediately), so the plain-browser boot path is
|
||||
unaffected.
|
||||
|
||||
Do NOT add a new console line, do NOT log the fullscreen state, and do NOT
|
||||
add any coordinate-bearing output — the route module's console law
|
||||
(`ARAHARAH-ROUTE state=…` / `ARAHARAH-GUIDE state=…` state tokens only) is
|
||||
unchanged.
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `grep -c "requestGuidanceFullscreen" web/src/route/route.ts` returns exactly 2 (activateGuidance + resume)
|
||||
- `grep -c "exitGuidanceFullscreen" web/src/route/route.ts` returns exactly 2 (teardownGuidance + stopGuidanceArtifacts)
|
||||
- `grep -c "requestGuidanceFullscreen(" web/src/route/route.ts` returns exactly 2 (activateGuidance + resume — the CALL sites; the import line does not match because it carries no `(`)
|
||||
- `grep -c "exitGuidanceFullscreen(" web/src/route/route.ts` returns exactly 2 (teardownGuidance + stopGuidanceArtifacts)
|
||||
- `grep -c "watchFullscreenEvents(" web/src/route/route.ts` returns exactly 1 (registered ONCE, at the guidance-start edge)
|
||||
- `grep -c "engageCloseProtection" web/src/route/route.ts` returns 2 (unchanged — the count did not move)
|
||||
- `grep -c "releaseCloseProtection" web/src/route/route.ts` returns 2 (unchanged)
|
||||
- `grep -c "watchSafeAreaChanges(" web/src/main.ts` returns exactly 1 (the boot-site CALL site)
|
||||
- `grep -c "watchSafeAreaChanges" web/src/telegram/safe-area.ts` returns ≥ 1 (the export still exists)
|
||||
- `grep -c "watchFullscreenEvents" web/src/telegram/fullscreen.ts` returns ≥ 1 (the export still exists)
|
||||
- `npm run web:typecheck` exits 0
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w web 2>&1 | tail -15 && npm run web:typecheck 2>&1 | tail -10</automated>
|
||||
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w web 2>&1 | tail -15 && npm run web:typecheck 2>&1 | tail -10 && echo "--- production call sites ---" && grep -c "watchSafeAreaChanges" web/src/main.ts && grep -c "watchFullscreenEvents" web/src/route/route.ts</automated>
|
||||
</verify>
|
||||
<done>
|
||||
Fullscreen is requested at guidance start (fresh and resumed) and released
|
||||
at trip end and walk-back, on the synchronous edges, best-effort, with no
|
||||
new logging.
|
||||
new logging; the fullscreen event watcher is registered exactly once at
|
||||
the guidance-start edge; and the safe-area watch is CALLED at the boot
|
||||
site in `web/src/main.ts` so the insets re-resolve on rotation and
|
||||
fullscreen at runtime.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -497,7 +582,7 @@ unchanged.
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-07-06 | Denial of Service | `web/src/telegram/fullscreen.ts` — a throwing/refused platform call | mitigate | The capability guard is BOTH halves (method presence AND `isVersionAtLeast("8.0")`); every platform call is wrapped in try/catch; `requestGuidanceFullscreen` returns a boolean the caller IGNORES, so a refusal can never fail the guidance start. Pinned by FS1–FS5 and the FS9 source-scan ban. |
|
||||
| T-07-06b | Denial of Service | `web/src/telegram/safe-area.ts` — a throwing inset-event handler | mitigate | `watchSafeAreaChanges` wraps each handler in try/catch; a client that throws on `safeAreaChanged` cannot break the boot or the guidance pump. Pinned by FS6/FS8. |
|
||||
| T-07-06b | Denial of Service | `web/src/telegram/safe-area.ts` — a throwing inset-event handler | mitigate | `watchSafeAreaChanges` wraps each handler in try/catch; a client that throws on `safeAreaChanged` cannot break the boot or the guidance pump. Pinned by FS6/FS8. The watch is ALSO CALLED at the production boot site in `web/src/main.ts` (grep: `watchSafeAreaChanges` in `web/src/main.ts` ≥ 1), so the insets re-resolve at runtime — an exported-but-uncalled watch would leave the cockpit under a notch after a rotation while every gate stayed green. |
|
||||
| T-07-09 | Information Disclosure | the fullscreen event path | mitigate | The module logs NOTHING (`grep -c "console\."` = 0); no coordinate, user id, or route payload is produced or emitted. `fullscreenFailed` is a category event, never a user-facing error. |
|
||||
| T-07-10 | Tampering | the DOM-fullscreen / generic-orientation-lock escape hatches | mitigate | The FS9 source-scan ban forbids `documentElement.requestFullscreen`, `screen.orientation.lock`, `webkitRequestFullscreen`, `mozRequestFullScreen` in the module's code — the Telegram-native path is the ONLY primary path. |
|
||||
| T-07-11 | Information Disclosure | `web/src/telegram/webapp.ts` contract growth | accept | The contract gains OPTIONAL members only; no existing member's shape changes, so no existing call site can be broken by the growth. Low-value target, no data path. |
|
||||
@@ -520,8 +605,8 @@ must all appear as `PASS`.
|
||||
<success_criteria>
|
||||
- `web/src/telegram/fullscreen.ts` exists with the both-halves 8.0 guard, try/catch on every call, no DOM-fullscreen, no generic orientation lock, and zero logging.
|
||||
- `TelegramWebAppLike` carries the five 8.0-optional members; the harness mock satisfies them in the same change.
|
||||
- `watchSafeAreaChanges` re-resolves the insets on `safeAreaChanged` / `contentSafeAreaChanged` through the existing union authority.
|
||||
- `web/src/route/route.ts` requests fullscreen at the guidance start (fresh + resumed) and releases it at trip end + walk-back — exactly 2 request sites and 2 exit sites, both on the synchronous edges.
|
||||
- `watchSafeAreaChanges` re-resolves the insets on `safeAreaChanged` / `contentSafeAreaChanged` through the existing union authority, AND it is CALLED at the boot site in `web/src/main.ts` (directly after the `applySafeArea(resolveSafeInsets(getWebApp()))` line) — the insets re-resolve at runtime, not only once at boot.
|
||||
- `web/src/route/route.ts` requests fullscreen at the guidance start (fresh + resumed) and releases it at trip end + walk-back — exactly 2 request sites and 2 exit sites, both on the synchronous edges — and registers `watchFullscreenEvents` exactly once at the guidance-start edge.
|
||||
- `web/test/fullscreen.test.mjs` carries FS1–FS9 and the whole `web` suite is green.
|
||||
</success_criteria>
|
||||
|
||||
|
||||
@@ -370,29 +370,56 @@ Extend `web/scripts/capture.mjs`:
|
||||
|
||||
2. Add ONE new landscape leg after the existing guidance legs, using the
|
||||
`?drive=corridor-follow` replay knob (the committed fixture the 06-08
|
||||
guidance legs already use) and the guidance wait mode:
|
||||
guidance legs already use) and the guidance wait mode.
|
||||
|
||||
```javascript
|
||||
/* ————— 07-02 — the landscape leg (PX-04.e) —————
|
||||
The driving posture: the phone rotated. The cockpit geometry adapts via
|
||||
the @media (orientation: landscape) block (07-02 task 1); the map stays
|
||||
Rams-constant. A 844x390 viewport IS the landscape orientation the media
|
||||
query matches (orientation: landscape + max-height: 560px). */
|
||||
await captureLeg({
|
||||
name: 'shell-guidance-landscape.png',
|
||||
query: 'mock=android&scheme=light&drive=corridor-follow',
|
||||
colorScheme: 'light',
|
||||
grantGeo: true,
|
||||
waitMode: 'guide',
|
||||
expectGuide: 'on_route',
|
||||
viewport: { width: 844, height: 390 },
|
||||
});
|
||||
```
|
||||
**COPY THE EXISTING CORRIDOR GUIDANCE LEG VERBATIM.** The new leg is the
|
||||
existing corridor guidance leg (the one that uses `corridorModel` +
|
||||
`guideQuery` — e.g. `shell-guidance-active.png`) with ONLY THREE things
|
||||
changed: the `name`, the `colorScheme`, and the `viewport`. Keep
|
||||
EVERYTHING else byte-identical — in particular:
|
||||
|
||||
Match the exact `expectGuide` value the existing guidance legs use for the
|
||||
corridor drive — READ the existing guidance legs in this file and copy
|
||||
their `expectGuide`/`waitMode` pair verbatim rather than guessing. If the
|
||||
existing corridor leg uses `waitMode: 'settle400'` instead, use that.
|
||||
- the `setup:` — `setup: (page) => stubGuidanceRoute(page, corridorModel)` —
|
||||
MUST be copied verbatim. WITHOUT this setup the `/api/route` stub is
|
||||
never installed, the route never resolves, and the leg TIMES OUT at the
|
||||
guide-state race with an `ARAHARAH-GUIDE state=… race timed out` error.
|
||||
- the `query:` — `guideQuery` MUST be copied verbatim. `guideQuery` is
|
||||
`mock=android&dest=${guidePair.destinationId}&fixpair=${guidePair.pairId}&drive=corridor-follow`
|
||||
— it carries BOTH the `dest` (the destination id the route resolves to)
|
||||
AND the `fixpair` (the fixture pair id the replay stream keys on).
|
||||
Dropping `dest` gives the leg no destination; dropping `fixpair` gives
|
||||
it no replay fixes. Both are required.
|
||||
- the `waitMode:` / `expectGuide:` pair — copy the existing corridor leg's
|
||||
pair verbatim (`waitMode: 'guide'`, `expectGuide: 'on_route'` — READ the
|
||||
file and confirm; if the existing leg uses `waitMode: 'settle400'`
|
||||
instead, use that).
|
||||
|
||||
So the leg is:
|
||||
|
||||
```javascript
|
||||
/* ————— 07-02 — the landscape leg (PX-04.e) —————
|
||||
The driving posture: the phone rotated. The cockpit geometry adapts via
|
||||
the @media (orientation: landscape) block (07-02 task 1); the map stays
|
||||
Rams-constant. A 844x390 viewport IS the landscape orientation the media
|
||||
query matches (orientation: landscape + max-height: 560px).
|
||||
The setup + query + waitMode/expectGuide are copied VERBATIM from the
|
||||
existing corridor guidance leg — only the name, the scheme, and the
|
||||
viewport differ. */
|
||||
await captureLeg({
|
||||
name: 'shell-guidance-landscape.png',
|
||||
setup: (page) => stubGuidanceRoute(page, corridorModel),
|
||||
query: guideQuery,
|
||||
colorScheme: 'light',
|
||||
grantGeo: true,
|
||||
waitMode: 'guide',
|
||||
expectGuide: 'on_route',
|
||||
viewport: { width: 844, height: 390 },
|
||||
});
|
||||
```
|
||||
|
||||
The `expectText` array and the `interact:` block are OPTIONAL — copy them
|
||||
from the source leg if you want the same DOM assertions, but the leg MUST
|
||||
at minimum carry the `setup`, the `query` (with `dest` + `fixpair`), and
|
||||
the `waitMode`/`expectGuide` pair.
|
||||
|
||||
3. Add ONE failure-state leg that photographs the designed pending card (the
|
||||
empty/error route class) so the surface is on film, not only in a Node
|
||||
@@ -433,6 +460,8 @@ Extend `web/scripts/capture.mjs`:
|
||||
- `grep -c "shell-guidance-landscape.png" web/scripts/capture.mjs` returns ≥ 1
|
||||
- `grep -c "shell-route-pending-landscape.png" web/scripts/capture.mjs` returns ≥ 1
|
||||
- `grep -c "width: 844, height: 390" web/scripts/capture.mjs` returns ≥ 2 (both new legs)
|
||||
- `grep -c "stubGuidanceRoute(page, corridorModel)" web/scripts/capture.mjs` returns ≥ 6 (the five existing corridor legs + the new landscape leg — the landscape leg MUST carry the route stub setup)
|
||||
- `grep -c "query: guideQuery" web/scripts/capture.mjs` returns ≥ 5 (the four existing corridor legs + the new landscape leg — the landscape leg MUST carry the `dest` + `fixpair` query)
|
||||
- `npm run capture -w web` exits 0 and the report lists BOTH new landscape shots as non-blank
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
|
||||
@@ -21,6 +21,8 @@ must_haves:
|
||||
- "The A* CPU cost over the real KL graph is MEASURED and PRINTED against the 8 ms budget — never assumed, never hoped for."
|
||||
- "The shipped route payload's gzipped size is measured and printed against the 200 KiB (204800 byte) budget."
|
||||
- "Every KV write class is recorded with its TTL — and the cron tick is proven to write ZERO times."
|
||||
- "The 50-subrequests-per-request ceiling has a per-request-class INVENTORY beside it — every class's external subrequest count is derived from the source, and a count that cannot be derived reads `not measured`, never `0`."
|
||||
- "The golden route and search corpora are re-run by the SAME `npm run budget-audit` command — so the budget record and the corpora can never disagree."
|
||||
- "Static assets carry an explicit cache policy: hashed /assets/* are immutable for a year, the HTML entry revalidates."
|
||||
- "The budget record compares every current number with the documented Cloudflare free-plan ceiling in ONE place."
|
||||
artifacts:
|
||||
@@ -28,7 +30,7 @@ must_haves:
|
||||
provides: "The consolidated budget CLI (npm run budget-audit) — measurement + free-tier comparison + exit codes"
|
||||
contains: "BUDGET AUDIT"
|
||||
- path: "scripts/lib/budget-gates.mjs"
|
||||
provides: "The measurement gates — CPU ladder, bundle bytes, KV write classes, cache headers"
|
||||
provides: "The measurement gates — CPU ladder, bundle bytes, KV write classes, cache headers, subrequest classes, golden corpora"
|
||||
contains: "P95_BUDGET_MS"
|
||||
- path: "web/public/_headers"
|
||||
provides: "The Workers static-assets cache policy for hashed assets and the HTML entry"
|
||||
@@ -45,6 +47,14 @@ must_haves:
|
||||
to: "bot/src/webhook.ts"
|
||||
via: "the cron tick's zero-write law is proven by scanning scheduled() for .put( — the source half of SC4.e"
|
||||
pattern: "cron"
|
||||
- from: "scripts/lib/budget-gates.mjs"
|
||||
to: "package.json"
|
||||
via: "the golden corpora (SC4.a/SC4.b) are RE-RUN as child processes — `npm run routing:corpus` and `npm run search:corpus` — so the ONE budget-audit command also enforces them"
|
||||
pattern: "routing:corpus"
|
||||
- from: "scripts/lib/budget-gates.mjs"
|
||||
to: "bot/src/webhook.ts"
|
||||
via: "the per-request-class subrequest inventory is derived from the Worker's route table — /api/route = 0 external subrequests; the cron tick's Telegram send is the only recurring subrequest class"
|
||||
pattern: "subrequest"
|
||||
- from: "web/public/_headers"
|
||||
to: "web/dist/assets"
|
||||
via: "the /assets/* rule covers Vite's hashed output; the live probe reads the deployed response headers back"
|
||||
@@ -241,7 +251,9 @@ It MUST export:
|
||||
export function runCpuLadderGate(); // id: "route-cpu" — RE-RUNS the existing script
|
||||
export function runBundleBudgetGate(); // id: "bundle-budget" — BA9's gzip measurement
|
||||
export function runKvWriteClassGate(); // id: "kv-writes" — write classes + the cron's zero-write law
|
||||
export function runSubrequestClassGate(); // id: "subrequests" — the per-request-class subrequest inventory
|
||||
export function runCacheHeaderGate(); // id: "cache-headers" — live headers (or NOT RUN)
|
||||
export function runGoldenCorpusGate(); // id: "golden-corpus" — RE-RUNS the routing + search golden corpora
|
||||
```
|
||||
|
||||
**`runCpuLadderGate()`** — DO NOT re-implement the A* measurement. Spawn the
|
||||
@@ -289,6 +301,50 @@ the TTL constant referenced. Assert every write references a NAMED constant
|
||||
the constants out of the three `bot/src/kv/*.ts` files with a regex on
|
||||
`export const <NAME> = <n>;` — never hard-code the numbers.
|
||||
|
||||
**`runSubrequestClassGate()`** — the SUBREQUEST inventory (D-06 names
|
||||
"Worker CPU/subrequests" — the 50-subrequests-per-request free-plan ceiling
|
||||
must have a number beside it, not just the ceiling). The gate is a
|
||||
SOURCE-DERIVED per-request-class inventory, and it must say plainly which
|
||||
numbers are measured and which are not:
|
||||
|
||||
- Enumerate the Worker request classes from `bot/src/webhook.ts`'s route table
|
||||
(read the file; the classes are the request paths the Worker answers — e.g.
|
||||
the session mint, the location pin, the guidance envelope, the route
|
||||
endpoint, the Telegram webhook).
|
||||
- For EACH class, count the EXTERNAL subrequests the code path can make.
|
||||
Two facts are derivable from the source and MUST be recorded:
|
||||
- the route endpoint (`/api/route`) makes **0 external subrequests** — the
|
||||
routing graph is loaded from the bundled KV binding, not fetched, and the
|
||||
A* search is pure CPU;
|
||||
- the cron tick's Telegram send is the ONLY recurring external-subrequest
|
||||
class — one `fetch` to the Telegram Bot API per due guidance envelope.
|
||||
KV operations are NOT subrequests — Cloudflare counts them against the
|
||||
separate KV-operations ceiling (1,000/invocation) — so KV ops are
|
||||
reported in the KV-write gate, and this gate says so explicitly rather
|
||||
than silently omitting them.
|
||||
- Where a class's subrequest count CANNOT be derived from the source (for
|
||||
example a count that depends on the number of live envelopes), print
|
||||
`NOT MEASURED` with the reason — never a zero and never an estimate.
|
||||
- The gate PRINTS the per-class table (class → external subrequests → budget
|
||||
50/request → headroom) and FAILS if any class exceeds 50.
|
||||
- HONESTY REQUIREMENT: this gate is a SOURCE-DERIVED inventory, not a live
|
||||
platform measurement. The gate's evidence string MUST say so verbatim
|
||||
("source-derived inventory, not a live Workers Logs metric"). The live
|
||||
Workers Logs subrequest metric is a SEPARATE measurement and is NOT
|
||||
available outside `--live` — when the live path cannot produce it, the row
|
||||
reads `not measured outside --live` and never a fabricated number.
|
||||
|
||||
**`runGoldenCorpusGate()`** — the golden corpora (SC4.a / SC4.b). The
|
||||
budget audit ORCHESTRATES the corpora so the ONE command that produces the
|
||||
budget record also proves the corpora are green (the VALIDATION map names
|
||||
this gate as the enforcement point for SC4.a / SC4.b):
|
||||
- Spawn `npm run routing:corpus` and `npm run search:corpus` as child
|
||||
processes (cwd = repo root), exactly as `runCpuLadderGate` spawns
|
||||
`routing/scripts/measure-route-cpu.mjs` — RE-RUN, never re-implemented.
|
||||
- FAIL when either child exits non-zero, carrying that child's own failure
|
||||
output verbatim in the evidence.
|
||||
- PASS when both exit 0, and the evidence names the two corpora by id.
|
||||
|
||||
**`runCacheHeaderGate()`** — the live header probe. Re-use the probe from
|
||||
`scripts/lib/cloudflare-probes.mjs` (import `runAssetHeaderProbe` from it — do
|
||||
NOT fork it) when the credentials/origin are available; otherwise return
|
||||
@@ -298,21 +354,24 @@ Print the observed `cache-control` for the HTML entry and for a hashed
|
||||
Cloudflare's own footnote (the evidence quotes it, it does not interpret it).
|
||||
</action>
|
||||
<acceptance_criteria>
|
||||
- `scripts/lib/budget-gates.mjs` exists and exports `runCpuLadderGate`, `runBundleBudgetGate`, `runKvWriteClassGate`, `runCacheHeaderGate`
|
||||
- `scripts/lib/budget-gates.mjs` exists and exports `runCpuLadderGate`, `runBundleBudgetGate`, `runKvWriteClassGate`, `runSubrequestClassGate`, `runCacheHeaderGate`, `runGoldenCorpusGate`
|
||||
- `grep -c "measure-route-cpu" scripts/lib/budget-gates.mjs` returns ≥ 1 (the ladder is RE-RUN, not re-implemented)
|
||||
- `grep -c "P95_BUDGET_MS = 8" scripts/lib/budget-gates.mjs` returns ≥ 1
|
||||
- `grep -c "204800" scripts/lib/budget-gates.mjs` returns ≥ 1 (the BA9 ceiling)
|
||||
- `grep -c "gzipSync" scripts/lib/budget-gates.mjs` returns ≥ 1
|
||||
- `grep -c "runGuidanceCronTick" scripts/lib/budget-gates.mjs` returns ≥ 1 (the cron zero-write law is source-proven)
|
||||
- `grep -c "routing:corpus" scripts/lib/budget-gates.mjs` returns ≥ 1 and `grep -c "search:corpus" scripts/lib/budget-gates.mjs` returns ≥ 1 (the golden corpora are RE-RUN)
|
||||
- `grep -c "subrequest" scripts/lib/budget-gates.mjs` returns ≥ 1 (the 50/request ceiling has an inventory beside it)
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && node --check scripts/lib/budget-gates.mjs && node -e "import('./scripts/lib/budget-gates.mjs').then(async m=>{for(const k of ['runCpuLadderGate','runBundleBudgetGate','runKvWriteClassGate','runCacheHeaderGate']){const r=m[k]();console.log(r.id+': '+(r.notRun?'NOT RUN':(r.pass?'PASS':'FAIL'))+' — '+String(r.evidence).slice(0,160));}})"</automated>
|
||||
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && node --check scripts/lib/budget-gates.mjs && node -e "import('./scripts/lib/budget-gates.mjs').then(async m=>{for(const k of ['runCpuLadderGate','runBundleBudgetGate','runKvWriteClassGate','runSubrequestClassGate','runCacheHeaderGate','runGoldenCorpusGate']){const r=m[k]();console.log(r.id+': '+(r.notRun?'NOT RUN':(r.pass?'PASS':'FAIL'))+' — '+String(r.evidence).slice(0,160));}})"</automated>
|
||||
</verify>
|
||||
<done>
|
||||
The four budget gates measure and print the CPU p95 (by re-running the
|
||||
The six budget gates measure and print the CPU p95 (by re-running the
|
||||
existing ladder), the bundle bytes, the KV write classes with the cron's
|
||||
zero-write count, and the live cache headers — each with a NOT-RUN path
|
||||
that is never a pass.
|
||||
zero-write count, the per-class subrequest inventory, the live cache
|
||||
headers, and the golden corpora (by re-running them) — each with a
|
||||
NOT-RUN path that is never a pass.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -332,13 +391,16 @@ Create `scripts/budget-audit.mjs` — the orchestrator (the same shape as
|
||||
Behaviour:
|
||||
- Accepts an optional `--live` flag. Without it, the cache-header gate is
|
||||
SKIPPED and reported as `NOT RUN (pass --live)`.
|
||||
- Runs `runCpuLadderGate()`, `runBundleBudgetGate()`, `runKvWriteClassGate()`;
|
||||
with `--live`, also `runCacheHeaderGate()`.
|
||||
- Runs `runCpuLadderGate()`, `runBundleBudgetGate()`, `runKvWriteClassGate()`,
|
||||
`runSubrequestClassGate()`, `runGoldenCorpusGate()`; with `--live`, also
|
||||
`runCacheHeaderGate()`.
|
||||
- Prints ONE line per gate: `<id>: PASS|FAIL|NOT RUN — <evidence>`.
|
||||
- Prints the FREE-TIER COMPARISON TABLE to stdout — every ceiling from
|
||||
07-RESEARCH.md §Pattern 5 with the CURRENT value beside it where the audit
|
||||
measured one, and `not measured` where it did not. This table is the
|
||||
console form of `docs/BUDGETS.md`.
|
||||
console form of `docs/BUDGETS.md`. The `Worker subrequests | 50/request`
|
||||
ceiling MUST appear in this table with the per-class inventory beside it
|
||||
(never a bare ceiling with no number).
|
||||
- Prints a summary line: `BUDGET AUDIT CLEAN — <n> gate(s) within budget` /
|
||||
`BUDGET AUDIT FAILED — <n> gate(s) over budget` /
|
||||
`BUDGET AUDIT NOT RUN — <reason>`.
|
||||
@@ -366,14 +428,37 @@ contain:
|
||||
with rows for: the A* CPU p95 (local ladder), the app bundle gz bytes,
|
||||
the route payload gz bytes, the Worker bundle size, the KV write count per
|
||||
request class (session mint / location pin / guidance envelope / **cron =
|
||||
0**), the static asset cache headers, and the KV daily write estimate.
|
||||
0**), the static asset cache headers, the KV daily write estimate, the
|
||||
**subrequests per request class**, and the golden-corpus status.
|
||||
The Current column carries the REAL measured number from the run; the
|
||||
Headroom column is the budget minus the current value; the Evidence column
|
||||
names the gate id that produced it.
|
||||
4. A `## The cron writes zero` section — one honest paragraph explaining WHY
|
||||
4. A `## Subrequests per request class` section — the D-06 subrequest row.
|
||||
It MUST contain a table with one row per Worker request class: `| Request class | External subrequests | Budget (50/request) | Headroom | Source |`.
|
||||
The table MUST include:
|
||||
- the route endpoint (`/api/route`) — **0 external subrequests** (the
|
||||
routing graph comes from the bundled KV binding; the A* search is pure
|
||||
CPU) — with the source site cited;
|
||||
- the cron guidance tick — the ONLY recurring external-subrequest class
|
||||
(one Telegram Bot API send per due envelope) — with the source site
|
||||
cited;
|
||||
- one row for every other Worker request class the route table answers.
|
||||
A row for KV operations with the note that KV operations are NOT
|
||||
subrequests — they are billed against the separate KV-operations ceiling
|
||||
(1,000/invocation) and are tracked in the KV-write row.
|
||||
Every row whose count cannot be derived from the source MUST read
|
||||
`not measured` with the reason — never `0`, never an estimate.
|
||||
The section MUST state, in one sentence, that the subrequest counts are a
|
||||
SOURCE-DERIVED INVENTORY, not a live Workers Logs metric; where the live
|
||||
metric cannot be produced, the record says `not measured outside --live`
|
||||
and does not substitute a number.
|
||||
5. A `## The cron writes zero` section — one honest paragraph explaining WHY
|
||||
(the tick lists, reads, and sends; it never writes), and naming the source
|
||||
gate that proves it.
|
||||
5. A `## Cross-metro CPU degradation (the c3 class)` section — the honest
|
||||
6. A `## Golden corpora` section — one short paragraph naming the two
|
||||
corpora (`routing:corpus`, `search:corpus`), the gate id that re-runs
|
||||
them (`golden-corpus`), and the outcome from THIS run.
|
||||
7. A `## Cross-metro CPU degradation (the c3 class)` section — the honest
|
||||
escalation from Phase 6: short product routes (2–8 km) measure 2–8 ms
|
||||
(inside the budget); cross-metro routes (20–30 km) measured p95 38–93 ms
|
||||
and degrade HONESTLY to the router-pending state; the mitigation ladder is
|
||||
@@ -383,15 +468,15 @@ contain:
|
||||
the ALT landmark preprocessing (a NEXT-MILESTONE item, not this phase).
|
||||
Cite `routing/scripts/measure-route-cpu.mjs`'s LADDER HISTORY and the
|
||||
06-04 plan summary as the evidence.
|
||||
6. A `## Static assets` section — the `_headers` policy and the observed live
|
||||
8. A `## Static assets` section — the `_headers` policy and the observed live
|
||||
headers (or an honest "not measured in this run").
|
||||
7. A `## Statement` section — the honest one-paragraph claim, naming which
|
||||
9. A `## Statement` section — the honest one-paragraph claim, naming which
|
||||
numbers are measured, which are NOT measured (the live Worker CPU metric —
|
||||
the local ladder and the live Workers Logs CPU number are DIFFERENT
|
||||
measurements and neither substitutes for the other), and which are
|
||||
estimated.
|
||||
8. A `## Signature` block with the maintainer's name and date lines left as
|
||||
`________________` (the human signs at the launch gate).
|
||||
10. A `## Signature` block with the maintainer's name and date lines left as
|
||||
`________________` (the human signs at the launch gate).
|
||||
|
||||
FILL the tables with the REAL run output — run `npm run budget-audit` and
|
||||
paste the printed numbers. Do NOT invent, round optimistically, or copy a
|
||||
@@ -401,7 +486,9 @@ number from another phase's summary.
|
||||
- `scripts/budget-audit.mjs` exists and contains the literals `BUDGET AUDIT CLEAN`, `BUDGET AUDIT FAILED`, `BUDGET AUDIT NOT RUN`
|
||||
- `grep -c "process.exit" scripts/budget-audit.mjs` returns ≥ 3
|
||||
- `grep -c '"budget-audit"' package.json` returns exactly 1
|
||||
- `docs/BUDGETS.md` exists and contains the literals `## Free-plan ceilings`, `## Current measurements`, `## The cron writes zero`, `## Cross-metro CPU degradation`, `## Static assets`, `## Statement`, `## Signature`
|
||||
- `grep -c "runGoldenCorpusGate\|runSubrequestClassGate" scripts/budget-audit.mjs` returns ≥ 2 (the CLI runs BOTH new gates)
|
||||
- `docs/BUDGETS.md` exists and contains the literals `## Free-plan ceilings`, `## Current measurements`, `## Subrequests per request class`, `## The cron writes zero`, `## Golden corpora`, `## Cross-metro CPU degradation`, `## Static assets`, `## Statement`, `## Signature`
|
||||
- `grep -c "50/request\|50 /request" docs/BUDGETS.md` returns ≥ 1 (the subrequest ceiling is stated)
|
||||
- `npm run budget-audit` exits 0 on the current tree and prints `BUDGET AUDIT CLEAN`
|
||||
</acceptance_criteria>
|
||||
<verify>
|
||||
@@ -409,8 +496,10 @@ number from another phase's summary.
|
||||
</verify>
|
||||
<done>
|
||||
`npm run budget-audit` measures and prints every budget with its free-plan
|
||||
ceiling, exits 0/1/2 correctly, and `docs/BUDGETS.md` carries the record
|
||||
with the real numbers, the c3 escalation, and a human signature block.
|
||||
ceiling (including the per-request-class subrequest inventory and the
|
||||
golden-corpus status), exits 0/1/2 correctly, and `docs/BUDGETS.md`
|
||||
carries the record with the real numbers, the subrequests section, the
|
||||
c3 escalation, and a human signature block.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -434,6 +523,7 @@ number from another phase's summary.
|
||||
| T-07-20 | Tampering | `web/public/_headers` — a stale HTML entry pointing at deleted hashed assets | mitigate | Only `/assets/*` (content-hashed, immutable for 31556952 s) gets the immutable rule; the HTML entry is `max-age=0, must-revalidate`. The live header probe prints the OBSERVED headers as evidence rather than assuming the policy took effect. |
|
||||
| T-07-21 | Information Disclosure | the budget audit's own output | accept | The audit prints sizes, counts, timings and header values only — no secret, no token, no coordinate. The bot token and webhook secret live only in `bot/.dev.vars` / Worker secrets and are never read by this tool. |
|
||||
| T-07-22 | Spoofing | the budget verdict — a missing graph binary or build reported as "within budget" | mitigate | A missing graph binary returns `{pass:false, notRun:true}` and the CLI maps a NOT RUN to exit code 2 with `BUDGET AUDIT NOT RUN` — a skip is never a pass. |
|
||||
| T-07-23b | Spoofing | the subrequest inventory — a fabricated zero beside the 50/request ceiling | mitigate | `runSubrequestClassGate` derives every class's external-subrequest count from the Worker's own route table and PRINTS the source site; a count that cannot be derived reads `not measured` with its reason — never `0`, never an estimate. The gate's evidence states that the inventory is SOURCE-DERIVED, not a live Workers Logs metric, and the record names the live metric `not measured outside --live` rather than substituting a number. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
@@ -454,9 +544,10 @@ NEVER exit 0 while reporting a measurement as skipped.
|
||||
|
||||
<success_criteria>
|
||||
- `web/public/_headers` exists and reaches `web/dist/`, carrying the immutable hashed-asset rule and the revalidating HTML rule, and nothing else.
|
||||
- `npm run budget-audit` measures and prints the A* CPU p95 (by re-running the existing ladder), the bundle bytes, the KV write classes with the cron's zero-write count, and the cache headers, each against a documented free-plan ceiling.
|
||||
- `npm run budget-audit` measures and prints the A* CPU p95 (by re-running the existing ladder), the bundle bytes, the KV write classes with the cron's zero-write count, the per-request-class subrequest inventory, the golden corpora (by re-running them), and the cache headers, each against a documented free-plan ceiling.
|
||||
- The subrequest row carries a per-request-class inventory (the route endpoint = 0 external subrequests; the cron tick's Telegram send = the only recurring class; KV ops tracked separately against their own ceiling), and any count that cannot be derived from the source reads `not measured` — never `0`, never an estimate.
|
||||
- The exit codes are 0 / 1 / 2 and a NOT RUN is never a pass.
|
||||
- `docs/BUDGETS.md` carries the record with the real numbers, the c3 cross-metro degradation class, the cron zero-write explanation, and a human signature block.
|
||||
- `docs/BUDGETS.md` carries the record with the real numbers, the `## Subrequests per request class` section, the `## Golden corpora` section, the c3 cross-metro degradation class, the cron zero-write explanation, and a human signature block.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -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)
|
||||
@@ -2,7 +2,7 @@
|
||||
phase: 7
|
||||
slug: hardening-private-launch
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: false
|
||||
created: 2026-10-25
|
||||
---
|
||||
@@ -56,11 +56,12 @@ created: 2026-10-25
|
||||
| 07-03 t2 | 07-03-PLAN.md | 1 | SC3.c | T-07-01 | Live KV keys TTL-expiring, only expected families exist | live probe | `npm run privacy-audit -- --live` | ❌ W0 | ⬜ pending |
|
||||
| 07-03 t1 | 07-03-PLAN.md | 1 | SC3.d | T-07-03 | Logs category-only; no coordinates/chat ids/payloads | source + bundle | `npm run privacy-audit` | ❌ W0 | ⬜ pending |
|
||||
| 07-03 t2 | 07-03-PLAN.md | 1 | SC3.e | T-07-04 | No analytics/beacon; only approved fetch hosts | bundle gate | `npm run privacy-audit` | ❌ W0 | ⬜ pending |
|
||||
| 07-04 t3 | 07-04-PLAN.md | 2 | SC4.a | — | Golden route corpus green | CLI + suite | `npm run routing:corpus && npm test -w routing` | ✅ existing | ⬜ pending |
|
||||
| 07-04 t3 | 07-04-PLAN.md | 2 | SC4.b | — | Search golden corpus green | CLI + suite | `npm run search:corpus && npm test -w search` | ✅ existing | ⬜ pending |
|
||||
| 07-04 t2 | 07-04-PLAN.md | 2 | SC4.a | — | Golden route corpus green (re-run by the budget audit's `golden-corpus` gate) | CLI + suite | `npm run budget-audit` (runs `routing:corpus`); `npm run routing:corpus && npm test -w routing` | ✅ existing | ⬜ pending |
|
||||
| 07-04 t2 | 07-04-PLAN.md | 2 | SC4.b | — | Search golden corpus green (re-run by the budget audit's `golden-corpus` gate) | CLI + suite | `npm run budget-audit` (runs `search:corpus`); `npm run search:corpus && npm test -w search` | ✅ existing | ⬜ pending |
|
||||
| 07-04 t2 | 07-04-PLAN.md | 2 | SC4.c | — | A* CPU p95 ≤8 ms local; live Worker CPU ≤10 ms | measurement | `node routing/scripts/measure-route-cpu.mjs`; live probe | ✅ local; ❌ live probe | ⬜ pending |
|
||||
| 07-04 t2 | 07-04-PLAN.md | 2 | SC4.d | — | Bundle/Worker sizes within recorded budgets | measurement | `npm run budget-audit` | ❌ W0 (`scripts/budget-audit.mjs`) | ⬜ pending |
|
||||
| 07-04 t2 | 07-04-PLAN.md | 2 | SC4.e | T-07-18 | KV writes by request class recorded; cron=0 | source + runtime | `npm run budget-audit` | ❌ W0 | ⬜ pending |
|
||||
| 07-04 t2 | 07-04-PLAN.md | 2 | SC4.i | T-07-23b | Subrequests per request class recorded; `/api/route` = 0 external; the cron tick's Telegram send is the only recurring class; unmeasurable rows read `not measured` | source + record | `npm run budget-audit` | ❌ W0 | ⬜ pending |
|
||||
| 07-04 t1 | 07-04-PLAN.md | 2 | SC4.f | T-07-20 | Static assets have intended cache headers | live header probe | `npm run budget-audit -- --live` | ❌ W0 | ⬜ pending |
|
||||
| 07-05 t2 | 07-05-PLAN.md | 1 | SC4.g | — | Known-limitations doc shipped, linked from `/help` en/ms | unit/doc gate | `npm test -w bot` | ❌ W0 | ⬜ pending |
|
||||
| 07-05 t1 | 07-05-PLAN.md | 1 | SC4.h | T-07-08 | INFO debts fixed; exact-900s boundary pinned | unit | `npm test -w bot` | ❌ W0 (`bot/test/initdata.test.mjs` 900s vector) | ⬜ pending |
|
||||
@@ -76,7 +77,7 @@ created: 2026-10-25
|
||||
- [ ] `web/test/fullscreen.test.mjs` — covers PX-04.a–d and old-client fallback → **07-01 t3** (FS1–FS9)
|
||||
- [ ] `web/test/failure-state-matrix.test.mjs` — covers SC2 named vectors and DOM state tokens → **07-02 t2** (FM1–FM7)
|
||||
- [ ] `scripts/privacy-audit.mjs` + `scripts/lib/*` — consolidated source/bundle/live audit → **07-03 t1–t3**
|
||||
- [ ] `scripts/budget-audit.mjs` + `scripts/lib/*` — consolidated budget record and live probes → **07-04 t2–t3**
|
||||
- [ ] `scripts/budget-audit.mjs` + `scripts/lib/*` — consolidated budget record (CPU ladder, bundle bytes, KV write classes, subrequest inventory, golden corpora) and live probes → **07-04 t2–t3**
|
||||
- [ ] `web/public/_headers` — hashed-asset immutable caching + HTML revalidation policy, if verified by audit → **07-04 t1**
|
||||
- [ ] `bot/test/initdata.test.mjs` exact-900s vector — D-07 IN-03 → **07-05 t1** (T4b)
|
||||
- [ ] `docs/KNOWN-LIMITATIONS.md` — D-08 → **07-05 t2**
|
||||
@@ -96,11 +97,11 @@ created: 2026-10-25
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 90 s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [x] Wave 0 covers all MISSING references
|
||||
- [x] No watch-mode flags
|
||||
- [x] Feedback latency < 90 s
|
||||
- [x] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
**Approval:** approved 2026-10-25
|
||||
|
||||
Reference in New Issue
Block a user