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

SAVE_LAYER3_PLAN.md



RELEASE

planning/2.8.5/plans/SAVE_LAYER3_PLAN.md

sha256 9dd30cb4ee7a6380 · 26991 bytes · original held in the private archive

# Save-Integrity Layer 3 — Read-Side Fail-Loud — OPUS PLAN (Protocol 8, Stage 1) **Snapshot as of 2026-07-16 · branch `dev` @ `e74c795` · PLAN ONLY — no code changed.** Source: `planning/WARNING_SURFACE_INVENTORY.md` gaps #1, #2, #6 + the QUEUE.md "Save integrity — Layer 3" unit. Line numbers below are verified against `e74c795` but WILL drift — Sonnet must re-verify each anchor before editing (Protocol 8 stage 2). --- ## 1. Confirmed current behavior (Protocol 27 — traced, not guessed) ### 1a. The silent delete (gap #1) `_hydrateStateFromStorage()` — `js/ui/ui-core.js:351-435`, called synchronously from `window.onload` (ui-core.js:1189), after `await _hydrateMetaFromIdb()` (line 1188) and before everything else that reads state. The v8 path (lines 358–380), quoted: ```js if (v8Str) { try { window.robco_v8 = JSON.parse(v8Str); let activeCampaign = window.robco_v8.campaigns[window.robco_v8.activeContext] || {}; state = { ...state, ...activeCampaign }; state.gameContext = window.robco_v8.activeContext; if (typeof window._migrateEventLog === 'function') window._migrateEventLog(state); if (typeof reconcileEquipped === 'function') reconcileEquipped(state); loadedOk = true; } catch (e) { console.error('[RobCo] Corrupt robco_v8 — quarantined, booting fresh:', e); localStorage.removeItem('robco_v8'); // ← DELETES. Nothing is quarantined. } } ``` And the v7 path (lines 397–400): ```js } catch (e) { console.error('[RobCo] Corrupt robco_v7 — quarantined, booting fresh:', e); localStorage.removeItem('robco_v7'); // ← same delete-despite-the-comment } ``` Confirmed mechanism: the comment says "quarantined"; the code **destroys the bytes**. The rolling-backup snapshot taken just above (line 353 → `snapRollingBackup()`, state.js:579) can't save it either — its own `JSON.parse(_curV8 || '{}')` (state.js:598) throws on the same corrupt string and its catch `return`s (state.js:603-605) without writing anything. After the delete, `!loadedOk` falls into the fresh-boot branch (lines 402–414) and the user boots a blank campaign with zero explanation. No banner, no chat line, no `_recordError()` — the catch swallows it before the global error net (ui-core.js:95-123) can ever see it. **A second, subtler defect inside the same try (found during this diagnosis — Protocol 42 applies):** the catch wraps more than the parse. `window.robco_v8.campaigns[...]` (shape access), `_migrateEventLog(state)` and `reconcileEquipped(state)` are all inside it. A bug that makes either *migration helper* throw on a **perfectly valid save** currently deletes that save. Worse: by the time a helper throws, line 362 has already merged the campaign into `state`, so the fresh-boot branch (line 405: `campaigns: { FNV: state }`) re-wraps the merged data under FNV — silently discarding any *other* game's campaign that lived in the container. Layer 3 must fix this trigger precision, not just bolt a banner onto it (see §4, false-positive design). ### 1b. Eviction is undetectable after the fact (gap #2) - `robco_booted_before` is a registered device pref (META_MANIFEST, state.js:68), set to `'true'` inside `runBootSequence()` (ui-audio.js:1658). Every `MetaStore.set` shadows to the IDB `'meta'` store (`_idbShadow`, state.js:138). - `_reconcileMetaFromIdb()` (ui-core.js:269-330) runs at boot (awaited, bounded to 1000 ms — ui-core.js:338-349) and **recovers** any registered device key that IDB has but localStorage lacks (checksum-verified, lines 297-305). Its `{recovered, backfilled}` return is currently **discarded** (line 343) — nothing records *which* keys were recovered. - Crucially for ordering: recovery runs (line 1188) **before** `_hydrateStateFromStorage()` (line 1189), and `runBootSequence()` re-sets the marker much later (line 1226 → `_runBootSequenceAndBriefing`). So at hydrate time, the marker's localStorage presence/absence still reflects the *pre-boot* truth. - Today the `!loadedOk` fresh branch (lines 402–414) makes no distinction: a genuinely new user and a user whose browser reclaimed the origin's localStorage look identical. ### 1c. Degraded writes report full success (gap #6) `_coldWriteObj()` — `js/core/state.js:525-538`, quoted: ```js async function _coldWriteObj(lsKey, idbKey, obj) { let idbOk = false; try { if (window.IdbStore) idbOk = (await window.IdbStore.set('campaign', idbKey, obj)) === true; } catch (_) {} let lsOk = false; try { localStorage.setItem(lsKey, JSON.stringify(obj)); lsOk = true; } catch (_) { /* quota / private-mode — non-fatal; the IDB copy is the durable home */ } return idbOk || lsOk; // ← divergence is erased here } ``` `saveToSlot()` (ui-saves.js:265-295) consumes that boolean: `if (ok)` → plain success chat line `> [SAVE] SLOT n [ctx] written at …` (line 281); `else` → the loud total-failure line (line 294). The two degraded modes — **(a)** localStorage mirror dropped (quota/private mode; IDB holds) and **(b)** IDB write failed (localStorage holds; the save has NO durability shadow and no ceiling relief) — both collapse into `true` and report identical full success. Confirmed callers of `_coldWriteObj`: `saveToSlot` (ui-saves.js:267, consumes return) and `applyBundleData` (state.js:2464, **ignores** return). --- ## 2. ⭐ Every boot / load / write path, with intended Layer-3 behavior **Load/boot paths (13):** | # | Path | Today | Layer-3 intended behavior | |---|------|-------|---------------------------| | L1 | **Fresh first boot** (no v8, no v7, no `robco_booted_before` anywhere) | fresh state | Unchanged. **NO banner**, no quarantine key, no pref writes beyond existing. | | L2 | **Valid `robco_v8`** (the 99.9% path) | loads | **Untouched, byte-identical load. NO banner. NO new reads/writes on this path except the same `getItem('robco_v8_quarantine')` existence check** (one cheap read; see L8). This is the hard invariant path — see §4. | | L3 | **Valid `robco_v7` → v8 migration** (legacy) | migrates | Unchanged. NO banner. `migrateState` succeeding = valid save. | | L4 | **Corrupt/unparseable `robco_v8`** (parse or container-shape failure) | console + **delete** → silent fresh | Quarantine the raw string (§5), THEN remove `robco_v8`, show the READ FAULT banner (trigger 1), `_recordError('error', …)` → FAULT lamp, record `robco_read_fault` pref. Boot continues into the v7-fallback/fresh branch exactly as today — never blocked. | | L5 | **Corrupt `robco_v8` + valid `robco_v7` present** | v7 loads, corrupt v8 deleted silently | v7 still loads (unchanged flow). Banner STILL shows — the user's newer campaign regressed to an old one; that must not be silent. Quarantine holds the v8 bytes. | | L6 | **Corrupt `robco_v7`** (no v8) | console + delete → silent fresh | Same treatment as L4: quarantine (envelope records `sourceKey: 'robco_v7'`), banner trigger 1, `_recordError`. | | L7 | **Eviction signature**: `robco_booted_before` was ABSENT from localStorage pre-reconcile, RECOVERED from IDB this boot, AND no v8 AND no v7 | indistinguishable from L1 | Banner trigger 2 (EVICTION DETECTED), `_recordError`, record `robco_eviction_detected` pref. No quarantine (there's nothing to quarantine). Fresh boot proceeds unchanged. | | L8 | **Boot after a quarantine** (no v8; marker PRESENT in localStorage; `robco_v8_quarantine` exists) | n/a (new) | NOT the eviction signature (marker was never absent — see §4). READ FAULT banner **re-shows** while the quarantine key exists (live-condition re-display, the Layer 2 precedent), until the user EXPORTs+PURGEs it from the saves list. No re-quarantine. | | L9 | **Cross-game switch** (`onGameContextChange`, ui-core-modulebay.js:680-694: persist-then-reload, `_contextSwitching` guard) | valid-v8 reboot | Unchanged — lands on L2 next boot. Guards untouched. | | L10 | **Slot load, same game** (`_applySlotEnvelope` in-place apply, ui-saves.js:459-475 — no reload) | in-place | Untouched by Layer 3 (no boot read involved). | | L11 | **Slot load cross-game / backup restore / file import / cloud pull / cloud version restore** (all persist-then-reload with `_loadingSave`: ui-saves.js:448, 687, 744, 845; cloud.js:876, 1014) | valid-v8 reboot | Unchanged — they write a fresh valid v8 and land on L2. The `_loadingSave` guard is untouched. NO banner. | | L12 | **IDB wholly absent** (blocked/private) | reconcile no-ops | Corrupt quarantine still works (localStorage leg + in-memory fallback, §5). Eviction detection silently unavailable (no recovery possible) — fail-quiet, never fail-noisy. Tail rider (§8) may compound the Layer-2 banner copy. | | L13 | **Slow IDB — the 1000 ms reconcile budget expires** (ui-core.js:338-345) | boot proceeds; recovery may finish late | Eviction check reads the recovery *stash*, set only when reconcile actually completed → late recovery = **no banner this boot** (false negative, the conservative direction). Never a late banner popping mid-session. | **Write paths (5):** | # | Path | Today | Layer-3 intended behavior | |---|------|-------|---------------------------| | W1 | **Debounced autosave** (`saveState`, state.js:2226-2280 — localStorage only, by design) | already loud on quota (SYS-WARNING at ~4MB, SYS-ALERT on QuotaExceeded) | **Unchanged.** Single-store by design; not a two-store divergence. | | W2 | **beforeunload flush** (ui-core.js:1147-1165) | deliberate console-only (no UI possible during unload; inventory: "not a gap") | **Unchanged.** | | W3 | **Slot save** (`saveToSlot` → `_coldWriteObj`) | divergence erased → false full success | `_coldWriteObj` returns `{ ok, idbOk, lsOk }`; `saveToSlot` posts a one-per-session-per-mode SYS line on divergence (§6). Total failure stays exactly as loud as today. Full success stays exactly as quiet as today. | | W4 | **Rolling backup ring** (`snapRollingBackup`, state.js:579-633) | ls-quota downgrade console-only; IDB copy retained | **Deliberately out of scope.** The ring is a device-local ephemeral safety net taken automatically before every load; a notice on each degraded snapshot would be noise, and its ls-quota case already retains the IDB copy. QUEUE scopes gap #6 to *slot* writes. Stated here so the scoping is a decision, not an omission. | | W5 | **Bundle-import slot writes** (`applyBundleData`, state.js:2464 — return ignored) | ignores result | Unchanged behavior (still ignores the result — now an object, still truthy-compatible where unused). Listed because the return-shape change must be audited at every call site (§6 hazard). | Mutual exclusion note: triggers 1 and 2 can never co-fire — trigger 1 requires a `robco_v8`/`robco_v7` blob to exist; trigger 2 requires both absent. The Layer-2 storage banner CAN stack with either (both `prepend` to body) — covered in verification (§9). --- ## 3. Design — the banner (one template, two triggers) **Template** (index.html, next to `storageWarningBannerTemplate` at ~line 162): a new `<template id="readFaultBannerTemplate">` containing `<div id="readFaultBanner" class="storage-warning-banner" role="alert">` with an inner `<span id="readFaultBannerMsg">` message node. Reuses the existing `.storage-warning-banner` CSS class (Protocol 22 — same look, no new CSS unless Fable wants a variant tint). Static-guarded inert (§9). **Shower** (ui-core.js, sibling of `_showStorageWarningBanner` at line 444): `_showReadFaultBanner(kind)` — clone, set the message node's `textContent` from one of two module-level string constants, `display:flex`, click-to-remove, `document.body.prepend`, whole body in `try {} catch (_) { /* a warning banner must never break boot */ }`, idempotent via `getElementById` check. Byte-for-byte the Layer-2 idiom. **Wording direction** (Fable polishes per Protocol 8 — direction only, game-agnostic per Protocol 38, no code identifiers per Protocol 21 spirit): - Trigger 1: `> MEMORY CORE READ FAULT — PRIOR CAMPAIGN DATA COULD NOT BE READ AND HAS BEEN QUARANTINED, NOT ERASED. RECOVER OR EXPORT IT FROM THE ARCHIVE BAY. (TAP TO DISMISS)` - Trigger 2: `> MEMORY CORE EVICTION DETECTED — THE HOST RECLAIMED LOCAL DATA. PRIOR SLOTS AND BACKUPS MAY SURVIVE IN COLD STORAGE — CHECK THE SAVES LIST OR IMPORT A SAVE FILE. (TAP TO DISMISS)` **Dismissal / re-display:** - Both: tap-to-dismiss, session-only (Layer 2 precedent). - Trigger 1 re-shows **every boot while `robco_v8_quarantine` exists** — an unrecovered campaign-loss artifact is a live condition, same rationale as the Layer 2 banner re-showing while persistence is denied. PURGE (below) ends it. - Trigger 2 fires **once by nature** — the signature only exists on the boot where recovery happened; the recovered marker is back in localStorage afterward. **Device prefs (META_MANIFEST, state.js — telemetry records, Protocol 23; NOT display gates):** `robco_read_fault` (string, default `''`, owner ui-core.js — timestamp of last quarantine event) and `robco_eviction_detected` (string, default `''`, owner ui-core.js — timestamp of last detected eviction). Inspectable via the Diagnostic Shell / error log; mirror the `robco_storage_persisted` pattern. **FAULT surface:** both triggers call `_recordError('error', …)` (ui-core.js:73) so the casing FAULT lamp and Service & Fault Console reflect a campaign-loss event — the inventory's rec (c). `_recordError` is already boot-safe (self-wrapped, never throws). --- ## 4. ⭐ The hard invariant + false-positive avoidance (Protocol 33) **Invariant: Layer 3 must never break loading a good save, never block/black-screen boot, and never become a new failure mode.** How each piece holds it: 1. **The valid-save path is near-untouched.** On L2 the only additions are: the (already-happening) reconcile-result stash, and one `localStorage.getItem('robco_v8_quarantine')` existence check for L8 re-display. No new parse, no new write, no reordering of `window.onload`. The banner code path is not even entered. 2. **Trigger 1 is narrowed, not widened.** Today's catch is *too broad* (§1a): it deletes a valid save if a post-merge migration helper throws. The new structure splits the try: - **Outer (quarantine trigger):** `JSON.parse` + container-shape/campaign access + the state merge — a failure here genuinely means the stored bytes are unreadable. - **Inner (fail-soft, NO quarantine, NO banner-1):** `_migrateEventLog(state)` and `reconcileEquipped(state)` each get their own `try/catch` → `console.error` + `_recordError`, then `loadedOk = true` proceeds with the un-helped state. A helper bug now degrades one nicety instead of destroying the campaign. (v7 path: `migrateState` stays inside the outer try — it is integral to loading a v7 at all, and a false-positive there now *quarantines* rather than *deletes*, which is strictly recoverable.) - Result: a **valid-but-old** or **valid-but-migrating** save can no longer be quarantined by anything except an actual unreadable/unshaped blob — and even a false positive preserves every byte for recovery. Deletion is gone from the codebase. 3. **Trigger 2 requires an affirmative three-part signature, defaulting to silence.** `robco_booted_before` **absent from localStorage pre-reconcile** AND **actually recovered from IDB this boot** (a stash set inside `_reconcileMetaFromIdb`'s recovery loop — not a later re-read) AND **no v8 and no v7**. This kills the known false positives: - *First-ever boot:* no marker anywhere → silent (L1). - *User opened the app once and the tab was killed before any save was written* (mobile swipe-away — `beforeunload` often doesn't fire): the marker IS in localStorage → not recovered → silent. This is why "recovered from IDB" is required rather than "marker is truthy." - *Post-quarantine boots (L8):* marker present in localStorage → silent on trigger 2. - *User deleted only `robco_booted_before` in devtools:* recovery fires but v8 exists → silent. - *Slow IDB (L13):* budget expiry → stash unset → silent this boot. False **negatives** are the accepted cost; the signal never fires without positive evidence. 4. **Everything new is wrapped.** Quarantine write, banner clone, pref records, `_recordError` — each in its own `try/catch` mirroring `_showStorageWarningBanner`. A quota failure while *writing the quarantine* degrades (IDB copy → in-memory copy → banner still shows) and can never abort boot. 5. **Nothing blocks.** All new boot work is synchronous-cheap (string capture, one setItem, one getItem) or fire-and-forget (the IDB quarantine write). No new awaits in `window.onload`. --- ## 5. Design — the quarantine **Where the blob goes.** On outer-catch: capture `v8Str` (already in memory) → build envelope `{ quarantinedAt: Date.now(), sourceKey: 'robco_v8'|'robco_v7', reason: String(e).slice(0,300), raw: <the exact corrupt string> }` → then, in order: 1. `localStorage.removeItem('robco_v8')` **first** (frees the corrupt blob's own bytes so the quarantine write below fits in roughly the same footprint — writing before removing would double the footprint and likely quota-fail), 2. `try { localStorage.setItem('robco_v8_quarantine', JSON.stringify(envelope)) } catch` — best-effort, 3. fire-and-forget `window.IdbStore.set('campaign', 'quarantine', envelope)` — the durable, ceiling-free home (campaign-side data belongs in the `'campaign'` store, never `'meta'` — Protocol 23 two-store boundary), 4. if BOTH failed, stash `window._quarantinedEnvelope = envelope` (in-memory) so the export affordance still works this session; the banner shows regardless. **Size bound.** The raw string is at most what localStorage already held (~5MB) — the remove-then-write ordering means no net localStorage growth; IDB has no meaningful ceiling. No truncation of `raw` ever (truncated quarantine = destroyed data with extra steps); only `reason` is capped. **Second corrupt boot.** The localStorage key holds the **first unresolved** quarantine and is never overwritten while it exists — the first corruption is almost certainly the long-lived campaign; a later one is at most a short-lived fresh campaign. A subsequent corruption while a quarantine exists writes only to IDB under a stamped key (`quarantine_<ts>`), swept to keep the newest 3 stamped entries. (Sonnet may simplify to "subsequent quarantines go to IDB only, unbounded is fine at n≤3" if the sweep proves fussy — the non-negotiable is: **never overwrite an existing unresolved quarantine, never delete corrupt bytes without a preserved copy**.) **Recovery/export affordance.** The SAVES LIST (`renderSavesList()`, js/ui/ui-account.js:108, which already renders local slots) gains a "QUARANTINED RECORD — <date>" row when the key (or the in-memory stash) is present, with two controls: - **EXPORT** — downloads the envelope as `robco_quarantine_<date>.json` via the same data-URI pattern `exportSaveFile` uses (state.js:2306-2309). Non-destructive; does not clear the banner condition. - **PURGE** — `confirmAction()`-gated (Protocol 34 destructive-op rule), removes the localStorage key + IDB `quarantine*` entries; this is what retires the L8 re-display. The banner's "RECOVER OR EXPORT IT FROM THE ARCHIVE BAY" copy points here — a durable affordance, not a button crammed into a dismissable banner. --- ## 6. Design — degraded-write notice (gap #6) **`_coldWriteObj` return shape** changes from `boolean` to `{ ok: idbOk || lsOk, idbOk, lsOk }`. ⚠ **Truthiness hazard (the one real regression trap in this unit):** an object is *always* truthy — any caller left doing `if (await _coldWriteObj(...))` would report success on total failure. Mitigation: (a) update **every** call site in the same commit — confirmed sites today: `saveToSlot` (ui-saves.js:267, the only consumer → switch to `res.ok`) and `applyBundleData` (state.js:2464, result unused → no change needed, but verified); Sonnet re-greps `_coldWriteObj` before editing; (b) `saveToSlot`'s no-`_coldWriteObj` fallback IIFE (ui-saves.js:268-275) returns the same object shape; (c) a static gate test locks the contract (§9). **The notice** — posted from `saveToSlot` (the UI layer — keeps state.js free of chat concerns beyond its existing precedent; Protocol 23), after the success line, once per session per mode via a module-level `{ lsOnly: false, idbOnly: false }` latch: - `lsOk && !idbOk` → `> [SYS] COLD STORAGE UNAVAILABLE — SLOT HELD IN LOCAL MEMORY ONLY.` - `idbOk && !lsOk` → `> [SYS] LOCAL MIRROR FULL — SLOT HELD IN COLD STORAGE ONLY. EXPORT A SAVE FILE WHEN CONVENIENT.` - Both ok → exactly today's plain success, no extra line. Total failure → today's `[ERROR]` line, unchanged. It's a transient chat line, not a banner — the save DID persist (QUEUE's explicit call). The success line still renders first so the user's mental model stays "saved, with a caveat." --- ## 7. Diagnostic Shell triggers (Protocol 44 — same commit, mandatory) These are exactly the hard-to-reproduce boot conditions Protocol 44 exists for. Register in `DIAGNOSTIC_SHELL_TOOLS` (js/dev/test-console.js): 1. **FORCE READ-FAULT BANNER** / **FORCE EVICTION BANNER** — call `_showReadFaultBanner('corrupt'|'evicted')` directly. Display-only, writes nothing durable → `tier: 'prod'`-eligible; `triggers: ['readFaultBanner']` (+ the pref names). 2. **PLANT CORRUPT CONTAINER** — writes a garbage `robco_v8` and prompts reload for a true end-to-end repro. Destroys the live campaign container → `tier: 'staging'`, `destructive: true` (Protocol 44 tiering — confirm by reading the write, not assuming). 3. **SIMULATE DEGRADED SLOT WRITE** — staging tool that flips a transient `window.__robcoForceColdWriteMode = 'no-idb'|'no-ls'` test seam consumed by `_coldWriteObj` (mirrors `window.__robcoBootFlavor`, ui-audio.js:1529), so the SYS lines are demonstrable on demand. `tier: 'staging'` (the subsequent save writes campaign data). The Protocol 44 gate suite cross-references `triggers` — new flags (`robco_read_fault`, `robco_eviction_detected`) must be covered or allowlisted; plan is to cover via tool metadata, not allowlist. --- ## 8. Tail rider (QUEUE's parenthetical — small, include) When `window.IdbStore` is null/absent AND the persist result is `denied`, the Layer-2 banner's condition is compounded: extend `_requestPersistentStorage`'s denied branch to append `— AND COLD STORAGE OFFLINE` to the banner message node (or clone with the extended string), and record `robco_storage_persisted = 'denied-noidb'`-style detail **only if** Sonnet confirms nothing consumes the exact `'denied'` string (grep first; if anything does, keep the pref value and add a second pref). ~5 lines; ship with this unit per QUEUE. --- ## 9. Files touched, tests, verification **Files:** | File | Change | |---|---| | `index.html` | new `readFaultBannerTemplate` (served file → **Protocol 1 cache bump** in `sw.js`) | | `js/ui/ui-core.js` | split-try quarantine logic in `_hydrateStateFromStorage`; `_showReadFaultBanner`; eviction check; recovery stash in `_reconcileMetaFromIdb`; tail rider in `_requestPersistentStorage` | | `js/core/state.js` | `_coldWriteObj` return shape; 2 new META_MANIFEST keys | | `js/ui/ui-saves.js` | `saveToSlot` degraded notice + `res.ok`; quarantine export/purge helpers | | `js/ui/ui-account.js` | quarantine row in `renderSavesList` | | `js/dev/test-console.js` | 3–4 Protocol 44 tools | | `sw.js` | CACHE_NAME bump | | tests + docs | below / Protocol 2, 2a | Non-ASCII (`—`, `⚠`) rides in banner copy → **Protocol 39: Edit tool / Node writes only, never PowerShell.** **Behavioral tests (the QUEUE hard rule) — `tests/save-survival.mjs` (full push gate, scripts/gate.js:272), new sections:** 1. **Corrupt boot:** seed `robco_v8 = '{corrupt'` via addInitScript → boot → assert: banner visible with READ FAULT copy; `robco_v8_quarantine.raw` equals the exact seeded string; `robco_v8` removed; boot completed (bootScreen cleared); error-log entry present. 2. **Valid boot (the invariant branch):** seed a valid container → assert state loaded, **NO banner, NO quarantine key**. 3. **Eviction:** boot once normally (marker + IDB shadow established) → `localStorage.clear()` only → reload → assert EVICTION banner + pref recorded. 4. **Eviction false-positive branch:** remove only `robco_v8` (marker left in localStorage) → reload → assert **NO banner**. 5. **Corrupt-v8 + valid-v7 (L5):** assert v7 campaign loaded AND banner shown. 6. **Degraded writes:** stub `IdbStore.set` to reject → `saveToSlot` → assert COLD STORAGE UNAVAILABLE line, and that a second save posts no duplicate; stub `localStorage.setItem` to throw for the slot key → assert LOCAL MIRROR FULL line; unstubbed save → assert plain success only, total-failure stub → assert today's `[ERROR]` line unchanged. **Static gate suites (`tests/robco-diagnostics.js`, Protocol 20/36b):** template-inert guard (mirror Suite 217.7b/c); a source guard that the corrupt-catch performs a quarantine capture before any `removeItem` (locks "deletion can never silently return"); `_coldWriteObj` contract guard (returns object; `saveToSlot` checks `.ok`); META_MANIFEST registration; Protocol 44 trigger coverage. **Protocol 2a count sync across all listed locations in the same commit; Protocol 13/42 satisfied by the suites above.** **Render verification (Protocol 10/17):** force each banner (Shell trigger or init script) at 360 / 412 / ≥1000px — no horizontal overflow, ≥28px tap target, banner+Layer-2-banner stacked case checked; `tests/render-check.mjs` run alongside. **Risk/regression notes:** the `_coldWriteObj` truthiness hazard (§6 — the one change that could *create* a false success; mitigated by call-site sweep + contract test); banner stacking layout; `window.onload` order untouched (load-bearing per ui-core.js:1172-1177); no auth surface (Protocols 29–31 not implicated); no new state field (Protocol 4 n/a — MetaStore keys only); no cloud writes (Protocol 34 touched only by the confirm-gated PURGE); UX additive-only (Protocol 25). --- ## 10. Self-audit against §2 (Protocol 8 plan-audit) Every path L1–L13 and W1–W5 has a stated intended behavior; L2 (valid save) is explicitly near-zero-touch; both false-positive families (helper-throw on valid save → fixed by the try split; marker-present-no-save → excluded by the recovered-from-IDB requirement) have both a design answer and a behavioral test; both banner triggers, the no-banner valid branch, the eviction false-positive branch, and both degraded-write modes are test-asserted; every deletion site is replaced by capture-then-remove. Open items deliberately left to stage 2 judgment: the stamped-IDB-quarantine sweep detail (§5) and the tail-rider pref value (§8) — both flagged inline with their constraint stated.
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).