RELEASE
planning/2.8.5/audits/FO3/AUDIT_FO3_SPINE.md
sha256 f20c4464d7aa4af0 · 14348 bytes ·
original held in the private archive
# AUDIT — FO3 Pip-Boy Spine (U0 / U1 / U2)
> Independent Opus audit (Protocol 8 stage 3). Read from the diff first; commit
> messages consulted only afterward. Base `1bd38ed` → head `95c5b1f` on `dev`.
> **READ-ONLY — nothing fixed, nothing committed.** Scratch render harness lives
> outside the repo.
## VERDICT: ✅ ACCEPT WITH FOLLOW-UPS
The three commits do exactly what the build plan scoped, no more and no less, and
the implementer's claims hold up under independent verification. NV is genuinely
untouched (proven by me, not accepted from the claim). The gate was **strengthened,
not weakened** — the one narrowed guard and the one rewritten matcher are both
legitimate. Editing survives on all boards. 3103 tests pass (I ran them). The only
real follow-up is **owner expectation management**: the spine is a faithful
functional *skeleton*, not yet a finished Pip-Boy — as the plan intended and the
implementer honestly disclosed.
---
## 1. Is NV genuinely untouched? — YES, mechanically proven ✅ (BLOCKER-class, cleared)
I booted the real app at 780×360 under both games via a headless file:// render
(styles load correctly that way — see §8) and read computed styles back:
| Probe | NV result | Verdict |
|---|---|---|
| `[data-subtab]` stamped on boards | **0** | axis dormant ✅ |
| `.subtab-active` present | **0** | ✅ |
| `#navkey-operator` border-radius | **5px** (the shared keycap, NOT the FO3 dome's `50%`) | FO3 dome skin never applied ✅ |
| `#fo3TopStrip` / `#fo3SubtabRail` display | **none / none** | invisible to NV ✅ |
| DIR keycap | **display:flex** (present) | ✅ |
| **`--bezel-wire`** | **`#ffb642`** (NV amber) — NOT FO3's `#ffa030` | **no custom-property leak** ✅ |
| horizontal overflow | **0** | ✅ |
**The custom-property leak the audit flagged does not exist.** `--bezel-wire` /
`--bezel-wire-rgb` are the only custom properties declared in the new file, and both
sit inside the `[data-game='FO3']` block (css/60-fo3-pipboy.css:38-41) — declared on
the FO3-matched `<html>` element, never `:root`. The live NV root still reports
`#ffb642`. Confirmed.
**Every rule in the new file is `[data-game='FO3']`-scoped.** I parsed all 30 rule
blocks by hand: every selector — including inside the single `@media
(orientation:landscape)` block, and every comma-separated part — begins with
`[data-game='FO3']`. No `:root`, `:where()`, `:has()`, or bare selector escapes the
scope.
**Zero bytes changed in the 12 existing CSS files** — `git diff --stat` shows only
`css/60-fo3-pipboy.css` added (+313). Confirmed independently, matching the
implementer's claim.
**New shared markup renders identically for NV.** `#fo3TopStrip` and
`#fo3SubtabRail` were added to shared markup, but both ship the HTML `hidden`
attribute and are only revealed by FO3-landscape CSS — for NV they are `display:none`,
take zero space, produce zero overflow (render-confirmed). This is the "new markup
hidden by CSS" trap done correctly.
The NV screenshot is the unchanged terminal: flat numbered keycaps `[1]`–`[6]`,`[0]`,
DIR present, accordion boards, NV telemetry LCD.
---
## 2. No game branch / no forked pipeline — CONFIRMED ✅
- The three new functions (`_applyRailGrouping`, `selectSubtab`, `_applyRails`) carry
**no `FO3`/`FNV`/`FO4` literal** (grep-confirmed; also locked by new test 223.1b).
The axis switches entirely on `getIdentity().rails` **truthiness** — a
presence-of-data check, exactly the sanctioned pattern (Protocol 38).
- **The render pipeline did not fork.** All 22 boards keep their single shared markup
and their existing `render*()` path. The second axis is a `data-subtab` attribute
stamp + a `.subtab-active` class toggle + **one** FO3-scoped CSS `display:none` rule.
No parallel FO3 renderer exists.
- One deviation from the plan's literal wording, and it is a **correct** one: the plan
said call `_applyRails()` from `selectSubsystem()`; the implementer placed it in
`switchTab()` instead, because `switchTab()` is the true choke point every entry path
(including the boot-time `initTabs()`) funnels through — `selectSubsystem()` is
bypassed on boot. Without this, "reload restores the last sub-tab" would silently
fail on every fresh load. This is the plan-review correction stage 2 is supposed to
produce, and it's covered by tests 223.9/223.10.
---
## 3. Editing survives on ALL boards — CONFIRMED ✅ (BLOCKER-class, cleared)
- Render-verified: on a visible FO3 board, an editing control reports
`pointer-events: auto`.
- The austere board re-skin (css:291-300) sets **only** cosmetic properties
(`border-radius`, `background`, `border-color`) on `.bay-board`/button/input — no
`display:none`, no `visibility:hidden`, no `pointer-events:none`. Guarded by test
224.6.
- Every rail'd board is reachable: the 5 sub-tab names per subsystem are all unique
across all three rails (STATUS/SPECIAL/SKILLS/PERKS/GENERAL · MANIFEST/CRAFT/BARTER/
SQUAD/CURIO · MAP/QUESTS/NOTES/LOG/QUERY), so the global `.subtab-active` toggle
selects exactly the intended group; each group is one tap on its rail button.
- No-rail boards (weigh bridge, 3× CHASSIS, 4× SETTINGS) carry no `data-subtab`, so the
hide rule can't match them — they stay stacked exactly as today. The no-rail
allowlist is exhaustively derived from index.html (test 222.7), so no future board can
fall through both lists silently.
- **Nothing goes read-only.** The only `display:none` in the file are on decorative bits
(`.nk-led`, `.nk-num`, `#navkey-settings::after`) and the DIR keycap (still fired by
hotkey `[0]`) — never an editing control.
---
## 4. Was the gate weakened? — NO, it was strengthened ✅ (BLOCKER-class, cleared)
**Suite 181.20 narrowing — legitimate.** The old guard string-matched the *whole*
`state.js` for `opVital|opSpecial|opHarness|…`. U0 legitimately added those board ids
as **data** inside `GAME_DEFS.FO3.identity.rails`, which would false-positive the
whole-file match. The narrowing routes through `extractStateKeys()` (a real brace-walk
of `let state = {…}`), so it still enforces the true invariant — "no new campaign-state
field" — without colliding with identity data. I verified `extractStateKeys` returns a
populated set of real state fields, so the assertion is **not vacuous**; it passes
because the invariant genuinely holds, not because it was defanged.
**Rewritten CSS matcher (224.3d) — genuinely stronger.** The comment documents (and I
confirm the reasoning) that the *old* count-based check passed even after the FO3 hide
rule's selector was deliberately unscoped — a count can't distinguish "one scoped rule
swapped for an unscoped one" from "totals still add up." The replacement is a real
brace-depth parse that asserts **every** top-level rule inside the media block is
FO3-scoped on **every** comma-separated part. This catches a class the old one missed.
Not loosened — tightened.
**+101 tests are real guards, not padding.** Suites 222/223/224 run the **real
extracted function bodies** in a `vm` sandbox against a fake DOM/MetaStore (not
reimplementations), and cover: rails shape, NV/FO4 carry no rails key, every id resolves
to one real board on the right tab, the exhaustive no-rail allowlist, the axis-inert-for-
NV no-op, per-subsystem persistence + reload restore, stale-pref fallback, the
choke-point call ordering, CSS scoping, zero image assets, and the board re-skin never
disabling a control. **224.10 records a genuine live-browser regression** (the sub-tab
buttons stacked full-width because the global `button{width:100%}` won — fixed with
`width:auto`, locked) — Protocol 42 applied honestly.
**I ran the runner: `ALL 3103 TESTS PASSED`.** Lint clean on all three changed JS files.
---
## 5. Cascade slot & registration — CORRECT ✅
`css/60-fo3-pipboy.css` sits after `55-feedback-animations.css` and before
`99-mobile.css` in: index.html `<link>` order, `sw.js` ASSETS, `check-boot-chain.js`
CANONICAL_CSS_ORDER (count 12→13), and the derived `CSS_SPLIT_FILES`. `CACHE_NAME`
bumped `r12`→`r15` (monotonic across U0/U1/U2 served-file commits). eslint globals added
for the 3 new functions. **No new `<script>` tag**, so LOAD-ORDER-GUARD is correctly
untouched; test.html/repomix correctly need no change (plan §7). CODE_MAP.md lives under
gitignored `library/` — not verifiable from the committed diff (plan says update it in
the same commit; taken on trust).
---
## 6. Accessibility — sound, one caveat ✅/NOTE
- Sub-tab buttons: `role="tab"` + `aria-selected` (dynamic). Rail: `role="tablist"` +
`aria-label="Sub-tabs"`. Re-skinned keycaps keep their existing `role="tab"` — only
the skin changes, so the tablist ARIA is intact.
- Hotkeys `[1]`–`[5]`/`[0]` and `#go=` deep-links route through the **unchanged**
`selectSubsystem()`/`SHORTCUT_ROUTES` — the only diff hits are in comments; no routing
logic changed.
- `.nk-num` is visually hidden but the hotkey number stays in the button's aria-label;
`#bezelTelemetry` is visually clipped but kept in the a11y tree (`aria-live`) so a
screen-reader user still hears updates.
- **NOTE (not a blocker):** I did not run the push-boundary axe baseline-diff (it's a
gate step, not part of this static audit). The markup is structurally correct, but a
second-level tablist nested near the first is worth a screen-reader sanity pass on real
hardware before the casing units (U3/U4) build on it.
---
## 7. Does it actually look like a Pip-Boy? — Honest skeleton, NOT yet the finished machine ⚠ NOTE (owner call)
Blunt read of the actual render (screenshot captured):
**What's there and works:** the bottom casing bar shows a **lit amber OPERATOR lamp**,
two dark unlit domes (OPERATIONS/DATABANK), a ribbed **radio knob** (UPLINK), a
white-face **gauge** (CHASSIS), and a **flick toggle** (SETTINGS) — the Pip-Boy hardware
vocabulary reads correctly. Inside the glass: the austere green boxed **top strip**
(`LVL 1 · HP 100/100 · Wg 1.0/200 · RAD 0 · Caps 0`, live data) and the **sub-tab rail**
(STATUS·SPECIAL·SKILLS·PERKS·GENERAL, active boxed, in one inline row). This is
recognizably the *beginning* of a Pip-Boy, and it is a real, faithful step.
**What's not there yet:** the **top half of the screen is still the generic RobCo
terminal** — the "UNIFIED OPERATING SYSTEM TERMINAL UNIT" header, the governor dial, the
PWR/UPLINK/FAULT lamps — not a Pip-Boy hood. The controls are a **flat bottom strip**,
not the real device's left-rail-gauge/knob + bottom-lamps + right-toggle layout. There
is no chunky device body wrapping the green screen. So as a *complete picture* it does
**not yet** read as "a genuine Pip-Boy recreation."
**My judgment: this is an honest scoping call, accurately disclosed** — the CSS header,
the CHANGELOG ("early, functional pass; a fully dressed casing … comes later"), and the
README ("_early, in progress_ … a fully dressed casing and the Vault Boy status view are
still to come") all say exactly this, and the build plan §5/§6 explicitly defers the
Vault Boy (U3) and the multi-column casing dressing (U4). Against the spine's own
contract (plan §6 U2 AC), **every criterion is met**. It is not a failed Pip-Boy; it is
an intentionally partial one.
**Follow-up for the owner (the reason this is ACCEPT-WITH-FOLLOW-UPS, not clean ACCEPT):**
the owner rejected three prior designs for not reading as a Pip-Boy, so he should judge
this as the **functional skeleton**, not the finished machine — and specifically **view
it on his real phone in landscape + installed PWA** (the plan's explicit Protocol
29-class gate) before forming a verdict. If he expects the U2 spine alone to look like
the mockup, he will be underwhelmed; that gap is U3+U4, by design. Recommend confirming
he's aligned on "skeleton first" before U3/U4 proceed.
---
## 8. The dev-server "CSS served as JS" bug — harness, not the app ✅
Confirmed a tooling/MIME problem, not an app defect. The app loads CSS via ordinary
`<link rel="stylesheet">` tags; my file:// render applied all styles correctly (the FO3
casing rendered, NV stayed unstyled-by-FO3). Nothing in the app's own CSS loading is
implicated.
---
## 9. `hidden` attribute vs `display:none` — holds ✅
Both `#fo3TopStrip` and `#fo3SubtabRail` ship the HTML `hidden` boolean attribute (tests
223.11b / 224.9b), so before any FO3 CSS applies they are `display:none` — no unstyled
fascia can flash, and nothing decorative is visible pre-CSS. Render-confirmed
`display:none` for NV with zero overflow. Correct.
---
## 10. Other notes to a colleague (all minor — NOTE severity)
- **Latent test-gap (NOTE):** the scoping guard (224.3b/224.3d) checks rules *inside* the
media block and counts `[data-game='FO3']` occurrences *before* it as `==1`, but does
**not** structurally reject a future `:root{ --x }` custom-property declaration added
before the media block. The current file is clean (no such leak), so this is a latent
gap, not a live defect — worth tightening when U3/U4 next touch the file (assert the
pre-media region contains exactly the one `[data-game='FO3']` token block and nothing
else).
- **Cosmetic no-op (NOTE):** `_applyRails()` now runs `rail.innerHTML = ''` on **every**
NV tab switch (a hidden, already-empty element). Harmless and render-confirmed inert;
noting only for completeness.
- **Pre-existing prose (NOTE, not this build):** CHANGELOG line ~25 (the PS-mirror
deletion entry) still says "all 3002 tests" in its narrative. It predates this build
and describes that change's own moment; not introduced here, but a pedantic sync some
may want.
---
### Bottom line
Clean, disciplined, honestly-reported work. NV is provably untouched (no CSS custom-
property leak). No game branch, no forked render pipeline. Editing survives everywhere.
The gate was strengthened, not weakened — the narrowed 181.20 and the rewritten CSS
matcher are both legitimate, and the +101 tests are real behavioral guards. 3103 pass.
The single thing to actively manage is the owner's expectation: **this is the faithful
functional skeleton the plan scoped, not the finished Pip-Boy** — best judged on real
hardware, with U3 (Vault Boy) and U4 (full casing) understood as what closes the visual
gap.
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).