366 lines
27 KiB
Markdown
366 lines
27 KiB
Markdown
# Phase 7: Hardening + Private Launch - Pattern Map
|
||
|
||
**Mapped:** 2026-10-25
|
||
**Files analyzed:** 16 (9 new, 7 modified/re-run)
|
||
**Analogs found:** 14 / 16 (2 have no in-repo analog: `_headers`, cloudflare live probes)
|
||
|
||
## File Classification
|
||
|
||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||
|-------------------|------|-----------|----------------|---------------|
|
||
| `scripts/privacy-audit.mjs` + `scripts/lib/source-gates.mjs` | utility (CLI gate tool) | batch (scan → report → exit) | `web/test/no-storage.test.mjs` + `bot/test/route-privacy.test.mjs` (scans) + `theme/scripts/audit.mjs` (CLI/exit codes) | exact |
|
||
| `scripts/lib/bundle-gates.mjs` | utility | batch | `web/test/build-artifact.test.mjs` (BA1/BA5/BA8/BA9) | exact |
|
||
| `scripts/lib/cloudflare-probes.mjs` | utility (live probe) | request-response (CLI → wrangler) | — no in-repo live probe exists (see No Analog) | role-match |
|
||
| `scripts/budget-audit.mjs` | utility (CLI measurement gate) | batch | `routing/scripts/measure-route-cpu.mjs` + BA9 | exact |
|
||
| `web/test/fullscreen.test.mjs` | test | event-driven | `web/test/homescreen.test.mjs` | exact |
|
||
| `web/test/failure-state-matrix.test.mjs` | test | event-driven (named replay vectors) | `web/test/guidance-replay.test.mjs` + `web/test/guidance-surface.test.mjs` | exact |
|
||
| `web/public/_headers` | config | n/a (static) | — none (Cloudflare doc format; RESEARCH §Code Examples) | no analog |
|
||
| `web/src/telegram/fullscreen.ts` (new) | service (capability wrapper) | event-driven | `web/src/guidance/close-protection.ts` + `web/src/telegram/homescreen.ts` | exact |
|
||
| `web/src/telegram/webapp.ts` (modify — grow contract) | model (typed contract) | n/a | itself — documented optional-member growth law | exact (self) |
|
||
| `web/src/shell.css` (modify — landscape media queries) | component (stylesheet) | transform | itself — token-var discipline + `[data-guidance]` block | exact (self) |
|
||
| `web/src/telegram/safe-area.ts` (modify — re-resolve on inset events) | service | event-driven | itself — resolve/apply pair | exact (self) |
|
||
| `bot/test/initdata.test.mjs` (modify — exact-900s vector, IN-03) | test | request-response | itself — T4 boundary vectors | exact (self) |
|
||
| `bot/src/security/initdata.ts` (modify — IN-01 debt) | model (security) | transform | itself — dead `ParsedInitData` export | exact (self) |
|
||
| `bot/wrangler.jsonc` (modify — IN-02 stale comment) | config | n/a | itself — line 2 | exact (self) |
|
||
| `bot/src/i18n/strings.ts` (modify — /help limitations pointer) + `bot/src/bot/commands/help.ts` | model (copy) | n/a | itself — `helpMenu` + D-05 law | exact (self) |
|
||
| `docs/KNOWN-LIMITATIONS.md` (new) | doc | n/a | `docs/CONFIGURATION.md` | role-match |
|
||
| `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` (new) | planning doc | n/a | `.planning/phases/06-.../06-DEVICE-GATE.md` | exact |
|
||
| `bot/scripts/register-commands.mjs` (re-run only, never modified) | utility | n/a | not modified — copy derives from `strings.ts` (CS5 pins this) | n/a |
|
||
|
||
## Pattern Assignments
|
||
|
||
### `scripts/privacy-audit.mjs` + `scripts/lib/*` (utility, batch)
|
||
|
||
**Analogs:** `web/test/no-storage.test.mjs` (client storage gate), `bot/test/privacy.test.mjs` (KV TTL gate), `bot/test/route-privacy.test.mjs` (host/import allowlists), `theme/scripts/audit.mjs` (CLI + exit-code contract), `web/test/build-artifact.test.mjs` (bundle gates)
|
||
|
||
The repo's gate lineage to consolidate: GEO3 (web memory-only) ← PG1/PG2 (Worker TTL writes) ← RP1/RP2 (Worker routing shape) ← BA1/BA5/BA8 (bundle). The audit tool extends the same laws repo-wide, it does not invent new ones.
|
||
|
||
**Comment-stripped source scan** — copy verbatim from `web/test/no-storage.test.mjs` lines 29–42 (identical twins exist in `bot/test/route-privacy.test.mjs` lines 42–59 and `web/test/homescreen.test.mjs` lines 240–244):
|
||
|
||
```javascript
|
||
function listFiles(dir, exts, acc = []) {
|
||
for (const entry of readdirSync(dir)) {
|
||
const full = path.join(dir, entry);
|
||
if (statSync(full).isDirectory()) listFiles(full, exts, acc);
|
||
else if (exts.some((e) => entry.endsWith(e))) acc.push(full);
|
||
}
|
||
return acc;
|
||
}
|
||
|
||
function stripComments(src) {
|
||
return src
|
||
.replace(/\/\*[\s\S]*?\*\//g, " ") // block comments
|
||
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (not URLs inside strings — benign over-stripping, gate is prefer-fail reading)
|
||
}
|
||
```
|
||
|
||
> Planner note: the `(^|[^:\\])` guard keeps `https://` URLs alive; the header comment says the over-stripping is deliberate "prefer-fail reading". Reuse exactly; do not write a smarter stripper (RESEARCH Pitfall 6).
|
||
|
||
**The banned-vocabulary shape** — `web/test/no-storage.test.mjs` lines 22–25 + the assertion style (lines 50–59):
|
||
|
||
```javascript
|
||
const BANNED = [
|
||
"localStorage", "sessionStorage", "indexedDB", "caches", "CacheStorage",
|
||
"document.cookie", "WebApp.DeviceStorage", "WebApp.SecureStorage", "WebApp.CloudStorage",
|
||
];
|
||
// per file, per token:
|
||
assert.ok(!code.includes(banned), `${path.relative(process.cwd(), file)} references ${banned} — GEO-03 violated`);
|
||
```
|
||
|
||
**The KV write-TTL law the audit must extend repo-wide** — `bot/test/privacy.test.mjs` lines 32–49 (PG1): every `.put(` line carries `expirationTtl` ON THE SAME LINE, and it must reference a NAMED constant, never an inline literal:
|
||
|
||
```javascript
|
||
lines.forEach((line, i) => {
|
||
if (line.includes(".put(")) {
|
||
assert.ok(line.includes("expirationTtl"), `${file}:${i + 1} — KV write without expirationTtl (PRIV-02 gate): ${line.trim()}`);
|
||
const ttl = line.match(/expirationTtl:\s*([A-Za-z0-9_]+)/)?.[1];
|
||
assert.ok(ttl && /^[A-Z_]{2,}$/.test(ttl), `${file}:${i + 1} — expirationTtl must reference a named TTL constant, not an inline literal`);
|
||
}
|
||
});
|
||
```
|
||
|
||
**Positive host allowlist (prefer allow over ban for runtime fetches)** — `bot/test/route-privacy.test.mjs` lines 94–105 show the negative-host form; BA1 lines 64–70 show the CDN ban list. D-01 flips this to a POSITIVE allowlist (OpenFreeMap tiles, Telegram API, own Worker) — keep the per-file `assert` message style from RP1c:
|
||
|
||
```javascript
|
||
const banned = /(overpass|project-osrm|osrm|valhalla|graphhopper|openrouteservice|routing\.openstreetmap)/i;
|
||
// ... per file: assert.ok(hit === null, `${relative} references routing-host "${hit?.[0]}" — <the law and why>`);
|
||
```
|
||
|
||
**CLI + exit-code contract** — `theme/scripts/audit.mjs` lines 20–24 define the repo's audit vocabulary (reuse it — OpenCode's discretion area explicitly covers exit codes, and this precedent already exists):
|
||
|
||
```text
|
||
* Exit codes (no silent passes — T-1-07):
|
||
* 0 clean — every checked declaration derives from tokens
|
||
* 1 violations — each printed as file:line: declaration
|
||
* 2 drift/error — token sheet degraded (see message)
|
||
```
|
||
|
||
And its driver shape (lines 797–814): print every violation, then a single `AUDIT FAILED — N …` / `AUDIT CLEAN — …` summary line, then `process.exit(0|1)`. RESEARCH Pattern 4 adds exit **2 = missing credentials/environment** so a skipped live probe is never mistaken for a pass.
|
||
|
||
**Bundle-scan mechanics** — `web/test/build-artifact.test.mjs`: the DirEnt walk (lines 79–87), the PRODUCTION-only scope law (BA5 lines 133–147 — `web/dist-harness/` is deliberately NOT clean; a gate pointed at it inverts itself), and BA8's JS-assets-only rule for crypto scans (lines 175–187 — the official Telegram SDK script in `index.html` is EXTERNAL and has its own crypto; the HTML fetch-host allowlist must whitelist `telegram.org`'s script tag explicitly, RESEARCH Pitfall 5/A4).
|
||
|
||
---
|
||
|
||
### `scripts/budget-audit.mjs` + `scripts/lib/*` (utility, batch)
|
||
|
||
**Analogs:** `routing/scripts/measure-route-cpu.mjs` (the measurement half — RE-RUN, not rewritten), `web/test/build-artifact.test.mjs` BA9 (the print-the-budget law)
|
||
|
||
**Header contract** — `routing/scripts/measure-route-cpu.mjs` lines 3–9 set the law the budget audit inherits:
|
||
|
||
```text
|
||
* A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
|
||
* web/src, or routing/src imports it. The free plan gives a Worker 10 ms of
|
||
* CPU per request (Error 1102 beyond it), so the A*-over-the-real-graph cost
|
||
* is MEASURED AND PRINTED, never hoped for ("budget gates print their
|
||
* measurement" — the BA9 precedent).
|
||
```
|
||
|
||
**Print-then-gate** — BA9, `web/test/build-artifact.test.mjs` lines 221–234:
|
||
|
||
```javascript
|
||
const total = inputs.reduce((sum, f) => sum + gzipSync(readFileSync(f)).length, 0);
|
||
/* The measurement is VISIBLE on every run (D-11: "enforced as a BA
|
||
* build-artifact gate" — a budget nobody sees is a hope, not a gate). */
|
||
console.log(`BA9 route bundle budget: ${total} / 204800 gz bytes`);
|
||
assert.ok(total <= 204800, `route payload is ${total} gz bytes — over the 204800-byte (200 KiB) D-11 budget by ${total - 204800}. …`);
|
||
```
|
||
|
||
**Gate-fail message carries the mitigation ladder** — `measure-route-cpu.mjs` lines 180–185:
|
||
|
||
```javascript
|
||
if (p95 > P95_BUDGET_MS) {
|
||
console.error(`GATE FAIL: p95 ${p95.toFixed(3)} ms exceeds the ${P95_BUDGET_MS} ms budget (A1) — apply the mitigation ladder: (a) … (b) … (c) …`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`GATE PASS: p95 ${p95.toFixed(3)} ms <= ${P95_BUDGET_MS} ms budget — A1 retired for this graph`);
|
||
process.exit(0);
|
||
```
|
||
|
||
The budget audit re-runs `node routing/scripts/measure-route-cpu.mjs` (do not fork its logic — D-06/RESEARCH "Don't Hand-Roll") and adds: gzip'd bundle bytes (BA9 inputs: `routing/src`, `web/src/route`, `web/src/guidance`, `route-fixtures.json` — lines 204–209), KV write classes per request source (`session:`/`chat:` at `SESSION_TTL_SECONDS` — `bot/src/kv/sessions.ts` line 43; `loc:` at `PIN_TTL_SECONDS` — `location-pin.ts` line 25; `guidance:` at `GUIDANCE_SESSION_TTL_SECONDS` — `guidance-sessions.ts` line 27; cron = 0 writes per `webhook.ts` lines 49–57), and live asset cache headers once `_headers` ships. Reference ceilings table is in RESEARCH §Pattern 5.
|
||
|
||
---
|
||
|
||
### `web/test/fullscreen.test.mjs` (test, event-driven)
|
||
|
||
**Analog:** `web/test/homescreen.test.mjs` — the WHOLE file is the template (fake WebApp + minimal DOM stub + guard tests + source-scan ban). Key excerpts:
|
||
|
||
**Injected fake, per-case install/restore** (lines 5–8, 87–127):
|
||
|
||
```javascript
|
||
/* … driven with INJECTED FAKES (the location-chain.test.mjs style: build the
|
||
fake, call, assert — globals stubbed per case and restored in a finally
|
||
block, never leaked). */
|
||
function fakeWebApp(opts = {}) {
|
||
const calls = { addToHomeScreen: 0, checkedStatus: false };
|
||
const events = new Map();
|
||
const wa = {
|
||
initData: "",
|
||
version: opts.version ?? "10.0",
|
||
isVersionAtLeast(v) { /* segment-wise numeric compare, lines 93–103 */ },
|
||
close() {},
|
||
onEvent(event, cb) { /* push into events map */ },
|
||
emitEvent(event) { /* fire registered callbacks — the test's trigger */ },
|
||
};
|
||
if (!opts.noAddToHomeScreen) { wa.addToHomeScreen = () => { calls.addToHomeScreen += 1; }; }
|
||
wa.__calls = calls;
|
||
return wa;
|
||
}
|
||
```
|
||
|
||
**The guard's BOTH-halves vectors** — HS1/HS2 (lines 135–156) map 1:1 to fullscreen's PX-04.a/b:
|
||
|
||
```javascript
|
||
test("HS1 a client without addToHomeScreen is a no-op — nothing thrown, no affordance", () => { /* method absent → no-op */ });
|
||
test("HS2 an old client (isVersionAtLeast 8.0 false) is a no-op", () => { /* version 7.9 → no-op, "the status was never even queried" */ });
|
||
```
|
||
|
||
**Throwing-platform-call-is-non-error** — HS7 (lines 214–219) is the exact shape for `requestFullscreen()` rejection:
|
||
|
||
```javascript
|
||
wa.addToHomeScreen = () => { throw new Error("platform refused"); };
|
||
assert.doesNotThrow(() => btn.dispatch("click"), "a rejected addToHomeScreen must not break the boot");
|
||
```
|
||
|
||
**The source-scan ban closer** — HS9 (lines 240–248): end the suite with a comment-stripped scan of the new module banning the anti-pattern APIs (here: `Element.requestFullscreen` as primary path, `screen.orientation.lock`).
|
||
|
||
Test IDs should follow the repo's prefix convention (`HS*`, `GS*`, `R*` → use e.g. `FS*`).
|
||
|
||
---
|
||
|
||
### `web/test/failure-state-matrix.test.mjs` (test, named vectors)
|
||
|
||
**Analogs:** `web/test/guidance-replay.test.mjs` (named-vector header + pure-reducer replay) and `web/test/guidance-surface.test.mjs` (DOM state-token assertions).
|
||
|
||
**Named-vector discipline** — `guidance-replay.test.mjs` header lines 15–33 enumerates cases R1–R12/T1–T3 with the law each pins; RESEARCH Pattern 3's `FAILURE_VECTORS` array is the data-driven form. The failure matrix test should import the REAL modules (`../src/route/route.ts`, `../src/guidance/guidance.ts`) and assert on the designed state tokens, e.g.:
|
||
|
||
```javascript
|
||
const g = await import("../src/guidance/guidance.ts");
|
||
const { createGuidanceSession, stepGuidance, OFF_ROUTE_THRESHOLD_M /* …named constants */ } = g;
|
||
```
|
||
|
||
**DOM state-token assertions** — `guidance-surface.test.mjs` style: drive the REAL builders through the minimal document fake (`makeEl`, lines 75+), assert `dataset`/`data-*` states ("never blanks mid-swap (D-04/D-06)", GS1). Existing behavior to COVER (not change — RESEARCH Pattern 3 "audit coverage, not behavior"): route `null`/throw already renders the pending card (`web/src/route/route.ts`); off-road is already a calm state machine (`web/src/guidance/guidance.ts` — grace, storm guard, `no_reroute`).
|
||
|
||
---
|
||
|
||
### `web/src/telegram/fullscreen.ts` (service, event-driven) — new module
|
||
|
||
**Analogs:** `web/src/guidance/close-protection.ts` (engage-at-guidance-start, version-gated) + `web/src/telegram/homescreen.ts` (guard both halves, onEvent, try/catch) + `web/src/telegram/webapp.ts` (contract growth).
|
||
|
||
**The capability-guard pattern, verbatim shape** — `close-protection.ts` lines 21–31:
|
||
|
||
```typescript
|
||
export function engageCloseProtection(wa: TelegramWebAppLike | null): void {
|
||
// layer 1 — Bot API 7.7+: gate BEFORE the call, then the ?. chain
|
||
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("7.7")) wa?.disableVerticalSwipes?.();
|
||
// layer 2 — Bot API 6.2+: the native-close net, gated the same way
|
||
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("6.2")) wa?.enableClosingConfirmation?.();
|
||
}
|
||
```
|
||
|
||
For fullscreen the guard is BOTH halves (method presence AND 8.0) — `homescreen.ts` line 33 is the tightest form:
|
||
|
||
```typescript
|
||
if (typeof wa.addToHomeScreen !== "function" || !wa.isVersionAtLeast("8.0")) return;
|
||
```
|
||
|
||
plus the best-effort try/catch (homescreen.ts lines 51–58) and the event observation (`wa.onEvent?.("homeScreenAdded", hide)` — line 64 → `fullscreenChanged`/`fullscreenFailed` for fullscreen).
|
||
|
||
**Wiring site:** `web/src/route/route.ts` calls `engageCloseProtection(deps.wa ?? null)` at the guidance-start edge (lines 341, 616) and `releaseCloseProtection` at trip end (lines 481, 692) — `requestGuidanceFullscreen(wa)` / `exitGuidanceFullscreen(wa)` ride the SAME two edges. Note RESEARCH Pitfall 1: the guidance-start transition (not an async callback) is where the call belongs.
|
||
|
||
**Contract growth law** — `webapp.ts` header lines 16–27 (must be extended IN THE SAME CHANGE as the fakes):
|
||
|
||
```text
|
||
* Phase 6 (PX-03/D-08) adds the guidance guards as OPTIONAL members only:
|
||
* … — the contract stays the contract, no existing member changes shape,
|
||
* every test fake and the harness mock satisfy exactly these members.
|
||
```
|
||
|
||
Add `requestFullscreen?`, `exitFullscreen?`, `isFullscreen?`, `lockOrientation?`, `unlockOrientation?` (all 8.0-optional) to `TelegramWebAppLike` (lines 35–86), and mirror them in BOTH fakes: `web/src/mock/telegram-mock.ts` and the per-test `fakeWebApp()` builders ("contract + fake + test together" law).
|
||
|
||
---
|
||
|
||
### `web/src/shell.css` landscape media queries + `web/src/telegram/safe-area.ts` re-resolution
|
||
|
||
**Analog:** the file itself. The token-var discipline is declared in `shell.css` lines 2–18 ("Every value is a token reference…; the only literals are the machined-border geometry the UI-SPEC enumerates") — a landscape block may add geometry literals ONLY if the UI-SPEC-style sanctioned list is extended in the header comment (precedent: the Phase 6 block lines 141–147 opens with exactly such a list: "banner min-height 96px, arrow plate 48px…").
|
||
|
||
**The state-attribute pattern** (no new markup): `[data-guidance="active"] .strip { … }` (lines 174–181) — landscape rules hang off `@media (orientation: landscape)` combined with the same token vars and `--safe-left/right`:
|
||
|
||
```css
|
||
[data-guidance="active"] .strip {
|
||
top: calc(var(--safe-top, 0px) + var(--spacing-space-sm) + 96px + 1px);
|
||
min-height: 24px;
|
||
padding-top: var(--spacing-space-xs); padding-bottom: var(--spacing-space-xs);
|
||
}
|
||
```
|
||
|
||
**Safe-area re-resolution** — `safe-area.ts` already exposes the pair: `resolveSafeInsets(wa)` (lines 35–50, env-union-Telegram with `Math.max` per edge and the old-client pure-env fallback) and `applySafeArea(insets)` (lines 52–58, writes `--safe-top/bottom/left/right` on `:root`). The fullscreen/orientation change path re-CALLS these on `safeAreaChanged`/`contentSafeAreaChanged` (RESEARCH Pitfall 3) — extend, don't duplicate (webapp.ts gains the two event names as `onEvent` usages; the union logic is untouched).
|
||
|
||
**CSS gate:** every new declaration must pass `npm run audit -w theme` (the DSGN-01 evidence command) — zero radius, no shadows/blur, tokens only.
|
||
|
||
---
|
||
|
||
### `bot/test/initdata.test.mjs` — exact-900s boundary vector (IN-03) + `bot/src/security/initdata.ts` debt (IN-01)
|
||
|
||
**Analog:** the file itself. T4 (lines 172–189) pins 899 → ok / 901 → stale; the comparator at `initdata.ts` lines 193–196 is strictly-greater:
|
||
|
||
```typescript
|
||
// — stale: age strictly beyond the D-13 window (899 ok / 900 ok / 901 stale) —
|
||
if (nowSeconds - authDate > maxAgeSeconds) {
|
||
return { ok: false, reason: "stale" as const };
|
||
}
|
||
```
|
||
|
||
The missing vector is the INCLUSIVE edge: age exactly **900 s → ok:true**. Copy the T4 harness shape verbatim (`signedUser(now - 900)` + `loadValidator()` lazy import, lines 43–93). The comment on line 193 already promises "900 ok" — the test makes the promise a pin. Also pin `maxAgeSeconds` override behavior if exercised (opts line 71–73).
|
||
|
||
**IN-01 targets** (same file, `initdata.ts`):
|
||
- Dead export: `ParsedInitData` (lines 59–66) — declared, never constructed or returned (the return type is `InitDataResult`, lines 75–83). Fix per the repo's debt style: either delete the interface or re-point it with an intent comment naming why it stays (prefer deletion; the review finding says "dead").
|
||
- Unreachable branch + read-helper intent comments: `parseFields` (lines 87–104) and the user/chat parse blocks (lines 154–165, 198–214) — add the intent comments documenting WHY each catch keeps raw/undefined (tampered-vs-malformed precedence, line 25–26 documents the vocabulary).
|
||
|
||
**IN-02:** `bot/wrangler.jsonc` line 2 — `// KV namespace id is filled in plan 03 task 1 after …` — is stale: the id IS filled (line 36). Replace with a present-tense comment in the file's own voice (see lines 9–11, 23–29 for the comment register).
|
||
|
||
---
|
||
|
||
### `bot/src/i18n/strings.ts` — `/help` limitations pointer (en/ms)
|
||
|
||
**Analog:** `strings.ts` itself — the D-05 law ("Every user-facing string the bot sends lives here and NOWHERE else", lines 3–7) and `help.ts` (`helpReplyText` lines 18–20 returns `strings[locale].helpMenu` verbatim).
|
||
|
||
The pointer line is appended to BOTH `helpMenu` strings (en line 94–95, ms line 132–133). Clinical+honest tone per the module header (lines 9–11): "no emoji, no marketing flourishes".
|
||
|
||
> **Planner gotcha (CS5):** `bot/test/commands.test.mjs` `menuEntries()` (lines 134–144) parses every helpMenu line STARTING WITH `/` and requires the `" — "` marker; CS5 (lines 146–155) asserts the parsed command list is EXACTLY `["help", "privacy", "cari"]`. The limitations pointer line must therefore NOT start with `/` (e.g. `Known limits: <url>` / `Had panduan: <url>` is safe — it is filtered out of the menu parse). If the copy instead extends an existing `/help` line, CS5 still passes (the marker stays); a NEW `/command`-shaped line fails CS5 by design.
|
||
>
|
||
> The URL should be a single constant per RESEARCH Open Question 5 (deployment config or one strings-module constant — never scattered). `register-commands.mjs` derives from `helpMenu` via `menuFor()` — a non-`/` line stays out of the registered command descriptions automatically, so the re-run remains copy-safe.
|
||
|
||
---
|
||
|
||
### `docs/KNOWN-LIMITATIONS.md` (new doc)
|
||
|
||
**Analog:** `docs/CONFIGURATION.md` — clinical engineering prose, dense tables with a leading "surfaces, by kind" table (lines 9–22), per-row `File | Applies to` scoping, backticked identifiers, bold for requirements, and a strong claim-surface discipline (each statement names its evidence). It joins the same directory (`docs/`) alongside CONFIGURATION/DEVELOPMENT/ROAD-GRAPH/TESTING. Content map from D-08: OSM data gaps · cross-metro CPU degradation (c3, cite `measure-route-cpu.mjs` LADDER HISTORY lines 30–40 as the evidence) · /cari menu defect + workaround · beta quality statement · roadmap pointers (WASM-router, ALT, engine B). Written for drivers AND forks.
|
||
|
||
---
|
||
|
||
### `.planning/phases/07-hardening-private-launch/07-LAUNCH-GATE.md` (new gate file)
|
||
|
||
**Analog:** `.planning/phases/06-turn-by-turn-guidance-live-driving/06-DEVICE-GATE.md` — copy its structure:
|
||
|
||
1. **Blocking status header** (lines 3–8): what is already green (suite counts + commands), the never-fake law verbatim — *"device results are never faked … this file records the protocol; the SUMMARY records the verdict verbatim once the drive runs. A FAIL holds the phase open"*.
|
||
2. **Part tables** (lines 32–43): `| # | Step | Verify |` rows — here: the consolidated D-05 walk (Android + iOS + Desktop + browser × boot / search→route / guidance / minimize-resume / trip-end / theme flip / landscape-fullscreen) and the D-04 battery record (device/OS, brightness, network, screen-on interval ≥30 min, start/end %, route, computed %/hr, caveats — RESEARCH Pitfall 11's field list).
|
||
3. **Executor notes** (lines 59–68): known-open items, R2/credentials caveats, and the honesty rule — *"if a step cannot be executed … mark it NOT RUN — do not invent an outcome"*.
|
||
4. **FINAL VERDICT section** (lines 72–82): part-by-part table + dated signature; the D-09 launch checklist (known-limitations shipped · privacy audit signed · budget record green · device matrix walked · /help + /privacy accurate · repo public GPL-3.0) signs here.
|
||
|
||
---
|
||
|
||
## Shared Patterns
|
||
|
||
### Capability guard: BOTH halves + `?.` chain + no-throw
|
||
**Source:** `web/src/guidance/close-protection.ts` lines 21–31; `web/src/telegram/homescreen.ts` line 33, lines 51–58
|
||
**Apply to:** `web/src/telegram/fullscreen.ts`, the `webapp.ts` contract growth, every fullscreen test vector
|
||
```typescript
|
||
if (wa !== null && typeof wa.isVersionAtLeast === "function" && wa.isVersionAtLeast("8.0")) wa?.requestFullscreen?.();
|
||
// a throw is caught and ignored — degradation is a supported state, never an error surface
|
||
```
|
||
|
||
### Contract + fake + test together
|
||
**Source:** `web/src/telegram/webapp.ts` header lines 16–27; `web/src/mock/telegram-mock.ts`; `web/test/homescreen.test.mjs` `fakeWebApp()`
|
||
**Apply to:** adding fullscreen members — `webapp.ts` optional members, the harness mock, and every test fake change in ONE commit.
|
||
|
||
### Comment-stripped source introspection (prefer-fail reading)
|
||
**Source:** `web/test/no-storage.test.mjs` lines 29–46 (canonical); twins in `route-privacy.test.mjs` 42–59, `homescreen.test.mjs` 58–66
|
||
**Apply to:** `scripts/lib/source-gates.mjs` — and to any new structural test (HS9/GS6 style).
|
||
|
||
### Gate vocabulary: print the measurement, exit 0/1/2
|
||
**Source:** `theme/scripts/audit.mjs` lines 20–24, 797–814; `measure-route-cpu.mjs` lines 180–185; BA9 lines 226–233
|
||
**Apply to:** both audit CLIs. 0 = pass, 1 = gate failure (evidence printed), 2 = environment/credential drift (never mistaken for pass). RESEARCH Pattern 4 is the orchestration shape:
|
||
```javascript
|
||
for (const result of results) console.log(`${result.id}: ${result.pass ? "PASS" : "FAIL"} — ${result.evidence}`);
|
||
process.exit(results.every((r) => r.pass) ? 0 : 1);
|
||
```
|
||
|
||
### Category-only log vocabulary
|
||
**Source:** `bot/src/webhook.ts` lines 63–68 (the law), lines 103/234/244/272/303/318/380/388 (every call site)
|
||
**Apply to:** the privacy audit's log-vocabulary gate — the scan asserts every `console.log` in `bot/src` matches the `[surface] verdict: category` shape and that no line can carry coordinates/ids/payloads. The fullscreen fallback path must also stay category-only (RESEARCH Pattern 1 required behavior).
|
||
|
||
### Named TTL constants + the KV key families
|
||
**Source:** `bot/src/kv/sessions.ts` line 43 (`session:`+`chat:` @ 900s), `location-pin.ts` line 25 (`loc:` @ 300s), `guidance-sessions.ts` line 27 (`guidance:` @ 7200s); PG1 same-line law
|
||
**Apply to:** the KV listing probe — assert every live key family is one of these four AND carries an `expiration` (RESEARCH A5/Pitfall 7: parse `expiration`, cursor, `list_complete`; report unexpected families, don't assume absence). Probe syntax (already used manually in 06-DEVICE-GATE.md step 7): `npx wrangler kv key list --namespace-id=59f58a54bc44483e89872b20a005a5eb --prefix "guidance:"`.
|
||
|
||
### Copy lives in strings.ts only (D-05)
|
||
**Source:** `bot/src/i18n/strings.ts` lines 3–7; `commands.test.mjs` CS5 lines 157–187 (register script must not inline copy)
|
||
**Apply to:** the /help pointer line + `docs/KNOWN-LIMITATIONS.md` link constant.
|
||
|
||
### Device-gate honesty protocol
|
||
**Source:** `06-DEVICE-GATE.md` lines 3–8, 67–68
|
||
**Apply to:** 07-LAUNCH-GATE.md — PASS/FAIL/NOT RUN with client version; a NOT RUN leg never blocks as fake PASS; battery numbers are recorded, never synthesized (D-04).
|
||
|
||
## No Analog Found
|
||
|
||
| File | Role | Data Flow | Reason | Planner guidance |
|
||
|------|------|-----------|--------|------------------|
|
||
| `web/public/_headers` | config | static | No static-asset config file exists anywhere in the repo (`web/public/` holds only glyphs/logos) | Use RESEARCH §Code Examples "Static Asset Cache Rules" verbatim (`/assets/*` → `public, max-age=31556952, immutable`; `/` → `public, max-age=0, must-revalidate`); verify live with the budget-audit `--live` probe; keep `web/dist` Vite output shape in mind (hashed `/assets/*`) |
|
||
| `scripts/lib/cloudflare-probes.mjs` | utility | request-response (live) | Every existing gate is static/source-level; the only live-KV precedent is a manual command recorded in 06-DEVICE-GATE.md step 7 | Follow `measure-route-cpu.mjs`'s maintainer-time-tool header law + theme-audit exit codes; `--live` flag gates the probe; missing credentials → exit 2 with the gate marked NOT RUN (never pass) |
|
||
|
||
## Metadata
|
||
|
||
**Analog search scope:** `web/src/`, `web/test/`, `web/scripts/`, `bot/src/`, `bot/test/`, `bot/scripts/`, `routing/scripts/`, `theme/scripts/`, `docs/`, `.planning/phases/{03,04,06,07}/`, root `package.json`
|
||
**Files read in full or in part:** 24 (no-storage, route-privacy, privacy, build-artifact, homescreen.ts/.test, harness.mjs ×2, runner.mjs, webapp.ts, close-protection.ts, safe-area.ts, shell.css, guidance-replay (head), guidance-surface (head), measure-route-cpu, theme/audit.mjs, initdata.ts, initdata.test.mjs, strings.ts, help.ts, wrangler.jsonc, webhook.ts, commands.test.mjs (CS5 region), CONFIGURATION.md (head), 06-DEVICE-GATE.md)
|
||
**Pattern extraction date:** 2026-10-25
|