Files
arah2/.planning/phases/07-hardening-private-launch/07-PATTERNS.md

27 KiB
Raw Permalink Blame History

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 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):

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 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

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