docs(07): revise phase 07 plans — wire the safe-area watch, fold the corpora into budget-audit, add the subrequests row, resolve the research open questions

Checker-issue round 1 on the Phase 7 plan set:

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

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

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

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

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

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

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

Waves, dependencies, autonomous flags, and the other three plans are unchanged.
This commit is contained in:
araharah-executor
2026-10-25 02:44:30 +08:00
parent 27b340fa6b
commit a7d8dbf345
6 changed files with 1048 additions and 78 deletions
@@ -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>