GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

DIAGNOSTIC_SHELL_PLAN.md



RELEASE

planning/2.8.0/plans/DIAGNOSTIC_SHELL_PLAN.md

sha256 95701b1c553f4ed9 · 34106 bytes · original held in the private archive

# DIAGNOSTIC SHELL — Dev Console Overhaul (Opus Plan, Protocol 8 Stage 1) > Rebrands and rebuilds the "TEST CONSOLE (DEV)" into the diegetic **DIAGNOSTIC SHELL**. > Owner-approved spec. PLANNING ONLY — no source edits, no commits. Sonnet units implement after. > Base: `dev` @ `c252138`. APP_VERSION 2.7.0 / `[Unreleased]` (v2.8.0 program). Cache r129. --- ## 0. TL;DR The current console is a single hand-wired `<details>` panel of ad-hoc controls, rendered in **document flow above the machine** (the mobile displacement bug), gated by one fail-safe env check. We keep the gate philosophy and every existing control, but re-found the whole thing on a **data-driven TOOL REGISTRY** that auto-filters by a **two-signal environment gate** and auto-confirm-gates destructive tools. One panel, two products: a full **STAGING TOOLBENCH** and a non-destructive **PROD-MINIGAME sandbox**, separated by construction so cheat/destructive/inspection tools can *never* reach a production player. It floats as a fixed overlay drawer (never displacing layout), wears a diegetic DIAGNOSTIC-SHELL skin with icons, and gains a new **Protocol 44** that forces every future hard-to-trigger feature to register a trigger in the same commit. Delivered in **5 Protocol-8 units**. Recommended first unit: **U1 — Registry spine + two-signal gate + shell scaffold (migrate every existing control, lose nothing).** --- ## 1. CURRENT-CONSOLE INVENTORY (what exists today, at `c252138`) ### 1.1 Files & wiring | Piece | Location | Notes | |---|---|---| | Console logic | `js/test-console.js` (IIFE, ~397 lines) → `window.initTestConsole` | classic script; can read the global `state`, `escapeHtml`, `MetaStore` directly | | Markup | `<template id="testConsoleTemplate">` `index.html:166–283` | inert WU-E2 `<template>`; cloned into `#testConsoleMount` (`index.html:284`) | | Mount point | `#testConsoleMount` `index.html:284` | **a body-level `<div>` BEFORE `.container.machine` (`:340`)** → renders at the very top of the page, in document flow → **pushes the whole machine down (the mobile "shoves content down" complaint)** | | Boot call | `initTestConsole()` in `window.onload`, `ui-core.js:3117` (after `initAmbientRuntime()`) | | | SW precache | `sw.js:33` `'./js/test-console.js'` | | | eslint global | `eslint.config.mjs:169` `initTestConsole: 'readonly'` | | | Guards | Suite 149 (16 tests) + extensions in Suites 150/151/154/161/162 | | ### 1.2 The gate (keep this philosophy verbatim) ```js function _devConsoleUnlocked() { try { return typeof window._isStagingEnv === 'function' ? window._isStagingEnv() : false; } catch (_) { return false; } // fail-safe → HIDDEN on any uncertainty } ``` - `_isStagingEnv()` (`ui-core.js:4686`): positive staging signals only — `<meta name="robco-env" content="staging">` (primary, stamped by `scripts/cf-staging-build.mjs`), `window.__ROBCO_ENV__==='staging'`, or hostname `localhost` / `127.0.0.1` / `*.pages.dev`. Defaults **false** (production). - Documented **MINIGAME-UNLOCK SEAM** on `_devConsoleUnlocked()` — the one hook the future hacking minigame flips to `true` on production. This is the canonical console, not a throwaway. ### 1.3 Existing controls (every one must survive the overhaul) 1. **RUNTIME STATE** readout — `#testConsoleState`, live (via an AmbientRuntime observer). 2. **FORCE TRANSITION** — buttons for `IDLE / STANDBY / SHUTDOWN / OFF` only (`VISIBLE_EFFECT_STATES`); each routes `AmbientRuntime.forceState()` (the TEST-ONLY escape hatch bypassing the LEGAL adjacency map). 3. **REBOOT** — resets `#bootScreen` display/classes then `runBootSequence()` (`_rebootFromConsole`). 4. **WAKE → ACTIVE** — `AmbientRuntime.forceState('ACTIVE')`; the one-click undo for the dim/shutdown states. 5. **IMMERSION TIER** select — `#testConsoleImmersionSelect` → `onImmersionChange()` + syncs the real `#immersionSelect` dial. 6. **REGISTERED OBSERVERS** — `#testConsoleObservers`, live list from `AmbientRuntime.listObservers()`. 7. **VIEW-ONCE CEREMONIES → REPLAY HATCH** — `#testConsoleReplayHatch` → `MetaStore.remove('robco_bay_opened')` + resets `#bayHatch` to closed/pre-ceremony. 8. **OPTICAL SCAN TEST (OCR Unit 1)** — `#ocrTestInput` + SCAN → `runVisualOcrTest()` → dumps raw OCR text to `#ocrTestOutput`. 9. **SCAN & PARSE TEST (OCR Unit 2)** — `#visualParseTestInput` + SCAN & PARSE → `runVisualOcr()` → real preview/confirm modal. Live refresh: the console's own readout is itself an AmbientRuntime observer (`test-console-refresh`, 500ms, tier `minimal`) — not a competing `setInterval`. **Hard boundary (Phase-2 invariant #1):** touches ONLY in-memory runtime state + the Immersion MetaStore pref. Never reads/writes campaign save/stats/event log; never pushes to cloud; no auto-anything. ### 1.4 Feature entry points already available to wire (no new plumbing needed) - **Feedback animations (the 33)** — driven two ways: - **Bus emits** (`window.RobcoEvents.emit(name, payload)`): `level.up`, `stat.change`, `limb.state`, `rad.tier`, `hp.critical`, `weight.seized`, `karma.tier`, `item.added`, `item.equipped`, `effect.applied`, `effect.expiring`, `faction.threshold`, `collectible.acquired`, `quest.status`, `craft.completed`, `craft.scrapped`, `trade.bought`, `trade.sold`, `sleep.completed`, `data.write`, `location.current`, `location.visited`, `runtime.state`. **The durable state write always happens at the CALL SITE before the emit; the subscriber is display-only — so emitting the event from the shell fires ONLY the animation, never a state write.** This is what makes "fire any animation" inherently non-destructive (and thus prod-tier-eligible). - **Pending-var + render** (`state.js` module vars consumed at next render): `_pendingRepStamp`, `_pendingQuestStamp`, `_pendingExhibitLight`, `_pendingSurveyPing`, `_pendingQuestFiled`, `_pendingPerkSeat`, `_pendingEffectWarmup`. Fire = set the var + call the owning `render*()`. - **Living core** — `setOverseerState(s)` (the ONE state-setter: thinking/speaking/listening/disabled/offline), `_coreFlare()`, `_coreStatBurst()`, `initChassisCore()`; core visuals also react to `runtime.state`, `data.write`, the error-log fault count, radio, and optic colour. - **Boot flavors** — `window.__robcoBootFlavor` override consumed by `_pickBootFlavor()` (`ui-audio.js:1494`): `'degraded'` / first-cold / normal; REBOOT via `runBootSequence()`. - **Ceremonies M1–M5 + hatch** — `_runCampaignIgnition(onComplete)` (M1), `_maybeGreetOverseer()` (M2, session flag `_overseerGreeted`), `_checkFirmwareFlash()` + firmware flourish (M3, `robco_last_seen_version`), `_checkLongAbsence()` (M4, `lastFlushAt`), `_motionSeat(el)` (M5 SEAT verb); hatch via `releaseBayHatch()` / `robco_bay_opened` / `#bayHatch`. - **Runtime** — `AmbientRuntime.forceState/transition/getState/listObservers/shutdown/STATES`. - **Immersion** — `getImmersionTier()`, `onImmersionChange()`. - **Feature flags** — `window.isFeatureEnabled(key)` (READ-ONLY today; `_featureFlags` is private in the `cloud.js` ES module). Flags: `cloudSync, googleSignIn, aiChat, keySync, saveMigration, offlineQueue, visualOcr, visualAiVision`. **A live toggle needs a new `window._setFeatureFlagOverride(key, bool)` export in `cloud.js` (U5 seam).** - **Error log** — `_recordError(type, msg)`, `showErrorLog()`, `_readErrorLog()`, `ERROR_LOG_KEY='robco_error_log'`. - **AI-failure sim** — `window._recordFeatureFailure(key, msg)` (`cloud.js`, the FAIL_THRESHOLD auto-disable path). - **Cache / SW** — `caches.keys()`, `navigator.serviceWorker.getRegistrations()`, the live `CACHE_NAME`. - **State inspector** — the global `state` (bare `let`, `state.js:1493`) + `snapshotActiveCampaign()`. - **Game context** — `onGameContextChange(ctx)` (reboots the terminal), `#gameContextSelect`; FO4 is `designOnly:true` (unreachable by design). - **Confirm / modal** — `confirmAction(opts)` (`ui-core.js:4988`, Promise-based diegetic confirm) + `openModal(opts)` (`:4927`). --- ## 2. THE BACKBONE — DATA-DRIVEN TOOL REGISTRY ### 2.1 Tool shape (one entry per tool; add a tool = add one entry) ```js // DIAGNOSTIC_SHELL_TOOLS — the single source of truth. Console renders from this. { id: 'fire-levelup', // unique, kebab-case label: 'LEVEL-UP CARD', // diegetic/short label shown in prod-fiction subLabel: 'RobcoEvents level.up', // REAL control label (searchable; Module-Bay pattern) icon: '▲', // every tool carries an icon (spec: icons everywhere) category: 'triggers', // section key (see §3.1) tier: 'prod' | 'staging', // 'prod' = non-destructive sandbox; 'staging' = owner-only destructive: false, // true → auto-wrapped in confirmAction() tooltip: 'Fire the Vault-Boy level card + XP shimmer (no state write).', triggers: ['level.up'], // Protocol 44 metadata (see §7) — events/flags this covers action: () => window.RobcoEvents.emit('level.up', { newLevel: /* synthetic */ }), } ``` Optional fields for two special renders: - **Inline resets** (`category:'inline'`): add `anchor:'#someControl'` + `placement:'after'|'before'|'prepend'` — where the small DEV-MARKER reset button attaches (see §5). - **Toggles / selects**: add `control:'toggle'|'select'`, `options:[…]`, `read:()=>…`, `write:(v)=>…` so the renderer draws a switch/dropdown instead of a button. (Keeps the registry declarative; the renderer branches on `control`.) ### 2.2 The two-signal gate (leak-proof by construction) Today one signal answers "show it or not." The overhaul needs two, because the same panel serves two audiences: ```js // _shellVisible — does the shell exist AT ALL? (unchanged philosophy: _devConsoleUnlocked) // true on staging OR on production-after-minigame; fail-safe → false (absent) on any uncertainty. function _shellVisible() { return _devConsoleUnlocked(); } // _shellTier — WHICH tools may render. // 'staging' ONLY when staging is POSITIVELY confirmed; otherwise 'prod' (the RESTRICTIVE tier). // Fail-safe → 'prod' on any uncertainty (a throw, a missing fn) — the same fail-safe direction // as the changelog viewer's "default to production." function _shellTier() { try { return (typeof window._isStagingEnv === 'function' && window._isStagingEnv()) ? 'staging' : 'prod'; } catch (_) { return 'prod'; } } // _toolVisible — the ONE filter every tool passes through before it ever enters the DOM. function _toolVisible(tool, tier) { if (tool.tier === 'prod') return true; // prod tools show in both tiers (owner sees all) return tier === 'staging'; // staging tools ONLY when staging is confirmed } ``` **Why this is leak-proof:** a `tier:'staging'` tool has exactly one way to render, and it requires `_shellTier()==='staging'`, which requires a *positive* staging signal. On production (default, or minigame-unlocked) `_shellTier()` is `'prod'` and every staging tool is filtered out **before DOM insertion**. Destructive / cheat / inspection tools are `tier:'staging'` **by construction**, so they cannot reach a prod-minigame player. This is the extensibility spine *and* the safety spine at once. ### 2.3 Auto-confirm-gate The renderer wraps any `destructive:true` tool's `action` in `confirmAction()` automatically: ```js function _invoke(tool) { if (!tool.destructive) return tool.action(); return confirmAction({ title: '> ' + tool.label, body: tool.tooltip || 'This is a destructive diagnostic action.', confirmLabel: 'EXECUTE', cancelLabel: 'ABORT', }).then(ok => { if (ok) tool.action(); }); } ``` No caller ever forgets a confirm gate — it's a property of the data, enforced by the one render path (Protocol 22 / 34). --- ## 3. SECTION / UX DESIGN ### 3.1 Categorized, collapsible sections (not a flat button wall) | Category key | Section title (staging / prod-fiction) | Contents | Predominant tier | |---|---|---|---| | `triggers` | TRIGGERS / **STIMULUS BENCH** | fire-any-of-33 animations, force living-core states, force boot flavor, replay ceremonies M1–M5 + hatch, SEAT verb, day/night-time control | **prod** (non-destructive) | | `state` | STATE SETUP / *(hidden in prod)* | scenario presets, quick-poke state buttons | staging | | `resets` | RESETS / *(hidden in prod)* | granular confirm-gated resets, first-run/view-once flag clears, error-log clear, device-pref reset | staging (destructive) | | `infra` | RESILIENCE & INFRA / *(hidden in prod)* | feature-flag toggles, simulate-AI-failure, cache/SW controls | staging | | `inspect` | INSPECT / **READOUTS** | state JSON, cache rev, SW version, runtime state, observers, perf/FPS, copy diagnostic bundle | staging (a couple prod readouts) | | `fixtures` | FIXTURES / *(hidden in prod)* | Load NV Test Campaign | staging (destructive) | | `env` | ENVIRONMENT & UNLOCK / **ACCESS** | env banner, minigame lock/unlock TEST + unlock ceremony, a11y/reduced-motion/immersion quick-toggles | mixed | | `inline` | *(not a section — rendered at anchors)* | inline dev-reset buttons (§5) | staging | Each section is a `<details class="sub-panel" data-sub-id="…">` (Protocol UI-1/UI-2 persistence), a section **hidden entirely** when it has zero visible tools for the current tier (so prod shows only STIMULUS BENCH / READOUTS / ACCESS — a clean, non-destructive sandbox). ### 3.2 Chrome - **ENVIRONMENT BANNER** at the top: `STAGING TOOLBENCH — ALL SYSTEMS EXPOSED` (amber) vs `RESTRICTED DIAGNOSTIC ACCESS — SANDBOX` (phosphor) — driven off `_shellTier()`. - **SEARCH / filter** input: live-filters tool `label` + `subLabel` (real control labels are the searchable sub-labels, Module-Bay pattern), across all sections. - **Sub-labels + tooltips**: every tool shows its real `subLabel` under the diegetic label; `title=` tooltip on each control. - **Icons everywhere** (spec): section icons + per-tool `icon`; inline resets carry the shared DEV-MARKER glyph (§5). ### 3.3 Render pipeline (single path) ``` _renderShell(): tier = _shellTier() for each category in ORDER: tools = TOOLS.filter(t => t.category===cat && _toolVisible(t, tier)) if !tools.length: skip section draw <details sub-panel> with tools (button | toggle | select per t.control) wire search; wire the env banner; the live-readout observer stays as-is ``` `_toolVisible` runs **before** any DOM node is created — nothing filtered ever touches the document. --- ## 4. FULL TOOL LIST WITH TIERS > "prod" = non-destructive immersion sandbox (safe for a minigame-unlocked production player). > "staging" = owner-only toolbench. Destructive tools (✔) are auto-confirm-gated. ### TRIGGERS (`triggers`) | id | label | tier | destr | mechanism | |---|---|---|---|---| | `fire-anim-<event>` (×~23) | one per bus event | **prod** | – | `RobcoEvents.emit(event, syntheticPayload)` | | `fire-pending-<name>` (×7) | rep-stamp / quest-stamp / exhibit-light / survey-ping / quest-filed / perk-seat / effect-warmup | **prod** | – | set `state.js` pending var + call owning `render*()` | | `core-state-<s>` | force living-core thinking/speaking/listening/disabled/offline | **prod** | – | `setOverseerState(s)` | | `core-flare` / `core-burst` | flare / stat-burst | **prod** | – | `_coreFlare()` / `_coreStatBurst()` | | `runtime-<state>` | force IDLE/STANDBY/SHUTDOWN/OFF/ACTIVE *(migrated #2/#4)* | **prod** | – | `AmbientRuntime.forceState()` | | `reboot` | replay boot *(migrated #3)* | **prod** | – | reset `#bootScreen` + `runBootSequence()` | | `boot-flavor-<f>` | force degraded / first-cold / normal next boot | **prod** | – | set `window.__robcoBootFlavor` then reboot | | `ceremony-ignition` | replay M1 campaign ignition | **prod** | – | `_runCampaignIgnition(()=>{})` | | `ceremony-greet` | replay M2 Overseer greeting (reset `_overseerGreeted` first, **no MetaStore clear**) | **prod** | – | reset session flag + `_maybeGreetOverseer()` | | `ceremony-firmware` | replay M3 firmware flash flourish | **prod** | – | call the flourish directly (no version-flag mutation) | | `ceremony-absence` | replay M4 long-absence boot line | **prod** | – | set the transient `_longAbsenceBoot` path + reboot | | `ceremony-seat` | fire M5 SEAT on a sample element | **prod** | – | `_motionSeat(el)` | | `replay-hatch` | replay Module-Bay hatch *(migrated #7)* | **staging** | ✔ | `MetaStore.remove('robco_bay_opened')` + reset `#bayHatch` | | `time-daynight` | day/night-time control | **prod** | – | drive the day/night visual state (transient) | > Ceremony replays **must not clear their real view-once flags** (owner directive). Replay by calling > the presentation function directly / resetting a *session* flag — never by wiping the persisted > `robco_last_seen_version` / `robco_bay_opened` (that's what the separate RESETS tools are for). ### STATE SETUP (`state`, all **staging**, all ✔destructive — they mutate the live campaign) `preset-low-hp`, `preset-crippled`, `preset-over-encumbered`, `preset-addicted`, `preset-high-level`, `preset-fresh-start`; plus quick-poke buttons: `poke-hp±`, `poke-rads±`, `poke-caps±`, `poke-xp±`, `poke-karma±`. Each writes `state.*` via the existing native setters (`_nativeSetHp` etc.) then `saveState()` + re-render. Guard for **no active campaign** (no-op + toast). ### RESETS (`resets`, all **staging** ✔destructive) `reset-first-run` (`robco_booted_before`), `reset-firmware-seen` (`robco_last_seen_version`), `reset-hatch-flag` (`robco_bay_opened`), `reset-greet-flag`, `reset-error-log` (clear `robco_error_log`), `reset-device-prefs` (clear registered MetaStore device keys — **never** campaign keys), `reset-bezel-view`, etc. Each `confirmAction`-gated automatically. ### RESILIENCE & INFRA (`infra`, all **staging**) - `flag-<key>` toggles (×8) — `control:'toggle'`, `read:()=>isFeatureEnabled(key)`, `write:(v)=>window._setFeatureFlagOverride(key, v)` **(new cloud.js export, U5 seam)**. - `sim-ai-failure` — drive `_recordFeatureFailure('aiChat'|'visualOcr', …)` to FAIL_THRESHOLD to exercise the auto-disable + fallback path. - `sw-clear-caches` (✔) — `caches.keys()` → delete all; `sw-unregister` (✔) — `getRegistrations()` → unregister; `sw-show` — readout only (→ INSPECT). ### INSPECT (`inspect`, **staging** except the two marked prod) `inspect-state-json` (pretty-print `state` in a modal), `inspect-cache-rev` (**prod** — live `caches.keys()`), `inspect-sw-version` (**prod** — active registration), `inspect-runtime` (state + observers — the migrated live readout #1/#6), `inspect-perf-fps` (a rAF FPS meter, transient), `copy-diag-bundle` (copy version + cache rev + flags + state-size + recent errors to clipboard). ### FIXTURES (`fixtures`, **staging** ✔destructive) `fixture-nv-test-campaign` — load a canned NV campaign into `state` (confirm-gated; guards existing campaign). ### ENVIRONMENT & UNLOCK (`env`) `env-banner` (readout, **prod**), `minigame-lock-test` / `minigame-unlock-test` (**staging** — flip the future persisted unlock flag to preview the prod-tier view without leaving staging), `unlock-ceremony` (**prod** — the earned "RESTRICTED ACCESS GRANTED" moment), `a11y-reduced-motion` / `a11y-immersion` quick-toggles (**prod** — device prefs). --- ## 5. INLINE DEV-RESET BUTTONS (share the registry + gate, not a parallel system) Small buttons rendered **on the thing itself** (e.g. a tiny reset next to a view-once ceremony control, or next to a device toggle), confirm-gated, each carrying a consistent **DEV-MARKER glyph** (propose `⌦` or a spanner `🔧`/`⚙` — pick one, use it for *every* inline reset), **staging-only**. They are **registry entries** with `category:'inline'` and an `anchor` selector. A shared `_mountInlineResets()`: ``` for tool in TOOLS where category==='inline' and _toolVisible(tool,'staging'): if _shellTier()!=='staging': continue // never in prod, by construction el = document.querySelector(tool.anchor); if !el: continue btn = <button class="dev-marker-reset" title=tool.tooltip> DEV_MARKER </button> btn.onclick = () => _invoke(tool) // same auto-confirm-gate as §2.3 insert(btn, el, tool.placement) ``` Same gate, same confirm path, same glyph, same registry — so an inline reset can never leak to prod and can never skip its confirm. Adding one = one registry entry with an `anchor`. --- ## 6. MOBILE-OVERLAY APPROACH (the owner's active complaint) **Problem:** `#testConsoleMount` is a body-level `<div>` *before* `.container.machine`, so the panel occupies document flow at the top of the page and shoves the machine down on mobile. **Fix — a floating toggle + fixed overlay drawer, never in document flow:** 1. **Floating toggle FAB** — a `position:fixed` button carrying the DEV-MARKER glyph, mounted at a body level (see below), visible only when `_shellVisible()`. Placed to avoid the fixed mobile **bezel dock** at the bottom (propose: top-right, or a small offset above the dock) — verified at 360/412 for no overlap with real controls. 2. **Overlay drawer** — a `position:fixed`, full-height slide-in panel (`right:0; top:0; bottom:0; width:min(92vw,420px)`), scrolls **internally** (`overflow-y:auto`), with a scrim behind it. Opening/closing never changes the machine's layout → **zero displacement**. 3. **CRITICAL placement rule (documented trade-off):** mount the FAB **and** the drawer at **body level, as siblings of `.container.machine` — NOT inside the casing** — mirroring exactly what `#locationCard` already does. The ambient-runtime idle/standby/shutdown states apply `filter`/`transform` to the casing, which makes the casing the containing block for any fixed descendant (the known bezel-dock degradation). A body-level shell escapes that entirely and stays pinned in every runtime state. 4. **Animation** — the slide is a plain `@keyframes` (Protocol UI-9), auto-neutralized to an instant open by the global `prefers-reduced-motion` block. No bespoke carve-out. 5. **A11y** — the open drawer is `role=dialog aria-modal`, traps focus, closes on `Esc` and scrim tap (reuse the `openModal`/`sysModal` focus-trap conventions where practical). ≥28px tap targets, ≥16px inputs (Protocol 17). 6. **Desktop** — unify on the same floating toggle + drawer (one code path). Desktop verification confirms it doesn't collide with the desktop shell; if the owner prefers a docked desktop panel, that's a one-line CSS media-query variant, but the recommendation is **one overlay path for all breakpoints** to keep it simple. The inert `<template>` (WU-E2) stays: the drawer's markup lives in the template and is cloned into a body-level mount only when `_shellVisible()`. --- ## 7. NEW PROTOCOL 44 — every hard-to-trigger feature registers a Diagnostic Shell trigger **Proposed text (for `CLAUDE.md`):** > ## Protocol 44 — Every Hard-to-Trigger Feature Ships a Diagnostic Shell Trigger > > Any new **ambient, conditional, time-gated, view-once, or otherwise hard-to-reproduce** feature — > a new `RobcoEvents` event, a new AmbientRuntime observer/state effect, a new boot flavor, a new > ceremony/view-once MetaStore flag, a new feedback animation — must register a **Diagnostic Shell > tool** (`DIAGNOSTIC_SHELL_TOOLS` entry, `js/test-console.js`) that fires it on demand, **in the > same commit**. The tool declares which event(s)/flag(s) it covers via its `triggers:[…]` metadata. > > **Why:** these features are exactly the ones that can't be exercised by normal play in a test pass, > so they silently rot. A guaranteed on-demand trigger keeps every one of them verifiable, and keeps > the shell a complete control surface rather than a stale subset. > > **Enforcement (self-improving — Protocol 36b):** a gate suite cross-references (a) every > `RobcoEvents.emit('<name>', …)` string literal in `js/*.js` and (b) every view-once MetaStore flag > against the union of all `triggers:[…]` arrays in `DIAGNOSTIC_SHELL_TOOLS`. A feature whose event > or flag has **no** registry trigger **fails the build**. (A tiny curated allowlist covers the > deliberately-internal events — e.g. `runtime.state` is infra, not a user feature — so the guard is > precise, not noisy.) This is enforceable **today** with the data we already have: the emit-site grep in §1.4 is the exact input the guard consumes. --- ## 8. GATE / LEAK-PROOF DESIGN (prove it structurally) Belt, suspenders, and a static proof: 1. **Existence gate** — `_shellVisible()`/`_devConsoleUnlocked()` fails to **false** (shell absent). 2. **Tier gate** — `_shellTier()` fails to **`'prod'`** (restrictive). Both fail in the *safe* direction independently. 3. **Filter before DOM** — `_toolVisible()` runs before any node is created; a `tier:'staging'` tool never enters the document unless staging is positively confirmed. 4. **Inert template** — markup lives in `<template>` (WU-E2), cloned only when `_shellVisible()`. 5. **Static leak-proof suite (new, both runners):** - Every tool with `destructive:true` **must** be `tier:'staging'` (no destructive prod tool). - Every `category:'state'|'resets'|'infra'|'fixtures'|'inline'` tool **must** be `tier:'staging'`. - A behavioral proof (VM sandbox, the Suite 62.5 / 149.8 technique): render the registry with `_shellTier()` stubbed to `'prod'` and assert **zero** `tier:'staging'` tools appear in the output, and that no destructive action is reachable. - `_shellTier()` returns `'prod'` when `_isStagingEnv` is missing / throws / unknown host. 6. **No campaign write from prod tools** — a static guard that every `tier:'prod'` tool's `action` contains no `saveState(`/`state.`-write/`robco_v8`/cloud identifier (the runtime.js no-durable-write grep pattern, applied to prod-tier actions). 7. **PWA-intact** — additive only; no SW/manifest behavior change (keep `sw.js` ASSETS entry). If the file is renamed (see §9), the rename escape-ratchet (Suite 128 pattern) applies. --- ## 9. IMPLEMENTATION SEAMS & DECISIONS (recommendations, not blockers) - **Filename:** **keep `js/test-console.js`** (rebrand the *identity* to DIAGNOSTIC SHELL in UI + comments only). Renaming to `diagnostic-shell.js` would trigger a Suite-128-style escape-ratchet across `sw.js` / eslint / docs for no functional gain. Recommend deferring any rename to a separate optional housekeeping commit. - **Feature-flag setter:** add `window._setFeatureFlagOverride(key, bool)` to `cloud.js` (writes the private `_featureFlags` + the LKG mirror) — the only new plumbing outside `test-console.js`. Staging-tool-only; the registry never exposes it to prod. - **DEV-MARKER glyph:** pick ONE (recommend `⚙` or `🔧`) and use it for the FAB and every inline reset, consistently. - **`state` inspector:** reads the global `state` directly (classic-script IIFE already can) — read-only pretty-print, never a write. - **Registry lives in `test-console.js`** (module-scope `const DIAGNOSTIC_SHELL_TOOLS = [...]`), exported to `window` only for the test harness (the VM behavioral proof), mirroring how the OCR test wiring is reachable. --- ## 10. PLAN-AUDIT — entry paths / states / edge cases (Protocol 8 self-audit) | Case | Intended behavior | |---|---| | Env: **production, locked (default)** | `_shellVisible()===false` → shell + FAB absent entirely; template inert; zero cost. | | Env: **production, minigame-unlocked** | `_shellVisible()===true`, `_shellTier()==='prod'` → FAB + drawer show, **only prod tools**, wearing the fiction; STIMULUS BENCH / READOUTS / ACCESS sections only; no destructive/cheat/inspection tool reachable. | | Env: **staging** (localhost / `*.pages.dev` / meta marker) | full toolbench, all sections, raw labels + STAGING banner. | | **Gate uncertainty** (throw / missing `_isStagingEnv` / unknown host) | shell absent (existence) AND tier `'prod'` (tools) — both fail safe. | | **Reduced-motion** | drawer slide + all shell animations neutralized to instant final frame by the global block. | | **Mobile 360/412** | FAB fixed, drawer fixed overlay, **zero document displacement**, no overflow, no bezel-dock overlap. | | **Desktop ≥1000px** | same overlay path; desktop shell unaffected; verified live. | | **Ambient filter/transform states** (IDLE/STANDBY/SHUTDOWN) | FAB + drawer are body-level (sibling of `.container.machine`) → stay pinned, mirroring `#locationCard`. | | **No active campaign** | STATE SETUP presets + quick-pokes no-op with a toast; inspector shows the default `state`. | | **Firing a bus animation** | fires ONLY the display reaction (state write is at the call site, not the subscriber) → non-destructive → safe as a prod tool. | | **Ceremony replay** | calls presentation fn / resets a *session* flag; **never** clears the persisted view-once flag (that's a separate RESETS tool). | | **PWA install / offline** | additive; SW ASSETS unchanged; no manifest change. | --- ## 11. STAGED UNIT BREAKDOWN (5 Protocol-8 units) ### U1 — Registry spine + two-signal gate + shell scaffold (MIGRATE, lose nothing) ★ recommended first Introduce `DIAGNOSTIC_SHELL_TOOLS`, `_shellTier()`, `_toolVisible()`, the auto-confirm `_invoke()`, the categorized-collapsible + search + env-banner render pipeline, and the DIAGNOSTIC-SHELL rebrand of the `<template>` — then **re-express every existing control (§1.3 items 1–9) as registry entries**, still rendered in the current mount location. No new tools, no mobile overlay yet. **Acceptance:** every prior control works identically via the registry (byte-identical behavior of force-states / reboot / wake / immersion / observers / replay-hatch / both OCR boards); staging shows all, a VM-stubbed `'prod'` tier shows only prod tools; new leak-proof + confirm-gate + fail-safe-tier suites pass; Suite 149 updated; both runners at parity; full gate green. ### U2 — Mobile overlay + identity + icons Convert the document-flow mount into a body-level fixed **floating toggle (DEV-MARKER) + fixed overlay drawer** (mirroring `#locationCard`'s body-level placement); full diegetic DIAGNOSTIC-SHELL frame; icons on every section + tool; ENVIRONMENT BANNER; sub-labels + tooltips; reduced-motion-safe slide; focus-trap/Esc/scrim. **Acceptance:** shell never displaces layout (`scrollWidth===innerWidth` at 360/412, machine top unchanged whether drawer open or closed); reachable above content; stays pinned in IDLE/STANDBY/SHUTDOWN; desktop unaffected; render-check + a11y baseline green. ### U3 — TRIGGERS catalog + Protocol 44 Fire-any-of-33 animations (bus-emit + pending-var), force living-core states, force boot flavor, replay ceremonies M1–M5 + hatch (no view-once flag clears), SEAT verb, day/night control — mostly prod-tier. Land **Protocol 44** + its cross-reference gate-guard. **Acceptance:** each of the 33 fires visibly at 360/412; ceremonies replay without consuming their persisted flags; the Protocol-44 guard fails the build when a synthetic new `RobcoEvents.emit` lacks a registry trigger. ### U4 — STATE SETUP + RESETS + FIXTURES + INSPECT + inline resets Scenario presets + quick-pokes, granular confirm-gated resets, Load NV Test Campaign, state-JSON inspector, cache-rev / SW-version / runtime readouts, perf/FPS meter, copy diagnostic bundle; plus the **inline dev-reset** buttons (registry `category:'inline'`, shared gate/confirm/glyph). All staging-tier. **Acceptance:** presets produce the intended state (verified live); every destructive tool confirm-gated; inline resets appear ONLY in staging, carry the DEV-MARKER, route through the same `_invoke()`; inspector never writes. ### U5 — RESILIENCE/INFRA + minigame unlock ceremony + prod-tier proof Feature-flag toggles (`window._setFeatureFlagOverride` seam in `cloud.js`), simulate-AI-failure, cache/SW controls, a11y/reduced-motion/immersion quick-toggles, the minigame lock/unlock TEST + the earned UNLOCK CEREMONY, and the final **prod-minigame presentation** (fiction wrapper + non-destructive sandbox). **Acceptance:** flag toggles flip live behavior; AI-failure sim exercises the fallback; unlock ceremony fires; a full leak-proof audit proves the prod view exposes ONLY non-destructive prod tools; feature is complete. **Sequencing rationale:** spine first (U1) so nothing is built twice; overlay next (U2) so the whole catalog is built in its final home, not moved later; then the three tool waves (U3 prod triggers → U4 staging toolbench → U5 infra+unlock), each independently shippable behind the same gate. One unit lands at a time (Protocol 12/19). --- ## 12. RISKS / OPEN ITEMS - **`_setFeatureFlagOverride` in an ES module** — must write both `_featureFlags` and the LKG mirror; verify no interaction with a live `loadRemoteConfig()` race (U5). - **Pending-var animations** need their owning `render*()` present; a couple only paint on a specific view (e.g. survey ping only on the WORLD GRID) — the trigger must switch to that view first (U3). - **Day/night control** — confirm a transient day/night visual state exists to drive; if not, this tool is deferred rather than inventing a new persistent field. - **FAB placement vs bezel dock** on the narrowest phones — resolve live at 360px (U2). - **Test-count churn** — each unit adds suites; Protocol 2a sync across all locations every commit. --- ## 13. RECOMMENDATION Start with **U1**. It is the highest-leverage, lowest-risk unit: it establishes the registry spine, the two-signal leak-proof gate, and the auto-confirm pipeline, and it proves them by migrating every existing control with **zero behavior change** — so the safety architecture ships and is gate-guarded *before* a single new (potentially destructive) tool is added on top of it. Everything after U1 is "add registry entries," which is exactly the extensibility the spine exists to provide.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).