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