# 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]}" — `); ``` **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: ` / `Had panduan: ` 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