RELEASE
planning/2.8.0/plans/MASTER_PLAN.md
sha256 60e8fcbc9c8314e3 · 123038 bytes ·
original held in the private archive
# MASTER_PLAN.md — RobCo U.O.S. Implementation Backlog (Stage C Synthesis · B3-revised)
> **PLAN-ONLY.** Untracked. Reconciles the 11 upstream plan-only docs (A0–A8, B1, B2) + the B3 deterministic-conversion pass + 2 shipped design specs into ONE coherent, dependency-ordered backlog. This is the hand-off to Stage D parallelization. **Implements nothing.**
> Baseline: `origin/main` @ `bc8c2eb` — v2.6.0, cache `robco-terminal-v2.6.0-r1`, 1078 tests / 89 suites, `APP_VERSION='2.6.0'`.
> Target end-of-chain version: **2.7.0** (lean MINOR — see WU-VER).
>
> **B3 REVISION (this version):** `DETERMINISTIC_FEATURES.md` reclassified 5 features from AI/network-gated to **fully deterministic native** (offline, no Gemini, no App Check): VATS, TRADE-core, THREAT, CONSULT, BIO-SCAN-core (+ LOOT as a deterministic-core hybrid). These are folded in as a new **Phase N (native, build-now)** that does **not** depend on App Check (WU-G0). The old AI phase (G) is **shrunk** to the genuinely-generative residue. Owner intent: prefer website-powered/deterministic wherever the local data computes it exactly; AI stays only for generative output.
---
## 0. Upstream-doc roll call (all present — no STOP)
| Doc | Stage | Role in this plan |
|---|---|---|
| `HOUSE_STANDARD.md` | A0-ref | Diegetic yardstick (reference, not a backlog source) |
| `DIEGETIC_AUDIT.md` | A0 | Voice/terminology findings → Phase E |
| `PERFORMANCE_AUDIT.md` | A1 | loadUI fan-out, map scoring, asset weight → Phase A/B |
| `ACCESSIBILITY_AUDIT.md` | A2 | focus, ARIA, reduced-motion, live regions → Phase C |
| `CONTENT_AUDIT.md` | A3 | Non-canon data, stat errors → Phase D |
| `MOBILE_AUDIT.md` | A4 | Zoom guard, flex-wrap, tap targets → Phase C |
| `CODE_QUALITY_AUDIT.md` | A5 | Prohibited-pattern violations, dead code, dup → Phase B |
| `TEST_STRENGTH_AUDIT.md` | A6 | Gate holes, presence-vs-behavioral gaps → Phase B |
| `UI_CONSISTENCY_AUDIT.md` | A7 | Tracker pattern, Protocols UI-1..5, Suite 88 → Phase A |
| `CLOUD_AUDIT.md` | A8 | Sanitize gaps, round-trip test, sync protocol → Phase B |
| `CAPABILITY_SLATE.md` | B1 | 18 native-capability ideas → Phase F |
| `AI_SLATE.md` | B2 | 12 AI-feature candidates → Phase G (now shrunk by B3) |
| `DETERMINISTIC_FEATURES.md` | B3 | 5 AI features → fully-deterministic native → **Phase N** |
| `GAME_AGNOSTICISM_AUDIT.md` | A9 | Hardcoded-game findings → Protocol 38 + Suite 89 → **WU-A4/A5** (Phase A); GA-7/GA-10 → WU-N1 |
| `GAME_THEMING_AUDIT.md` | A10 | Per-game optics/identity → `GAME_DEFS.theme` + Suite 90 → **WU-T1/T2/T3** (Phase T); GT-4/GT-5 → Round 2; FO4 theme → Round 3 |
`CRAFT_MENU_DESIGN.md` and `SKILLBOOKS_TRACKER_DESIGN.md` are **already shipped** in v2.6.0 (r35–r37) — excluded from the backlog.
---
## 1. RECONCILIATION PASS
### 1.1 DEDUPE — cross-doc overlaps collapsed to one work-unit
| Overlap | Sources | Collapses into |
|---|---|---|
| Tracker toggle is a non-accessible `<span onclick>` | A2 §3 (CRITICAL), A4 M-L2 (LOW), A7 UC-CTL-1 + UC-DENS-1 (HIGH) | **WU-A1** — one atomic `.tracker-toggle` button refactor across all 5 render fns |
| Faction label says "±50", real delta is ±5 | A5 (implied), A7 UC-CTL-2 (MEDIUM), prior faction-rep work | **WU-A4** — one string + increment-correctness fix |
| CRT motion ignores `prefers-reduced-motion` | A0 CR-1, A2 CR-1 (22 keyframes) | **WU-C1** — one global reduced-motion guard |
| Cloud pull round-trip never behaviorally tested | A6 TS-GAP-6 / CC-cross, A8 CC-RT-1 | **WU-B5** — one behavioral round-trip test (Phase-6 fields) |
| `innerHTML +=` in api.js + Suite 23 carve-out | A5 QA-PROHIB-1, A6 GH-1, A6 TS-WEAK-1 | **WU-B1** — fix + remove carve-out + ratchet guard |
| Unescaped localStorage/Firestore → innerHTML | A5 QA-PROHIB-2/3, A6 GH-2 | **WU-B2** — escape + scan guard |
| `localStorage.getItem()` in AI hot paths | A5 QA-PROHIB-4/5, A6 GH-3 | **WU-B3** — read-from-state + guard |
| Phase-6 array fields unsanitized + factions un-coerced | A8 CC-SAN-1/2 | **WU-B4** — sanitize completeness (pairs with WU-B5 test) |
| `#updateBanner` dead CSS + `fo3WarningBanner` "2.0.0" message | A4 M-M4, A5 QA-DEAD-7 | **WU-E2** — CONVERT-TO-TEMPLATE (owner override; **never delete**) |
| Sub-panel collapse not persisted (4 panels) | A7 UC-STR-1/2/3/4 | **WU-A2** — `data-sub-id` standardization batch |
**Net effect:** ~64 raw findings across A0–A8 collapse to **34 work-units** (29 substantive + 5 protocol/version units).
### 1.2 RESOLVE — conflicts, one choice recorded
- **UC-DENS-2 density tiers (12px legacy vs 11px tracker):** RESOLVE → **Option B (document-only)**. Formalize the split in Protocol UI-2 (tracker tier = registry-backed read-only lists; legacy tier = user-created mutable lists). No migration. *Why: Option A re-flows every legacy panel for cosmetic parity — high regression surface, zero user benefit.*
- **QA-DEAD-4 `ticksToGameTime()` / QA-DEAD-1..8 dead code:** RESOLVE → purge all confirmed-dead (DUP-2/3/4, DEAD-1/2/3/5/6/8) **except** the two TEMPLATE items (WU-E2) and `ticksToGameTime`/`gameTimeToTicks` which become an **OPEN DECISION** (intentional keep vs remove).
- **CC-RT-2 `migrateState()` ignores its version param:** RESOLVE → **SPEC-FIRST/defer** (safe as-is for all current unconditional migrations; revisit only if a version-gated migration is ever needed).
- **CC-RT-3 `sessionStart` restore / CC-BACK-1 / CC-CHK-1:** RESOLVE → **SKIP** (intentional design; documented as INFO).
### 1.3 MERGE — shared touchpoints coordinated into one edit
- **`js/ui-render.js` tracker render fns** (`renderCollectibles`, `renderLincolnMemorabilia`, `renderTraits`, `renderSkillBooks`, `renderMagazines`): touched by WU-A1 (button refactor) **and** WU-A4 (faction label is in `renderFactionRep` — separate fn, no collision) **and** WU-B2 (escape). → WU-A1 is the **single owner** of the 5 tracker fns; WU-A4/B2 rebase after it.
- **`tests/check-persistence.{js,ps1}`** + **Suite 88**: every Phase-A/B unit that adds a guard writes BOTH runners in its own commit (Protocol 15). Suite 88 is **created by WU-A2** and **appended to** by WU-A1 (GATE-UI-3) — sequence A2 → A1 so the suite exists first.
- **`index.html`**: WU-A2 (`data-sub-id` × 4), WU-A1 (no markup — JS-rendered rows), WU-C-series (ARIA), WU-E2 (templates). Sequence so structural (A2) precedes a11y attribute adds (C-series).
- **`css/terminal.css`**: WU-A1 (`.tracker-toggle`), WU-C1 (reduced-motion media block), WU-C2 (`:focus-visible`), WU-C3 (tab contrast), WU-A2 (DENS-3 alert theming). Coordinated as additive blocks — no rule collisions.
### 1.4 BAKE — audit fixes folded into each new feature so features are born compliant
Every Phase-F/N/G new feature inherits, as acceptance criteria baked into its own spec:
- **UI-1..5** (panel/row/badge/persistence/label conventions) — any new panel uses `.tracker-toggle` + `data-sub-id` + `emptyState()` + `> ` h2 from day one.
- **Protocol 17** (≥28px / ≥16px) + **WU-C1** reduced-motion + **WU-C2** focus-visible — any new animated/interactive surface honors them at birth.
- **Protocol 32** kill-switch flag + graceful fallback — every networked B1/B2 feature ships with its flag and offline fallback in the same commit.
- **WU-B2/B4** escaping + sanitize — any new field that renders user/AI/cloud data uses `escapeHtml()` and gets a sanitize entry in the same commit.
- **Protocol 38 / Suite 89 (A9)** — every feature is **game-agnostic**: no hardcoded game literal, no two-game assumption; per-game variation rides `GAME_DEFS` + the active registry. Any new feature must **pass Suite 89** in its own commit. *(Added by WU-A4; binding on all units that land after it.)*
### 1.5 B3 REFRAME — deterministic-over-AI (supersedes parts of B2)
`DETERMINISTIC_FEATURES.md` applied the owner rule "if the local data computes it exactly, build a website UI not a Gemini call" to every B2 candidate and every AI-routed COMM-LINK command. Net reclassification:
| Feature | Was (B2 / current code) | Now (B3) | Moves to |
|---|---|---|---|
| **VATS** | already native math but self-labels "AI resolves it" + dead `targetDT=0` | **FULLY DETERMINISTIC** (complete the calc; add AP-strike mode) | **WU-N1** (out of G) |
| **TRADE / BARTER** | SPEC-FIRST AI · `[NET][DATA]` (AI-fed modal `api.js:1260`) | **FULLY DETERMINISTIC core** · `[DATA]`-only, offline (+ optional AI banter) | **WU-N2** (out of G) |
| **THREAT** | AI-routed via `macroCommand` | **FULLY DETERMINISTIC** bestiary + TTK | **WU-N3** (out of G) |
| **CONSULT** | SPEC-FIRST AI (AI-G) | **FULLY DETERMINISTIC** databank lookup (native router) | **WU-N4** (out of G) |
| **BIO-SCAN** | SPLIT (AI-J) | **FULLY DETERMINISTIC core** (+ optional AI phrasing) | **WU-N5** (out of G) |
| **LOOT** | AI-routed via `macroCommand` | **HYBRID** — deterministic add/value; optional generative drop | **WU-N6** (out of G) |
**Key consequences:** these 6 units are **offline, no `[NET]`, and do NOT depend on WU-G0 (App Check)** — so they rank ahead of the AI phase. The biggest single upgrade: **TRADE drops from `[NET][DATA]` to `[DATA]`-only**. The thin **optional** AI-banter/phrasing layers for TRADE (haggle flavor) and BIO-SCAN (bedside manner) remain in the shrunken Phase G as SPEC-FIRST *enrichment* units that never block the native feature. B3 surfaced **3 data gaps** (per-vendor stock, ranged-accuracy/barter coefficients, ammo-per-attack) folded into Phase D / CONTENT_AUDIT cross-refs (WU-D4).
### 1.6 MELEE-SCOPE RULE (owner 2026-06-29 — cross-cutting)
The site originated as a melee-only experience, so legacy code may gate melee behavior on **mode alone**. The owner's rule: **melee-only guards/behaviors apply when `playstyle === 'melee'` OR a melee weapon is in use in ANY mode.** Canonical gate shape: **`playstyle === 'melee' || weaponIsMelee`** — never mode alone. (`weaponIsMelee` = the equipped/in-use weapon classifies as melee/unarmed via its `WEAPONS.CSV` row — `Reach`/`Special_Rules`/ammo-type `None`.)
- **WU-N1 (VATS):** the melee/AP-strike mode applies for melee weapons in any mode **and** in melee-only mode; ranged hit-% for ranged weapons (folded into the WU-N1 spec above).
- **Cross-cutting backlog note (fold into implementation):** **AUDIT/scope any LEGACY melee-assuming code** during the build — anywhere that gates on `playstyle === 'melee'` (mode) alone where a melee *weapon* in another mode should also qualify. Fix each to the `playstyle==='melee' || weaponIsMelee` shape. This is an implementation-time sweep (not a separate work-unit); findings that are behavior-relevant get a Protocol-13 regression.
---
## 2. UNIFIED BACKLOG
> Legend — Tags: `[AUTH]` `[DATA]` `[NET]` `[CACHE]` `[TEST]`. Tier: **BUILD** (build-now) · **SPEC** (spec-first, needs design sign-off) · **SKIP/DEFER**.
> Every code-touching unit implies `[CACHE]` (served-file change → bump per Protocol 1) + Protocol 2/2a docs; listed explicitly only where it is the *primary* concern.
### PHASE A — Shared Infrastructure & Standards (land FIRST; everything rebases on these)
**WU-A2 · Sub-panel persistence + Protocol UI-1..5 + Suite 88 (structural guards)** — *born-compliant foundation*
- **Does:** Adds missing `data-sub-id` to `ammoSubPanel` (UC-STR-1), Audio Systems panel (UC-STR-3), Campaign Config sub-panels ×3 (UC-STR-4); converts faction "MINOR FACTIONS" rendered `<details>` to `class="sub-panel"` + `data-sub-id` (UC-STR-2). Codifies Protocols **UI-1** (panel structure), **UI-2** (tracker row pattern + density-tier doc), **UI-3** (badge format), **UI-4** (collapse persistence required), **UI-5** (button-label convention). Creates **Suite 88** with GATE-UI-1,2,4,5,6,7 (GATE-UI-3 added by WU-A1).
- **Files:** `index.html`, `js/ui-render.js` (renderFactionRep template), `tests/check-persistence.js`, `tests/check-persistence.ps1`, `RULES.md`, `CLAUDE.md`, `sw.js` (cache), + Protocol 2/2a docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none (first unit) · **Note:** creates Suite 88 → must precede WU-A1.
**WU-A4 · Game-agnostic refactor + Protocol 38 + Suite 89** — *A9 GAME_AGNOSTICISM_AUDIT (GA-1..GA-11); lands EARLY so it guards everything after*
- **Does:** Behavior-neutral literal→`GAME_DEFS`/registry refactors for the safe A9 findings: the ~12-site `=== 'FO3' ? 'FO3' : 'FNV'` coercion → `GAME_DEFS[ctx] ? ctx : 'FNV'` (GA-2); the context-switcher allowlist `if (ctx !== 'FNV' && ctx !== 'FO3') return` → `if (!GAME_DEFS[ctx]) return` (GA-3); crossroads hardcoded faction-key arrays → `getFactionRegistry().map(f => f.key)` (GA-4); **`seedNewCampaignInventory` literal → `GAME_DEFS[ctx].seedInventory` (GA-6 — fixes the LIVE Canteen-in-FO3 bug, below)**; `[CONTEXT: …]` hints → iterate `GAME_DEFS` (GA-8). Leaves the sanctioned literals (the `|| 'FNV'` fail-safe defaults, the legacy v7→v8 bootstrap GA-9, the `GAME_DEFS` declaration block). Codifies **Protocol 38 — Game-Agnostic Architecture** into `RULES.md` + `CLAUDE.md`. **Also codifies Protocol 39 — README Currency** (lands early so it governs the whole build — see below). Adds **Suite 89** (both runners, ~10–12 static assertions: no coercion anti-pattern, no literal allowlist, no two-game ternary/array/map, no hardcoded faction/skill arrays in feature code, positive presence guards) with the sanctioned game-literal exceptions allowlisted. *(GA-1 boot map → WU-A5; GA-5 AI directives → folds into the api.js directive work; GA-7/GA-10 → WU-N1.)*
- **Protocol 39 — README Currency (codified here, owner):** keep `README.md` holistically true whenever a change makes its description / feature list / architecture / current-state inaccurate — update those sections **in the same commit**. This **strengthens Protocol 2's README clause** from "bump the test-count/feature-table numbers" to "keep the whole description an accurate picture of what the site actually is." Codified early in WU-A4 (alongside Protocol 38) so every later unit in this build is bound by it; the **comprehensive holistic rewrite happens at WU-VER** (final shipped state).
- **LIVE BUG — Canteen-in-FO3 (concrete symptom of GA-6; also a content-correctness fix):** the **Vault 13 Canteen** (an FNV survival item, auto-seeded by `seedNewCampaignInventory`, `ui-core.js:1300`) is appearing in **FO3** new-campaign inventories — it must be **FNV-ONLY**. Root cause: the seed is gated `if (ctx !== 'FNV') return` by literal AND the item is hardcoded — but the fix makes it data-driven and game-scoped: declare seed items per-game in `GAME_DEFS[ctx].seedInventory` (FNV declares the Canteen; FO3 and future games declare their own or `[]`), and `seedNewCampaignInventory` iterates `_activeDef().seedInventory`. So no game gets another game's seed items. *(This is the headline reason GA-6 is in this build.)*
- **Protocol-13 regression (required):** a **new FO3 campaign seeds NO Vault 13 Canteen** (and no FNV-only seed item), AND a **new FNV campaign still seeds the Canteen** (existing behavior preserved) — both in both runners.
- **Files:** `js/api.js`, `js/ui-core.js` (`seedNewCampaignInventory` → iterate `_activeDef().seedInventory`), `js/ui-saves.js`, `js/ui-account.js`, `js/cloud.js`, `js/state.js` (`GAME_DEFS.*.seedInventory` — FNV: Vault 13 Canteen; FO3: []), `tests/check-persistence.js`, `tests/check-persistence.ps1` (Suite 89 + the Canteen FNV-scope regression), `RULES.md`, `CLAUDE.md` (Protocol 38 + Protocol 39 text), `sw.js`, + Protocol 2/2a docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD (behavior-neutral refactor + a live-bug fix; dual-runner suite is the safety net) · **Deps:** WU-A2 (placed right after the standards) · **Note:** Suite 89 must exist early so every later unit is guarded; extends the born-compliant rule (§1.4) with "passes Suite 89 / Protocol 38."
**WU-A5 · Boot db/reg manifest** — *A9 GA-1 (boot-loader hardcoded file map)*
- **Does:** Replaces the hardcoded `ctx === 'FO3' ? [fo3 files] : [nv files]` boot map (`index.html:1914`) with a single static **boot manifest** (`GAME_FILES = { FNV:[…], FO3:[…] }`) selected by `GAME_FILES[ctx] || GAME_FILES.FNV` — the ONE sanctioned game→files map (must precede `state.js`/`GAME_DEFS`, so it can't live inside `GAME_DEFS`). A new game declares its data files with one manifest line + two files.
- **Files:** `index.html` (boot script), `tests/check-persistence.{js,ps1}` (Suite 56 load-order guard update + Suite 89 sanctioned-map check), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** SPEC-FIRST (boot/loader critical path — enumerate every boot/reload/cache-clear path per Protocol 8; Suite 56 coordination) · **Deps:** WU-A4 (Suite 89 sanctioned-site allowlist must recognize the manifest).
**WU-A1 · `.tracker-toggle` accessible-button refactor** — *the big cross-cutting unit (A2+A4+A7)*
- **Does:** Replaces the 5× repeated inline-styled `<span onclick>` tracker toggles with a single shared `.tracker-toggle` `<button>` class: keyboard-focusable, `min-height/min-width:28px` (Protocol 17), `aria-pressed` state, role semantics. Extracts Lincoln `<select>` styling to a shared class (UC-CTL-3). Adds GATE-UI-3 to Suite 88 (no `<span onclick…toggle>` in tracker fns).
- **Files:** `js/ui-render.js` (renderCollectibles, renderLincolnMemorabilia, renderTraits, renderSkillBooks, renderMagazines — single owner), `css/terminal.css` (`.tracker-toggle`, `.tracker-select`), `tests/check-persistence.{js,ps1}` (update regression Suites 66/67/85/87 for new markup + add GATE-UI-3), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (atomic 5-fn + CSS change; must be one commit) · **Deps:** WU-A2 (Suite 88 must exist).
**WU-A3 · `loadUI()` render fan-out optimization** — *A1 §3, biggest perf win*
- **Does:** Replaces the full unconditional render fan-out in `loadUI()` with active-tab/visible-panel gated rendering; defers off-screen panel renders until their tab is shown. Foundation so later panels plug into the optimized path.
- **Files:** `js/ui-core.js` (loadUI), possibly `js/ui-render.js` (idempotency), `sw.js`, docs. Add a perf-guard test (render-call count / lazy-gate presence).
- **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (lifecycle-sensitive — enumerate every load/reload/tab-switch path per Protocol 8) · **Deps:** WU-A1/A2 (stable render contracts).
### PHASE B — Security, Correctness & Gate-Ratchet (before any new networked feature)
**WU-B1 · Fix `innerHTML +=` in api.js + close Suite 23 carve-out** — *A5 QA-PROHIB-1 / A6 GH-1 / TS-WEAK-1*
- **Does:** Rewrites the `api.js:257` `.forEach` `innerHTML +=` loop to `map().join('')` single-assignment; **removes** the Suite 23 carve-out that excludes api.js from the `innerHTML +=` guard (escape-ratchet, Protocol 36b).
- **Files:** `js/api.js`, `tests/check-persistence.{js,ps1}` (Suite 23 extend to api.js), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-B2 · Escape untrusted values before `innerHTML`** — *A5 QA-PROHIB-2/3 / A6 GH-2*
- **Does:** Wraps the `localStorage`-derived value at `ui-core.js:158` (apiModelInput) and the Firestore-derived value at `cloud.js:586` in `escapeHtml()`. Adds a static guard (GH-2) scanning all `.innerHTML =` RHS in served JS for literal / `escapeHtml(...)` / safe-`map().join('')` only.
- **Files:** `js/ui-core.js`, `js/cloud.js`, `tests/check-persistence.{js,ps1}` (extend XSS suite + GH-2 scanner), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-B3 · Remove `localStorage.getItem()` from AI hot paths** — *A5 QA-PROHIB-4/5 / A6 GH-3*
- **Does:** `getSystemDirective()` reads playstyle from `state` (already in save envelope) not localStorage; `transmitMessage()` (api.js:1080–81) reads from cached state. Adds guard (GH-3) that these fns contain no `localStorage.getItem`.
- **Files:** `js/api.js`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-B4 · Cloud sanitize completeness** — *A8 CC-SAN-1/2*
- **Does:** Adds Phase-6 array fields (traits, skillBooks, magazines, collectibles, lincolnItems) to `sanitizeImportedContainer()`; coerces `factions` fame/infamy values to numbers. Born-compliant rule: any future state field gets a sanitize entry in its own commit.
- **Files:** `js/cloud.js`, `tests/check-persistence.{js,ps1}` (new sanitize behavioral test), `sw.js`, docs.
- **Tags:** `[DATA][CACHE][TEST]` · **Tier:** BUILD · **Deps:** none · **Pairs with:** WU-B5.
**WU-B5 · Cloud pull round-trip behavioral test + cloud-sync protocol + setDoc-merge guard** — *A6 TS-GAP-6 / A8 CC-RT-1, §9*
- **Does:** Adds a VM-sandbox behavioral test exercising push→sanitize→migrate→apply, asserting all Phase-6 fields survive unchanged (the single highest-value test gap). Codifies the **cloud-sync-determination protocol** (every new state field states its cloud-sync path in the commit; serialized-whole fields ride automatically). Adds the `setDoc`-without-`{merge:true}` ban guard (Protocol 34 permanence).
- **Files:** `tests/check-persistence.{js,ps1}` (VM round-trip + setDoc guard), `RULES.md`, `CLAUDE.md`. (No served-file change unless test harness counts — likely doc/test-only → **no cache bump**.)
- **Tags:** `[DATA][TEST]` · **Tier:** BUILD · **Deps:** WU-B4 (sanitize must cover the fields the test asserts).
**WU-B6 · Behavioral test-gap closure (kill-switch + save paths)** — *A6 TS-GAP-1..5,7,8,9*
- **Does:** Adds behavioral tests for: kill-switch fail-open (TS-GAP-1, CRITICAL — Protocol 33's core guarantee currently has zero behavioral coverage), `isFeatureEnabled` unknown-key (TS-GAP-8), `signOutAccount` re-anon gating (TS-GAP-9, Protocol 31), `restoreRollingBackup` (TS-GAP-2), `checksum_mismatch` consumer (TS-GAP-3), export→re-import round-trip (TS-GAP-4), `syncLocalSavesToCloud` dedup (TS-GAP-5). TS-GAP-7 (autoImportState happy-path in VM) is **SPEC** (needs api.js VM sandbox) — split out.
- **Files:** `tests/check-persistence.{js,ps1}` only (+ docs). No served change → **no cache bump**.
- **Tags:** `[NET][TEST]` · **Tier:** BUILD (most) / SPEC (TS-GAP-7) · **Deps:** none.
**WU-B7 · Dead-code purge + duplication consolidation** — *A5 QA-DEAD-1/2/3/5/6/8, QA-DUP-2/3/4*
- **Does:** Removes confirmed-dead `showDeltaGhost()` (+ guard it stays absent), `_inputHistory`, `_cleanKey`, `gameTimeToTicks` (wrong comment), `targetDT=0`, commented `gameContext` line; consolidates `listLocalSaves` duplication (DUP-2), memory-cycle/uptime interval copy-paste (DUP-3/4). **Excludes** the TEMPLATE items (→ WU-E2) and `ticksToGameTime` (→ OPEN DECISION).
- **Files:** `js/ui-audio.js`, `js/ui-core.js`, `js/api.js`, `js/ui-account.js`, `js/ui-render.js`, `tests/check-persistence.{js,ps1}` (absence guards), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-B8 · `renderSkillBooks`/`renderMagazines` shared helper** — *A5 QA-DUP-1*
- **Does:** Extracts the ~90% identical render logic into one parameterized helper (registry array + state set + summary count + found-first list). Born-compliant: future binary trackers call the helper.
- **Files:** `js/ui-render.js`, `tests/check-persistence.{js,ps1}` (update Suites 85/87), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (non-trivial refactor; must preserve both render contracts) · **Deps:** WU-A1 (button refactor lands first so the helper inherits `.tracker-toggle`).
**WU-B9 · Boundary fix: `cloud.js` direct `state.*` read** — *A5 QA-PROHIB-6*
- **Does:** Routes `cloud.js`'s `state.gameContext` read through a `state.js` accessor (Protocol 23 boundary). SPEC because it needs a new state.js API.
- **Files:** `js/state.js` (accessor), `js/cloud.js`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** SPEC · **Deps:** none · **Tier note:** low priority — currently correct, removes a hidden ordering assumption.
**WU-B10 · Boot-drone autoplay timing** — *LIVE BUG (2.7.0 build testing); audio*
- **Does:** The boot drone can't play during boot (browser autoplay policy blocks audio before the first user gesture), so it fires **awkwardly on the first menu tap mid-session**. Fix in `ui-audio.js`: only play the boot drone within the **actual boot window** — trigger it on the first user gesture **IF still booting**, and **suppress a stale boot drone if boot already completed** before the first interaction (no random drone on a later first click). Net: drone plays as part of boot or not at all — never detached.
- **Files:** `js/ui-audio.js` (boot-drone trigger + "still booting?" guard + stale-suppression flag), `tests/check-persistence.{js,ps1}` (regression/structural guard: drone gated on boot-active state, suppressed post-boot — if feasible), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none · **Coordinate with WU-F6 (Cold-Start Boot)** — both touch the boot/audio path in `ui-audio.js`; sequence them together (ui-audio.js cluster: B7 dead-code → B10 boot-drone → F2/F5/F6).
**WU-B11 · FO3 autocomplete/autofill broken** — *LIVE BUG (2.7.0 build testing); game-awareness; cross-ref A9/Protocol 38 + WU-A5 boot manifest*
- **CONFIRMED ROOT CAUSE (read from code):** `registrySearch()` (+ its `_registrySearchCache`) is defined **only in `js/reg_nv.js` (line ~1889)** — **NOT in `js/reg_fo3.js`** (both files carry the `/* exported … registrySearch */` comment, but only NV has the function body). The boot manifest loads **reg_nv.js OR reg_fo3.js, never both** — so in an **FO3 campaign `registrySearch` is `undefined`**, and `ui-saves.js:488` (`registrySearch(category, q)`) throws/returns nothing → **every wired autocomplete input (quest/item/perk) is dead in FO3**. (The `FALLOUT_REGISTRY` const itself IS game-correct per-game — the gap is the missing *function*, not the data. `initLocationDatalist` reads `FALLOUT_REGISTRY.locations` directly so the location datalist may partly work, but the `registrySearch`-backed inputs do not.)
- **FIX (game-agnostic, preferred):** **extract `registrySearch` + `_registrySearchCache` into a shared, always-loaded module** (e.g. `js/registry-core.js`) that reads whichever active `FALLOUT_REGISTRY` const is loaded — so it works for FNV, FO3, and a future FO4 with zero per-game duplication (Protocol 22 extend-before-creating; Protocol 38 agnostic). Add it to the boot manifest **after** the per-game `reg_*.js` (coordinate with **WU-A5** — boot/load-order + Suite 56 + `sw.js` ASSETS). *Minimal fallback (noted, not preferred): duplicate `registrySearch` into `reg_fo3.js` — closes FO3 but is an N-copy Protocol-22 violation and an FO4 footgun; prefer the extraction.* Either way `ui-saves.js` should guard `typeof registrySearch === 'function'` defensively.
- **SELF-IMPROVING GUARD (both runners):** assert `registrySearch` is defined and **resolves against the active game's registry in FO3 context** (e.g. a quest/item query returns FO3 results when ctx=FO3) — not just FNV. Catches the "function only ships in one game's reg file" class so it can't silently break per-game again.
- **Files:** **new `js/registry-core.js`** (extracted `registrySearch` + cache) OR `js/reg_fo3.js` (fallback duplicate), `js/reg_nv.js` (remove the moved fn if extracted), `js/ui-saves.js` (`typeof registrySearch` guard in `wireInput`/`initRegistryAutocomplete`), `index.html` (boot manifest + script tag if a new file — WU-A5 coordination), `tests/check-persistence.{js,ps1}` (FO3-context registrySearch guard + Suite 56 load-order if new file), `sw.js` (ASSETS + cache), `CHANGELOG.md` (`[Unreleased]` Fixed entry). · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A5 (if the shared-module path is taken — boot manifest); otherwise none.
### PHASE C — Accessibility & Mobile (user-facing, no new features)
**WU-C1 · Global `prefers-reduced-motion` guard** — *A0 CR-1 / A2 CR-1 (22 keyframes)*
- **Does:** One `@media (prefers-reduced-motion: reduce)` block disabling/limiting CRT flicker, scanline, type-out, pulse — 22 keyframes. Standby ≠ reduced-motion (keep distinct). HOUSE_STANDARD §3 non-negotiable.
- **Files:** `css/terminal.css`, `tests/check-persistence.{js,ps1}` (static guard the media block + key animations are covered), `sw.js`, docs.
- **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-C2 · `:focus-visible` indicator** — *A2 2a (CRITICAL)*
- **Files:** `css/terminal.css`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-C3 · Tab ARIA state + contrast** — *A2 2c + tab opacity:0.55 contrast fail*
- **Does:** Adds `role=tab`/`aria-selected` state to tab buttons; raises inactive-tab opacity to meet AA contrast.
- **Files:** `index.html`, `js/ui-core.js` (initTabs/switchTab), `css/terminal.css`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC · **Deps:** WU-A2.
**WU-C4 · sysModal focus management + ARIA** — *A2 2b (CRITICAL)*
- **Does:** Focus trap + `role=dialog`/`aria-modal`/labelledby + restore-focus-on-close for `sysModal` (mirror the existing `updateModal` trap from Suite 65).
- **Files:** `js/ui-core.js` (or wherever sysModal lives), `index.html`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC · **Deps:** none.
**WU-C5 · ARIA labels for anonymous icon/text buttons** — *A2 2d, 4a, 4b*
- **Does:** `aria-label`s for inventory USE/DELETE (4a), faction F+/F-/I+/I- (4b), limb state-via-text buttons (2d). Literal-layer text (HOUSE_STANDARD §5: a screen reader says "Use stimpak", not diegetic).
- **Files:** `js/ui-render.js`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A1 (faction/inventory buttons may share the new button styling).
**WU-C6 · chatDisplay live region** — *A2 5a (CRITICAL)*
- **Files:** `index.html` (`aria-live` on `#chatDisplay`), `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none.
**WU-C7 · Mobile layout fixes + systematic label-wrap closure** — *A4 M-H1, M-H2, M-M1, M-M2, M-L1 + the A4 label-wrap finding class*
- **Does:** Auto-zoom guard for 481–999px landscape (M-H1); `flex-wrap` on export/import/restore row (M-H2); `.modal-content` `overflow-wrap:anywhere` (M-M1); input-group `<select>` flex 481–999px (M-M2); D-pad 35→≥44px (M-L1, WCAG). Verify per Protocol 10 (render at 360/412/≥1000) + `tests/render-check.mjs`.
- **LIVE BUG — config label wrap (same family as the shipped OPTICS fix):** in Campaign Configuration the **"GAME:" and "PLAYSTYLE:" labels wrap to a second line** ("GAME\n:" / "PLAYSTYL\nE:") because the label column is too narrow. The OPTICS `.optics-label{white-space:nowrap}` (Suite 86) was the **first** instance of this class — these are more.
- **SYSTEMATIC fix (not one-off):** apply the no-wrap/label-fit treatment to GAME + PLAYSTYLE **and sweep the other config/panel `<label>`s** (PLAYTHROUGH TYPE, Security/Optics/Audio labels, etc.) for the same wrap risk. **Generalize the pattern** — a shared label class (e.g. `.cfg-label{white-space:nowrap}` + adequate `min-width`, or fix the label column width) so the whole bug-class is closed at once rather than chasing each label. **Both-breakpoint check is mandatory:** the OPTICS fix taught that a label fix can break the *other* breakpoint — verify GAME/PLAYSTYLE/PLAYTHROUGH and the swept labels render correctly at **360, 412, AND desktop** with no overflow and no clipped label (Protocol 10 / `render-check.mjs`).
- **Files:** `css/terminal.css` (shared label class + `.optics-label` consolidation), `index.html` (apply the class to GAME/PLAYSTYLE/PLAYTHROUGH + swept config/panel labels), `tests/check-persistence.{js,ps1}` (Suite 61 extend; presence guard that the shared label class carries `white-space:nowrap` and is applied to the config labels — generalizing the existing Suite 86 OPTICS guard), `tests/render-check.mjs` (manual verify both breakpoints), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A1/A2 (avoid touching the same rows twice).
**WU-C8 · World-map cell text truncation** — *LIVE BUG (2.7.0 build testing); cross-ref MOBILE_AUDIT*
- **Does:** Map grid cells truncate location names with a hard mid-word cut and trailing underscore ("Republic_", "Museum o_") — broken in both the grid and the zoomed view. Fix to clean fit: allow up to **2 lines** with proper ellipsis (or a readable abbreviation) so names stay legible; the **full name stays available on tap + `title`/`aria-label`** (a11y label is always the full name, never the truncated form). Consistent at **360 / 412 / desktop** (Protocol 10). Read-only.
- **Files:** `js/ui-render.js` (`renderWorldMap` — cell text + `title`/`aria-label`), `css/terminal.css` (`.map-cell` — line-clamp/ellipsis/`overflow-wrap`), `tests/check-persistence.{js,ps1}` (static guard: cell carries full-name `title`/`aria-label`; CSS clamp present), `tests/render-check.mjs` (no overflow at 360/412), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A2 (UI standards) · **Note:** keep desktop unchanged; verify per Protocol 10.
**WU-C9 · Collectible "[?]" marker stacks vertically when zoomed** — *LIVE BUG (2.7.0 build testing); render fix*
- **Does:** The `[?]` collectible badge on a map location renders as a vertical stack `[ / ? / ]` when zoomed into a location, instead of inline `[?]`. Root cause is almost certainly a `white-space`/`flex`/`width` constraint on the zoomed `.map-cell` badge forcing each glyph to wrap. Fix: keep the badge **inline and non-wrapping** (`white-space:nowrap`, no width squeeze, no per-char flex item) in both grid and zoomed states.
- **Files:** `js/ui-render.js` (map badge render — the `[?]` pip markup), `css/terminal.css` (badge `white-space:nowrap`/sizing), `tests/check-persistence.{js,ps1}` (structural/regression guard: badge markup is single-token + `nowrap` rule present, if feasible), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A2 ; pairs with WU-C8 (same `renderWorldMap`/`.map-cell` surface — sequence C8 → C9 or land together to avoid double-touching the map render). · **Note:** verify zoomed state at 360/412/desktop (Protocol 10).
**WU-C10 · Inventory row layout broken (mobile)** — *LIVE BUG (2.7.0 build testing); same squeezed-to-0-width wrap family as WU-C9; cross-ref MOBILE_AUDIT + A2 tap-target*
- **Does:** In BACKPACK INVENTORY rows: **(a)** the **USE button is ~40% of the row width**, squeezing the item name into a narrow column that wraps to 3–4 lines ("1x Sunset Sarsaparilla Star Bottle Cap"); **(b)** the **item-type tag renders VERTICALLY/broken** — "[WEAPON]"→"[WE / APO / N]", "[MISC]"→"[MI / SC]", "[AID]" stacked — because the tag column is crushed to ~0 width and wraps one cluster per line. **Same root cause as WU-C9** (element squeezed to ~0 width with default wrapping).
- **Fix:** rework the inventory row layout so the **item NAME gets the majority of the width**; **USE becomes a compact fixed-width control** (owner: smaller USE) that **stays ≥44px tall** (A2 tap-target — "smaller" is width-only, not height); the **type tag is inline + non-wrapping** (`white-space:nowrap` + adequate `min-width`) or repositioned — never vertical; item name + qty + weight/value fit cleanly with minimal wrapping at **360/412/desktop**; the **X (delete) button stays reachable** (≥44px tap target).
- **SYSTEMIC — close the bug-class, don't chase instances:** the "element squeezed to ~0 width → vertical wrap" pattern now recurs across the map `[?]` badge (WU-C9), the config labels (WU-C7), and these inventory type-tags. **Establish/reuse a single shared "badge/tag = inline + `white-space:nowrap` + `min-width`" treatment** (a shared `.tag`/`.badge` class) applied here AND retro-applied to C9's badge + C7's labels so the whole class is closed once. (Coordinate the shared class with WU-C7's `.cfg-label` + WU-C9's badge so they converge, not duplicate — Protocol 22 extend-before-creating.)
- **Files:** `js/ui-render.js` (`renderInventory` row markup), `css/terminal.css` (`.inventory-list` / row grid / USE button / `.tag` type-tag), `tests/check-persistence.{js,ps1}` (Suite 61 extend: USE button width-bounded + ≥44px height, type-tag `nowrap` + shared-class presence, row name gets majority width), `tests/render-check.mjs` (no overflow / no vertical tag at 360/412), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A1/A2 ; converges the shared tag/label treatment with WU-C7 + WU-C9 (sequence after or with them so the shared class lands once).
**WU-C11 · Changelog viewer fix + glow-up** — *LIVE BUG (correctness regression) + owner UI set (all 5); cross-ref Protocol 21 + WU-A2 collapsible standard*
> **PARTIAL — env-aware `[Unreleased]` display SHIPPED (pull-forward before WU-B6, commit on `dev`).** Implemented: `_isStagingEnv()` (meta `robco-env` marker injected by `cf-staging-build.mjs` + `*.pages.dev`/localhost hostname, fail-safe default = production) + `_visibleChangelog(text, isStaging)`; BOTH the boot-time viewer and `showFullChangelog()` now gate on it (staging shows `[Unreleased]` in source/chronological order, prod hides it — and this also fixed the prior prod leak where `showFullChangelog()` dumped `[Unreleased]`). Both-sides gate guard live (Suite 62.3–62.6, both runners). **STILL QUEUED (the visual glow-up):** version dropdown, collapsible category `<details>`, diegetic "FIRMWARE REVISION LOG" framing + category tags, ~700px reading column, summary-first — items (1)–(5) under LAYOUT below.
- **CORRECTNESS (the rule) — ENVIRONMENT-AWARE (owner 2026-06-29, Protocol 43 split):** the in-app changelog viewer currently dumps the whole markdown (incl. `[Unreleased]`) as a cluttered wall. The viewer must be **environment-aware** (supersedes the old "never renders on the site" wording — see the "Environment-aware in-app changelog viewer" rule in RULES.md/CLAUDE.md under Protocol 21):
- **Production** (GitHub Pages, built from `main`): render **ONLY the latest RELEASED version** and **SKIP `[Unreleased]` entirely**. Public users never see unreleased work.
- **Dev / staging** (Cloudflare `robco-uos-dev.pages.dev`, built from `dev`; plus `localhost`): **DO render `[Unreleased]`** (as the top/most-recent entry) so the owner reviews unreleased work while testing.
- **Detection — fail-safe, default to production.** Primary signal = a **build-injected staging flag**: extend `scripts/cf-staging-build.mjs` to stamp the staged `index.html` with an explicit marker (`<meta name="robco-env" content="staging">` and/or `window.__ROBCO_ENV__ = 'staging'`); prod (`deploy.yml`) never emits it, so absent/unknown/stale-cached ⇒ hide `[Unreleased]` (can't leak to prod). Hostname (`*.pages.dev`, `localhost`/`127.0.0.1`) is an acceptable secondary signal. `[Unreleased]` **always stays in `CHANGELOG.md`**; only the *rendering* is gated. *(Both the boot-time `needsChangelog` viewer and `showFullChangelog()` in `ui-core.js` must honor the env gate — today the boot viewer already skips `[Unreleased]`, but `showFullChangelog()` dumps everything; the full viewer must hide `[Unreleased]` in prod and show it in dev.)*
- **SELF-IMPROVING GUARD (both runners):** a gate guard that asserts the **env-aware** behavior on BOTH sides — **(a)** in production mode the viewer filters/never renders `[Unreleased]`, **(b)** in staging/dev mode the viewer DOES render `[Unreleased]` — so neither the prod-hide nor the dev-show can silently regress (Protocol 36b escape-ratchet). Also guard that `cf-staging-build.mjs` injects the staging marker and `deploy.yml`/prod does not. Plus a **`[Unreleased]` completeness process note:** every not-yet-released code change is logged under `[Unreleased]`, which consolidates into the dated block on release (Protocol 2/2a).
- **CHRONOLOGICAL ORDER (the invariant the viewer must respect):** entries within each category are authored in chronological order, earliest first (ascending) — `[Unreleased]` is maintained that way (RULES.md/CLAUDE.md "Chronological ordering" rule) and released blocks freeze that order. The viewer must **render entries in source/document order within each category** (no re-sorting, no reversing) so the on-site log reads oldest→newest exactly as written. **GUARD (if feasible):** a both-runners structural check that the viewer's category-render preserves source order (e.g. maps the source bullet list straight to DOM, no `.reverse()`/`.sort()` on the entry array) — else **document** the source-order invariant as a code comment the viewer respects. Ships in this WU so it lands together.
- **LAYOUT (all 5, clean PC + mobile):** **(1)** show **ONE version at a time** — latest release by default + a **VERSION DROPDOWN** (`<select>` pattern) to pick older releases; **(2)** **collapsible category sections** (Added/Fixed/Changed/Removed) using the existing `<details>` sub-panel pattern (latest open, rest collapsible) — consistent with the WU-A2 UI-consistency protocols (UI-1..5, `data-sub-id`); **(3)** **diegetic framing** — "FIRMWARE REVISION LOG" header (version • date • separator), `>` bullets, category tags `[+] ADDED` / `[*] FIXED` / `[~] CHANGED` / `[-] REMOVED` (HOUSE_STANDARD voice); **(4)** **typography + reading column** — PC caps content to a centered **~700px** column (no edge-to-edge stretch); mobile full width + comfortable padding; line-height + inter-entry spacing on both; **(5)** **summary-first optional** — one line per category, expand for detail; keep only if it fits cleanly, else the dropdown + collapsibles already declutter.
- **Files:** `js/ui-*.js` (changelog-viewer parse/render fn — **env-aware `[Unreleased]` gate** (prod hide / dev show) in BOTH the boot `needsChangelog` viewer and `showFullChangelog()` + version dropdown + collapsible category render + diegetic tags), `scripts/cf-staging-build.mjs` (**inject the staging marker** — `<meta name="robco-env" content="staging">` and/or `window.__ROBCO_ENV__`), `css/terminal.css` (viewer/modal styling, ~700px reading column, category sections), `tests/check-persistence.{js,ps1}` (**env-aware guard: prod-mode hides `[Unreleased]` AND dev-mode shows it** + cf-staging injects marker / prod does not + structure guards: dropdown present, category `<details>` sub-panels, latest-release-default), `tests/render-check.mjs` (reading column at 360/412/desktop, no overflow), `sw.js`, `CHANGELOG.md` (`[Unreleased]` entry). · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A2 (collapsible/UI-consistency pattern + `data-sub-id`) · **Note:** A2 contrast preserved; verify per Protocol 10 at 3 breakpoints; env-aware viewer rule lives in RULES.md/CLAUDE.md under Protocol 21.
**WU-C12 · Macro command buttons wrap inconsistently across devices** — *LIVE BUG (2.7.0 build testing); same nowrap family as WU-C7/C9/C10, but a surface they did NOT cover*
- **Does:** The COMM-LINK macro buttons `[THREAT]` / `[VATS]` / `[TRADE]` / `[LOOT]` render on one line on some phones but **wrap** (e.g. `[THREAT` + `]` on a second line) on others running the **same version** — a device/default-font-size-dependent wrap. These were **not** covered by the WU-C7/C9/C10 inline-`nowrap` fix (Suite 92 guards `.tag`/`.badge`, not the macro-button grid).
- **Fix:** apply `white-space:nowrap` to the macro command buttons **AND size each button so its bracketed label fits on ONE line at a reasonable font on ALL devices** — not nowrap-and-overflow, but nowrap + adequate width. Verify the **2-col macro grid still fits** at **360 / 412 / desktop** (Protocol 10 / `render-check.mjs`) with no horizontal overflow.
- **SELF-IMPROVING:** extend the nowrap guard (**Suite 92** or a sibling) to cover the macro command buttons too — assert they carry the `nowrap` treatment — so this device-inconsistency can't regress.
- **Files:** `css/terminal.css` (the macro button / `.d-pad-macro` grid styles — `white-space:nowrap` + width), possibly `index.html` (button markup if a class hook is needed), `tests/check-persistence.{js,ps1}` (Suite 92 extend: macro buttons carry nowrap), `tests/render-check.mjs` (2-col grid no overflow at 360/412), `sw.js`, `CHANGELOG.md` (`[Unreleased]` Fixed: "Macro command buttons [THREAT]/[VATS]/[TRADE]/[LOOT] no longer wrap on devices with larger default fonts"). · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A2 ; same nowrap family as WU-C7/C9/C10 (reuse the shared treatment where it fits) · **Note:** verify at 3 breakpoints (Protocol 10).
**WU-C13 · SAVE MENU help "?" affordance** — *owner request (2026-06-29); same help/affordance family as the command-reference "?" + WU-C11 changelog viewer*
- **Does:** Adds a small diegetic **"?" help button** to the SAVE MENU header (the save/data panel that exposes EXPORT SAVE, IMPORT SAVE, RESTORE BACKUP, save slots A/B/C, and cloud sync push/pull) that opens a help modal explaining how to use each save feature — mirroring the existing command-reference "?" / `[LOGS]` help pattern. Reuse the shared **`sysModal`** entry point (`_openSysModal`, WU-C4) so the modal inherits the focus-trap + ARIA dialog semantics for free; content is HOUSE_STANDARD diegetic voice. **Game-agnostic** (Protocol 38): copy describes save mechanics generically, no game-specific literals.
- **Files:** `index.html` (the "?" button in the save-panel summary/header + help-modal content), `js/ui-*.js` (open-help handler reusing `_openSysModal`), `css/terminal.css` (button hook if needed; reuse existing "?" styling), `tests/check-persistence.{js,ps1}` (structural guard: SAVE MENU "?" present + opens via `_openSysModal`, documents each save action), `sw.js`, `CHANGELOG.md` (`[Unreleased]` Added). · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC · **Deps:** WU-C4 (sysModal focus management) · **Note:** ships with the help/affordance polish (near WU-C11); verify per Protocol 10 at 3 breakpoints; ≥28px tap target (Protocol 17).
**WU-C15 · Changelog viewer — consistent width + defaults** — *owner request (2026-06-30); extends WU-C11 (changelog viewer); owner picked sizing options 2+3.* — **QUEUED (owner will dispatch the build separately).**
- **Does:** Polishes the WU-C11 changelog viewer (`_showChangelogModal` / `.modal-box.changelog-wide`) so it no longer narrows/jumps as category `<details>` open and close:
1. **Lock the modal width to the EXPANDED ("open") size at every breakpoint** — opening/closing a category changes only **height, never width** (kills the narrow box when all categories are collapsed). On mobile, lock to the **same full-width-minus-margin** it uses when expanded.
2. **Default open state:** the **newest (top) category stays expanded** on open; all others collapsed.
3. **Add an "EXPAND ALL / COLLAPSE ALL" toggle** control at the top of the viewer (diegetic label, HOUSE_STANDARD voice; addEventListener wiring — no inline handler, mirrors WU-C11c).
- **PRESERVE:** the env-aware `[Unreleased]` gate (prod hide / staging show) + source-order rendering — **Suite 62 guards must still pass**. **Extend** the guard for the locked-width CSS (width fixed regardless of `<details>` state) + the expand/collapse-all toggle present and wired.
- **Files:** `js/ui-core.js` (`_showChangelogModal` default-open logic + expand/collapse-all handler), `css/terminal.css` (`.changelog-wide` fixed width at all breakpoints + mobile full-width), `index.html` (none expected — modal is JS-rendered), `tests/check-persistence.{js,ps1}` (extend Suite 62: locked-width CSS + toggle wired + defaults), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC · **Deps:** WU-C11 ✅ (the structured changelog renderer + `.changelog-wide` modal mode). · **Note:** verify per Protocol 10 at 360/412/desktop — width must not shift on collapse/expand.
**WU-C14 · "COMPLETE RNG" label crushed to vertical on desktop** — *LIVE BUG (2.7.0 build testing); same squeezed-to-~0-width vertical-wrap family as WU-C7/C9/C10/C12*
- **Does / diagnosis:** In the Campaign / **Playthrough Type** config, the **"COMPLETE RNG"** label/toggle — the one with the *"COMPLETE RNG ACTIVE. All SPECIAL, traits, tag skills, skill points, and perk selections will be randomised…"* warning box — renders **correctly on MOBILE**: the COMPLETE RNG label sits **full-width ABOVE** the active-warning box. But on **DESKTOP/PC** the label column is **crushed to ~0 width** and the label **wraps one character per line vertically** ("C / O / M / P / L / E / T / E …"). Classic "element squeezed to near-zero width → wraps one piece per line" class — same root as the map `[?]` badge (WU-C9), inventory type-tags (WU-C10), macro buttons (WU-C12), and the GAME/PLAYSTYLE config labels (WU-C7).
- **Fix:** desktop must render **identically to mobile** — the **COMPLETE RNG label on its own full-width row ABOVE the warning**, never vertical. Likely cause is a desktop flex/grid row where the label is a flex child that collapses (default `min-width:auto`→effectively 0 under a wide sibling/warning box), or the desktop breakpoint is missing the `flex-direction:column` / `flex-wrap` that mobile has. Apply the shared no-crush treatment: `white-space:nowrap` + a `min-width` floor on the label (and/or `flex:none`), and force the label onto its own row above the warning at desktop too. **Game-agnostic** (Protocol 38) — the RNG label/warning copy is generic, no game literals.
- **SELF-IMPROVING guard (both runners):** per the established squeeze-wrap class (Suite 92 `.tag`/`.badge` + the OPTICS / GAME-PLAYSTYLE-label / macro-button precedents), add a guard asserting the COMPLETE RNG label/control carries the no-wrap + min-width treatment so it can't be crushed to per-character vertical wrap again — extend **Suite 92** (the nowrap anti-recurrence suite) or a sibling.
- **Files:** `css/terminal.css` (Playthrough Type / RNG label + warning-box layout — `white-space:nowrap` + `min-width` + desktop row/column fix), possibly `index.html` / `js/ui-*.js` (RNG label markup / class hook + the renderer that builds the Playthrough Type block), `tests/check-persistence.{js,ps1}` (Suite 92 extend: RNG label nowrap/min-width treatment present), `tests/render-check.mjs` (RNG label not vertical, no overflow at 360/412/desktop), `sw.js`, `CHANGELOG.md` (`[Unreleased]` Fixed). · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A2 ; same nowrap/squeeze family as WU-C7/C9/C10/C12 (reuse the shared `.tag`/`.cfg-label` treatment where it fits — Protocol 22) · **Note:** desktop must match mobile; verify at 360/412/desktop (Protocol 10).
### PHASE D — Content / Data Corrections (data-only; fallout.wiki-sourced per Protocol 3)
**WU-D1 · FO3 quest canon fixes** — *A3 CS-1, CS-2 + zone-name + Suite 74 guard*
- **Does:** Removes non-canon `'Fires of Anchorage'` (reg_fo3.js:62) and fabricated duplicate `'Strictly Business (Paradise Falls)'` (reg_fo3.js:59); FO3 quests **64→62**; fixes FO3 zone naming error; adds unique-name guard to Suite 74/79.
- **Files:** `js/reg_fo3.js`, `tests/check-persistence.{js,ps1}` (Suite 82 64→62 + Suite 74/79 guard), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** none · **Source:** fallout.wiki.
**WU-D2 · Weapon/armor stat & ammo-type corrections** — *A3 (FO3 weapons, NV-armor-bleed-into-FO3 db, FNV ARMOR stats)*
- **Does:** Corrects FO3 WEAPONS ammo-type/stat errors; removes NV armor bleeding into FO3 DB; fixes FNV ARMOR stat errors. All against fallout.wiki.
- **Files:** `js/db_fo3.js`, `js/db_nv.js`, `js/reg_nv.js`/`reg_fo3.js` as needed, `tests/check-persistence.{js,ps1}` (CSV integrity suites), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD (HIGH) · **Deps:** none.
**WU-D5 · FO3 quest/location canon completeness — Anchorage gap + 'Vault 92 South'** — *surfaced during WU-D1 fallout.wiki verification (Protocol 3); additive/accuracy companion to WU-D1's removals*
- **Does (both fallout.wiki-sourced — Protocol 3, verify before adding):**
1. **Add the two missing real Operation: Anchorage quests** that WU-D1's verification surfaced: **'The Guns of Anchorage'** and **'Paving the Way'** (both `dlc: 'Operation: Anchorage'`). fallout.wiki confirms the add-on's real quest chain is *Aiding the Outcasts → The Guns of Anchorage → Paving the Way → Operation: Anchorage!* — the registry currently lists only the first and last (the middle two were never added; WU-D1 only *removed* the fabricated 'Fires of Anchorage'). Adding both takes FO3 quests **62 → 64 with the REAL entries** (vs. the old fabricated 64). Pick the correct `type:` per wiki (main vs side) for each.
2. **Verify/fix the dubious `'Vault 92 South'` location string** in the renamed (6,4) zone (`reg_fo3.js` — the cell WU-D1 renamed 'Vault 92' → 'Bethesda Ruins'; its `locations:` array still reads `['Vault 92 South', 'Calverton', 'Bethesda Ruins']`). 'Vault 92 South' is not a recognized FO3 location (the real Vault 92 is the far-NW (1,1) cell). Confirm against fallout.wiki whether it should be a real nearby marker (e.g. a Bethesda/Calverton-area location) or removed; correct or drop it so the cell's `locations[]` are all canon.
- **SELF-IMPROVING:** if feasible, extend the Suite 82 FO3-quest guard so DLC quest *counts per add-on* match the canon set (Operation: Anchorage = exactly 4), catching the "missing real DLC quest" class — not just the dedup/fabrication class WU-D1 closed. Re-sync Suite 82 count 62 → 64 (REAL) + both runners + Protocol 2a.
- **Files:** `js/reg_fo3.js` (quests[] + the (6,4) zone `locations[]`), `tests/check-persistence.{js,ps1}` (Suite 82 count 62→64 + per-add-on quest-count guard; Suite 74/79 if the location string changes), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-D1 (builds on its corrected baseline) · **Source:** fallout.wiki only (Protocol 3).
**WU-D6 · Weapon/armor data flags surfaced during WU-D2 verification** — *surfaced during WU-D2 fallout.wiki verification (Protocol 3); verify-before-changing companion to WU-D2, mirroring WU-D5↔WU-D1*
- **Does (each fallout.wiki-sourced before any change — Protocol 3; do NOT guess):**
1. **"O'cta Brain" FO3 weapon — AWAITING OWNER DECISION (remove vs rename).** This weapon entry appears in both `js/db_fo3.js` (WEAPONS.CSV) and `js/reg_fo3.js` (weapons[]) but has **no fallout.wiki match** — the name looks fabricated/garbled (likely a corrupted "Ol' Painless" / mangled entry). **Owner must decide:** remove it entirely, or rename to the real weapon it was meant to be. **Blocked on owner input — do not edit until the decision lands.** Once decided: apply to both files, keep DB↔registry parity (Suite 38/93), re-sync any affected count + both runners (Protocol 2a/15).
2. **1st Recon Assault/Survival Armor DT — VERIFY AGAINST INFOBOX before changing.** The FNV ARMOR DB currently lists DT **22** for the 1st Recon Assault/Survival Armor; a wiki *snippet* suggested **15**, but a snippet is not authoritative. **Do not change on the snippet alone** — pull the actual fallout.wiki **infobox** value (and confirm which exact variant the row represents) first; correct the DB only if the infobox disagrees, then add/extend the relevant ARMOR CSV stat guard + re-sync per Protocol 2a. If the infobox confirms 22, leave it and record the verification.
- **Files (when unblocked):** `js/db_fo3.js`, `js/reg_fo3.js`, `js/db_nv.js`, `tests/check-persistence.{js,ps1}` (DB↔registry parity + ARMOR/weapon stat guards), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD (HIGH for #2 once infobox confirmed; #1 BLOCKED on owner) · **Deps:** WU-D2 (builds on its corrected baseline) · **Source:** fallout.wiki infobox only (Protocol 3).
**WU-D3 · Low-priority typos / additive data gaps** — *A3 LOW batch*
- **Tier:** DEFER (optional cleanup batch; no correctness impact) · **Deps:** none.
**WU-D4 · B3 deterministic-feature data gaps** — *DETERMINISTIC_FEATURES.md §3 (cross-ref CONTENT_AUDIT, fallout.wiki only — Protocol 3)*
- **Does:** Sources/confirms the data the Phase-N natives need where the local schema is incomplete: **(a)** ranged-accuracy coefficient set for VATS hit-% (weapon schema has `Special_Attack_AP`/`Reach` but no spread/range falloff — the melee/unarmed AP-strike mode is already exact; only ranged needs this); **(b)** canonical FNV/FO3 **barter price coefficient** for TRADE buy/sell multipliers; **(c)** **ammo-per-attack** count for ammo-burn (VATS/THREAT) — default 1/attack or extend schema; **(d)** *optional* per-vendor stock mapping (`VENDOR_STOCK`) — TRADE works without it (uses the full items DB as catalog + `VENDORS.Base_Caps` as purse), so this is enrichment, not a blocker.
- **Files:** `js/db_nv.js`, `js/db_fo3.js` (coefficient constants / optional new columns), possibly `js/reg_*.js`, `tests/check-persistence.{js,ps1}` (CSV/constant guards), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD (a–c) / DEFER (d) · **Deps:** none · **Feeds:** WU-N1 (ranged), WU-N2 (barter), WU-N3 (ammo-burn). *The exact-math cores (melee VATS, barter shape, TTK) don't block on this — only the flagged coefficients do.*
### PHASE E — Diegetic Voice & Templates (A0)
**WU-E1 · Diegetic terminology corrections** — *A0 DIEGETIC_AUDIT findings vs HOUSE_STANDARD*
- **Does:** Brings modern-web "tells" into the locked terminology table (Loading→RETRIEVING…, settings→CONFIGURATION, etc.) and `> `/ALL-CAPS/bracket conventions across the surfaces A0 flagged. Literal layer (ARIA + Gemini-key label) stays plain (§5).
- **Files:** `index.html`, `js/ui-*.js` (string literals), `tests/check-persistence.{js,ps1}` (optional voice guards), `sw.js`, docs. · **Tags:** `[CACHE]` · **Tier:** SPEC (enumerate strings; avoid breaking existing test string-matches) · **Deps:** none.
**WU-E2 · CONVERT-TO-TEMPLATE: `#updateBanner` + `fo3WarningBanner` "2.0.0" message** — *OWNER OVERRIDE — never delete*
- **Does:** Instead of deleting the dead `#updateBanner` CSS (QA-DEAD-7/M-M4) and the now-stale `fo3WarningBanner` "no FO3 data until 2.0.0" message (index.html:1009–1012, ui-core.js:382), converts both into reusable, parameterized banner templates for future "feature-not-available / firmware" notices. **Explicitly overrides** any A4/A5 "remove dead code" instruction on these two artifacts.
- **Files:** `index.html`, `css/terminal.css`, `js/ui-core.js`, `tests/check-persistence.{js,ps1}` (guard the templates EXIST, not absent), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC · **Deps:** none.
**WU-E3 · FEATURES menu / command-reference refresh** — *owner report (2026-06-30): the in-app FEATURES/help menu is OUTDATED.*
- **Does:** Audit + update the in-app **FEATURES menu** (the `[FEATURES]` help modal → `showHelpModal`) and the **command reference** (`COMMAND_REGISTRY` in `js/ui-core.js`, surfaced via the `[?]` menu + Suite 36 KEYBOARD SHORTCUTS group) so they reflect the **CURRENT** command set. The reference drifted during Phase N and the macro retirements and no longer matches reality:
- **Add the 6 native deterministic commands** with their real tokens/aliases: VATS (`[VATS SIM]`/`[VS]`/`[VATS]`), TRADE (BARTER UPLINK panel), THREAT (`[THREAT]`/`[TH]`), CONSULT (`> CONSULT`/`[CONSULT]`/`[CON]`), BIO-SCAN (`[BIO-SCAN]`/`[BIO]`), LOOT (`[LOOT]`/`[LT]`) — all now **offline/native, no AI**.
- **Remove / fix retired AI macros:** the old AI-routed TRADE/THREAT/VATS/LOOT/BIO-SCAN macro descriptions, and any command that was removed or repointed (verify each listed token still resolves — to the native router, a panel, or a real handler).
- **Reconcile against the live `NATIVE_COMMAND_ROUTER`** (the single source of truth for native tokens) so the help text can't drift from the router again. Consider a guard that every command the FEATURES menu advertises actually resolves (router entry, panel route, or defined handler) and vice-versa.
- **Cross-ref (parked command-list cleanup):** fold this together with any parked "dead/stale command-reference" cleanup — none is formally in this plan yet, so **WU-E3 IS that cleanup**: it owns reconciling `COMMAND_REGISTRY` ↔ `NATIVE_COMMAND_ROUTER` ↔ the `[FEATURES]` modal ↔ the macro buttons in `index.html`. Also surface **WU-N4b**'s new CONSULT affordance here once it lands.
- **Files:** `js/ui-core.js` (`showHelpModal` content + `COMMAND_REGISTRY`), `index.html` (macro-row labels if stale), `tests/check-persistence.{js,ps1}` (new guard: advertised commands resolve to a router entry / panel / handler — extend Suite 36 + the native-router suites 105–110), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (enumerate the current command set first; HOUSE_STANDARD diegetic voice per WU-E1) · **Deps:** Phase N ✅ (all six natives) + the VATS retirement fix (2026-06-30) ; **coordinate with WU-N4b** (CONSULT affordance).
### PHASE F — New Native Capabilities (B1 BUILD-NOW; each ships kill-switch + fallback per Protocol 32)
Each = its own additive work-unit; all inherit UI-1..5 + Protocol 17 + reduced-motion baked in (§1.4). Networked/IO ones register a kill-switch flag.
| ID | In-world name | Capability | Tags | Tier |
|---|---|---|---|---|
| WU-F1 | Sustained Power Cell | Wake Lock API | `[CACHE]` | BUILD |
| WU-F2 | Haptic Solenoid | Vibration API | `[CACHE]` | BUILD |
| WU-F3 | Eject Holotape | Web Share (export) | `[CACHE]` | BUILD |
| WU-F4 | Pending-Directives Tally | Badging API | `[CACHE]` | BUILD |
| WU-F5 | Pip-Boy Radio | WebAudio synth (zero-byte) | `[CACHE]` | BUILD |
| WU-F6 | Cold-Start / Degraded-Tube Boot | First-cold-start full POST **+ rare degraded boot on ANY reboot** (see roster note) | `[CACHE][TEST]` | BUILD |
| WU-F7 | Overseer's Log | Session/event log surface | `[CACHE]` | BUILD |
| WU-F8 | High-Lumen Optics | Brightness/contrast OPTICS mode | `[CACHE]` | BUILD |
| WU-F9 | TERMLINK Command Console | Native command surface | `[CACHE]` | BUILD |
- **Common files (per unit):** `index.html`, relevant `js/ui-*.js`, `css/terminal.css`, `tests/check-persistence.{js,ps1}` (presence + behavior), `sw.js`, docs. Net/IO units also touch the kill-switch flag registration + `firestore.rules`/config if applicable.
- **Deps:** Phase A (UI standards) + Phase C (a11y baseline) so each is born-compliant. **SPEC-FIRST B1 ideas** (gyro parallax, hacking minigame, share_target receiver, TTS, ambient-light) are deferred to a later design pass — not in this backlog's build set.
### PHASE N — Native Deterministic Features (B3; offline, no `[NET]`, NOT App-Check-gated)
The B3 conversions. Each computes its result from local data (`FALLOUT_REGISTRY` + `databaseCSVs` via `lookupItemInDb()`/`registrySearch()` + `state`) — **zero Gemini, works offline, free.** All inherit UI-1..5 + Protocol 17 + reduced-motion + a11y baked in (§1.4/§1.5). Read-only units take no `[DATA]` tag; TRADE/LOOT mutations are **additive + confirm-gated (Protocol 34)**, reusing the craft-style confirm. **None depend on WU-G0 (App Check).**
**WU-N1 · VATS — complete the native calculator** — *B3 §2.1 (supersedes the AI deferral in `showVATSOverlay`) + A9 GA-7/GA-10 (agnostic + live FO3 bug fix)*
- **Does:** Finishes the already-deterministic `showVATSOverlay()` — wires a real `targetDT` (+ distance) input (deletes the dead `targetDT=0`, QA-DEAD-6), pulls the **equipped weapon's** skill/AP from `WEAPONS.CSV` instead of "best combat skill," drops the "ACTUAL OUTCOME DETERMINED BY AI" label, and adds a **melee/unarmed AP-strike optimizer** mode (per-region AP cost, strikes-per-AP, damage-per-AP — exact from `Special_Attack_AP`/`Base_Damage`/`Attacks_Per_Second`). Read-only.
- **A9 agnosticism (folded in — GA-7 + GA-10):** the body-region modifier/AP table must be `GAME_DEFS[ctx].vats.regions`, NOT the hardcoded "FNV standard" table (GA-7); the combat-skill set must derive from `getSkillKeys()` filtered by `GAME_DEFS[ctx].combatSkills`, NOT the hardcoded `{guns,…}` object (GA-10). **GA-10 fixes a LIVE BUG:** the current code uses FNV's `guns` and patches `small_guns` but **omits FO3 `big_guns` entirely**, so FO3 Big Guns is invisible to VATS today. **Protocol-13 regression required:** assert VATS includes the active game's full combat-skill set, incl. FO3 `big_guns`.
- **Melee-scope rule (owner 2026-06-29):** the melee/AP-strike mode applies whenever **`playstyle==='melee' || weaponIsMelee`** (a melee weapon equipped/in-use in ANY mode, OR melee-only mode) — never gated on mode alone; ranged hit-% applies for ranged weapons. See the cross-cutting melee-scope note in §1.6.
- **Data:** `state.equipped.weapon`→`lookupItemInDb()`→`WEAPONS.CSV` (incl. `Reach`/`Special_Rules` to classify melee vs ranged); `GAME_DEFS[ctx].vats.regions` + `.combatSkills`; `getSkillKeys()`; `state.skills`; `state.playstyle`; SPECIAL from DOM (`s_p`/`s_a`); `state.status[]` chem buffs; user `targetDT`/distance.
- **Files:** `js/ui-core.js` (`showVATSOverlay` upgrade + AP-strike mode + melee/ranged classify), `js/state.js` (`GAME_DEFS.*.vats.regions` + `.combatSkills`), `index.html` (modal inputs), `css/terminal.css`, `tests/check-persistence.{js,ps1}` (formula + AP-cost lookup + GA-10 full-combat-skill regression incl. FO3 big_guns + melee-scope gate-shape guards), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD (melee/AP-strike + GA-7/GA-10 refactor) / SPEC (ranged hit-% — needs WU-D4a coefficient) · **Deps:** WU-A1/A2 (UI standards), WU-A4 (Protocol 38 / Suite 89 — VATS must pass agnosticism) ; WU-D4a for ranged.
**WU-N2 · TRADE — native barter terminal** — *B3 §2.2 (replaces the AI-fed TRADE modal `api.js:1260`)*
- **Does:** Deterministic vendor UI — browse stock (items DB catalog), skill-adjusted buy/sell prices, live caps. **Additive + confirm-gated** caps/inventory/ammo mutation (buy: `caps -= price`, push item; sell: `caps += price`, splice/clamp ≥0). No auto-cloud-push (manual only).
- **Data:** `VENDORS.CSV` (`Base_Caps`/`Repair_Skill`/`Location`); items DB `Value` via `lookupItemInDb()`; `state.caps`/`inventory[]`/`ammo{}`/`skills.barter`. Price = `base_Value × buy/sellMult(barter)` (canonical FNV shape — coefficient from WU-D4b).
- **Files:** `index.html` (TRADE panel; retire the AI TRADE modal path), `js/ui-render.js` (renderTrade + buy/sell, confirm-gate), `js/api.js` (drop `[TRADE]` from AI macro routing + remove/repoint `mType==='TRADE'`), `css/terminal.css`, `tests/check-persistence.{js,ps1}` (price formula, additive+clamp+confirm+no-overwrite guards), `sw.js`, docs. · **Tags:** `[DATA][CACHE][TEST]` · **Tier:** BUILD (core) · **Deps:** WU-A1/A2 ; WU-D4b (barter coefficient) ; reuses the craft-confirm pattern. **Note:** drops B2's `[NET]` entirely.
**WU-N3 · THREAT — native bestiary + TTK** — *B3 §2.3*
- **Does:** `BESTIARY.CSV` lookup → enemy stat card + TTK = `ceil(HP / max(1, weaponDPS − DT))`, weakness highlight, ammo-burn estimate. `NO ENTRY IN BESTIARY` when absent (Protocol 3). Read-only.
- **Data:** `BESTIARY.CSV` (`DT/HP/Base_Damage/Attack_Rate/Weakness_Weapon/Resistances`); equipped weapon `Base_Damage`/`Attacks_Per_Second`/`Ammo_Type`.
- **Files:** `js/ui-render.js` or `js/ui-core.js` (renderThreat), `js/api.js` (drop `[THREAT]` AI macro), `index.html`, `css/terminal.css`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A1/A2 ; WU-D4c (ammo-per-attack) for ammo-burn only.
**WU-N4 · CONSULT — native databank lookup** — *B3 §2.4 (supersedes AI-G)*
- **Does:** `> CONSULT <topic>` → exact local record (item/perk/location/creature/quest) via `registrySearch`/`lookupItemInDb`; `NO ENTRY IN DATABANK` if absent. Added to `NATIVE_COMMAND_ROUTER` (mirrors `[CROSSROADS]`). No AI in the default path.
- **Files:** `js/api.js` (`NATIVE_COMMAND_ROUTER` entry + handler), `js/ui-render.js` (result render + reuse registry autocomplete), `index.html` (optional button), `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A1/A2.
**WU-N4b · CONSULT macro button / affordance** — *owner request (2026-06-30): "should there be a button for this?"* — **QUEUED, needs owner sign-off on placement.**
- **Does:** CONSULT is currently **command-only** (`> CONSULT <topic>` / `[CONSULT]` / `[CON]`) while THREAT, VATS, TRADE, LOOT, and BIO-SCAN all have on-screen buttons. Add a discoverable CONSULT affordance for parity. Because CONSULT needs a **topic argument**, design it like the **THREAT macro-target pattern**: a `[CONSULT]` macro button that reads the shared `#macroTarget` input (→ `macroCommand('[CONSULT]')`, which the native router already intercepts) **or** a dedicated topic input that calls `renderConsult(topic)` directly. Reuse registry autocomplete on the input (Protocol 6) so topics complete against the active game's registry. No new mechanic — just an affordance over the existing native handler.
- **OWNER SIGN-OFF REQUIRED — placement:** (1) a `[CONSULT]` button in the COMM-LINK macro row next to `[THREAT]`/`[VATS]` reusing `#macroTarget`; or (2) a dedicated CONSULT panel/sub-panel with its own autocomplete topic input; or (3) both. Owner to pick before build.
- **Files:** `index.html` (button/input + aria-label, ≥28px tap target Protocol 17), `js/ui-core.js` (`COMMAND_REGISTRY` entry for discoverability if a panel), `js/ui-render.js` (if a dedicated input), `tests/check-persistence.{js,ps1}` (affordance-present + routes-to-renderConsult guard, extend Suite 108), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (placement owner-gated) · **Deps:** WU-N4 ✅ (native CONSULT handler); mirrors the THREAT/VATS macro-target pattern. · **Cross-ref:** WU-E3 (FEATURES/help-reference refresh — list the new affordance + current command set there too).
**WU-N5 · BIO-SCAN — native medical advisory** — *B3 §2.5 (core of AI-J)*
- **Does:** Limb states + HP% + active addictions → deterministic advisories ("RIGHT LEG CRIPPLED — STIMPAK ADVISED", "ADDICTION RISK: MED-X"). Pure local rules, read-only. Added to native router.
- **Data:** `state.la/ra/ll/rl/hd`, `hpCur/hpMax`, `status[]`; `CHEMS.CSV` (`Addiction_Risk/Addiction_Debuff/Effect`).
- **Files:** `js/ui-render.js`/`ui-core.js` (renderBioScan), `js/api.js` (native router entry), `index.html`, `css/terminal.css`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-A1/A2.
**WU-N6 · LOOT — deterministic add/value (hybrid)** — *B3 §2.6*
- **Does:** Deterministic "add loot to inventory" flow — pick item(s) from DB, **additive** push, value from DB `Value`, confirm-gated. The generative *drop-table* (what a defeated enemy drops) is optional → seeded-local-random or the AI residue in Phase G; the add/value math stays deterministic.
- **Files:** `js/ui-render.js`, `js/api.js` (drop `[LOOT]` AI macro or repoint), `index.html`, `css/terminal.css`, `tests/check-persistence.{js,ps1}`, `sw.js`, docs. · **Tags:** `[DATA][CACHE][TEST]` · **Tier:** BUILD (core) · **Deps:** WU-A1/A2 ; reuses craft-confirm.
### PHASE T — Per-game Theming (A10; rides the agnostic seam — after WU-A4)
The A10 BUILD-NOW set: per-game optics + identity, declared in `GAME_DEFS[ctx].theme`, driven by the active registry (Protocol 38). Makes an FO3 campaign feel distinct from FNV. **robco green `#14fdce` is the canonical New Vegas terminal color (owner-confirmed) — a permanent selectable option (`robco` theme key) and the FNV default; never removed.** Amber is a selectable option, not a per-game default.
**WU-T1 · Data-driven `THEMES` + `GAME_DEFS.theme` shape** — *A10 GT-1 (foundation; behavior-neutral)*
- **Does:** Replaces the hardcoded optics if/else chain (`changeOpticsColor`, `ui-audio.js`) AND the duplicated palette (`fgMap`, `ui-saves.js`) with ONE shared read-only **`THEMES`** table (each entry: `{ rgb, hex, glow, dark, refresh, label, contrastSafe }`; includes `robco` = `#14fdce` the canon NV green, `amber`, etc.); `changeOpticsColor(key)` becomes a table lookup + apply-the-5-vars loop. Adds the **`GAME_DEFS[ctx].theme`** shape `{ defaultOptics, framing, pipBoyModel, bootFlavor, saveLabel }`. Resolution order: **explicit per-game pick → `GAME_DEFS[ctx].theme.defaultOptics` → `robco` (canon NV green) fallback** (all via `_activeDef().theme`, no game literal — Protocol 38). Behavior-neutral (robco green still applies until T2). Fixes the palette duplication (Protocol 22).
- **Adds Suite 90 (self-improving gate guard, both runners):** every `GAME_DEFS` entry declares a `theme` whose `defaultOptics` exists in `THEMES`, and every per-game-default `THEMES` entry computes **AA contrast ≥4.5:1** against the dark bg (legion/neon stay manual-only, flagged `contrastSafe:false`). A new game can't ship an illegible terminal.
- **Files:** `js/ui-audio.js` (`changeOpticsColor` → table-driven), `js/ui-saves.js` (use `THEMES`, drop `fgMap`), `js/state.js` (`GAME_DEFS.*.theme` + `THEMES` const or a small read-only theme module), `tests/check-persistence.js`, `tests/check-persistence.ps1` (Suite 90 + no-duplicate-palette guard), `RULES.md`/`CLAUDE.md` (theme-declared requirement note), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** **WU-A4** (the `GAME_DEFS`/agnostic seam + Protocol 38/Suite 89).
**WU-T2 ✅ ABSORBED INTO WU-T1 (commit 7ba27e9) — core per-game-default-on-context shipped + verified; ONLY remaining T2-spec nuance = per-game explicit-pick storage (robco_optics_<ctx> + legacy migration), owner-accepted as global pick in T1 — OPTIONAL follow-up · Per-game default optics on context** — *A10 GT-2 (the headline "feels different per game" win)*
- **Does:** On game context (boot + `onGameContextChange`), default optics = the game's `theme.defaultOptics` (**FNV → robco green `#14fdce`** = canon NV color / existing default; **FO3 → a classic/duller green** = RESOLVED, a muted/darker olive-leaning terminal green clearly distinguishable from robco green so FO3 ≠ FNV at a glance), applied **only if the user hasn't explicitly overridden** optics for that game. **Manual override preserved (Protocol 25):** store the explicit pick **per game** (e.g. `robco_optics_<ctx>` or an explicit flag), not one global key; migrate the legacy global `robco_optics` to the per-game scheme (fail-safe → robco green, so existing users are unchanged). **Net behavior change is FO3 only** (FNV stays robco green). A2: robco green ~12:1 passes; the FO3 classic/duller green **must pass Suite 90's ≥4.5:1 gate** — the concrete hex is chosen at build to clear it (a muted green dims toward the threshold). robco green + amber + the FO3 green are all selectable options for any game.
- **Files:** `js/ui-audio.js` (apply-default helper respecting override), `js/ui-core.js` (boot optics init + re-apply on context switch), `js/state.js` (`theme.defaultOptics` consumed), `tests/check-persistence.{js,ps1}` (per-game default applied; explicit override wins; legacy migration), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-T1.
**WU-T3 ✅ DONE (commit 62c1396, r58, Suite 127) · Per-game identity strings + native save header** — *A10 GT-3 (cheap immersion)*
- **Does:** Surfaces `GAME_DEFS[ctx].theme.pipBoyModel` + `bootFlavor` in the boot sequence and `saveLabel` as the save-manager header — FO3 "VAULT-TEC ARCHIVE — CAPITAL WASTELAND", FNV "MOJAVE / LUCKY 38 TELEMETRY". Text only, HOUSE_STANDARD voice. Gives the save manager its native identity without layout work.
- **Files:** `js/state.js` (theme strings), `js/ui-audio.js` (`runBootSequence` reads `bootFlavor`/`pipBoyModel`), `js/ui-saves.js`/`js/ui-account.js` (`renderSavesList` header reads `saveLabel`), `tests/check-persistence.{js,ps1}` (presence guards), `sw.js`, docs. · **Tags:** `[CACHE][TEST]` · **Tier:** BUILD · **Deps:** WU-T1 ; **coordinate with WU-F6 (Cold-Start Boot)** + WU-B10 (same `runBootSequence`/`ui-audio.js`).
### PHASE G — AI Features (B2 RESIDUE only; gated on App Check enablement) — SHRUNK by B3
After B3 pulled VATS/TRADE/THREAT/CONSULT/BIO-SCAN/LOOT into Phase N, Phase G holds **only genuinely-generative output the data cannot compute**, plus the **optional** AI-enrichment layers for the natives.
- **PRECONDITION WU-G0 · App Check enablement** — App Check is currently DISABLED (placeholder site key). Enabling it is a **precondition for any `[DATA]` AI feature** that hits Firestore/Gemini through the project. Real reCAPTCHA/App Check site key + `firestore.rules` enforcement + kill-switch. **Tier:** SPEC · **Tags:** `[AUTH][DATA][NET]`. **Now gates ONLY the units below — not the Phase-N natives.**
- **Generative residue (stay AI, SPEC-FIRST, mock Gemini in the gate):** INTERCEPT distress/numbers signals (AI-H; static-library core is a non-AI BUILD-NOW), RADIO banter (AI-A), AUDIO-LOG narration/TTS (AI-B), SCAN AREA encounter gen (AI-I), COMPANION MEMORY (AI-K, heaviest — Firestore memory + privacy note), OVERSEER directive (AI-L), HACK-FAIL taunt AI variant (AI-C; static taunt lib is non-AI BUILD-NOW), SHARE-TARGET → VISUAL UPLOAD vision parse (AI-D), and open-ended COMM-LINK roleplay (the existing AI core, unchanged).
- **Optional native-enrichment layers (SPEC-FIRST; never block the native feature):** **WU-G-TRADEBANTER** — AI haggle flavor + bounded price delta on top of WU-N2 (flag `aiTradeBanter`, fail-open, offline = no banter; transaction math stays deterministic). **WU-G-BIOPHRASE** — AI bedside-manner phrasing on top of WU-N5 (flag `aiBioScan`, core works offline).
- **SKIP (confirmed):** AI-E AI-routed console commands (redundant; highest injection surface).
- **Tier:** DEFER/SPEC; **Deps:** WU-G0 + all of Phase B (security hardening precedes new AI I/O); the two enrichment layers also dep their native (N2 / N5).
### PHASE R — Test-runner rename (isolated, LATE — immediately before close-out)
**WU-REN ✅ DONE (commit 360c1a6, Suite 128, no cache bump) · Rename test runners to RobCo theme** — *owner-approved; slotted into 2.7.0 as its own clean commit*
- **Does:** Renames `tests/check-persistence.js` → `tests/robco-diagnostics.js` and `tests/check-persistence.ps1` → `tests/robco-diagnostics.ps1` (owner picked "robco-diagnostics" — echoes the boot sequence's "HARDWARE DIAGNOSTICS [OK]"). **Wide mechanical rename — every reference updated in lockstep.** Use **`git mv`** to preserve history.
- **Reference sweep (all must move together):** `scripts/gate.js` (runner invocations + the parity check that pairs the two), `package.json` scripts (test/gate), both git hooks `scripts/pre-commit` + `scripts/pre-push` (+ the installed `.git/hooks` copies + the hook installer `scripts/install-hooks*`), `.github/workflows/ci.yml` (+ `release.yml`/`deploy.yml` if they reference the runners), the runners' own cross-references / printed names if any, and docs `RULES.md`, `CLAUDE.md`, `ARCHITECTURE.md`, `README.md` (every "check-persistence" mention, incl. Protocol 2a/15 runner-pair references).
- **Escape-ratchet guard (Protocol 36b):** after the rename, **grep that ZERO `check-persistence` references remain anywhere** in the repo (code, scripts, hooks, CI, docs) — make that a build/CI assertion so a stale reference fails the gate. Suite 50/56/meta-parity guards that reference the runner filenames update to the new names in the same commit.
- **Files:** `tests/robco-diagnostics.js` + `tests/robco-diagnostics.ps1` (renamed), `scripts/gate.js`, `package.json`, `scripts/pre-commit`, `scripts/pre-push`, `scripts/install-hooks*`, `.git/hooks/*` (installed copies), `.github/workflows/ci.yml` (+ release/deploy if referenced), `RULES.md`, `CLAUDE.md`, `ARCHITECTURE.md`, `README.md`, `repomix.config.json` (if it names the runners — Protocol 37).
- **Tags:** `[TEST]` (harness/config; **likely NO `[CACHE]` bump — no served file changes**, verify the staged set against the served/precached list per Protocol 1). · **Tier:** BUILD (mechanical) · **Deps:** **ALL prior test-content units** (lands after every unit that adds/edits tests, so the rename is a clean isolated diff) · **Serial — singleton-heavy:** touches both runners, gate, hooks, CI, docs (guaranteed singletons) → **nothing else in flight when it lands.** · **MUST run the FULL gate** under the new names (both runners + parity + CI-equivalent boot-smoke/render-check) to prove nothing broke — even without a cache bump.
### PHASE H — Version close-out
**WU-VER · End-of-chain version bump → 2.7.0 + comprehensive README refresh** *(final unit, owner-confirmed)*
- **Does:** Sets `APP_VERSION='2.7.0'`, final `CACHE_NAME='robco-terminal-v2.7.0-r1'`, consolidates all Phase-A–N changelog entries into one plain-English 2.7.0 block (Protocol 21), syncs every doc + final test count (Protocol 2a). MINOR is correct (new user-facing capabilities; no breaking change). **Owner confirms before bump.**
- **`[Unreleased]` COMPLETENESS SWEEP (owner — do BEFORE consolidating into the dated block):** the `[Unreleased]` block must reflect **every** shipped change in this build before it freezes into the 2.7.0 release block. Walk the full `git log` of `dev` for the cycle and confirm each user-visible change has a clear, plain-English entry in the right category (Protocol 21), with nothing scattered, duplicated, or missing — then consolidate. Owner wants `[Unreleased]` *complete* by end of queue, not reconstructed at release. Concretely: (1) `git log origin/main..dev --oneline` → map every commit to a changelog entry (or a deliberate "internal, no entry" call); (2) confirm the six native macro tools (VATS/THREAT/TRADE/CONSULT/BIO-SCAN/LOOT), all Phase-A–N features, and every fix are each represented in user-facing wording; (3) dedupe per-feature lines that a single summary line now covers; (4) only then collapse `[Unreleased]` → the dated `2.7.0` block (preserving earliest-first order within each category, Protocol 43) and open a fresh empty `[Unreleased]`.
- **README REFRESH sub-task (owner — holistic, NOT just a count bump):** rewrite `README.md` to accurately describe the **current** app at final shipped state (done at release so it reflects reality, per Protocol 39):
- **Both games:** FNV + FO3 (per-game registries/DBs, `GAME_DEFS`, per-game theming/optics).
- **Feature set:** the AI companion / Tri-Node JSON contract; trackers (collectibles, Lincoln memorabilia, traits, skill books, magazines); the Craft menu; the native deterministic features (VATS, TRADE, THREAT, CONSULT, BIO-SCAN, LOOT); the Phase-F capabilities (Wake Lock, Vibration, Web Share, Badge, Pip-Boy Radio, degraded/cold-start boot, Overseer's Log, High-Lumen Optics, TERMLINK Console); cloud saves + auth + App Check posture; offline PWA; remote kill-switch; the gate + CI.
- **Architecture:** the split `ui-*.js` (audio/render/saves/account/core), the `GAME_DEFS` per-game system + `_activeDef()`, the boot loader/manifest, script load order.
- **Project state/version:** 2.7.0, current cache rev, current test count + suite count.
- **Files:** `js/state.js` (APP_VERSION), `sw.js` (CACHE_NAME), `manifest.json` (version-less name — verify no version leaked), `index.html` (`<title>` 2.6.0→2.7.0), `CHANGELOG.md`, `ARCHITECTURE.md`, **`README.md` (full holistic rewrite, not just counts)**, `RULES.md`/`CLAUDE.md` (final counts). · **Tags:** `[CACHE]` · **Deps:** ALL prior units (refresh reflects the final shipped state).
---
## 3.0 ACTIVE QUEUE — CURRENT STATUS (reorg 2026-06-29, after WU-B4)
> Live build tracker. Reconciled against the actual `dev` tree + git history (not just commit messages — several units shipped under r-series / differently-worded commits and were verified by their signature suites/features). The topological map in §3 below is the unchanging reference; THIS block is the current state.
### ✅ DONE — shipped & live on `dev`
**Out-of-band hardening (pre/inter-phase):**
- Save-load reload clobber fix (Suite 95) · `tests/test.html` runtime-mirror refresh + auto-sync guards (Suite 96, **Protocol 40**) · CHANGELOG category-dedup guard (Suite 97) · planning-folder move to gitignored `planning/` + root cleanliness (Suite 98, **Protocol 41**) · **Protocol 42** (fix flaws found during testing) · **Protocol 43** (dev-branch workflow / release gating) + Cloudflare staging build · **Environment-aware changelog-viewer rule** (Protocol 21 update; impl folded into WU-C11).
**Phase A — standards/infra (COMPLETE):** WU-A2 ✅ · WU-A4 ✅ (Protocol 38 + Suite 89) · WU-A5 ✅ (boot manifest) · WU-A1 ✅ (tracker-toggle button refactor) · WU-A3 ✅.
**Phase B — security/correctness (5 of 11):** WU-B11 ✅ (FO3 autocomplete → registry-core.js, Suite 93) · WU-B1 ✅ (innerHTML += fix, Suite 23.1b) · WU-B2 ✅ (escape untrusted → innerHTML, GH-2 Suite 21) · WU-B3 ✅ (comm-config cache, GH-3 Suite 23) · WU-B4 ✅ (sanitize completeness, Suite 44.13).
**Phase C — a11y/mobile/UI (11 of 13):** WU-C1 ✅ (reduced-motion) · WU-C2 ✅ (:focus-visible) · WU-C3 ✅ (tab ARIA + contrast) · WU-C4 ✅ (sysModal focus-trap, Suite 94) · WU-C5 ✅ (button aria-labels) · WU-C6 ✅ (chatDisplay live region) · WU-C7 ✅ (config label-wrap) · WU-C8 ✅ (map cell truncation) · WU-C9 ✅ (`[?]` badge nowrap) · WU-C10 ✅ (inventory row) · WU-C12 ✅ (macro-button nowrap, Suite 92).
### ⏳ REMAINING — in dependency order (next-up first)
**Phase B tail (finish security/correctness first — gate-ratchet before new features):**
1. **WU-B5** ◄ B4 ✅ — **NEXT** · cloud pull round-trip behavioral test + cloud-sync-eval protocol + `setDoc`-merge guard. *(no served change likely → maybe no cache bump)*
2. ~~**WU-B6** — behavioral test-gap closure~~ ✅ **DONE** (commit on `dev`). Net-new behavioral: TS-GAP-8 (isFeatureEnabled fail-open, 48.12), TS-GAP-1 (loadRemoteConfig fail-open, 48.13), TS-GAP-2 (restoreRollingBackup end-to-end, 51.57). Reconciled: TS-GAP-3 (checksum_mismatch, 51.23) + TS-GAP-4 (export round-trip, 51.24/25) already behavioral; TS-GAP-5 (dedup, 46.9) + TS-GAP-9 (re-anon, 45.x) already structural (only feasible form for async-Firebase in a sync runner). TS-GAP-7 stays SPEC. +3 tests, no cache bump.
3. **WU-B9** — route cloud.js `state.gameContext` through a state.js accessor (Protocol 23 boundary). *independent*
4. **WU-B7** — dead-code purge + dup consolidation (`showDeltaGhost` etc.); **heads the `js/ui-audio.js` serial cluster** →
5. **WU-B10** ◄ B7 — boot-drone autoplay-timing live bug (ui-audio cluster; leads into F2/F5/F6 + T3).
6. **WU-B8** ◄ A1 ✅ — shared `renderSkillBooks`/`renderMagazines` helper. *(SPEC; can land any time after A1)*
**Phase C tail (finish a11y/UI):**
7. **WU-C11** ◄ A2 ✅ — changelog viewer fix + glow-up. **(a) env-aware `[Unreleased]` gate (prod hide / dev show) + `cf-staging-build.mjs` marker + (b) chronological/source-order rendering — ✅ SHIPPED (pull-forward, Suite 62.3–62.6).** **REMAINING (c):** version dropdown + collapsible category `<details>` + diegetic framing + ~700px reading column (the visual glow-up).
8. **WU-C13** ◄ C4 ✅ — SAVE MENU "?" help affordance (reuses `_openSysModal`). *(SPEC; ships near C11)*
**Phase D — data/canon (parallel; D4a/b/c feed Phase N):**
9. WU-D1 · 10. WU-D2 · 11. WU-D4a (VATS ranged coeff) · 12. WU-D4b (barter coeff) · 13. WU-D4c (ammo-per-attack).
**Phase N — native deterministic ✅ COMPLETE (all 6 shipped, Suites 105-110):**
14. WU-N1 (VATS) ✅ · 15. WU-N4 (CONSULT) ✅ · 16. WU-N3 (THREAT) ✅ · 17. WU-N5 (BIO-SCAN) ✅ · 18. WU-N2 (TRADE) ✅ · 19. WU-N6 (LOOT) ✅ — Suite 110, cache r32, _lootAdd pure core + renderLoot/renderLootList/doLoot, getTradeCatalog reuse, confirm-gated additive, [LOOT]/[LT] router, AI path retired. **Next phase: E → F → T → WU-REN → WU-VER.**
**Phase E — voice/templates (parallel):**
20. **WU-E1 ✅ DONE** (2026-06-30, commit 2a3c2db, cache r34, Suite 111 — diegetic terminology pass: UPLINK auth framing, RETRIEVING ARCHIVES… cloud states, DIRECTOR-not-AI narrative errors, ALL-CAPS placeholders + empty-states, manifest game-agnostic, VISUAL UPLOAD, FIRMWARE update modal). · 21. WU-E2 (templates-not-deleted override). · 21b. **WU-E3** (FEATURES/help command-reference refresh — owner 2026-06-30; reconciles COMMAND_REGISTRY ↔ NATIVE_COMMAND_ROUTER ↔ [FEATURES] modal; dep Phase N ✅ + VATS retirement).
**Owner-queued follow-ups (2026-06-30 — dispatched separately):**
- **WU-N4b ✅ DONE — option C (BOTH)** — owner upgraded A→C. **Part 1** (commit bc3a2f1, r38): full-width [CONSULT] macro button → macroCommand('[CONSULT]') → native router → renderConsult (0 AI). **Part 2** (commit e6e5d74, r39, Suite 108 13→18): persistent **DATABANK panel** on the DATA tab (#databankPanel, inline search) — refactored renderConsult into a SHARED core (_consultSearch + _consultRenderHTML, Protocol 22) consumed by both the modal (renderConsult) and the panel (renderDatabankPanel via loadUI); read-only, offline, 0 AI both paths. Pairs with WU-E3 (surface both affordances in the FEATURES menu refresh). [INCIDENT: a concurrent in-folder session hard-reset the worktree mid-build, wiping uncommitted Part-2 once; re-applied + pushed.]
- **WU-C15 ✅ DONE** (2026-06-30, commit b429f03, cache r37, Suite 62 19→22) — changelog viewer consistent locked width + default-open-newest + EXPAND/COLLAPSE-ALL toggle (owner options 2+3). Locked width via explicit `width: min(740px,94vw)` on `.modal-box.changelog-wide`; env-aware gate + source-order preserved (Suite 62.1-18 still pass).
- **Barter vendor-switch fix ✅ DONE** (2026-06-30, commit 3464bb4, cache r35, Suite 106.19/106.20 — Protocol 42).
- **VATS-still-AI retirement ✅ DONE** (2026-06-30, commit 67bade7, cache r36, Suite 105.19–105.21 — Protocol 42; [VATS]/[VATS SIM]/[VS] now native via NATIVE_COMMAND_ROUTER, AI directive retired).
**Phase F — native capabilities (born-compliant on Phase-A ✅ + Phase-C a11y ✅):**
- parallel-isolated: 22. WU-F1 (Wake Lock) · 23. WU-F3 (Web Share, +manifest) · 24. WU-F4 (Badge, +manifest; sequence vs F3 on manifest) · 25. WU-F7 (Overseer's Log) · 26. WU-F8 (High-Lumen Optics).
- ui-audio serial (after B7→B10): 27. WU-F2 (Haptic) · 28. WU-F5 (Radio) · 29. WU-F6 (Cold-Start/Degraded Boot).
- api.js (after Phase-B api.js + N2–N6): 30. WU-F9 (TERMLINK Console).
**Phase T — per-game theming (rides agnostic seam ◄ A4 ✅; serialize T1→T2→T3):**
31. WU-T1 (THEMES + GAME_DEFS.theme + Suite 90) · 32. WU-T2 (per-game default optics) · 33. WU-T3 (identity strings + save header; coordinates with B10/F6 on `runBootSequence`).
**Phase R — runner rename (penultimate, isolated, after ALL test-content units):**
34. WU-REN (`check-persistence.{js,ps1}` → `robco-diagnostics.{js,ps1}`).
**Phase H — close-out (last):**
35. WU-VER (APP_VERSION → 2.7.0, final cache rev, doc + count consolidation onto the renamed runners).
**Deferred to Round 2 (NOT in this queue):** Phase G (WU-G0 App Check + AI residue + TRADEBANTER/BIOPHRASE) · B1 SPEC-FIRST capabilities · TS-GAP-7 VM test · WU-D3 (low-pri typos) · WU-D4d (per-vendor stock).
---
## 3. DEPENDENCY-ORDERED SEQUENCE (topological)
```
PHASE A (standards/infra — serialize internally A2→A4→A1→A3)
WU-A2 ──► WU-A4 (Protocol 38 + Suite 89 — agnostic refactor) ──► WU-A5 (boot manifest, SPEC-FIRST)
WU-A2 ─┬─► WU-A1 ──► WU-A3
│ └─► WU-B8 (shared render helper, after A1)
(WU-A4 lands right after the standards so Suite 89 guards EVERY later unit; WU-A5 after A4)
PHASE B (security/correctness — mostly parallel; B4→B5)
WU-B1, WU-B2, WU-B3, WU-B6, WU-B9 (independent — parallelizable)
WU-B4 ──► WU-B5
WU-B7 (dead-code) → WU-B10 (boot-drone) ◄ ui-audio.js cluster, lead into F2/F5/F6 + T3
WU-B11 (FO3 autocomplete) ◄ WU-A5 if shared registry-core module (boot manifest); else independent
PHASE C (a11y/mobile — parallel; A2 gate on C3/C7)
WU-C1, WU-C2, WU-C4, WU-C6 (independent)
WU-C3 ◄ WU-A2 ; WU-C5 ◄ WU-A1 ; WU-C7 ◄ WU-A1/A2
WU-C8 → WU-C9 ◄ WU-A2 (both live map bugs; same renderWorldMap/.map-cell surface — sequence together)
WU-C10 (inventory row) ◄ WU-A1/A2 ; converges shared tag/label class with C7+C9 (land after them)
WU-C11 (changelog viewer fix + glow-up) ◄ WU-A2 (collapsible/UI-consistency pattern); independent
WU-C12 (macro buttons nowrap) ◄ WU-A2 ; same nowrap family as C7/C9/C10 (extend Suite 92)
PHASE D (data — fully parallel)
WU-D1, WU-D2 (independent) ; WU-D4a/b/c (B3 coefficients — feed Phase N)
[DEFERRED: WU-D3 low-pri typos ; WU-D4d per-vendor VENDOR_STOCK]
PHASE E (voice/templates — parallel)
WU-E1, WU-E2 (independent)
PHASE N (native deterministic — after Phase A; offline, NOT App-Check-gated)
WU-N1 (VATS) ◄ WU-D4a for ranged (melee/AP-strike core ships now)
WU-N4 (CONSULT) ; WU-N3 (THREAT) ◄ WU-D4c ; WU-N5 (BIO-SCAN) ; WU-N2 (TRADE) ◄ WU-D4b ; WU-N6 (LOOT)
(each ◄ WU-A1/A2 ; N2–N6 serialize on the api.js macro-router after Phase-B api.js edits)
PHASE F (native capabilities — after Phase A standards + Phase C a11y; PULLED IN by owner)
parallel-isolated code bodies: WU-F1 (Wake Lock) ; WU-F3 (Web Share, +manifest) ;
WU-F4 (Badge, +manifest) ; WU-F7 (Overseer's Log) ; WU-F8 (High-Lumen Optics)
serial-backbone (shared private files):
WU-F2 (Haptic) , WU-F5 (Radio) , WU-F6 (Cold-Start Boot) ◄ all touch js/ui-audio.js → serialize among themselves
WU-F9 (TERMLINK Console) ◄ touches js/api.js NATIVE_COMMAND_ROUTER → after B1/B3/B7 + N2–N6 api.js edits
(each ◄ WU-A1/A2 UI standards + WU-C1/C2 a11y so it's born compliant)
PHASE T (per-game theming — rides the agnostic seam; after WU-A4)
WU-T1 (THEMES table + GAME_DEFS.theme + Suite 90) ◄ WU-A4
──► WU-T2 (per-game default optics) ──► WU-T3 (identity strings + save header)
(T1 touches ui-audio.js/ui-saves.js/state.js ; T3 coordinates with WU-F6 + WU-B10 on runBootSequence)
PHASE R (test-runner rename — isolated, serial, after ALL test-content units)
WU-REN ◄ every test-touching unit (clean isolated rename; runs FULL gate; likely no cache bump)
PHASE H
WU-VER (after everything, incl. WU-REN)
── DEFERRED to Round 2 (NOT in this sequence) ──
PHASE G WU-G0 App Check + AI generative residue + WU-G-TRADEBANTER/BIOPHRASE
B1 SPEC-FIRST capabilities ; TS-GAP-7 VM test ; WU-D4d ; WU-D3
```
**Critical path (THIS BUILD):** WU-A2 → WU-A4 (Protocol 38 / Suite 89) → WU-A1 → (WU-A3 / WU-A5 / WU-B8 / WU-C5 / WU-C7 / WU-C10 / WU-N* / WU-F* / WU-T1→T2→T3) → … → **WU-REN (isolated rename) → WU-VER**.
**Phase T placement:** WU-T1 rides the agnostic seam (◄ WU-A4); T1→T2→T3 serialize (each builds on T1's `THEMES`/`GAME_DEFS.theme`). T3 shares `runBootSequence`/`ui-audio.js` with WU-F6 + WU-B10 → coordinate the boot/audio edits.
**ui-audio.js cluster:** WU-B7 (dead-code) → WU-B10 (boot-drone) → WU-F2/F5/F6 (audio capabilities) → WU-T3 (boot identity strings) — serialize on `js/ui-audio.js`.
**Tail ordering:** WU-REN is the **penultimate** unit — it lands after every test-content unit so the runner rename is a clean isolated diff, then WU-VER closes out. WU-VER's final test-count sync targets the **renamed** `tests/robco-diagnostics.{js,ps1}`.
**Foundation-first rationale:** the tracker-button refactor (A1) re-owns the 5 render fns, so every unit that touches a tracker/faction/inventory button (B8, C5, C7, N2–N6) must rebase on it. Suite 88 (A2) must exist before A1 appends GATE-UI-3. Every Phase-F capability rebases on the Phase-A UI standards + Phase-C a11y (reduced-motion WU-C1, focus-visible WU-C2) so it's born compliant.
**B3 placement:** Phase N (natives) is the headline new functionality — offline, not App-Check-gated. The native cores (melee VATS, barter math, TTK, lookups) don't block on WU-D4 — only their flagged coefficients (ranged accuracy / barter / ammo-burn) do, and those (D4a/b/c) are in this build.
**Phase F placement (owner pull-in):** the 9 capabilities follow Phases A+C. **Five have isolated code bodies** (F1 Wake Lock, F3 Web Share, F4 Badge, F7 Overseer's Log, F8 High-Lumen Optics) — parallel-developable. **Three share `js/ui-audio.js`** (F2 Haptic, F5 Radio, F6 Cold-Start Boot — Protocol 7 audio discipline) → serialize among themselves. **One shares `js/api.js`** (F9 TERMLINK Console extends `NATIVE_COMMAND_ROUTER`) → lands after the Phase-B/Phase-N api.js edits. F3/F4 also touch `manifest.json` (a shared singleton — sequence them, not concurrent).
**Round-2 note:** Phase G (AI residue + App Check) and the SPEC-FIRST capabilities are removed from the active sequence per the resolved decisions — see §5A.
---
## 4. SHARED-SINGLETON COORDINATION
These resources are touched by many units; uncoordinated edits collide. **Rule: one unit owns each edit window; serialize, never parallelize, writes to a singleton.**
| Singleton | Risk | Coordination rule |
|---|---|---|
| **`CACHE_NAME` (`sw.js`)** | Every served-file unit bumps `-rN`; parallel units race the rev. Phase-F units adding new assets also edit the `sw.js` **ASSETS** precache list. | Per Protocol 19 (batch before push) — Stage D lands units **sequentially**, each bumping `-rN` by exactly +1 in its own commit. The pre-commit monotonic guard (same-APP_VERSION) enforces it. New assets are added to the ASSETS array in the same commit. WU-VER does the final APP_VERSION change (rev resets to r1). |
| **Test count + both runners (`check-persistence.{js,ps1}` → `robco-diagnostics.{js,ps1}` after WU-REN)** | Every `[TEST]` unit changes the total; Protocol 15 parity + 2a count-sync. **WU-REN renames both runners + every reference** (gate/hooks/CI/docs) | Each unit updates BOTH runners + ALL doc count locations **in its own commit** (Protocol 2a). Never defer a count update. The `Select-String "\d+[- ]tests?"` sweep runs before each commit. Frozen released-version CHANGELOG entries stay untouched. **WU-REN is the single isolated rename, penultimate (before WU-VER), serial — nothing else in flight; full gate + zero-`check-persistence`-references guard.** Earlier units reference the `check-persistence` names (correct at their time); WU-VER's final sync targets the renamed files. |
| **`state.js`** | **WU-A4** (`GAME_DEFS.*.seedInventory`), WU-B9 (accessor), **WU-N1** (`GAME_DEFS.*.vats.regions` + `.combatSkills`), WU-VER (APP_VERSION); future fields | A4 adds per-game data fields to `GAME_DEFS` (additive — no collision with B9/VER's distinct regions); N1 adds the `vats` block. Sequence A4 → N1 (N1 reads the agnostic shape A4 establishes). Any new state field follows Protocol 4 + cloud-sync-determination (WU-B5). |
| **`api.js`** | **WU-A4** (~12-site `==='FO3'?…` coercion refactor + crossroads faction arrays GA-4), WU-B1 (innerHTML), WU-B3 (hot-path reads), WU-B7 (dead-code), GA-5 AI tracker directives, WU-N2/N3/N4/N6 (drop `[TRADE]`/`[THREAT]`/`[LOOT]` AI macros; add `[CONSULT]` native router), **WU-F9** (extend `NATIVE_COMMAND_ROUTER`) | The **api.js chain is the tightest serial cluster**: A4 (agnostic refactor — broadest touch) → B1/B3/B7 → GA-5 directives → N2–N6 → F9. Land A4 first so the coercion/faction-array sites are agnostic before the feature edits stack on them; **same owner reviews** to avoid line-drift. |
| **`js/ui-audio.js`** | WU-B7 (dead-code `showDeltaGhost`), **WU-B10** (boot-drone timing), **WU-F2** (Haptic), **WU-F5** (Radio synth), **WU-F6** (Cold-Start Boot `runBootSequence`), **WU-T1** (`changeOpticsColor` → `THEMES` table), **WU-T3** (boot identity strings in `runBootSequence`) | The audio module is a tight serial cluster → **serialize: B7 → B10 → F2/F5/F6 → T1 (optics) → T3 (boot strings)** (Protocol 7 audio discipline: `ensureAudioCtx`, `AudioSettings` guards, mute keyMap). B10/F6/T3 all touch the boot path — same owner reviews `runBootSequence`. |
| **`js/state.js` — `GAME_DEFS` / `THEMES`** | WU-A4 (`seedInventory`), **WU-T1** (`GAME_DEFS.*.theme` + `THEMES` const), **WU-T3** (theme strings), WU-N1 (`vats`), WU-B9, WU-VER | All additive `GAME_DEFS`/const additions — sequence A4 → T1 (theme shape) → N1/T3 (consume it). `THEMES` is read-only (Protocol 23). |
| **`manifest.json`** | **WU-F3** (Web Share affordance), **WU-F4** (Badging), WU-VER (version-less name verify) | F3 + F4 both edit the manifest → **sequence, not concurrent.** Keep `name`/`short_name` version-less (Protocol 2). |
| **`index.html`** | WU-A2 (data-sub-id), WU-C-series (ARIA), WU-E (templates), WU-N (VATS modal inputs, TRADE/THREAT/BIO-SCAN/LOOT panels; retire AI TRADE modal), WU-F (capability panels/toggles: Radio in Audio Systems, Overseer's Log panel, TERMLINK surface, etc.) | Sequence: structural (A2) → a11y attrs (C) → templates (E) → native feature panels (N) → capability panels (F). Each unit reads HEAD before editing (line numbers drift). |
| **`css/terminal.css`** | A1, C1, C2, C3, A2(DENS-3), C7, N1/N2/N3/N5/N6, **F6 (boot), F7 (log panel), F8 (High-Lumen/`prefers-contrast`)** | All additive blocks; land in dependency order so `.tracker-toggle` (A1) + reduced-motion (C1) exist before C5/C7/N/F units reference them. |
| **Docs (`RULES.md`/`CLAUDE.md`)** | **WU-A4 (Protocol 38)**, WU-A2 (Protocols UI-1..5), WU-B5 (cloud-sync + setDoc protocols), every unit (Protocol 2/2a) | New protocol text lands in its carrier unit's commit (kept identical across both files); WU-VER does the final consolidation + plain-English changelog (Protocol 21). |
| **Docs (`RULES.md`/`CLAUDE.md`/`ARCHITECTURE.md`/`README.md`/`CHANGELOG.md`)** | Every unit per Protocol 2/2a | Per-unit doc updates ride that unit's commit; WU-VER does the final consolidation + plain-English changelog rewrite (Protocol 21). |
---
## 5. DECISIONS — RESOLVED ✅ (owner accepted ALL 10 per recommendation, 2026-06-29)
Every open decision is closed; the resolutions below are now binding scope for the backlog.
1. **`ticksToGameTime()` / `gameTimeToTicks()` (QA-DEAD-4):** ✅ **PURGE `gameTimeToTicks`** (wrong comment, dead). **Purge `ticksToGameTime` too** unless a concrete reserved use exists at build time — else remove both. *(Folded into WU-B7.)*
2. **Density tiers (UC-DENS-2):** ✅ **Option B (document-only)** — formalize 12px-legacy vs 11px-tracker in Protocol UI-2; **no migration.** *(Folded into WU-A2.)*
3. **`migrateState()` version param (CC-RT-2):** ✅ **Leave unconditional** (safe for all current migrations); revisit only when the first version-gated migration is needed. *(No work-unit — confirmed as-is.)*
4. **B1 SPEC-FIRST capabilities** (gyro parallax, hacking minigame, share_target receiver, TTS, ambient-light): ✅ **HOLD for a later program.** Not in this build. *(→ DEFERRED.)*
5. **AI residue / App Check (Phase G):** ✅ **Ship the 6 Phase-N natives this chain; DEFER the Phase-G AI generative residue + App Check (WU-G0) to a later program.** *(→ DEFERRED.)*
6. **Final version number:** ✅ **2.7.0** (lean MINOR). *(WU-VER.)*
7. **TS-GAP-7** (autoImportState happy-path VM test): ✅ **DEFER** to a focused test-infra task in a later program. *(→ DEFERRED.)*
8. **TRADE catalog model (B3 §3 gap-a):** ✅ **Ship TRADE against the full items DB as catalog** (vendor `Base_Caps` = purse). **Per-vendor `VENDOR_STOCK` (WU-D4d) DEFERRED** as later enrichment. *(WU-N2 this build; WU-D4d → DEFERRED.)*
9. **VATS ranged hit-% (B3 §3 gap-b):** ✅ **Ship melee/AP-strike + the estimated ranged overlay now; refine ranged when the FNV/FO3 accuracy coefficient is sourced.** **WU-D4a STAYS IN THIS BUILD** (source the coefficient this chain). *(WU-N1 + WU-D4a this build.)*
10. **Optional AI-enrichment layers (WU-G-TRADEBANTER / WU-G-BIOPHRASE):** ✅ **Leave the natives flavor-free this chain; DEFER the optional AI banter/phrasing layers.** *(→ DEFERRED.)*
---
## 5A. BUILD SCOPE — THIS BUILD (→ 2.7.0) vs DEFERRED (Round 2)
Per the resolved decisions, the 42-unit backlog splits into a shippable **THIS BUILD** set and an owner-parked **DEFERRED (Round 2)** set. **Owner update (2026-06-29): Phase F (the 9 BUILD-NOW capabilities) pulled INTO this build.**
### ✅ THIS BUILD — the v2.7.0 program (53 work-units)
| Group | Units | Notes |
|---|---|---|
| **Phase A** — standards/infra | WU-A2, **WU-A4**, **WU-A5**, WU-A1, WU-A3 | incl. Protocols UI-1..5 + Suite 88 (A2); **Protocol 38 + Suite 89 game-agnostic (A4, A9)**; **boot manifest (A5)**. A4 lands right after A2 so Suite 89 guards everything after. (dec. #2 = Option B doc-only) |
| **Phase B** — security/correctness | WU-B1, WU-B2, WU-B3, WU-B4, WU-B5, WU-B6, WU-B7, WU-B8, WU-B9, **WU-B10, WU-B11** | WU-B7 carries the dec.#1 dead-code purge; WU-B5 carries the cloud-sync protocol + setDoc-merge guard; **WU-B10 = live boot-drone bug; WU-B11 = live FO3 autocomplete bug** (`registrySearch` missing in reg_fo3 → extract to shared registry-core); dec.#3 `migrateState` left as-is (no unit) |
| **Phase C** — a11y/mobile/UI | WU-C1, WU-C2, WU-C3, WU-C4, WU-C5, WU-C6, WU-C7, **WU-C8, WU-C9, WU-C10, WU-C11, WU-C12, WU-C13, WU-C14** | C7 = live config label-wrap (systematic); C8/C9 = live map bugs; **C10 = live inventory-row layout**; **C11 = changelog viewer fix + glow-up**; **C12 = live macro-button device-wrap** (`[THREAT]`/`[VATS]`/`[TRADE]`/`[LOOT]` nowrap + width); **C14 = live "COMPLETE RNG" label crushed to vertical on desktop**. C7/C9/C10/C12/C14 are the inline-`nowrap`/squeeze-wrap family (Suite 92). |
| **Phase D** — data/canon | WU-D1, WU-D2, **WU-D4a/b/c**, **WU-D5** | WU-D4a (VATS ranged coeff, dec.#9), D4b (barter coeff), D4c (ammo-per-attack). **WU-D5 = Anchorage missing-quest gap + 'Vault 92 South' location fix** (surfaced during WU-D1 fallout.wiki verify; FO3 quests 62→64 with REAL entries). **WU-D4d (per-vendor stock) DEFERRED (dec.#8).** WU-D3 (low-pri typos) DEFERRED-low. |
| **Phase E** — voice/templates | WU-E1, WU-E2 | WU-E2 = templates-not-deleted override |
| **Phase N** — native deterministic | WU-N1, WU-N2, WU-N3, WU-N4, WU-N5, WU-N6 | the 6 B3 natives; offline, no `[NET]`, not App-Check-gated |
| **Phase F** — native capabilities | WU-F1, WU-F2, WU-F3, WU-F4, WU-F5, WU-F6, WU-F7, WU-F8, WU-F9 | **PULLED IN (owner, 2026-06-29).** B1 BUILD-NOW; each born-compliant on Phase-A standards + Phase-C a11y; kill-switch + fallback for any `[NET]`/IO. Backbone-contention notes in §3. |
| **Phase T** — per-game theming | **WU-T1, WU-T2, WU-T3** | A10 BUILD-NOW: `THEMES` table + `GAME_DEFS.theme` (T1, +Suite 90 contrast guard) → per-game default optics (T2: **FNV = robco green `#14fdce`** canon NV default; **FO3 = classic/duller green** — RESOLVED, muted/darker, hex picked at build) → identity strings + native save header (T3). robco green + amber + FO3 green all selectable for any game. Ride the agnostic seam (◄ WU-A4). |
| **Phase R** — runner rename | **WU-REN** | Rename `tests/check-persistence.{js,ps1}` → `tests/robco-diagnostics.{js,ps1}` + every reference (gate/hooks/CI/docs). Isolated, serial, penultimate; full gate; likely no cache bump. |
| **Phase H** — close-out | WU-VER | APP_VERSION → 2.7.0, final cache rev, doc + count consolidation (targets the renamed runners) |
New protocols/gate guards (UI-1..5 + Suite 88; cloud-sync-determination + setDoc-merge guard) are **folded into** their carrier units (A2/A1/B5), not separate units.
**Phase F roster (in-world name → capability → shared-singleton/backbone touchpoints):**
- **WU-F1 Sustained Power Cell** — Wake Lock API · `index.html`, `js/ui-*.js` hook, tests, `sw.js`, docs · `[NET]`-ish (device API; kill-switch + graceful no-op fallback)
- **WU-F2 Haptic Solenoid** — Vibration API · **`js/ui-audio.js`** (paired with feedback path), `index.html`, tests, `sw.js`, docs
- **WU-F3 Eject Holotape** — Web Share (export) · `index.html`, `js/ui-saves.js`/`ui-*.js`, **`manifest.json`** (share affordance), tests, `sw.js`, docs
- **WU-F4 Pending-Directives Tally** — Badging API · `index.html`, `js/ui-*.js`, **`manifest.json`**, tests, `sw.js`, docs
- **WU-F5 Pip-Boy Radio** — WebAudio synth (zero-byte) · **`js/ui-audio.js`**, `index.html` (Audio Systems panel toggle — Protocol 7), tests, `sw.js`, docs
- **WU-F6 Degraded-Tube / Cold-Start Boot** *(scope expanded — owner)* — two boot behaviors: **(a)** the distinct **first-cold-start full POST** sequence (first-boot detection, the full power-on self-test); **(b)** a **rare degraded/glitchy boot variant** that can fire on **ANY boot — including warm reboots** — at a **low random chance per boot, independent of first-boot detection**, so even a returning/account user with warm cache occasionally gets the "messed up" degraded boot. The degraded variant **honors `prefers-reduced-motion`** (WU-C1 guard — the glitch effect is suppressed/toned for reduced-motion users; no flicker/shake then). · **`js/ui-audio.js`** (`runBootSequence` — first-boot path + the random degraded-variant branch), `index.html`, `css/terminal.css` (glitch effect, reduced-motion-gated), `tests/check-persistence.{js,ps1}` (degraded-variant gated on a random roll AND independent of first-boot; reduced-motion suppression guard), `sw.js`, docs · in the `ui-audio.js`/`runBootSequence` serial cluster with WU-B10 + WU-T3.
- **WU-F7 Overseer's Maintenance Log** — session/event log surface · `js/ui-render.js`/`ui-core.js`, `index.html` (panel — Protocol 5), `css`, tests, `sw.js`, docs
- **WU-F8 High-Lumen Optics** — `prefers-contrast` / brightness OPTICS mode · `css/terminal.css`, `js/ui-*.js`, `index.html`, tests, `sw.js`, docs
- **WU-F9 TERMLINK Command Console** — native command surface · **`js/api.js`** (extends `NATIVE_COMMAND_ROUTER`), `index.html`, `css`, tests, `sw.js`, docs
#### Owner-folded (2026-06-30, after WU-F9) — two queued units
- **WU-F10 ✅ DONE (commit a773429, r56) · Consolidate Session Statistics into the OVERSEER'S LOG panel** — *owner 2026-06-30: "shouldn't session statistics be merged into Overseer's Log?"*
- **Audit (current state):** there are **two separate DATA-tab panels** today — **#11 SESSION STATISTICS** (`<details id?>` summary "> SESSION STATISTICS", `index.html:1111`) rendered by **`renderSessionStats()`** (`js/ui-render.js:600`) from **`state.stats`** = `{kills, capsEarned, damageDealt, sessionStart}` (AI-updated deltas, accumulated client-side; the panel also shows a `LOCATION VISITS` count) — and **#11c OVERSEER'S LOG** (`index.html:1124`, `#overseerLogDisplay`) rendered by the WU-F7 device-telemetry log (`bootCount`, live session uptime, `longestSessionMs`, `totalPowerOnMs`, all local-only in `robco_overseer_log`).
- **Does:** fold the **campaign session statistics** into the **OVERSEER'S LOG** panel so it is **one consolidated readout** (gameplay stats + device telemetry together), and **retire/absorb the standalone SESSION STATISTICS panel** (Protocol 22 — extend the existing Overseer renderer + panel; do NOT create a third surface; Protocol 25 — preserve the data, just relocate it). Keep `state.stats` as the source (no schema change expected); the Overseer renderer reads both `state.stats` and the local telemetry.
- **⚠ OWNER SIGN-OFF NEEDED (scope):** confirm **which** statistics merge in — all of `kills` / `capsEarned` / `damageDealt` / `LOCATION VISITS`, and whether `sessionStart`-derived "current campaign session length" also belongs alongside the device-uptime telemetry (two different notions of "session" — campaign vs power-on). Lock the exact field list before building.
- **Constraints:** game-agnostic (Protocol 38 — no game literals; reads `state.stats` generically), diegetic (WU-E1 voice), accessible (UI-1 heading, UI-2 sub-panel persistence if it becomes a sub-panel, Protocol 17). Read-only display; no AI; no cloud-push.
- **Files:** `index.html` (remove/repoint the #11 panel; keep one OVERSEER'S LOG panel), `js/ui-render.js` + `js/ui-core.js` (merge `renderSessionStats` into the Overseer render path; update `loadUI()` wiring + `_updatePanelBadges` + `_isDirty('sessionstats',…)`), `css`, `tests/check-persistence.{js,ps1}` (panel-consolidation guard: SESSION STATISTICS no longer a standalone panel; Overseer readout includes the agreed stats), `sw.js` (Protocol 1), docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (owner scope sign-off) · **Deps:** WU-F7 ✅.
- **WU-F11 ✅ DONE (commit b7e49de, r57) · Native "mark location visited / discovered" control** — *owner 2026-06-30: "there's no good system to say you've visited a location, I need a native way to do that without telling the AI."*
- **Audit (current state):** `state.locationHistory` (`js/state.js:497`) is the discovered-set; the **single-source** `recordLocationVisit(locName)` helper (`js/state.js:576`, dedup + permanent + game-agnostic, no cloud) is the ONLY writer, and today it is only called when the **current location changes** (`onLocationChange` + `autoImportState`, the 4efa9ba fog-of-war fix). The WORLD GRID map detail rows (`js/ui-render.js:1330–1349`) already render a per-location **`[UNKNOWN]` / `[VISITED]` / `[CURRENT]`** tag via `locVisited(loc)`. **There is no manual control** to mark a place visited without moving there (or letting the AI set location).
- **Does:** add a **native UI affordance on the WORLD GRID** — a per-location **"mark visited"** toggle/`<button>` on each map detail row (and/or a row action in the zoomed region view) that calls the existing **`recordLocationVisit(loc)`** (Protocol 22 — reuse the single-source helper) → `saveState()` → `renderWorldMap()`, flipping that row's **`[UNKNOWN]` → `[VISITED]`** immediately and persisting it. **Zero AI** (never routes to the Director), local-only (cloud stays manual).
- **⚠ OWNER SIGN-OFF NEEDED (un-mark?):** `recordLocationVisit` is **permanent** by design (fog-of-war never reverts). Confirm whether the control is **add-only** (matches the current permanent model — recommended) or should also support **un-marking** a location (which would need a new `forgetLocationVisit` helper + a guard that it can't erase the CURRENT location). Default to add-only pending owner word.
- **Constraints:** game-agnostic (Protocol 38 — operates on the location string only), diegetic (WU-E1 voice — e.g. "LOG VISIT" / "MARK CHARTED", literal aria-label per UI-3, never narrative), accessible (`<button>` not `<span onclick>` per UI-5; ≥28px tap target Protocol 17; visible focus). Reuses the existing map render + helper; no new state field.
- **Files:** `js/ui-render.js` (`renderWorldMap` detail-row button + click handler), `js/state.js` (only if un-mark is approved → `forgetLocationVisit`), `index.html`/`css` (button styling, overflow-safe), `tests/check-persistence.{js,ps1}` (control present + routes through `recordLocationVisit` + 0-AI + `[UNKNOWN]`→`[VISITED]` flip behavioral guard), `sw.js` (Protocol 1), docs. · **Tags:** `[CACHE][TEST]` · **Tier:** SPEC (add-only vs un-mark sign-off) · **Deps:** 4efa9ba fog-of-war helper ✅.
### ⏸ DEFERRED — Round 2 (owner-parked for a future program)
- **B1 SPEC-FIRST capabilities** (dec.#4): CRT-tilt/gyro parallax, hacking minigame, `share_target` receiver, TTS audio-log, ambient-light calibration.
- **Phase G — AI generative residue + App Check** (dec.#5): INTERCEPT, RADIO banter, AUDIO-LOG/TTS, SCAN AREA, COMPANION MEMORY, OVERSEER directive, HACK-FAIL taunt (AI variant), SHARE-TARGET vision parse — plus enabling App Check (WU-G0).
- **Optional AI-enrichment layers** (dec.#10): WU-G-TRADEBANTER, WU-G-BIOPHRASE (thin AI flavor on the native TRADE/BIO-SCAN).
- **TS-GAP-7** (dec.#7): the autoImportState happy-path VM-sandbox test-infra unit.
- **WU-D4d** (dec.#8): per-vendor `VENDOR_STOCK` data set.
- **WU-D3:** low-priority typos / additive data gaps (no correctness impact).
- **GT-4 (A10): per-game framing/accent CSS** — `data-game` attribute + per-game border/density/accent (FO3 heavy / FNV vegas / FO4 clean). Bigger layout work; 3-breakpoint × N-game render matrix.
- **GT-5 (A10): full game-styled save manager** — per-game restyle of `renderSavesList` (badge per row, framing). Data hooks (saveLabel) land in 2.7.0 via WU-T3; the full visual restyle depends on GT-4 → Round 2.
---
## 6. FO4-readiness (forward consideration only — NOT in this backlog)
The per-game registry/DB split (`reg_nv`/`reg_fo3`, `db_nv`/`db_fo3`, `GAME_DEFS`, `getSkillKeys()`/`_activeDef()`) is already the seam a future FO4 context would plug into. Phase A's UI standardization (UI-1..5, `.tracker-toggle`, shared render helper WU-B8) and Phase B's sanitize/round-trip generality make a third game cheaper to add later. The B3 Phase-N natives reinforce this: VATS/TRADE/THREAT/CONSULT/BIO-SCAN read **per-game `databaseCSVs` + `FALLOUT_REGISTRY` via `lookupItemInDb()`** — so they extend to an FO4 data set with no logic change (data-driven, not game-hard-coded). The A10 theming seam adds another drop-in point: **FO4's theme is Round-3 data** — a `GAME_DEFS.FO4.theme` entry (`green4`/Mark IV/`clean`) + one `THEMES` palette row, ZERO feature-code change (proves the WU-T1 architecture). **No FO4 work is scheduled here** — noted only so Stage D doesn't accidentally hard-code two-game assumptions where a registry-driven loop would do.
---
## 7. BACKLOG SUMMARY (hand-off to Stage D — B3-revised)
- **Work-units:** **THIS BUILD (→2.7.0): 54 units** — **5 Phase A (A2, A4, A5, A1, A3 — A4/A5 added by A9 game-agnosticism)**, **11 Phase B (incl. WU-B10 boot-drone + WU-B11 FO3 autocomplete)**, **13 Phase C (incl. WU-C8/C9 map bugs + WU-C10 inventory-row + WU-C11 changelog viewer + WU-C12 macro-button wrap + WU-C13 save-menu help "?")**, 3 Phase D (D1/D2/D4a–c), 2 Phase E, 6 Phase N, 9 Phase F capabilities (pulled in by owner 2026-06-29), **3 Phase T (WU-T1/T2/T3 per-game theming — A10)**, **1 Phase R (WU-REN runner rename)**, 1 Phase H (WU-VER). **DEFERRED to Round 2:** Phase G AI residue + App Check (WU-G0), WU-G-TRADEBANTER/BIOPHRASE, the 5 B1 SPEC-FIRST capabilities, **GT-4 per-game framing CSS + GT-5 full game-styled save manager (A10)**, TS-GAP-7, WU-D3, WU-D4d. **Round 3:** FO4 theme as data. *(New protocols/gate guards (UI-1..5/Suite 88; Protocol 38/Suite 89; Suite 90 theme-contrast; cloud-sync/setDoc) are folded into carrier units, not counted separately; dec.#3 left `migrateState` as-is with no unit.)*
- **Live bugs folded into this build (found during 2.7.0 build testing):** Canteen-in-FO3 (→ WU-A4/GA-6, FNV-scoped seed + Protocol-13 regression), world-map cell text truncation (→ WU-C8), collectible `[?]` badge stacks when zoomed (→ WU-C9), config GAME:/PLAYSTYLE: label wrap (→ WU-C7, systematic label-wrap closure), **inventory-row layout — oversized USE button + vertical type-tag (→ WU-C10)**, **boot-drone autoplay-timing — fires on first mid-session tap (→ WU-B10)**, **changelog viewer renders the whole markdown incl. `[Unreleased]` wall (→ WU-C11, skip-`[Unreleased]` correctness + regression guard + layout glow-up)**, **FO3 autocomplete dead — `registrySearch` ships only in reg_nv.js, absent in FO3 (→ WU-B11, extract to shared registry-core + FO3-context guard)**, **macro buttons `[THREAT]`/`[VATS]`/`[TRADE]`/`[LOOT]` wrap on devices with larger default fonts (→ WU-C12, nowrap + width)**. WU-C7/C9/C10/C12 are the inline-`nowrap` family closing the whole "wraps when squeezed / on large fonts" class (Suite 92). Plus the FO3 VATS `big_guns` omission already folded into WU-N1.
- **Projected test count:** ~1078 → **~1112** (Suite 89 ~10–12 game-agnosticism guards + Suite 90 ~6–8 theme-contrast/declared guards + the Canteen FNV-scope regression + WU-C8/C9/C10 map+inventory guards + per-game-default-optics/override/migration guards + the WU-B10 boot-drone regression + the WU-C11 changelog `[Unreleased]`-filter + viewer-structure guards + the WU-B11 FO3-context registrySearch guard + the WU-C12 macro-button nowrap guard (Suite 92 extend); other units add their own — synced both runners + all doc locations per Protocol 2a; final total assigned at WU-VER). **WU-REN is count-neutral** (rename only) but re-points every count reference to `tests/robco-diagnostics.{js,ps1}`.
- **B3 reclassification:** **5 features moved out of the AI phase into native Phase N** (VATS, TRADE, THREAT, CONSULT, BIO-SCAN) + 1 hybrid (LOOT) — all **offline, no `[NET]`, not App-Check-gated**. TRADE specifically dropped from `[NET][DATA]` to `[DATA]`-only. **Phase G shrank** to the generative residue (INTERCEPT/RADIO/AUDIO-LOG/SCAN/COMPANION/OVERSEER/HACK-taunt/SHARE-TARGET/roleplay) + 2 thin optional enrichment layers that no longer block anything.
- **Dupe collapses:** 10 cross-doc overlaps reduced ~64 raw findings → the unit set above (tracker-span ×3 docs → WU-A1; ±50 label ×2 → WU-A4 folded into A1/render; reduced-motion ×2 → WU-C1; cloud round-trip ×2 → WU-B5; innerHTML/escape/hot-path/sanitize merges; template override ×2 → WU-E2; persistence ×4 → WU-A2).
- **New protocols codified:** UI-1..5 + Suite 88 (WU-A2/A1); cloud-sync-determination + setDoc-merge guard (WU-B5); Protocol 38 + Suite 89 game-agnostic (WU-A4); **Protocol 39 README Currency (WU-A4, holistic README refresh at WU-VER)**; Suite 90 theme-declared + AA-contrast guard (WU-T1).
- **Owner override honored:** `#updateBanner` + `fo3WarningBanner`/"2.0.0" message → CONVERT-TO-TEMPLATE, never delete (WU-E2 overrides A4/A5 removal items).
- **Born-compliant rule:** every Phase F/N/G feature inherits UI-1..5, Protocol 17, reduced-motion, escaping+sanitize (and kill-switch+fallback for any `[NET]` unit) as baked-in acceptance criteria.
- **Re-hand-off:** this B3-revised plan **re-feeds Stage D** — the parallelization analysis must re-run with the 6 Phase-N units (note: N1/N3/N4/N6 touch `api.js` macro-routing + the native router, and all touch `index.html`/`css`/tests/sw — so they join the serial-backbone contention, not the parallel set).
```
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).