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

WARNING_SURFACE_INVENTORY.md



RELEASE

planning/2.8.5/audits/WARNING_SURFACE_INVENTORY.md

sha256 bd29c565d9090c65 · 21831 bytes · original held in the private archive

# Warning-Surface Inventory — silent failures that should warn the user **Snapshot as of 2026-07-16 · branch `dev` @ `d2329da` · ANALYSIS ONLY — no code changed.** ARCHIVE-class planning doc (3-class library model): reasoning is reusable, measurements are point-in-time. The owner's question, in one line: the "MEMORY CORE UNSTABLE" banner proves the app CAN warn about a detected platform risk — what else hits a risky or degraded condition and stays silent? --- ## 1. The storage banner — mechanism confirmed against the code **Confirmed. It works exactly as described, with one nuance.** - **Trigger:** at boot, `window.onload` calls `_requestPersistentStorage()` (js/ui/ui-core.js, lines 471–493) fire-and-forget, right after the campaign hydrates. It feature-detects `navigator.storage`, checks `persisted()` first, then calls `persist()`. The result (`'granted'`/`'denied'`) is recorded to the `robco_storage_persisted` device pref (MetaStore — a browser property, never campaign state, Protocol 23). - **On `denied` only:** `_showStorageWarningBanner()` (lines 444–459) clones the hidden `<template id="storageWarningBannerTemplate">` (index.html, lines 162–172) into the top of `<body>`. The banner reads: **"> MEMORY CORE UNSTABLE — HOST WILL NOT GUARANTEE PERSISTENCE. EXPORT A SAVE FILE REGULARLY. (TAP TO DISMISS)"** — diegetic, game-agnostic, `role="alert"`, tap to dismiss. - **Safety properties:** idempotent (second call is a no-op), fully wrapped (a warning banner must never break boot), never shown on granted/unknown, and the whole check can never throw into or block boot. - **Nuance (accurate behavior, slightly different from the code comment):** the banner re-appears on **every** boot while the result is denied — dismissal is session-only. The MetaStore record is a telemetry record; it does not gate re-display. That's arguably the right behavior for a live risk, but worth knowing it's per-boot, not once-ever. - **Tests:** `tests/save-survival.mjs` (lines ~1175–1235) drives the real page with `persist()` mocked to denied and to granted, and asserts the banner renders only on denied and the pref records both ways. `tests/robco-diagnostics.js` (line ~40224, Suite 217.7b/c) statically guards that the banner markup lives inert inside the template — it can never render by accident. **The reusable pattern this establishes:** a hidden diegetic banner `<template>` in index.html (inert by default, guarded inert by a static test) + a boot-time or event-time detector that clones it in only when the risk is real + a device-pref record of the result + tap-to-dismiss + a behavioral test of both branches. Every recommendation below that says "banner" means: reuse exactly this pattern. --- ## 2. Every user-facing notice mechanism that already exists These are the tools available for reuse — nothing below requires inventing a new surface. 1. **Hidden banner templates** (index.html): `storageWarningBannerTemplate` (live, above); `updateBannerTemplate` (non-blocking amber strip, currently a kept-for-future spare); `fo3WarningBannerTemplate` (the "data systems not yet active" notice — currently unmounted; reserved for FO4's "no data yet" selection per QUEUE 3.0, and named as the info element for the UX-clarity pass). 2. **The blocking update modal** — `#updateModal` + `_triggerUpdate()` (index.html script): the "REBOOT TERMINAL" prompt when a new service worker is genuinely waiting; tap posts SKIP_WAITING. 3. **`openModal({title, body})`** — the diegetic result modal used at point-of-use everywhere in the cloud path (SAVE TO CLOUD results, offline-queue notices, sign-in prompts, key validation). 4. **`confirmAction()`** — the confirm-gated warning modal for destructive/risky proceeds (slot delete/overwrite, checksum-mismatch "LOAD ANYWAY", older-save and future-version warnings). 5. **`appendToChat(…, 'sys')` SYS-ALERT transcript lines** — the terminal's own voice: quota warnings, AI faults, rate-limit notices, feature auto-disable messages, the remote operator broadcast from `/config/flags`. 6. **The `#locationCard` top-right toast** — transient arrival/confirmation card (body-level, reused by the faction path; the sanctioned single toast component). 7. **The global error net + FAULT surface** (ui-core.js lines ~73–125 + ui-core-chassis.js ~297–350): uncaught errors and unhandled rejections post a "> ⚠ SYSTEM FAULT — … save your data and reload." chat line, record to the `robco_error_log` ring buffer, light the casing **FAULT lamp**, and feed the Service & Fault Console. **Important limitation: anything caught in a `try/catch` never reaches this net** — every silent spot in §3 is deliberately caught, so none of them light the lamp today. 8. **The breaker rack** (CHASSIS SYSTEM STATUS, `renderSystemStatus()`): a passive per-flag readout of every remote kill-switch flag. 9. **Point-of-use disabled-feature messages:** every kill-switched feature says so when used ("CLOUD SYNC TEMPORARILY UNAVAILABLE", "DIRECTOR LINK TEMPORARILY DISABLED BY OPERATOR"). 10. **Account-panel status strings** (ui-account.js): "UPLINK OFFLINE — ARCHIVES STORED LOCALLY", "NO CARRIER", "RETRIEVING ARCHIVES…", and an "⚠ ARCHIVE LINK FAILED" state (which — see Gap 3 — is currently unreachable). **Already warning correctly (verified — no gap):** live-save quota (proactive ~4MB SYS-WARNING + SYS-ALERT on actual QuotaExceeded, state.js ~2250–2277); total slot-write failure ("[ERROR] Save slot write failed — storage unavailable."); AI network/HTTP/rate-limit/key-rejected faults (api.js retry ladder, all chat-surfaced); missing narrative+modal nodes (SYS-ALERT); feature auto-disable at threshold (aiChat, cloudSync, visualOcr all pass a user-facing message); checksum/version mismatch on every load path (confirm-gated); the offline cloud-push queue (modals both ways + reconnect upload notice); OCR failure → visible "ROUTING TO DIRECTOR VISION…" fallback; missing API key at transmit (the FATAL EXCEPTION line). --- ## 3. The gaps — silent or degraded conditions with no user-facing signal Ranked most user-impactful first. Each entry: what happens today → exact rec → queue placement (§4 has the summary). ### Gap 1 — A corrupt live campaign is DELETED at boot and the app silently starts fresh ⚠ (worst gap) - **Today:** `_hydrateStateFromStorage()` (ui-core.js lines 376–379): if `robco_v8` fails to parse, the catch logs `console.error('Corrupt robco_v8 — quarantined, booting fresh')` and calls `localStorage.removeItem('robco_v8')`. Despite the word "quarantined," the blob is **removed, not preserved** — and the rolling-backup snapshot taken just before (line 353) also bails on unparseable data, so the corrupt bytes are gone. The user boots into a blank campaign with **zero explanation** — no banner, no chat line, not even a FAULT-lamp record. Same pattern for `robco_v7` (line 397–400). The shipped save-integrity Layer 1 set the fail-loud bar for **writes**; this is the un-covered **read** side of the same contract. - **Rec:** (a) actually quarantine — copy the raw corrupt string to a `robco_v8_quarantined` key (and/or the IDB campaign store) before removing it, so recovery/diagnosis is possible; (b) clone in a persistent diegetic banner via the storage-banner pattern — direction: "> MEMORY CORE READ FAULT — PRIOR CAMPAIGN DATA COULD NOT BE LOADED AND HAS BEEN QUARANTINED. CHECK SAVED SLOTS / VERSION HISTORY. (TAP TO DISMISS)"; (c) route through `_recordError()` so the FAULT lamp and Service & Fault Console reflect it. Severity: **persistent banner** — this is a campaign-loss event. - **Placement: GENUINELY-NEW** → the near-term **save-integrity follow-up** (a "Layer 3" sibling of shipped QUEUE item 5 — read-side fail-loud). Not covered by the 2.9.0 bootstrap-isolation item: that isolates boot-phase *throws*; this catch never throws. ### Gap 2 — Storage eviction is never detected after the fact - **Today:** Layer 2 warns *ex-ante* that eviction MAY happen (persist denied). But when eviction actually HAPPENS (iOS ~2-week/low-storage reclaim — the exact scenario Layer 2 exists for), boot finds no `robco_v8` and silently starts a fresh campaign. Nothing distinguishes "new user" from "your campaign was evicted." - **The detection signal already exists:** `robco_booted_before` (MetaStore, set on first boot) is shadow-mirrored to IndexedDB and **recovered from IDB by `_reconcileMetaFromIdb()` when localStorage is wiped**. So at boot: `robco_booted_before === true` (recovered) + no `robco_v8` + no v7 = an eviction signature, essentially free. (IDB slot/backup copies may also have survived — worth saying so in the notice.) - **Rec:** in the `!loadedOk` fresh-boot branch, check that signature and clone in the same banner family — direction: "> MEMORY CORE EVICTION DETECTED — THE HOST RECLAIMED LOCAL DATA. PRIOR SLOTS/BACKUPS MAY SURVIVE IN COLD STORAGE — CHECK THE SAVES LIST, OR IMPORT A SAVE FILE." Severity: **persistent banner** + `_recordError()`. - **Placement: GENUINELY-NEW** → same **save-integrity follow-up** unit as Gap 1 (one banner mechanism, two triggers). This is the prompt's "detected save-eviction on boot" lead — confirmed absent today. ### Gap 3 — A failed cloud-archive fetch masquerades as "NO ARCHIVES ON FILE" - **Today:** `listCloudSaves()` (cloud.js lines 656–659) swallows every Firestore error and returns `[]`. The SAVES LIST's own failure UI ("⚠ ARCHIVE LINK FAILED", ui-account.js lines 195–199) is therefore **unreachable** — its try/catch can never fire. A user whose cloud fetch fails sees the same "NO ARCHIVES ON FILE" as a user with genuinely zero saves. That's the scariest possible mislabel: it reads as "your cloud saves are gone." - **Rec:** make the failure distinguishable — have `listCloudSaves` rethrow (or return a failure sentinel) so the already-written ARCHIVE LINK FAILED state actually renders. No new UI needed; this is reconnecting an existing notice. Severity: **existing panel state** (plus its summary line). - **Placement: GENUINELY-NEW, tiny** — a near-term small fix (fits naturally in the same follow-up batch as Gaps 4–5; a truth-in-reporting trio). The 2.9.0 cloud audit is round-trip field coverage, not this. ### Gap 4 — "SYNC COMPLETE" can be false - **Today:** `syncLocalSavesToCloud()` (cloud.js lines 761–785): a per-item upload failure is `console.warn` only, then the summary modal reports "SYNC COMPLETE — N uploaded, M already synced" with **failures silently omitted**. 2 of 4 fail → the user is told sync completed. - **Rec:** count failures and report them in the same modal ("2 FAILED — RETRY LATER"), and don't say COMPLETE when the count is nonzero. Severity: **existing modal**, one line. - **Placement: GENUINELY-NEW, tiny** — same near-term batch as Gap 3. ### Gap 5 — A real Google sign-in failure shows nothing - **Today:** `signInWithGoogle()` (cloud.js lines 383–388): user-cancelled popups are rightly silent, but any *real* failure (network, blocked popup, provider error) is `console.warn` only. The user taps SIGN IN and nothing visibly happens — the r54-class "silently broken sign-in" experience, in miniature. The credential-fallback failure (line 380) and `signOutAccount()` failure are silent the same way. - **Rec:** on non-cancel errors, `openModal` — direction: "> SIGN-IN FAILED — UPLINK COULD NOT AUTHENTICATE. CHECK POPUP BLOCKING / CONNECTION AND RETRY." Severity: **modal at point-of-use**. (Any change here re-triggers Protocol 29 real-device mobile verification.) - **Placement: GENUINELY-NEW, tiny** — same near-term batch as Gaps 3–4. ### Gap 6 — Degraded slot writes report full success (the confirmed ui-saves.js ~232 lead) - **Today — confirmed:** `_coldWriteObj()` (state.js lines 525–538) returns true if **either** store accepted the write, and `saveToSlot()` reports plain success on that basis. Two silent degraded modes: (a) the localStorage mirror dropped under quota (the IDB copy holds — the documented "no longer fatal" case; **the user is never told the fallback fired**); (b) the **IDB write failed** and only localStorage took it — meaning the save has *no* durability shadow and no ceiling relief, silently. `snapRollingBackup()` similarly downgrades a double-failure to `console.warn` (state.js line 629). Total failure IS surfaced; partial failure is not. - **Rec:** have `_coldWriteObj` report *which* stores accepted; on divergence, post a one-time-per-session SYS chat line (not a banner — the save did persist) — direction for (a): "> [SYS] LOCAL MIRROR FULL — SLOT HELD IN COLD STORAGE ONLY. EXPORT A SAVE FILE WHEN CONVENIENT."; for (b): "> [SYS] COLD STORAGE UNAVAILABLE — SLOT HELD IN LOCAL MEMORY ONLY." Severity: **transient chat line**, once per session per mode. - **Placement: GENUINELY-NEW** → the **save-integrity follow-up** (it is literally the residual of shipped Layer 1's fail-loud write bar, which covered total failure but not divergence). ### Gap 7 — The AI's state update can silently fail while the narrative plays (Protocol 24) - **Today:** `autoImportState()`'s outer catch (api-import.js lines 859–861) is `console.error('Auto-import failed')` only. The narrative has already rendered — so the user reads the story, believes the sync happened, and the campaign state silently didn't change. Same file: the registry/game-context mismatch guard (line 690) silently skips five whole field families with only a console.error, and the [DELTA] reporter swallows its own errors. - **Rec:** in that catch, post a SYS-ALERT chat line — direction: "> ⚠ SYNC FAULT — DIRECTOR STATE UPDATE REJECTED. NARRATIVE RETAINED; CAMPAIGN STATE UNCHANGED. RE-SYNC OR EDIT MANUALLY." — plus `_recordError()` so the FAULT lamp lights. Same treatment (a one-line "REGISTRY MISMATCH — TRACKED-ITEM FIELDS SKIPPED THIS SYNC") for the registry-mismatch skip. Severity: **transient chat line** in the transcript where the user is already looking. - **Placement — honest call: GENUINELY-NEW as a named criterion.** The task brief assumed the 2.9.0 hardening gate already covers "Protocol 24 surfacing bad AI output" — **it does not**: the gate's four items (cycle inversion, bootstrap isolation, event-bus hardening, the stray interval) plus post-deploy truth never mention the AI import path, and the diegetic audit only restyles error states that already exist. Recommend adding this as an explicit bullet under the hardening gate's fail-loudly umbrella (it's boundary-hardening work, exactly that gate's character) — or, cheaper, into the same near-term follow-up since it's ~3 lines. ### Gap 8 — IndexedDB wholly unavailable = the durability layer silently absent - **Today:** idb.js is fail-safe by construction — absent/blocked/quota-full IDB degrades the app to pure-localStorage with every op a soft no-op. Correct engineering, but silent: a user in this state has no cold store, no oversized-save relief, no pref recovery — and if `persist()` was ALSO denied, they're in the maximum-risk configuration with only the generic banner. - **Rec:** cheap composite check at boot: if IdbStore resolved null AND persist was denied, the existing MEMORY CORE UNSTABLE banner's condition is compounded — either extend its copy ("…AND COLD STORAGE OFFLINE") or record a distinct pref for the Diagnostic Shell / System Status readout. Severity: **fold into the existing banner**, no new surface. - **Placement: GENUINELY-NEW, minor** → tail item of the save-integrity follow-up. ### Gap 9 — Corrupt slot envelopes and corrupt chat history silently vanish - **Today:** `listLocalSaves()` (ui-saves.js lines 204–224) drops an unparseable slot from the SAVES LIST with `catch {}` — the slot just isn't there, no explanation. Corrupt `robco_chat` at boot (ui-core.js lines 518–521) is silently deleted and replaced with the fresh-boot greeting. - **Rec:** render a corrupt slot as a visible dead row ("SLOT 2 — RECORD UNREADABLE") rather than omitting it; post one SYS line when chat history is reset. Severity: **panel state / one chat line**. Low priority. - **Placement: fold into the 2.9.0 diegetic audit's** every-screen/every-state (loading, empty, **error**, offline) walk — that audit is the natural sweep that will touch these surfaces anyway; not worth a unit of their own. ### Gap 10 — Gemini key cloud sync fails silently (both directions) - **Today:** `loadGeminiKeyFromCloud` / `saveGeminiKeyToCloud` (cloud.js ~1147–1192) are console.warn-only. Net effect: a user who enabled key sync gets no key on their second device and no explanation — though the eventual transmit does fail loudly with NO API KEY DETECTED, so the failure isn't invisible forever, just unattributed. - **Rec:** on a *pull* failure while the sync toggle is on, one SYS line ("> [SYS] KEY RELAY UNREACHABLE — ENTER ACCESS KEY MANUALLY OR RETRY."). Severity: **transient chat line**. Low priority. - **Placement: fold into the 2.9.0 diegetic audit / cloud audit** — lowest of the ranked gaps. ### Noted, but NOT gaps (deliberate, and correctly so) - **The beforeunload flush failure** (ui-core.js lines 1154–1163): no UI is possible during unload; the code comment documents the console.error as the deliberate, Protocol-42-audited loud trail, and the loss window is <500ms of edits. If ever revisited, a next-boot flag is the only possible surfacing — not worth it today. - **The kill-switch boot read failing** (`loadRemoteConfig`) — Protocol 33 requires that to be silent-and-fail-open. Correct as is. - **The event bus swallowing listener errors** — real, but it's a *developer/session*-visibility problem, and the 2.9.0 hardening gate's event-bus item already requires the failure to "surface somewhere a session can see it." That criterion is session-facing, not a user banner — which is the right shape for it. ALREADY-COVERED. --- ## 4. Queue placement — the honest cross-reference **ALREADY-COVERED (named queue item exists AND its criterion genuinely includes a user-facing signal):** | Condition | Queue home | User-facing signal confirmed in the criterion? | |---|---|---| | Boot-phase failure isolation notice | 2.9.0 hardening gate → "Bootstrap isolation" | ✅ yes — "one clear, persistent notice" for degradable failures; a recovery path for fatal ones | | SW install/update failure after deploy | 2.9.0 hardening gate → "Post-deploy TRUTH" | ✅ yes — "the **user** must be shown it, not just a log: detection is not degradation." (Local evidence for that item: `register('sw.js')` has no catch, and the all-or-nothing `cache.addAll` install can fail with zero signal.) | | Offline indicator | 2.9.0 Round-2 deferred infra → "Full PWA offline shell… with an offline indicator" | ✅ yes — the indicator is the deliverable | | Event-bus listener-error swallow | 2.9.0 hardening gate → event-bus hardening | ⚠ session-facing surfacing, not a user banner — appropriate for what it is | | Fail-loud save **writes** (total failure) | SHIPPED — save-integrity Layer 1 + existing "[ERROR] Save slot write failed" | ✅ already live | **GENUINELY-NEW (ranked, most user-impactful first), with exact homes:** 1. **Corrupt-campaign boot deletion, unannounced** (Gap 1) → new near-term **save-integrity follow-up ("Layer 3 — read-side fail-loud")**, directly after current work, before 2.9.0. 2. **Eviction detection at boot** (Gap 2) → same unit — same banner, second trigger; the signal (`robco_booted_before` recovered from IDB + no campaign) already exists for free. 3. **Cloud list failure masked as "no archives"** (Gap 3) → same near-term batch (or its own micro-fix) — the failure UI already exists, it's just unreachable. 4. **False "SYNC COMPLETE"** (Gap 4) → same batch — one counter, one modal line. 5. **Silent Google sign-in failure** (Gap 5) → same batch — one modal (re-verify on real device, Protocol 29). 6. **Degraded (one-store) slot writes report full success** (Gap 6) → save-integrity follow-up — the direct residual of Layer 1's bar. 7. **AI state-apply failure silent while the narrative plays** (Gap 7) → add as a **named criterion of the 2.9.0 hardening gate's fail-loudly umbrella** (the brief's assumption that it's already listed there is not borne out by QUEUE.md's text) — or pull into the near-term follow-up, since it's tiny. 8. **IDB-absent + persist-denied compound state** (Gap 8) → tail of the save-integrity follow-up. 9. **Corrupt slot/chat silently vanish** (Gap 9) → fold into the 2.9.0 diegetic audit's error-state walk. 10. **Key-sync silent failure** (Gap 10) → fold into the 2.9.0 diegetic/cloud audits. **The single highest-value NEW banner:** one boot-time **"MEMORY CORE READ FAULT / EVICTION DETECTED"** banner covering Gaps 1+2 together — one clone-the-template mechanism, two trigger conditions, plus real quarantine instead of deletion. It protects the exact same "sacred campaign" the shipped Layer 2 banner exists for, closes the read-side of the fail-loud bar Layer 1 set for writes, and reuses the pattern (template + detector + device pref + behavioral test) wholesale. Everything else on the list is smaller than this. **Overall verdict:** the point-of-use surface (AI faults, cloud buttons, quota, checksums, kill-switches, OCR fallback) is genuinely strong — most failure paths the user *initiates* already speak. The silence is concentrated at **boot** (corrupt/evicted campaign) and in **success-reporting that hides partial failure** (degraded writes, false SYNC COMPLETE, empty-vs-failed cloud list). Roughly: two big gaps (1–2), five small truthfulness fixes (3–7), three minor polish items (8–10).
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).