RELEASE
planning/2.8.5/audits/AUDIT_save_layer3.md
sha256 ae1dc5278c434d4a · 8152 bytes ·
original held in the private archive
# OPUS AUDIT — Save-integrity Layer 3 (commit `404347c`)
**Protocol 8 stage-3 independent audit. Date: 2026-07-16. Branch `dev`, worktree clean.**
**Verdict: PASS** (with two named residual findings, both pre-existing and both strictly *improved* by this change — details below).
Everything below was verified by running the code myself — an independent Node/VM harness (48 fixture assertions, built separately from Suite 233), plus the full gate — not by trusting the implementer's report.
---
## 1. THE CRUX — can a healthy save ever be quarantined or lost? **No.**
I built my own fixtures and ran the *real* `_hydrateStateFromStorage` + `_quarantineCorruptContainer` from the shipped file:
- **Valid v8** (with extra unexpected fields planted on purpose): loads fully (lvl/caps/inventory intact), `robco_v8` byte-identical after boot, **no** quarantine key, **no** banner, **no** fault telemetry, **no** error records, no in-memory stash. The unexpected fields are carried, never treated as corruption.
- **Valid-but-old v7** (migration path): migrates and loads, original v7 bytes still present, migrated v8 written (that write is the migration working as designed), no quarantine, no banner.
- **Valid v8 while a migration helper is deliberately broken** (both `_migrateEventLog` and `reconcileEquipped` throwing): save stays byte-identical, campaign loads, both faults logged fail-soft, **never** quarantined or bannered.
- **Valid v8 plus a stray eviction stash** (`_bootMarkerRecovered=true` forced): still completely silent — a present campaign container always wins.
- The only localStorage write on a healthy boot is the pre-existing rolling-backup snapshot (`snapRollingBackup`, untouched by this commit) — it *copies* the save, it never mutates it.
- The corrupt trigger is genuinely narrow: I traced every statement inside the outer try. Only `JSON.parse` failure or a container whose `campaigns` dict is structurally absent (e.g. `{"activeContext":"FNV"}` with no campaigns at all — not a save) can reach quarantine. The `|| {}` fallback tolerates a missing active campaign; object spread of any JSON value cannot throw.
The gate's browser-level checks independently confirm the same invariant on a real page with real IndexedDB ("LAYER3 valid boot (L2, hard invariant) — save loads, NO banner, NO quarantine, NO fault telemetry" — PASS).
## 2. The latent-bug fix — reproduced old-vs-new
Same fixture (valid v8 + throwing helper) against the **pre-change** `ui-core.js` (from `404347c~1`): the old catch swallowed the helper throw and **deleted the valid save from localStorage** (it survived in memory only until the next reload). Against the **new** code: save untouched, loaded, fault logged. The latent defect was real, and it is dead.
## 3. Silent-delete → quarantine, red-then-green
Planted genuinely corrupt bytes: the exact bytes land whole in `robco_v8_quarantine` (verified byte-for-byte), the READ FAULT banner fires, the fault is recorded, and boot continues into a usable fresh campaign. A **second** corrupt boot does **not** overwrite the earlier unresolved quarantine (the newer envelope goes to the in-memory stash + stamped IDB keys, swept to newest 3). Corrupt v8 + valid v7: the v7 still loads and the corruption is still announced. EXPORT recovers the whole envelope (browser check green); PURGE is confirm-gated (Protocol 34) and is what retires the banner.
## 4. Eviction banner — no crying wolf
All four silent cases verified silent in my harness *and* in the browser checks: first-ever boot, marker-present-no-save (swipe-away), post-quarantine boot (correctly shows READ FAULT, never EVICTION), slow/absent IDB (recovery stash never set → silent; false-negatives are the designed direction). The banner fires only on the true three-part signature, and the stash is set at the recovery moment inside the reconcile loop itself, so a late IDB response can never pop a mid-session banner.
## 5. Return-shape trap — swept
`_coldWriteObj` has exactly **two** callers in the codebase (I grepped both the new and pre-change trees): `saveToSlot` (now checks `.ok`; its no-helper fallback IIFE returns the same shape) and `applyBundleData` (never consumed the return value, before or after). No bare-truthiness consumer exists; Suite 233.6 gate-locks the contract. Behavior verified in the gate: healthy write silent, one-store write posts one SYS line (once per session per mode, latch verified), total failure still loud.
## 6. No regression, nothing weakened
- Full save/load/slot/cloud/backup/import path checks in save-survival all green (PATH1–6, two-store boundary, fail-loud audits).
- Counts synced 3278→3292 / 233 suites everywhere Protocol 2a lists (I ran the drift scan; every live count reads 3292; the Suite 28 meta-guard reconciles the *executed* count against the canonical one at runtime — and passed).
- I read every removed line in the test diff: all are honest syncs (registry 159→165 for the 6 new shell tools, save-row template 2→3, `if (ok)`→`if (res && res.ok)`, the `_coldWriteObj` return-shape assertion updated to the new contract). No assertion was loosened.
- Protocol 1: cache `r42`, APP_VERSION stays 2.8.0 ✓. Protocol 4: the two new keys are META_MANIFEST device prefs (registered, string, default '') — correct home; they're telemetry records, never display gates ✓. Protocol 44: all 6 shell tools real; the two prod-tier banner tools are display-only (I read their action bodies — they only call the shower), plant + seam setters are staging/destructive ✓. Banner template is inert (`<template>`), message set via `textContent` (no XSS surface). Protocol 41: worktree clean, no junk.
## 7. Full gate — green
`npm run gate` run in full: Node runner **ALL 3292 TESTS PASSED**, Playwright boot-smoke, render-check, test.html runtime audit, all 21 LAYER3 save-survival checks, offline-first — **[GATE] All checks passed!**
---
## Residual findings (named precisely — neither blocks; both are pre-existing and strictly improved)
**F1 — a valid v7 can be quarantined under quota pressure or a `migrateState` bug.** In the v7 branch, `migrateState` *and* the `localStorage.setItem('robco_v8', …)` migration write sit inside the corrupt-catch. I reproduced it: force a QuotaExceededError on that setItem with a perfectly valid v7 → the v7 is quarantined (bytes preserved whole, exportable, banner shown) and the session boots fresh. **This is not new:** the identical fixture against the pre-change code **deleted the v7 outright with no copy**. The in-code comment shows this trade-off was deliberate ("a false positive here now quarantines — recoverable — rather than deletes"). It only touches the legacy v7→v8 bootstrap path, and the failure now ends in a recoverable envelope instead of loss. Recommended follow-up (not this unit): distinguish a write-side throw (quota) from a read-side one in that branch, or move the container write out of the corrupt-catch.
**F2 — banner false-negative when both localStorage quarantine legs fail.** If the quarantine envelope could only be held in IDB (localStorage refused both the primary key and the current boot's write), the *next* boot's banner check (ls key + in-memory stash) won't re-show the banner — but the saves-list QUARANTINED RECORD row still finds the envelope via its IDB fallback, so nothing is lost and the recovery affordance survives. Conservative-direction miss, consistent with the layer's stated design.
## Audit method note
Two of my own 48 harness assertions initially "failed" and both were my assertions, not the code: (a) I demanded a byte-identical *entire* localStorage on a healthy boot — the one write was the pre-existing rolling-backup copy; (b) I expected the old code's in-memory state to be fresh after the helper-throw — it deleted the *disk* copy while the session kept the merged state (the loss lands on the next reload, which is exactly why the bug was latent).
No repo files were modified during this audit (harness + old-code snapshot live in the session scratchpad); nothing was committed or pushed.
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).