RELEASE
planning/2.8.0/plans/STEP2_B2_PLAN.md
sha256 8badd90726c9b47d · 26446 bytes ·
original held in the private archive
# STEP 2 (v2.8.0) — B2 MODULE BAY — Design Audit + Build Plan
> **Stage 1 (Opus — Diagnose & Plan). PLAN ONLY. No app changes, no commits.**
> **Baseline:** `dev` @ `81aa10f` — v2.7.0, cache `robco-terminal-v2.7.0-r24`. Approved mockup: `planning/mockups/module-bay-mockup.html` + `module-bay-rationale.md` (gitignored).
> **Owner's 5 LOCKED decisions:** (1) hatch = first-visit-only then bay opens with the panel; (2) flip the 13 mute chips to *installed = audible* (presentation-only, storage/semantics unchanged); (3) keep the Schematic View permanently as a light-touch fallback (not the star); (4) keep the amber identity for SLOT 05 (AI Uplink); (5) yes to install/eject SFX, wired through the audio system with master-mute + a dedicated toggle (Protocol 7 each).
> This file lives in gitignored `planning/`.
---
## 0. The real settings surface (grounded against live dev)
The **SECURITY & CONFIGURATION** panel — `index.html:1462`, `<details class="panel" style="border-color:var(--robco-alert)">`, heading `<summary><h2>> SECURITY & CONFIGURATION</h2></summary>` (line 1465). This panel IS the settings surface B2 reframes; the bay replaces its *contents*, the panel itself (location, tab, `<details>`+h2) stays. Current contents:
| Control (live) | index.html | Setter (unchanged by B2) | Bay home |
| --- | --- | --- | --- |
| Gemini API key | `#apiKeyInput` (1471) | `saveApiKey…` / handshake path | SLOT 05 cipher-key slot |
| Key-sync jumper | `#geminiKeySyncToggle` (1492) | `setGeminiKeySync` | SLOT 05 jumper |
| Model select | `#apiModelInput` (1520) | `fetchAuthorizedModels` populate | SLOT 05 engine selector |
| Optics color | `#opticsColorInput` (1536) `onchange="changeOpticsColor(this.value)"` | `changeOpticsColor` + `_updateOpticsDefaultLabel` | SLOT 01 phosphor-tube rack |
| High-lumen | `#highLumenToggle` (1559) `onchange="toggleHighLumen(this.checked)"` | `toggleHighLumen` | SLOT 01 high-lumen coil |
| Immersion dial | `#immersionSelect` (1584) `onchange="onImmersionChange(...)"` | `onImmersionChange` | SLOT 04 rotary dial |
| **AUDIO SYSTEMS** sub-panel | `<details data-sub-id="audio_systems">` (1611) | — | SLOT 02 board |
| · Master mute | `onchange="toggleMasterMute(this.checked)"` (1638) | `toggleMasterMute` (already preserves the 13 individual prefs) | SLOT 02 board-eject |
| · Pip-Boy radio | `#radioToggle` (1651) `onchange="toggleRadio(...)"` | `toggleRadio` | SLOT 02 receiver module |
| · **13 mute checkboxes** | 1674–1816, `onchange="toggleAudio('robco_X_muted', this.checked)"` | `toggleAudio` | SLOT 02 DIP chips |
| · Typer speed | `#typerSpeedSlider` (1843) inline `MetaStore.set('robco_typer_speed', …)` | inline setter | SLOT 02 print-rate trim |
| **POWER MANAGEMENT** sub-panel | `<details data-sub-id="power_systems">` (1864) | — | SLOT 03 board |
| · Sustained power cell | `#wakeLockToggle` (1881) `onchange="toggleWakeLock(...)"` | `toggleWakeLock` | SLOT 03 power cell |
| · Haptic solenoid | `#hapticToggle` (1902) `onchange="toggleHaptic(...)"` | `toggleHaptic` | SLOT 03 solenoid coil |
| Utility: campaign log | `exportCampaignLog('txt')` (1927), `('md')` (1952), `('html')` (1964) | — | SVC tray |
| Utility: eject holotape | `#ejectHolotapeBtn` `ejectHolotape()` (1937) | — | SVC tray |
| Utility: changelog | `#btnViewChangelog` `showFullChangelog()` (1974) | — | SVC tray |
| Utility: PWA install | `#btnInstallPwa` `installPwa()` (1984, **conditional `display:none`** until `beforeinstallprompt`) | — | SVC tray (keep conditional) |
**The exact 13-mute keyMap** (from `toggleAudio`, `ui-audio.js`) — the chips MUST map to these, using each channel's REAL label (the mockup's "LIMB SFX"/"WAKE SFX" are approximations — use the live labels):
| # | META key | AudioSettings | live label (source of truth) |
| --- | --- | --- | --- |
| CH-01 | `robco_sfx_muted` | `typing` | TYPING SFX |
| CH-02 | `robco_hum_muted` | `hum` | CRT HUM |
| CH-03 | `robco_geiger_muted` | `geiger` | GEIGER |
| CH-04 | `robco_tinnitus_muted` | `tinnitus` | TINNITUS |
| CH-05 | `robco_ambient_muted` | `ambient` | (live label — likely limb/ambient SFX) |
| CH-06 | `robco_wake_muted` | `wake` | WAKE SFX |
| CH-07 | `robco_panelclick_muted` | `panelClick` | PANEL CLICKS |
| CH-08 | `robco_bootdrone_muted` | `bootDrone` | BOOT DRONE |
| CH-09 | `robco_levelup_muted` | `levelUp` | LEVEL UP |
| CH-10 | `robco_heartbeat_muted` | `heartbeat` | HEARTBEAT |
| CH-11 | `robco_questcomplete_muted` | `questComplete` | QUEST COMPLETE |
| CH-12 | `robco_questfail_muted` | `questFail` | QUEST FAIL |
| CH-13 | `robco_factionthreshold_muted` | `factionThreshold` | FACTION ALERTS |
Boot-restore functions that sync the UI controls from stored prefs (the bay needs an equivalent `renderModuleBay()`): `initHighLumen`, `initRadio`, `initWakeLock`, `initHaptic`, `initImmersion`, `_restoreOpticsPreference`, `_updateOpticsDefaultLabel`, `_restoreDevicePrefs` (audio checkboxes + typer).
---
## 1. Design AUDIT — nothing lost, authority + parity preserved
### 1.1 Every device pref has a correct home (completeness)
Cross-checked the mockup mapping against **`META_MANIFEST` (33 keys)**:
- **User-facing device prefs → all mapped** (13 audio mutes + master + radio + typer + 2 optics-family + high-lumen + wake + haptic + immersion + gemini key/model/sync = every user-facing key). ✔ No orphan.
- **Correctly EXCLUDED (verified against the manifest):**
- `robco_input_mode` — the OVERSEER/TERMINAL pill stays inline in Comm-Link where it's used (rationale §"NOT moved"). ✔
- Internal bookkeeping — `robco_panel_state`, `robco_active_tab`, `robco_overseer_log`, `robco_error_log`, `robco_version`, `robco_booted_before`, `robco_feature_flags`, `robco_sw_installed`, deprecated `robco_optics` — not user-facing; no module. ✔
- Saves & Account — data, not device hardware; stay in their panels; the SVC tray carries the explicit "SAVE ARCHIVE & ACCOUNT REMAIN IN THEIR OWN PANELS" status line so nothing is hunted-for. ✔
- Campaign config (game/playstyle/RNG) — campaign state, not device prefs — stays in CAMPG (Protocol 23 boundary held). ✔
- **Utility ACTIONS** (not prefs) — all 6 present in the panel today (`exportCampaignLog` ×3, `ejectHolotape`, `showFullChangelog`, `installPwa`) map to the SVC tray; the PWA button keeps its conditional visibility. ✔
**Verdict: comprehensive. No setting is lost or orphaned.**
### 1.2 Player-authority + no-functionality-loss
- Pure additive UI reframe: every module action calls the **existing** setter (`toggleAudio`/`toggleMasterMute`/`changeOpticsColor`/`toggleHighLumen`/`toggleWakeLock`/`toggleHaptic`/`toggleRadio`/`onImmersionChange`/`setGeminiKeySync`/`exportCampaignLog`/…). **Zero new state, zero new campaign write, zero AI involvement.** Player authority intact (nothing here is AI-driven).
- **Same tap counts**: every change is still one tap (tube tap, chip pull, INSTALL/REMOVE, dial rotate). No setting takes *more* taps than today.
- **Master-mute-preserves-chips is already real**: `toggleMasterMute` only sets `robco_master_muted`; the 13 individual keys are untouched — the "eject board dims chips in place, reseat restores the mix" fiction maps 1:1 onto existing behavior. No new logic needed for it.
- **Schematic View** = today's flat controls, one tap away, permanent → the old mental model is never removed (light-touch fallback per LOCKED-3).
### 1.3 Mobile + desktop
- **Mobile 360/412:** mockup verified headless — single-column board stack, chips wrap into an auto-fill grid (`minmax(104–122px)`), tap targets ≥28px (chips `min-height:44px`; buttons `padding` ≥ threshold), inputs `font-size:16px` (no focus-zoom), **no horizontal overflow at 360**. Carry this exactly (Protocol 10/17). Screenshots present: `module-bay-360.png`, `module-bay-412.png`, `module-bay-hatch-412.png`.
- **Desktop ≥1000px:** mockup uses a two-column bay grid. **⚠ CONSTRAINT (see §4 risk):** the mockup is a full-width standalone page; in-app the Security & Config panel renders inside the desktop shell's content column, which is *narrower* than the viewport. The two-column grid must be gated on the **container** width (or kept single-column within the panel), not the raw `@media (min-width:1000px)` viewport query, or it can overflow the panel column. **Verify at desktop in-app** (Protocol 10) — do not assume the mockup's viewport breakpoint transfers.
- High-lumen genuinely flattens to pure black + no glow; `prefers-reduced-motion` neutralizes all bay animation (hatch slide, chip/tube/dial transitions). Both carried from live invariants (Protocol 17).
---
## 2. Concrete IMPLEMENTATION plan
### 2.1 Files touched
- **`index.html`** — replace the *contents* of the Security & Config panel (1462–~2000) with the Module Bay markup (hatch + bay grid + 5 slots + SVC tray + service log) and the Schematic View container. The panel `<details>`+`<summary><h2>` stays.
- **`css/terminal.css`** — port the mockup's bay/hatch/chip/tube/dial/cell/coil/jumper/tray/schematic CSS (namespaced, using the existing `--robco-*` vars and CRT/high-lumen/reduced-motion hooks, not the mockup's private `--phos-*`).
- **`js/ui-core.js`** — `renderModuleBay()` / `initModuleBay()` (boot restore of every module's visual state from stored prefs), hatch first-visit logic, schematic toggle, service-log helper, chip-flip render (B2b). Wire from `window.onload` (one named phase, next to `initImmersion`/`initHighLumen`).
- **`js/ui-audio.js`** — B2c only: the install/eject SFX functions (`playModuleThunk`/`playChipClick` via `ensureAudioCtx`, guarded by `masterMute` + the new key) + keyMap/masterMute-restore entries (Protocol 7).
- **`js/state.js`** — register the new device-pref keys in `META_MANIFEST`: `robco_bay_opened` (bool, hatch first-visit), `robco_modulesfx_muted` (bool, B2c), optional `robco_bay_view` (string, schematic-vs-bay). (Device prefs — the U6 boundary suite auto-accepts registered keys; not campaign state.)
- **`sw.js`** — cache bump per served change.
- **`eslint.config.mjs`** — register any new cross-file globals (`renderModuleBay`/`initModuleBay`/`playModuleThunk`/…).
- **Tests** — new suite(s) in both runners at parity + `test.html` behavioral (chip-flip state integrity).
### 2.2 The two-views-one-truth model (avoids dual-state drift)
There is **one source of truth**: the stored prefs (MetaStore) + the existing setters. The bay and the schematic are both *projections*:
- Every module action → existing setter (persists to MetaStore) → then `renderModuleBay()` re-syncs the visible view from the stored prefs. The schematic, when open, is **regenerated** from the same prefs (not a persistent parallel DOM) so it can never drift from the bay. This is the cleanest way to honor "schematic is a light-touch afterthought": it's a generated flat view over the same state, not a second wired control set.
### 2.3 Per-slot build
- **SLOT 01 · VDU OPTICS DRIVER** — phosphor-tube rack from the **THEMES table** (game-agnostic; the 7 tubes are theme keys, not game literals), each tube a `<button>` → `changeOpticsColor(value)` + `_updateOpticsDefaultLabel` for the dynamic "(DEFAULT)" tag (per-game default already handled by P8-era code). High-lumen coil = INSTALL/REMOVE `<button>` → `toggleHighLumen(!installed)`. Status line preserved.
- **SLOT 02 · SONIC PROCESSOR** — board-eject `<button>` → `toggleMasterMute(removed)`; 13 DIP chips (B2b, §2.4); radio receiver module → `toggleRadio`; print-rate trim `<input type=range>` → the existing inline `MetaStore.set('robco_typer_speed', …)` + `getTypewriterSpeed` consumers. Board-removed state dims chips in place (they keep their pref) — mirrors `toggleMasterMute`.
- **SLOT 03 · POWER CELL BAY** — power cell module → `toggleWakeLock`; solenoid module → `toggleHaptic`. Status lines kept verbatim ("> POWER CELL IDLE — DISPLAY MAY DIM", etc.).
- **SLOT 04 · ATMOSPHERIC REGULATOR** — 3-position rotary `<button>` cycling FULL→BALANCED→MINIMAL → `onImmersionChange(tier)` (the P8 dial). Legend + description reflect `getImmersionTier()`.
- **SLOT 05 · AI UPLINK CO-PROCESSOR (amber, LOCKED-4)** — cipher-key `<input type=password>` + HANDSHAKE `<button>` (the existing validate+`fetchAuthorizedModels` path); engine `<select>` (`#apiModelInput`); key-sync jumper as a `<button role="switch">` → `setGeminiKeySync`. Amber identity retained.
- **SVC TRAY** — the 6 utility `<button>`s wired to their existing handlers; PWA button keeps conditional visibility; the "saves & account live elsewhere" status line.
### 2.4 The chip-polarity flip (B2b — the precision unit, LOCKED-2)
- **Presentation inverts; storage does NOT.** Chip **installed** (default, lit LED, solid border) = channel audible = `robco_X_muted === false`. Chip **pulled** (dashed, dim) = silenced = `robco_X_muted === true`.
- **Action:** pulling/reseating chip CH-n → `toggleAudio('robco_X_muted', /* muted = */ !nowInstalled)`. The boolean passed to `toggleAudio` is the SAME semantic it gets today (checkbox `this.checked` = muted); only the visual mapping flips.
- **Boot restore:** `installed = !MetaStore.get('robco_X_muted')` (with the string/bool coercion the store uses). Every chip's initial visual comes from the stored pref — no default corruption.
- **Invariant to test (both runners + test.html):** for each of the 13 keys, (a) pulling a chip writes `true`, reseating writes `false`; (b) boot with a pref of `true` renders the chip pulled, `false` renders it installed; (c) master-eject leaves all 13 keys unchanged; (d) the AudioSettings in-memory cache stays consistent (`toggleAudio` already syncs it). **No pref is ever inverted in storage.**
### 2.5 First-visit hatch (LOCKED-1)
- `robco_bay_opened` (MetaStore bool). On the panel's first expand ever: show the closed hatch (screws + warning + `▶ RELEASE HATCH`); on release, set the flag and reveal the bay. On every subsequent visit: the flag is set → the bay is shown directly when the panel expands (no hatch). Reduced-motion: instant reveal. (A "reset hatch" is not needed; owner locked first-visit-only.)
### 2.6 Schematic View (LOCKED-3 — light-touch fallback)
- A header `⌕ SCHEMATIC VIEW` `<button>` swaps the bay for a **generated** flat list (checkbox/select/slider rows) reading the same prefs and calling the same setters, with a `▲ BACK TO BAY` button. Minimal polish (owner: afterthought). Optionally persist the chosen view in `robco_bay_view` (or keep session-only — minor, see §5).
### 2.7 Install/eject SFX (B2c — Protocol 7, LOCKED-5)
New audio source(s): a socket *thunk* (board/module install/eject) and a chip *click* (chip pull/reseat), synthesized via the existing `ensureAudioCtx`/WebAudio. **Full Protocol 7 checklist:**
- Function(s) in `ui-audio.js` using `ensureAudioCtx`; first guard `if (AudioSettings.masterMute) return;`; second guard `if (AudioSettings.moduleSfx) return;`.
- Add `moduleSfx` to the `AudioSettings` init block (ui-core.js) seeded from `robco_modulesfx_muted`.
- A dedicated toggle — recommend a **14th chip (CH-14 · SERVICE SFX)** on the Sonic board (thematically apt; it's a device sound) → `toggleAudio('robco_modulesfx_muted', …)`.
- Add `robco_modulesfx_muted → moduleSfx` to the `toggleAudio` keyMap; add to `toggleMasterMute` un-mute restore; add `robco_modulesfx_muted` to `META_MANIFEST`; add a row to ARCHITECTURE's Settings table.
- Fires on install/eject/chip actions; silent under master-mute or its own mute.
---
## 3. Recommended UNIT SPLIT — B2a / B2b / B2c
B2 is a large reframe; **split into 3 dependency-ordered sub-units**, isolating the two highest-precision pieces (the chip flip, the new audio source) from the big structural reframe.
| Unit | Scope | Why isolated | Risk | Model |
| --- | --- | --- | --- | --- |
| **B2a — Module Bay core reframe** | The whole hardware bay: hatch (first-visit), bay grid, SLOTs 01/03/04/05 + SVC tray + the SLOT-02 board FRAME (master-eject + radio + typer trim), Schematic View, `renderModuleBay`/`initModuleBay` boot restore, service log. **The 13 audio channels stay in their CURRENT polarity for this unit** (rendered inside SLOT 02 as-is) — so the big structural change carries ZERO risk of audio-state corruption. CSS port, markup contract guards, mobile/desktop verify. | The star UI + all the structural churn, with no state-semantics change. | **Med–High** / L (widest) | **Opus** IA/CSS-fit; Sonnet impl; Opus audit |
| **B2b — Chip-polarity flip** | Convert the 13 SLOT-02 channels into DIP chips with inverted PRESENTATION (installed = `!muted`); storage/semantics unchanged; boot-restore chip visuals from stored prefs. Dedicated state-integrity tests (§2.4). | The one change that touches control *meaning* — the owner's "get this exactly right, don't corrupt state." Small, surgical, heavily tested in isolation. | **Med** / S–M (high precision) | **Sonnet** impl; **Opus** audit (state integrity) |
| **B2c — Install/eject SFX + service-log polish + close-out** | New audio source(s) (Protocol 7 full checklist) + CH-14 toggle; diegetic service-log line on each change; final a11y (UI-5 buttons, focus-visible, ARIA), reduced-motion, game-agnostic sweep; any remaining polish. | Self-contained Protocol-7 addition + the finishing pass; nothing else depends on it. | **Low–Med** / S–M | **Sonnet** |
**Dependency order:** B2a → B2b → B2c. (B2b needs the SLOT-02 board frame from B2a; B2c's SFX fires on actions built in B2a/B2b.) Each is a full-gate, cache-bumping, independently-verified commit. **Split point is flexible** — if the owner prefers, the SLOT-02 board frame can move from B2a into B2b so the entire audio board lands as one unit; recommended as above so B2a delivers a complete-looking bay and B2b is the pure, isolated polarity flip.
---
## 4. Risks
- **Desktop container width (§1.3).** The mockup's `@media (min-width:1000px)` two-column bay assumes full-viewport width; in-app the panel is in a narrower shell column → the two-column grid could overflow it. Mitigation: gate the bay grid on the container (container query or a JS width check on the panel), or keep single-column within the panel; **verify at desktop in-app** before calling B2a done (Protocol 10). This is the top layout risk.
- **Chip-polarity flip corrupting state (LOCKED-2).** Isolated to B2b with per-key round-trip tests (pull→`true`, reseat→`false`, boot-render from pref, master-eject leaves keys untouched). The `toggleAudio` call receives the same boolean semantics as today — only the visual mapping inverts.
- **Two-views drift.** Avoided by the one-truth model (§2.2): both views are projections of the stored prefs; the schematic is regenerated, never a persistent parallel wired control set.
- **CSS collision / theming.** Port the mockup CSS onto the existing `--robco-*` vars + CRT/high-lumen/reduced-motion hooks (not the mockup's private `--phos-*`), namespaced under a bay container class so it can't leak into other panels. Suite-guard the new markup contracts (Protocol 20).
- **Sub-panel persistence change (see §5.2).** If the bay's slots become static sections, the `audio_systems`/`power_systems` collapse-persistence (UI-2) is retired — a deliberate UX change needing owner sign-off.
- **Boot-order (the standing lesson).** `renderModuleBay`/`initModuleBay` read state.js globals (`MetaStore`, `getImmersionTier`) — must run inside a `window.onload`-called function, never at top-level parse (ui-core.js is a static tag that can execute before state.js's dynamic load).
---
## 5. PROTOCOL CHANGES needing OWNER SIGN-OFF (do NOT assume — listed for the owner)
Per the standing rule, protocol amendments need owner sign-off *before* implementation. These are the changes the reframe requires:
### 5.1 Protocol 25 (UX Stability) — **AMENDMENT (the big one)**
Protocol 25 says *"Existing user workflows must not change unless the requested feature requires it… Do not redesign or relocate working UI unprompted."* The Module Bay **intentionally redesigns** the entire settings surface AND **inverts the 13 mute toggles' presentation** — exactly what Protocol 25 forbids *unprompted*. **This redesign IS owner-prompted** (the approved mockup).
- **Proposed amendment:** add a sanctioned-exception clause — *"An owner-approved redesign (e.g. the Module Bay) is a sanctioned exception to the no-unprompted-redesign rule, provided it preserves: same panel location + tab, no increase in tap-count for any control, a fallback that preserves the prior mental model (e.g. Schematic View), and the real control labels riding along as searchable sub-labels. A control's stored semantics must not change even when its presentation is inverted."* This both authorizes B2 and codifies the mitigation bar for future owner-approved redesigns.
- **Rationale to bring to owner:** without this, B2 reads as a Protocol-25 violation to the gate/reviewers; the amendment records the owner's prompt + the guardrails the bay actually meets.
### 5.2 Protocol UI-2 (Sub-Panel Persistence) — **CLARIFICATION / owner ruling**
Today `audio_systems` and `power_systems` are `<details class="sub-panel" data-sub-id>` (collapsible, persisted per Protocol UI-2). The mockup renders slots as **static always-visible `<section>` boards** (no collapse). If B2 adopts static boards, the two settings sub-panels' collapse-persistence is **retired**.
- **Owner ruling needed:** (A) adopt static boards (retire that persistence — the bay shows everything at once, which is the point) → a UI-2 exception note is needed; or (B) keep the slots as `<details data-sub-id>` (collapsible, persisted) to fully honor UI-2. Recommend **(A)** with a UI-2 clarification ("Module Bay slots are static hardware boards, exempt from the collapsible-sub-panel persistence rule"), since always-visible is the design intent — but this is the owner's call.
### 5.3 Protocol UI-1 (Panel Heading Standard) — **CLARIFICATION (minor)**
The panel keeps `<summary><h2>> SECURITY & CONFIGURATION</h2></summary>` (UI-1 satisfied). The mockup's internal `bay-head` uses `<h1>` and slots use `<h2>` — in-app these are *content under* the panel's h2, so they must drop to **h3** (matching UI-1's sub-panel `<h3>` convention) to keep heading order valid. Likely just build-conformance, but flagged as a clarification to confirm: *"reframed panel contents use `<h3>` for internal board headings (the panel's own `<h2>` summary is unchanged)."*
### 5.4 Build-conformance (NOT amendments — the bay must satisfy these existing protocols)
Listed for completeness; no protocol text change:
- **UI-5:** every interactive element is a real `<button>` — tubes, chips, modules, dial, master-eject, **and the key-sync jumper** (the mockup renders it as a `<div role="switch">`; convert to `<button>`).
- **Protocol 7:** the install/eject SFX follows the full checklist (B2c, §2.7).
- **Protocol 17:** ≥16px focusable inputs, ≥28px tap targets, no 360px overflow, no color-only meaning (LEDs pair with border/label state), reduced-motion honored.
- **Protocol 20:** static gate guards for the new bay markup contracts (module IDs, chip↔key wiring, the h2 summary, the schematic toggle).
- **Device-pref additions** (`robco_bay_opened`, `robco_modulesfx_muted`, optional `robco_bay_view`): registered in `META_MANIFEST`; the U6 boundary suite (Suite 134) accepts registered device keys — no amendment.
---
## 6. House-standard constraints the build must hold (every sub-unit)
- All on **`dev`**; full gate green each commit (Protocol 43); **no `APP_VERSION` bump** (stays 2.7.0 under `[Unreleased]`).
- **Protocol 1** cache-rev bump on each served-file commit (r24 → r25/r26/r27 across B2a/b/c).
- **Protocol 15** both-runners parity; **Protocol 2a** counts synced everywhere; **Protocol 40** `test.html` for the chip-flip state-integrity behavioral.
- **Protocol 39** UTF-8 (the bay markup has ⏏ ▶ ▲ ═ ╪ ✕ ● glyphs — Edit/Node writes only, never PowerShell); **Protocol 90** mojibake guard.
- **Protocol 10** render-verify at **360/412/desktop** (no overflow — carry the mockup's verified-360 constraint; add the in-app desktop-column check §4); **Protocol 17** mobile invariants + reduced-motion + high-lumen.
- **Protocol 22/23** — reuse existing setters, zero new campaign state, device prefs via MetaStore only (two-store boundary held).
- **Protocol 38** game-agnostic (tube names from THEMES; no FNV/FO3 literals in the bay).
- **Protocol 13** regression tests (chip-flip integrity, markup contracts) in the same commit; **Protocol 20** static invariant guards.
---
## 7. Report summary (for the owner)
- **Recommended split:** **B2a** (Module Bay core reframe — hatch + all slots + SVC tray + schematic + boot restore, 13 audio channels un-flipped) → **B2b** (the isolated 13-chip polarity flip, installed=audible, storage unchanged, heavily tested) → **B2c** (install/eject SFX per Protocol 7 + service-log + a11y/close-out). Dependency-ordered; split point flexible.
- **Key implementation decisions:** one-truth model (bay + schematic are projections of the stored prefs; schematic regenerated, never a parallel wired set); reuse every existing setter (zero new campaign state); chips invert presentation only (`installed = !muted`), the `toggleAudio` boolean unchanged; master-eject already preserves the 13 prefs; tubes from THEMES (game-agnostic); 3 new *device* keys (`robco_bay_opened`, `robco_modulesfx_muted`, optional `robco_bay_view`); the SFX toggle as a 14th chip; desktop two-column gated on container width, not viewport.
- **Protocol changes needing your sign-off:** (1) **Protocol 25** amendment — sanctioned-exception clause for owner-approved redesigns (authorizes the bay + the chip-polarity inversion, codifies the mitigation bar); (2) **Protocol UI-2** ruling — retire the two settings sub-panels' collapse-persistence (static boards) vs keep them collapsible; (3) **Protocol UI-1** clarification — internal board headings drop to `<h3>` under the panel's `<h2>`. Build-conformance items (UI-5 buttons incl. the jumper, Protocol 7 SFX, Protocol 17 mobile) need no protocol change.
---
*END — Stage 1 plan. Nothing implemented, committed, or pushed. `dev` @ `81aa10f` untouched; `planning/` is gitignored.*
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).