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

CODE_HEALTH_PLAN.md



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