RELEASE
planning/2.8.5/audits/AUDIT_U7_ui-render-split.md
sha256 38dca3564b51beeb · 11064 bytes ·
original held in the private archive
# AUDIT — 2.8.5 U-A4: `ui-render.js` split (commit `f37c44e`, parent `775d8aa`)
**Independent auditor, Protocol 8 stage 3. Read-only. Started from the diff, not the implementer's account.**
## VERDICT: **ACCEPT**
The move is provably pure, the gate is not weakened, both runners' source-readers resolve to the full 10-file corpus, and the two rescoped test checks are genuine corrections that preserve the original intent exactly. One thing to flag to the orchestrator: **the implementer's headline claim — that it caught a "pre-existing defect where ~40 suites were passing while checking almost nothing" — is not supported by the evidence.** There was no such defect at the parent, and had the split introduced one, the gate would have gone red, not silently green. That doesn't change the ACCEPT — the shipped code is correct — but the narrative is overstated.
---
## The headline: the `uiSource` / `$uiSrc` "defect"
**Claim (as relayed):** a shared source-reader that ~40 suites depend on was reading the literal single-file path after the split, silently shrinking its corpus while every test stayed green.
**What I found, verified by execution:**
1. **No pre-existing defect at `775d8aa`.** At the parent, `uiSource` was `['js/ui-audio.js','js/ui-render.js','js/ui-saves.js','js/ui-account.js'].map(readFile).concat([readGroup('ui-core')])`. `readFile('js/ui-render.js')` does **not** read a flat path — it falls back through `_jsFileLocation()` to `js/ui/ui-render.js`, the **full 4,473-line monolith**. Confirmed: the parent's `readFile` carries that subfolder fallback, and the parent's `ui-render.js` is 4,473 lines. **The parent's corpus was complete.** No suite was checking a truncated corpus before this commit.
2. **The split created a truncation *risk*, which the implementer correctly handled.** Had the split kept the single `'js/ui-render.js'` literal, `readFile` would now resolve it to `js/ui/ui-render.js` = the **32-line hub**, dropping ~199 KB across 9 files. The implementer avoided this by switching to `.concat([readGroup('ui-core'), readGroup('ui-render')])`. Correct fix — but a fix to a risk the split itself introduced, **not** the discovery of a live bug.
3. **A truncated `uiSource` would have failed LOUDLY, not silently.** The ~40 dependent suites extract render functions via `extractFunctionBody(uiSource, 'renderX')` inside `try { … } catch (e) { fail(\`Cannot extract …\`) }`. On a hub-only corpus those throws hit `fail()` → **red gate**. Example call sites: Render Contracts (`renderFactionRep`/`renderWorldMap`, line ~1012), Suite 21.2 (`renderSquad`, line ~1396). So even if the risk had materialized in a committed state, the gate would have caught it. The "silently green while checking a fraction" framing is unsupported.
4. **`uiSource` / `readGroup('ui-render')` now resolve to the full corpus — verified in BOTH runners:**
- **Node** `readGroup('ui-render')` → 10 files, **200,822 bytes**; full `uiSource` = 685,233 chars; contains `renderInventory`, `renderWorldMap`, `renderSquad`, `renderCraft`, `renderKarmaCenter`.
- **PowerShell** `Read-Group 'ui-render'` → 10 files, **200,822 bytes** (byte-identical to Node); contains `renderInventory`, `renderWorldMap`, `renderBioScan`.
5. **Did any suite pass "for the wrong reason"?** No. The corpus content is essentially identical at `775d8aa` and `f37c44e` (same render code; only 9 new file-header comment blocks added). A "must-NOT-contain" assertion can only flip if the *content* shrank or grew into a banned literal — content didn't shrink, and the only render-scoped game-literal absence checks (Suite 108.10, 109's `bioRegion`) are scoped to `ui-render-databank.js`, which is game-agnostic. Verified 108.10's region has no FNV/FO3/Fallout literal and still passes meaningfully.
**Bottom line on the headline:** good engineering (use `readGroup`, don't keep a stale literal), but **not** the catch of a silent 40-suite hole. Report it as a non-finding, corrected wording.
---
## Pure move — PROVEN
- **Code byte-identical.** Stripping all comments/blanks and multiset-comparing lines: original render layer = **3,452 code lines**, reconstructed union of 10 files = **3,452 code lines**, **0 lines only-in-original, 0 only-in-new.**
- **Function integrity.** 117 top-level functions in the monolith; 117 across the 10 files; `diff` of the sorted name sets is empty; **no duplicates.**
- **Comments preserved.** A comment-inclusive comparison flagged 68 "missing" original comment lines — all 68 are the function-preceding comment blocks that became each sibling's leading comments (my header-stripper's artifact). Spot-confirmed present: CRAFT PANEL, FACTION REPUTATION inline, CARTOGRAPHY, Session Statistics, SHELF/RACK, THREAT WU-N3, SQUAD ROSTER, LOOT WU-N6.
- **Only additions:** the 9 new per-file header comment blocks. No code rewrites, no drive-by fixes.
---
## Rescoped test checks (Suites 108.10, 110 — and PS-only 109) — GENUINE corrections
The family is concatenated in **alphabetical** order (`ui-render-*` sort before `ui-render.js`), so the monolith's source order is gone. The old "slice from marker A to marker B" logic relied on that order.
- **108.10** (game-literal absence). Old: `slice(indexOf('const _CONSULT_CATS'), indexOf('function _updateContextPanels'))`. On the new corpus this spans **7 files, 156,579 chars** and **contains FO3/FNV literals** → the assertion `!/FNV|FO3/` would **FAIL**. New: reads `readGroup('ui-render-databank')` and slices `_CONSULT_CATS → EOF` = 17,589 chars = the *same* databank section the monolith slice covered (databank's EOF is exactly where `_updateContextPanels` used to begin). Verified: region non-empty, starts at `_CONSULT_CATS`, reaches BIO-SCAN, no game literals. **Correction, not narrowing.**
- **110** (LOOT core). Old: `slice(indexOf('function _lootAdd'), indexOf('// ── WU-N3: THREAT'))`. On the new corpus THREAT (databank) sorts *before* loot, so `end (32,883) < start (134,429)` → guard fails → **empty/vacuous region**. New: `readGroup('ui-render-loot')`, `_lootAdd → EOF` = the same LOOT section. Verified: non-empty, contains `doLoot` + OCR flow, does **not** spill into THREAT. **Correction.**
- **PS-only 109** rescope (`$databank109`) has no JS counterpart because JS 109 uses `extractFunctionBody` (brace-counting), which is order-immune; the PS runner sliced to the moved `_updateContextPanels` marker and needed fixing. `renderBioScan` is the last function in `databank.js`, so PS's substring-to-EOF ≈ JS's brace-counted body — **equivalent outcome, parity preserved.**
---
## +9 tests (2,971 → 2,980) — real, auto-scaled, not padding
Suite 90 (UTF-8 corruption guard) iterates `allJsFiles()` and asserts one per file. The 9 new `ui-render-*.js` files add 9 real per-file mojibake checks. **Zero `assert(`/`Check ` literals were added or removed** in the diff (confirmed by counting added/removed lines) — the +9 is purely the runtime glob scaling. Suite 59's file-list grew by 9 entries but concatenates into one corpus with a fixed assert count (loops over HTML handlers, not files), so it stays stable.
---
## Gate not weakened — evidence
- **No `assert`/`Check` deleted, skipped, or made vacuous** (grep of diff: 0 removed, 0 `.skip`/`xit`/`assert(true`).
- **No matcher loosened.** The only replaced logic is the two region slices, both proven to reproduce the original region.
- **Corpus content unchanged** (same code + 9 headers), so no presence/absence assertion flips. The 3 header blocks that mention FO3/FNV (character/factions/record) are not scanned by any render-family absence assertion (the render-scoped ones are databank-only or brace-extracted; `saveHelp103b` scans ui-core, not render).
- **ESLint change** adds two now-cross-file globals (`getFactionStanding`, `_bioChemHasRisk`) — necessary and consistent with the codebase's established global-scope cross-file pattern; not a suppression of a real undefined-reference.
- **CHANGELOG framing is honest** ("fixed a couple of internal test checks that were quietly scanning the wrong slice of code after the split") — it does *not* claim the overstated 40-suite narrative.
---
## Load order & global scope — correct
All 10 files are static `<script>` tags before `window.onload`, in `js/ui/`, loaded after `ui-audio.js` and before `ui-saves.js` (same slot the monolith held). **No top-level executable code** in any of the 10 files (every col-0 statement is a `function`/`async function`/`const` declaration), so inter-file order is genuinely dependency-free — the header comment saying "intra-group order is flexible" is accurate. Load-order guard blocks in `CLAUDE.md` and `ARCHITECTURE.md` list all 10 in the correct order (Suite 220 check 3).
## Visual-regression risk from the missing pixel screenshot — NONE material
A pure JS-file split produces byte-identical render functions → identical DOM. The only regression class a static gate could miss is a load-order failure yielding a blank panel — and that's ruled out (no top-level code, all tags before onload). The pixel-screenshot gap is immaterial here; the automated render-check plus the load-order proof fully cover it.
---
## Minor notes (NOTE severity — no action required to accept)
- **NOTE — stale `owner:` annotations & prose comments.** `js/core/state.js` META_MANIFEST still tags `robco_cargo_drawer`, `robco_faction_channel`, `robco_databank_qdrawer` with `owner: 'ui-render.js'` (now inventory/factions/databank siblings); prose comments in `state.js` and `db_fo3.js` reference `renderWorldMap`/`_durationToTicks` "in ui-render.js" (now map/character siblings). Pure-move discipline correctly left unrelated files untouched, and `ui-render.js` still exists as the hub, so nothing is broken and Suite 220 (path-existence) still passes. Coarsely stale; worth a follow-up sweep, not a blocker. **Recommended correction:** in a later non-move commit, repoint the three `owner:` strings and the two prose comments to their sibling files.
- **NOTE — split-boundary cohesion.** `ui-render-character.js` (764 lines) groups squad + clock/calendar + faction-standing + status + perks + quests under "CHARACTER & FIELD STATUS" — the largest and least cohesive sibling, though a defensible panel grouping. The 32-line hub holding only `_updateContextPanels()` is thin but honest (genuine cross-panel visibility glue owned by no single sibling). Acceptable for a pure move; if a future pass wants tighter cohesion, character could shed the clock/calendar helpers. Not a blocker.
---
## What the gate cannot see, that I checked and cleared
- Pure-move byte-equality (gate only counts tests, doesn't diff the reconstruction).
- Both source-readers resolving to the full corpus (executed, not inferred).
- The rescopes preserving vs. narrowing intent (executed old + new logic on the live corpus).
- Whether a hypothetical truncation would fail loud or silent (traced the `fail()` guards).
- Doc/comment/annotation drift.
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).