RELEASE
planning/2.8.5/plans/CODE_HEALTH_PLAN.md
sha256 4ba7428b21fcb94f · 40812 bytes ·
original held in the private archive
# CODE + TEST HEALTH PLAN — 2.8.5
**Phase:** 2.8.5 "Code + Test Health" — runs **before** the FO3 Pip-Boy skin, on purpose (the restructure touches
everything; FO3 must be built on the clean codebase, not refactored into it afterward).
**Document status:** PLAN — Protocol 8 stage 1 (Opus diagnoses + plans). A later Sonnet session implements, unit by unit,
after the owner reviews. This doc is gitignored (`planning/`) and commits no code.
**Author snapshot:** `dev` @ `f9f5e80`, APP_VERSION 2.8.0, cache `-r3`. Measured 2026-07-11.
**Accuracy contract:** every number in §1 was **measured on this machine at this HEAD**, not estimated. Where a number could
not be measured cleanly, it says so. This is deliberate — a prior audit asserted a dramatic finding its own tooling had gotten
wrong, and the owner does not want that repeated.
---
## 0. The one-paragraph version
The single biggest, cheapest win is **strand B (the CLAUDE.md / token split)**: ~59k tokens of pure reference history are
loaded into every session, and moving them out is a docs-only change with near-zero code risk — do it first. The hardest,
most dangerous strand is **A (the file restructure)**, because ~470 static tests across both runners read exact file paths and
grep for literals that a split would move — a naive split breaks hundreds of tests at once, and "just update the greps" is how a
gate silently gets weakened. The plan defuses that with a **test-infra unit that lands before any code moves**: logical-bundle
read helpers, so a content-grep keeps asserting the same literal regardless of which physical sub-file it ends up in. Everything
else (test-health depth, perf/a11y/bundle, protocol consolidation) is real but secondary, and some of the accumulated scope
should be cut. The realistic size is **large — 8–12 shippable units over an extended run**, not one pass.
---
## 1. Measurement pass (real numbers, measured — not estimates)
### 1.1 Source file sizes (js/, measured `wc`)
| File | Lines | Bytes | Top-level `function`s | Internal section dividers |
| --- | ---: | ---: | ---: | ---: |
| **js/ui-core.js** | **8082** | **360 KB** | **248** | **0** |
| js/test-console.js | 5211 | 203 KB | 0* | — |
| js/ui-render.js | 4473 | 197 KB | 117 | — |
| js/api.js | 2384 | 108 KB | 40 | — |
| js/state.js | 2025 | 96 KB | 31 | — |
| js/reg_nv.js | 1866 | 89 KB | (data) | — |
| js/ui-audio.js | 1692 | 89 KB | 72 | — |
| js/cloud.js | 1173 | 51 KB | (module) | — |
| js/db_nv.js | 1133 | 60 KB | (data) | — |
| js/reg_fo3.js | 1098 | 47 KB | (data) | — |
| js/ui-saves.js | 1064 | 48 KB | 20 | — |
| js/db_fo3.js | 785 | 39 KB | (data) | — |
| js/ocr.js | 526 | 23 KB | — | — |
| js/ui-account.js | 321 | 14 KB | — | — |
| js/runtime.js | 312 | 14 KB | — | — |
| js/idb.js | 181 | 7 KB | — | — |
| js/registry-core.js | 81 | 3 KB | — | — |
*test-console.js uses `const _x = () => …` / object-method style, not top-level `function` declarations, so the naive counter
reads 0; it is ~5200 lines of real code.
**The headline:** `ui-core.js` is **8082 lines, 248 functions, and zero section dividers** — a single flat grab-bag. It is the
one file where "open it and understand it cleanly" is currently impossible. This is the primary split target.
### 1.2 Non-JS surface
| File | Lines | Bytes | Note |
| --- | ---: | ---: | --- |
| **css/terminal.css** | **13169** | **386 KB** | one file; 153 informal `/* --- lower --- */` sub-markers; **0 `@import`** |
| index.html | 4470 | 215 KB | whole DOM + inline pre-paint + boot manifest + SW reg |
| **CLAUDE.md** | 730 | **296 KB** | see §1.5 — the token line-item |
| ARCHITECTURE.md | 3094 | 306 KB | canonical deep-dive |
| CHANGELOG.md | 1091 | 89 KB | plain-English history (already exists) |
| tests/robco-diagnostics.js | 40083 | 1.93 MB | Node runner |
| tests/robco-diagnostics.ps1 | 26612 | 1.70 MB | PowerShell runner |
| tests/test.html | 1856 | 85 KB | runtime mirror |
### 1.3 Test composition (measured, Node runner)
- **2951 tests, 221 suites.** (CLAUDE.md/brain-dump say 2945/220 — already drifted since they were written. Always re-verify
by running a runner, never by trusting a doc number. This drift is itself evidence for strand B.)
- Suite declarations: **231** (`header(` 232 incl. a helper def; `section(` 4 for the test.html-style path).
- **Behavioral signal:** 346 `vm` sandbox references, **2481 `assert`-family calls**. The suite is **not** mostly static —
there is a large behavioral core (the vm-sandbox pattern).
- **Static signal:** `readFileSync` appears 36 times as named module-level source vars, **plus many ad-hoc per-suite re-reads**
(see §1.4). Static source-grep tests are a large minority, concentrated in the structural/invariant suites (20, 55, 56, 88–96,
113, 158, 220, …).
### 1.4 Refactor-fragility (the coupling crux — measured)
Count of exact file-path references inside the **Node runner** (the PowerShell runner mirrors these):
| Referenced path | Refs in Node runner |
| --- | ---: |
| js/ui-core.js | **123** |
| js/ui-render.js | 70 |
| js/state.js | 66 |
| js/api.js | 61 |
| js/ui-saves.js | 35 |
| js/ui-audio.js | 31 |
| js/cloud.js | 24 |
| js/reg_nv.js | 24 |
| js/ui-account.js | 23 |
| js/db_fo3.js / test-console.js / reg_fo3.js / ocr.js | ~20 each |
| **index.html** | **324** |
| css/terminal.css | 80 |
| sw.js | 54 |
**Read pattern (measured):** a few files are slurped once into a module-level `const xSource = readFile('js/x.js')`
(state/api/cloud/index/registry/db/css/sw at lines 208–1035). **But the big UI files are re-read ad-hoc inside individual suites**
with local names (`uiCoreSrc32`, `uiSrc35`, `renSrc213`, …). So splitting `ui-core.js` does **not** touch one line — it touches
~123 read sites spread across dozens of suites, **and** every content-grep in those suites (`uiCoreSrc.includes('someFn')`)
fails the moment `someFn` moves to a new physical file. This is doubled across both runners.
**This is the single fact that shapes the whole plan.** A naive split is an unreviewable ~400-test breakage, and the tempting
"just repoint the reads and update the greps" is exactly how the gate gets silently weakened. §4/§5/§6 are built around defusing
it *before* any code moves.
**Also found (dead references):** the runner still names `js/ui.js` (5×, mostly the intentional Suite-56 MUST-NOT-EXIST guard),
`js/registry.js` (1×), `js/database.js` (1×) — stale strings to sweep.
### 1.5 CLAUDE.md token weight (measured — the strand-B headline)
CLAUDE.md is auto-loaded **in full, every session**.
- Total: **296 KB ≈ ~80k tokens** (chars ÷ ~3.7).
- **Protocols section (lines 1–694): 78 KB (~21k tokens)** — load-bearing rules; keep inline.
- **"Architecture Quick Reference" tail (line 695+): 218 KB (~59k tokens) — 74% of the file** — reference history, not rules.
- **A single line (725) is 135,951 bytes (~34k tokens)** — the running per-unit build narration.
So **~59k tokens of pure reference are paid on every Dispatch and every build session, whether relevant or not.** That is the
biggest token line-item in the whole workflow, and it is docs-only to move.
### 1.6 Gate timing (measured, this machine, warm)
| Stage | Time | Notes |
| --- | ---: | --- |
| eslint . | 3.6 s | bug-rules only (see §1.7) |
| prettier --check . | 5.3 s | |
| **Node runner** | **1.2 s** | 2951 tests |
| **PowerShell runner** | **15.7 s** | **same 2951 tests — ~13× the Node runner** |
| **gate:fast total (pre-commit)** | **~26.5 s** | (docs claim ~10–15 s — stale) |
| + Chromium boot-smoke + render-check + a11y + test.html | ~43 s | reuse one Chromium |
| **gate full total (pre-push / CI)** | **~69.3 s** | |
**The gate-speed headline:** the **PowerShell runner alone is 15.7 s** — 60% of the fast gate, and ~13× slower than the Node
runner for byte-identical coverage. It is the highest-ROI gate-speed target by a wide margin.
### 1.7 ESLint today (measured)
`eslint.config.mjs` enforces **only bug-catching rules** (`no-undef`, `no-unused-vars` as warn, `no-unreachable`, `use-isnan`,
`no-dupe-keys`, …). It explicitly turns **off** all style (`semi`/`quotes`/`indent`/`comma-dangle` → Prettier owns those) and has
**no readability rules at all** — no `max-lines-per-function`, `complexity`, `max-depth`, `max-params`, or naming rules. The
"tighten ESLint to enforce readability" strand is therefore greenfield, not a tweak. (It also carries a ~380-line manual globals
block — the cost of plain-global-scope; a file split does **not** change globals unless a currently-local function becomes
exposed, so globals are largely refactor-stable.)
### 1.8 Prior audit docs (measured staleness)
`planning/` already holds prior parked-audit outputs — **all dated 2026-06-28 to 07-01, i.e. pre-2.8.0**:
`CODE_QUALITY_AUDIT.md` (771 ln), `PERFORMANCE_AUDIT.md` (404), `ACCESSIBILITY_AUDIT.md` (599), `TEST_STRENGTH_AUDIT.md` (664),
`TOKEN_USAGE_AUDIT.md` (319), `UI_CONSISTENCY_AUDIT.md` (879), `CLOUD_AUDIT.md` (676), `FILE_AUDIT.md` (111).
They audited a codebase **~30% smaller** — before `test-console.js` (5211 ln), `ocr.js`, `runtime.js`, the whole Diagnostic
Shell, feedback animations, and the CHASSIS/Overseer core. **Reuse their structural reasoning as archive-class input; do not
trust their measurements or file-split proposals — those were redone here against the current code.** (ROI note: this means the
folded-in audits are partly pre-drafted; reconcile, don't re-run from zero.)
### 1.9 What could not be measured (stated honestly)
- **Objective code-coverage %** — the app is plain global-scope with no build/instrumentation; standard coverage tooling
(c8/nyc) can't attach without wrapping the load. Coverage is inferred from the suite structure, not measured. Flagged as
NICE-TO-HAVE with a feasibility caveat (§8).
- **Real device frame budgets** — measured perf belongs to strand D on a real 360/412 page via the browser tools; not attempted
in this analysis-only pass (it needs the running app, not source reading).
- **True per-suite static-vs-behavioral split** — 346 vm refs and 2481 asserts bound it, but an exact per-suite classification
needs a parse pass (proposed as the first deliverable of the test-health unit, §5-U6).
---
## 2. Proposed file structure (concrete, by responsibility)
**Governing constraint (does not bend):** the app loads **plain global-scope scripts in a fixed order** (only `cloud.js` is a
module). Every split file joins the **static-tag group** (boot step 4), all of which load after the per-game `GAME_FILES`
manifest and before `window.onload` fires. Nothing reads `state`/globals until `window.onload`, so **intra-group order is
flexible as long as every function is defined before `window.onload` runs.** That is the architecture's saving grace — but the
re-sync checklist (§3) is still mandatory for every new file.
**Naming convention for splits:** a split family shares a stem so the test read-helper can glob it (§4). E.g. `ui-core.js`
splits into `ui-core.js` + `ui-core-*.js`; the logical bundle "ui-core" = `js/ui-core*.js`.
### 2.1 `ui-core.js` (8082 ln) → SPINE split
The grab-bag. Proposed responsibility split (final boundaries refined per-unit by Sonnet against live line numbers):
| New file | Owns | ~ lines |
| --- | --- | ---: |
| **js/ui-core.js** (stays) | `AudioSettings` cache, `loadUI()` master render + `_isDirty`, `updateMath()`, `appendToChat()`, the `window.onload` boot orchestrator + the ~12 boot-phase fns, `_isStagingEnv()`, `expandPanelForCategory()`, `_updatePanelBadges()` — the load-bearing hub every other ui file leans on | ~2000 |
| **js/ui-nav.js** | bezel subsystem nav: `selectSubsystem`, `switchTab`, `_syncBezelNav`, `SHORTCUT_ROUTES`, `#go=` routing, hotkeys, DIR modal | ~800 |
| **js/ui-overseer.js** | Director Uplink: `setOverseerState`, `_scopeShouldAnimate`, scope canvas, composer wiring, `#modePill`, Tool Deck launcher | ~1200 |
| **js/ui-chassis.js** | the Living Core: `_coreRefresh`, `_coreShouldAnimate`, all core behaviors, mini-core mirror, reactor-hum hooks | ~1500 |
| **js/ui-modulebay.js** | Module Bay wiring, phosphor-tube rack, chip grid, schematic view, campaign-config board wiring | ~1200 |
| **js/ui-terminal-cmd.js** | input-mode/composer command layer: `submitCommandInput`, `transmitTerminal`, `QUICK_LOG_PATTERNS`, native stat-edit/quick-log setters, `COMMAND_REGISTRY`, Tool Deck routing | ~1000 |
Justification is **responsibility, not line count**: nav / Overseer / chassis / module-bay / command-line are five genuinely
separable subsystems that today share one file only by accident of growth. Each has its own event-bus subscribers, its own DOM
region, and its own gotchas.
### 2.2 `css/terminal.css` (13169 ln) → HIGH-VALUE, but evaluate the mechanism
Two options, and the plan **recommends option A**:
- **Option A (recommended): stay single-file, add real top-level banner sections + a table-of-contents header.** The file already
has 153 informal sub-markers; it lacks *top-level* sectioning. This captures ~80% of the readability win at **near-zero risk**
(no SW ASSETS change, no `<link>` order, no FOUC/cache-bump surface). Pair with the dead-CSS sweep (strand D).
- **Option B (optional, later): split into multiple `<link>` files** (base/layout, chrome/bezel, subsystems, overseer+composer,
module-bay, themes, mobile, a11y). Cost: each file is a new SW ASSETS entry + a new cache-bump surface, `@import` is banned
(serial-fetch cost) so it must be multiple `<link>` tags in `index.html` in a defined order, and a mis-ordered cascade is a
visual regression. **Verdict: do NOT split terminal.css into files in the spine.** Reassess in strand D only if option A
proves insufficient; the ROI does not justify the boot-surface risk mid-restructure.
### 2.3 `api.js` (2384 ln) → MEDIUM split
Already internally sectioned (7 directive builders + router + import). Extract cleanly:
| New file | Owns |
| --- | --- |
| **js/api.js** (stays) | `transmitMessage()` lifecycle, `_resetTransmitUI`, retry/abort, the network layer |
| **js/ai-directive.js** | `getSystemDirective()` + its 7 `_directive*` section builders (guarded by the Suite-131 golden-master — a strong safety net for this move) |
| **js/ai-import.js** | `autoImportState()`, `_validateTriNode`, `_g`, field mapping |
| **js/native-router.js** | `NATIVE_COMMAND_ROUTER`, `_routeNativeCommand`, regex intercepts |
Load order: all remain in the static-tag group; `api.js` currently loads last of the plain scripts (before `cloud.js`), so the
new siblings slot immediately before it. The golden-master (Protocol 14) makes the directive extraction one of the *safer* big
moves.
### 2.4 `ui-render.js` (4473 ln, 117 fns) → MEDIUM split by subsystem
Split the `render*()` set by subsystem, mirroring the bezel grouping: `ui-render-operator.js`, `ui-render-operations.js`,
`ui-render-databank.js`, and a shared `ui-render-core.js` (escape/list helpers, `_updateContextPanels`, faction/game-time
helpers). Lower urgency than ui-core; cohesive today.
### 2.5 Deliberately NOT split (with reasons)
- **`state.js` (2025 ln):** cohesive (state + `GAME_DEFS` + `THEMES` + `MetaStore` + `RobcoEvents` + migration). `GAME_DEFS` is a
large data table that *could* move to `js/game-defs.js`, but it is injected by the per-game `GAME_FILES` manifest and holds
both games — moving it is load-order-delicate for modest gain. **Defer; header + internal sectioning only.**
- **`test-console.js` (5211 ln):** one cohesive subsystem (the Diagnostic Shell). **Header + internal sectioning only.**
- **The two test runners (40k / 26k ln):** physically splitting them fights the parity checker + suite-numbering machinery
(load-bearing). **Header + internal reorg only; no physical split in this phase.** (The generated-catalog work reduces their
churn instead.)
---
## 3. The exhaustive re-sync checklist (every place a moved/new file must touch)
**Missing any one of these is a black screen or a silently weakened guard.** For EVERY file split/add/move/rename:
1. **`index.html` `<script>` tags** — add the new file(s) in the static-tag group, correct order.
2. **`GAME_FILES` boot manifest** (inline in `index.html`) — only if a per-game-injected file (`db_*`/`state`/`reg_*`/
`registry-core`) is touched. The ui-* / api splits are NOT in the manifest (they are static tags) — do not add them there.
3. **`sw.js` `ASSETS` precache list** — add the new file(s). (Served-file change → **Protocol 1 cache bump** required.)
4. **`repomix.config.json`** `include`/`ignore` (Protocol 37) — confirm the new file matches the pack globs.
5. **`tests/test.html`** boot chain (Protocol 40) — its `<script src="../js/…">` tags must match `index.html`'s order; update
the `Suites: N` marker only if a runtime suite changes (it won't for a pure code move).
6. **Boot-order gate (Suite 56)** — file-exist + in-ASSETS + script-tag + load-order-before-api/before-ui-core guards for each
new file; the MUST-NOT-EXIST guard for any retired name.
7. **Load-order guard (Suite 220 / Protocol 45)** — the `<!-- LOAD-ORDER-GUARD -->` blocks in **CLAUDE.md and ARCHITECTURE.md**
are machine-compared against `index.html`. Update both docs' numbered lists in the same commit or the build fails.
8. **ARCHITECTURE.md** "Script load order" narrative + any file-responsibility sections (Protocol 2).
9. **`CLAUDE.md`** "Architecture Quick Reference" load-order list + the Protocol 4/5/6/7 file-reference tables + the Reference
Pointer Index (§7).
10. **`eslint.config.mjs`** — only if a currently-local function becomes a cross-file global (add to the globals block) or a new
module-syntax file appears. A pure move of already-global functions needs no change.
11. **The two test runners** — every read site of the moved physical file. **After U-A0 (§5) this collapses to updating the
read-helper glob once per family, not per site** — that is the entire point of U-A0.
12. **`library/BRAIN_DUMP.md`** §4/§5 (repo layout + load order) — flagged for the final re-baseline (not per-unit).
13. **The Reference Pointer Index + Code Map** (new, §7) — the "where does X live" entry for the moved code.
A machine-checkable pre-flight (proposed helper `scripts/check-boot-chain.js`, §5-U-A0) that asserts the set
`{index.html script tags} == {sw.js ASSETS js/*} == {files on disk} == {test.html boot chain}` are consistent, run in the gate,
turns "did I miss one?" from a black-screen-in-prod into a gate failure.
---
## 4. The coupling defused: logical-bundle read helpers (the load-bearing mechanism)
This is the crux mechanism and the answer to "how will you make gate-weakening impossible rather than merely discouraged."
**The problem restated:** a content-location test does `readFile('js/ui-core.js').includes('literal L')`. Split ui-core.js and
`L` moves to `ui-core-nav.js`; the test still reads the old file → fails. Multiply by ~470 refs across both runners. The lazy fix
is to repoint the read AND soften the assertion — that is a silent coverage loss.
**The fix — introduce a logical-bundle reader in BOTH runners, before any code moves:**
- Node: `readGroup('ui-core')` → concatenates `js/ui-core*.js` (glob) into one string.
- PowerShell: the mirror (`Get-ChildItem js/ui-core*.js | Get-Content -Raw`, joined).
- Migrate every content-location test from `readFile('js/ui-core.js')` to `readGroup('ui-core')`. **The asserted literal never
changes — only the read target widens from one file to its family.** For a not-yet-split file the family is one file, so the
concatenation is byte-identical and the test is provably coverage-identical *today*.
**Why this makes the restructure safe and un-weakenable:**
- After migration, moving `L` from `ui-core.js` to `ui-core-nav.js` keeps `L` inside `readGroup('ui-core')` → the test still
passes, untouched. The split becomes behavior-and-test-identical.
- A genuine **boundary assertion** (Protocol 23: "X is in ui-core, NOT in ui-render") stays a two-group check
(`readGroup('ui-core').includes(X) && !readGroup('ui-render').includes(X)`) and still holds across the split.
- The **rule that makes weakening impossible:** a content-grep migration may change ONLY the read target (file → group); it may
**never** alter the asserted literal or loosen the matcher. Any diff that touches an assertion string in the same commit as a
read-target change is rejected in review. Because the assertion is untouched, there is no room to "soften" it.
**This unit (U-A0) lands first in the code strand and is pure test-infra: coverage-identical, full-gate-green, zero code moved.**
---
## 5. Sequencing + unit decomposition (shippable, gate-green, behavior-identical)
**Hard rule throughout:** Protocol 12 — **only one repo session touches this repo at a time.** "Parallel" below means *logical
independence* (an ordering degree of freedom), never concurrent execution. Every unit lands full-gate-green, both runners at
parity, render-checked at 360/412 where UI is touched, and never `--no-verify`.
### The coupling, stated as an ordering law
1. **Strand B (token split) is independent of the code splits** → do it FIRST for the immediate token win at near-zero code risk.
2. **Strand C's refactor-resilience prep (U-A0) MUST precede strand A's physical splits** → it is what makes them landable.
3. **Strand A (physical splits) after U-A0**, one file-family per unit.
4. **ESLint tightening + headers + naming + dead-code** interleave with / follow the splits.
5. **Test-health depth** (strength gaps, assertion strength, static→behavioral, consolidation) after/alongside the splits.
6. **Strand D** (perf/a11y/bundle/deps/dead-CSS/protocol consolidation) LAST, on the clean codebase.
7. **Brain-dump re-baseline** — the hard exit condition — dead last.
### The units
**U-B1 — Token split: generate the catalog, gut CLAUDE.md's tail.** (SPINE, do first)
- Write `scripts/gen-test-catalog.js`: parse each runner's `header('Suite N — …')` + per-suite `[PASS]` counts → emit
`library/TEST_CATALOG.md` (generated; a "GENERATED — do not hand-edit" header).
- Recognize that CLAUDE.md's tail conflates two things: (a) a **suite catalog** (generatable) and (b) a **per-unit build
narration** (already substantially in `CHANGELOG.md`). Delete BOTH from CLAUDE.md; (a) → generated `TEST_CATALOG.md`, (b) →
dropped (it lives in CHANGELOG.md / an archive snapshot).
- Replace the tail with a short pointer ("Full per-suite catalog: `library/TEST_CATALOG.md`, generated by
`scripts/gen-test-catalog.js`").
- Gate guard: a test that regenerates the catalog and diffs against the committed copy (stale catalog → build fails).
- **Acceptance:** CLAUDE.md drops from ~296 KB to ~80 KB (protocols + a lean quick-ref + the pointer index); the catalog is
generated and current; `gen-test-catalog.js` is in the gate; full gate green. No code file changes.
- **Token result (measured target):** ~59k always-loaded tokens removed per session.
**U-B2 — Reference Pointer Index + Code Map (build out the seed).** (SPINE)
- Expand the existing CLAUDE.md pointer table (topic → `library/…` path) to cover: AI contract, boot lifecycle, state shape,
event bus, native router, audio model, the two-store boundary, the render pipeline, the Diagnostic Shell, the test system.
- Create `library/CODE_MAP.md` ("where does X live" — function → file, updated as splits land).
- New protocol (§7) + extend Suite 220 to assert every pointer path resolves to a real `library/` file (stale pointer → build
fails).
- **Acceptance:** every pointer resolves; Suite 220 guards it; full gate green.
**U-A0 — Logical-bundle read helpers + boot-chain preflight (the coupling defuser).** (SPINE, must precede all splits)
- Add `readGroup(stem)` to the Node runner and its PowerShell mirror.
- Migrate every content-location test from `readFile('js/x.js')` → `readGroup('x')`, assertion strings untouched (§4 rule).
- Add `scripts/check-boot-chain.js` (the §3 consistency preflight) to the gate.
- Sweep the stale `js/ui.js`/`js/registry.js`/`js/database.js` runner strings (keep only the intentional Suite-56 guard).
- **Acceptance:** both runners still report **2951/221** at exact parity (concatenation-of-one = identity, so counts and results
are byte-unchanged); boot-chain preflight passes; full gate green. **This unit moves zero code and changes zero assertion.**
**U-A1 … U-A5 — Physical splits, one family per unit** (SPINE for ui-core; MEDIUM for the rest)
- **U-A1: split `ui-core.js`** per §2.1 → `ui-nav`, `ui-overseer`, `ui-chassis`, `ui-modulebay`, `ui-terminal-cmd` + slimmed hub.
Full §3 re-sync. Because U-A0 landed, the content-greps read `readGroup('ui-core')` and stay green as functions move.
- **U-A2: split `api.js`** per §2.3 (directive builders behind the Suite-131 golden-master).
- **U-A3: split `ui-render.js`** per §2.4.
- **U-A4: internal reorg + headers** for the not-split big files (`state.js`, `test-console.js`, `ui-audio.js`, `ui-saves.js`):
per-file header standard + top-level section banners, no code moved between files.
- **U-A5: terminal.css option A** — top-level banner sections + a ToC header (no file split).
- **Acceptance (each):** behavior-identical (both runners green + render-check 360/412 + boot-smoke), full §3 checklist done,
cache bumped (served files changed), docs synced, one file-family only. Each leaves the app fully working.
**U-A6 — ESLint readability tightening + naming/dead-code pass.** (HIGH-VALUE)
- Add `max-lines-per-function`, `complexity`, `max-depth`, `max-params`, `max-nested-callbacks` (warn→error ratcheted), a naming
convention rule set, `consistent-return`. **Introduce as warnings, ratchet to errors** so the huge existing files don't block
the gate on day one (self-improving, Protocol 36b).
- Dead-code sweep (orphaned fns, the stale refs, retired-feature remnants) as small, test-proven removals.
- Magic numbers → named constants where the number is load-bearing.
- **Acceptance:** eslint clean at the chosen ratchet level; every removal proven behavior-neutral by the gate; full gate green.
**U-C1 — PowerShell-runner speedup (the gate-speed win).** (HIGH-VALUE)
- Profile *within* the PS runner (the 15.7 s): find the hot pattern (likely repeated large-file `Get-Content -Raw` re-reads and
the per-suite `node <tempfile>` shell-outs for behavioral proofs). Cache each source file read once; batch/avoid per-proof node
spawns where a pure-PS assertion is equivalent.
- **Acceptance:** PS runner materially faster (target: close the ~13× gap toward a small multiple of Node's 1.2 s) with **zero
coverage change** — same 2951 tests, same per-suite composition, exact parity. Full gate green.
**U-C2 — Flaky hardening.** (HIGH-VALUE)
- The two known flake classes (the stdin-pipe corruption from `| node`, and cross-suite deferred-async ordering) are already
patched in spots — sweep for any un-patched instance and lock the pattern with a guard.
- **Acceptance:** the temp-file-invoke and self-re-emit-header patterns are the only sanctioned forms; a guard rejects a bare
`| node` or a shared deferred header.
**U-C3 — Test-health depth (analysis-first, owner-approved per finding).** (HIGH-VALUE, staged)
- Produce a per-suite classification (static vs behavioral) — the measurement §1.9 flagged as not-yet-done.
- **Strength gaps:** propose new behavioral tests for uncovered paths (reconcile the stale `TEST_STRENGTH_AUDIT.md` against
current code first).
- **Assertion strength:** flag vacuous/tautological tests (grep-a-name-without-running-it) → propose behavioral upgrades.
- **Static→behavioral:** identify static greps that should execute the code (vm-sandbox), **keeping the load-bearing Protocol-20
source-invariant guards static on purpose.**
- **Coverage-preserving consolidation:** each proposed removal/merge listed **individually with a coverage-equivalence argument**;
owner approves each. ONLY exact duplicates, strictly-superseded tests, and dead-feature tests. **Never cut the number to cut the
number.**
- **Acceptance:** each change lands in both runners at parity + Protocol 2a; coverage equal-or-stronger; owner signed off each
removal.
**U-D1 … U-Dn — Strand D on the clean codebase.** (mix of HIGH-VALUE and NICE-TO-HAVE, LAST)
- **U-D1 Performance** — real 360/412 frame budgets, `loadUI()` fan-out, animation-when-hidden, OCR load, reflow hotspots.
- **U-D2 Accessibility** — the deep mobile/touch pass beyond the axe baseline.
- **U-D3 Asset/bundle + caching** — boot payload on mobile, SW precache-vs-runtime split, the ~9.5 MB OCR data.
- **U-D4 Dependency/security hygiene** — Tesseract.js + Firebase pins, CSP, vendored files, free-only rule.
- **U-D5 Dead-CSS sweep** — after U-A5's sectioning makes terminal.css navigable.
- **U-D6 Protocol consolidation** — dedupe/clarify/retire the ~45+ sprawled protocols (pairs with the doc trim).
- **Acceptance (each):** measured, real improvement; behavior-identical or owner-approved change; full gate green.
**U-Z — Brain-dump re-baseline (the hard exit condition).** (SPINE, dead last)
- Re-verify `library/BRAIN_DUMP.md` §4/§5 (repo layout + load order) and every internal file/function reference against the
restructured code; rewrite the moved sections; update the snapshot date.
- **Acceptance:** brain dump matches the restructured codebase; §1's counts re-verified by running a runner. **2.8.5 is not "done"
until this passes.**
---
## 6. Risk register
| # | Risk | Detection | Rollback / prevention |
| --- | --- | --- | --- |
| R1 | **Boot chain breaks → black screen** (a split file missing from index.html / sw.js / test.html / GAME_FILES) | `scripts/check-boot-chain.js` preflight (U-A0) + Suite 56 + Suite 220 + boot-smoke in the full gate — all run **before push** | `git revert` the unit (small, single-family), Protocol 1 cache bump, push (Protocol 16 restore-first). Units are one-family so the blast radius is one file group. |
| R2 | **Gate silently weakened** ("just update the greps" softens an assertion) | The §4 rule: a content-grep migration changes ONLY the read target, never the asserted literal — **enforced in review**; any assertion-string diff in the same commit as a read-target change is rejected | U-A0 removes the *need* to touch greps at all (the family read stays green as code moves), so the temptation never arises. This is prevention-by-construction, not discipline. |
| R3 | **SW cache serves the old build** (forgot Protocol 1 bump) | Pre-commit cache-bump guard (already enforced) fires on any served-file change | The guard blocks the commit until `CACHE_NAME` increments. |
| R4 | **Save/migration path regressed** by a split touching state.js / api.js import | The behavioral vm-sandbox suites (autoImportState hostile-input, migrateState, the save round-trip) + test.html runtime mirror | state.js is deliberately NOT split (§2.5); api.js import moves are guarded by the Suite-131 golden-master + import round-trip tests. Revert the unit. |
| R5 | **Both runners drift out of parity** during a test-infra change | `checkSuiteParity` (per-suite title+count) in every gate run | The gate blocks any non-parity push; U-A0/U-C1 must update both runners in the same commit (Protocol 15). |
| R6 | **A "pure move" wasn't behavior-neutral** (a hoisting/order dependency surfaced) | Full gate every unit; render-check 360/412; boot-smoke | Intra-group load order is flexible (all defined before `window.onload`), but if a top-level execution dependency exists, boot-smoke catches it → revert the unit. |
| R7 | **The catalog generator drifts from reality** | The U-B1 regenerate-and-diff gate guard (stale catalog → build fails) | Regenerate; the doc is generated-not-maintained so it self-heals. |
| R8 | **Restructure runs long / scope creep** (Protocol 28) | §8's SPINE/HIGH/NICE tiering; owner reviews each unit | Ship the SPINE, stop; HIGH-VALUE and NICE-TO-HAVE are independently droppable without leaving the app half-restructured (every unit is complete on its own). |
**The most dangerous failure mode — a refactor that breaks tests then "fixes" them by weakening them — is made structurally
impossible by U-A0 + the R2 review rule, not merely discouraged.** That is the plan's core safety claim.
---
## 7. Protocols — new, changed, retired
### New protocols
- **Protocol 46 — Keep the Code Map + Pointer Index current.** When a file is split/added/moved/removed or a major function
relocates, update `library/CODE_MAP.md` and the CLAUDE.md Reference Pointer Index in the **same commit**. Gate-guarded: Suite
220 extended so every pointer path must resolve to a real `library/` file (stale pointer → build fails). (Self-improving,
Protocol 36b.)
- **Protocol 47 — Generated-not-maintained docs.** If a doc can be generated from source, generate it; do not hand-maintain it.
The per-suite test catalog (`library/TEST_CATALOG.md`) is generated by `scripts/gen-test-catalog.js` and regenerated in the same
commit as any suite change; the gate diffs the committed copy against a fresh generation. Audit the library for anything else
derivable (load order from `index.html`, the flag list from `cloud.js`, the tool registry from `test-console.js`).
- **Protocol 48 — Per-file header standard.** Every `js/*.js` opens with a header: purpose, load-order slot, what it exposes, key
entry points, dependencies. Gate-guarded that every served `js/*.js` carries the header block.
- **Protocol 49 — Comment-the-WHY / file-size guideline.** Comments document intent, gotchas, and protocol rationale — not
line-by-line WHAT. A **soft** max-file-size guideline (warn, not block) flags a file that grows past a threshold as a split
candidate. ESLint carries the enforceable readability rules (max-lines-per-function, complexity, max-depth, naming).
### Changed protocols
- **Protocol 2a — test-count sync (major simplification).** Today it hand-syncs a hardcoded count across ~8 locations. Under the
generated catalog, **the runner's own printed total is the single source of truth** and the catalog is generated. Revised rule:
*no hardcoded test count lives in any hand-maintained doc; the catalog is generated (Protocol 47) and the gate proves it
current; suite additions/removals still land in both runners at parity (Protocol 15), but the doc-sync burden collapses to
regenerating one file.* This is the single biggest maintenance-burden reduction in the phase and directly answers the owner's
"if the catalog is generated, much of 2a evaporates."
- **Protocol 2 / Protocol 45 / Suite 56 / Suite 220** — extend the file-reference tables and load-order guards to the new split
files (mechanical, per §3). Every protocol that names a file that moves (Protocol 4/5/6/7 file tables, the load-order lists)
updates in the moving unit's commit.
- **Protocol 40 (test.html)** — its boot chain must include every new split file; update in the splitting unit.
### Retired / confirmed-dropped
- The **diegetic code-renaming** idea — already scrapped by the owner (readability beats flavor in source). Confirmed out of scope.
- CLAUDE.md's **Architecture Quick Reference per-unit narration** — retired from the always-loaded doc (→ generated catalog +
CHANGELOG).
- **Protocol consolidation (U-D6)** will retire whichever of the ~45+ protocols are superseded (that pass proposes the specific
retirements; not pre-judged here).
---
## 8. Scope + cost — SPINE / HIGH-VALUE / NICE-TO-HAVE
**Protocol 28 reality:** this phase has accreted a lot. It should be **prioritized and sequenced within**, not done at maximum
depth in one blind pass. Below is the honest triage, with cuts argued.
### SPINE (must happen, in this order — this is 2.8.5's real definition of done)
1. **U-B1** token split (generate catalog, gut CLAUDE.md tail) — biggest ROI, lowest risk, docs-only. **Do first.**
2. **U-B2** pointer index + code map + Protocol 46.
3. **U-A0** logical-bundle read helpers + boot-chain preflight — the coupling defuser; gates everything in strand A.
4. **U-A1** split `ui-core.js` — the one file where the readability north star is currently impossible.
5. **U-A4 / U-A5** headers + internal sectioning for the not-split big files and terminal.css (option A).
6. **U-Z** brain-dump re-baseline — the hard exit condition.
### HIGH-VALUE (should happen; drop only under real time pressure)
- **U-A2 / U-A3** split `api.js` and `ui-render.js` (api.js has the golden-master safety net; ui-render is cohesive but large).
- **U-A6** ESLint readability tightening + naming/dead-code (locks in the clarity so it can't rot — self-improving).
- **U-C1** PowerShell-runner speedup (the measured 15.7 s → the single best gate-speed lever).
- **U-C3** test-health depth (strength gaps + assertion strength + static→behavioral + coverage-preserving consolidation).
- **U-D1 / U-D2 / U-D3** performance, accessibility, asset/bundle — the owner is mobile-primary; these earn their place.
### NICE-TO-HAVE (drop if the phase runs long)
- **U-D4** dependency/security hygiene (real but low-churn; the deps are pinned and few).
- **U-D5** dead-CSS sweep (do it only after U-A5 makes the CSS navigable; otherwise defer).
- **U-D6** protocol consolidation (valuable housekeeping, but not blocking).
- **U-C2** flaky hardening (the known flakes are already patched in spots; this is belt-and-suspenders).
### What I recommend CUTTING from the accumulated scope (argued)
- **Splitting `css/terminal.css` into multiple `<link>` files (§2.2 option B).** The SW-ASSETS + cache-bump + `<link>`-order +
FOUC surface is real risk for a readability gain that option A (internal sectioning) captures ~80% of at near-zero risk. **Cut
the file split; keep the sectioning.**
- **Physically splitting the two test runners.** Fights the parity + suite-numbering machinery (load-bearing) for a readability
gain the generated catalog + internal headers mostly deliver. **Cut; headers/reorg only.**
- **Splitting `test-console.js` and `state.js`.** Cohesive single-subsystem files; the split cost (load-order care for state.js,
no natural seam for test-console) exceeds the gain. **Cut to header + internal sectioning.**
- **Objective code-coverage instrumentation.** Plain global-scope + no build makes c8/nyc hard to attach without wrapping the
load; the behavioral vm-sandbox suites already give strong path coverage. **Cut unless a cheap wrapper proves feasible in
U-C3's analysis pass.**
### Realistic size
- **SPINE alone: ~6 units.** A meaningful chunk — U-A1 (ui-core split) is the heavy one; the rest are moderate. This is the
minimum that delivers "lean CLAUDE.md + the worst file made navigable + re-baselined brain dump."
- **SPINE + HIGH-VALUE: ~12–14 units.** This is the honest full scope of "code + test health." At the observed one-repo-session,
one-family-per-unit cadence (Protocol 12), this is an **extended multi-session run**, not a single push — measured in weeks of
Dispatch sessions, not a day.
- **The biggest single risk** remains R1/R2 — the restructure breaking the boot chain or the gate. U-A0 + the boot-chain
preflight + one-family-per-unit + full-gate-every-unit are the whole reason the plan front-loads test-infra before touching a
line of product code.
---
## 9. Open questions for the owner (decide before implementation)
1. **CLAUDE.md tail — confirm the two-way split** (suite catalog → generated `library/TEST_CATALOG.md`; per-unit narration →
dropped, since CHANGELOG.md already carries the plain-English history). OK to drop the narration entirely, or keep a frozen
archive snapshot of it?
2. **terminal.css — confirm option A** (internal sectioning, single file) over option B (multi-`<link>` split). Plan recommends A.
3. **ui-core.js split boundaries** (§2.1) — approve the five-way responsibility split, or prefer a coarser 2–3 way split first?
4. **ESLint ratchet** — introduce readability rules as **warnings first** (so the existing giant files don't block the gate),
ratcheting to errors as files shrink? Plan recommends warn-first.
5. **Scope line** — is SPINE + HIGH-VALUE the target, or SPINE-only for now with HIGH-VALUE as a follow-on?
---
_End of plan. Nothing here was implemented; this document is the only artifact produced. Implementation is per-unit, owner-gated,
one repo session at a time (Protocol 12), full gate every unit, never `--no-verify`._
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).