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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user