27 KiB
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):
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 keepshttps://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):
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:
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:
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):
* 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:
* 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:
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:
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):
/* … 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:
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:
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.:
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:
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:
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):
* 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:
[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:
// — 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 isInitDataResult, 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.mjsmenuEntries()(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/helpline, 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.mjsderives fromhelpMenuviamenuFor()— 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:
- 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".
- 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). - 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".
- 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
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:
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