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

FEATURE_REMAKES.md



RELEASE

planning/2.8.0/slates/FEATURE_REMAKES.md

sha256 a2bc32fac2463fa3 · 32635 bytes · original held in the private archive

# FEATURE REMAKES — Ground-Up Rebuilds of Existing Features > **Framing (the whole point):** every proposal below is a **from-scratch REMAKE of a > feature that ALREADY EXISTS on the site.** None are net-new/standalone/wildcard ideas. > For each one: _"take this existing feature, imagine rebuilding it from scratch — how > would you make it the BEST, most-improved version possible?"_ > > **Status:** ANALYSIS ONLY. Nothing in this run was committed or pushed. `planning/` is > gitignored. No served code (`index.html`, `js/`, `css/`, `sw.js`, `manifest.json`) was > touched. Started from a clean `origin/dev` (`git reset --hard origin/dev`). > > **Hard constraints honored by every proposal:** vanilla JS, global scope, no modules, > no build step (Vite is dev-server-only; GitHub Pages serves raw files), no `APP_VERSION` > bump, popup-only auth untouched, free / bring-your-own-key, game-agnostic (Protocol 38, > data-driven from `GAME_DEFS`), offline-safe, cache-first SW, additive cloud writes, > full gate + parity tests. Any new precached asset is justified inline. The four remakes, one per effort tier — treated as four equally-serious standalone proposals: | Tier | Existing feature remade | Primary file/function | |------|-------------------------|-----------------------| | **Quick** | WORLD MAP | `renderWorldMap()` — `js/ui-render.js:1225` | | **Medium** | V.A.T.S. SIMULATOR | `showVATSOverlay()` / `recomputeVATS()` — `js/ui-core.js:1723` / `1746` | | **Ambitious** | BACKPACK INVENTORY | `renderInventory()` + item DB — `js/ui-render.js:64`, `js/db_nv.js` / `db_fo3.js` | | **MEGA** | AI COMM-LINK (conversation engine) | `transmitMessage()` / `getSystemDirective()` / `appendToChat()` — `js/api.js`, `js/ui-core.js` | --- ## ★ QUICK — WORLD MAP remake: "Phosphor Cartography" Remakes: **`renderWorldMap()`** (`js/ui-render.js:1225–1475`), its data (`zones[]` with `gridRow`/`gridCol` in `js/reg_nv.js:1186+` and `js/reg_fo3.js:747+`), the `#worldMapDisplay` panel (`index.html:1150`), and the map CSS block (`css/terminal.css:271–296`, `2179–2331`). ### 1. WHAT The current map is an **HTML CSS-Grid of boxed cells** (`grid-template-columns:14px repeat(cols,minmax(0,1fr))`). It renders a 6×6 zone grid (default cropped to a 4×4 "core" via `state.mapView`), each cell tagged `[CURRENT]` / `[VISITED]` / `[UNKNOWN]` with a red `[?]` pip when a zone holds an uncollected collectible. Tapping a cell zooms to a flat text list of that zone's locations (`zoomMapToZone` → `_mapActiveZone`), each row with a `LOG VISIT` button (`markLocationVisited`, `ui-render.js:1218`). Fog-of-war persists via `recordLocationVisit()` (`state.js:659`) into `state.locationHistory`. **The remake:** a single **inline SVG phosphor minimap** — a real spatial canvas with a sweeping radar scanline, glyph nodes drawn at each zone's grid coordinate, connecting "known-route" lines between discovered zones, a blinking `[YOU]` reticle, and keyboard arrow-key navigation between nodes. Same data (`gridRow`/`gridCol`), same fog-of-war model, same `recordLocationVisit` single-source — only the _presentation layer_ is rebuilt from scratch as vector graphics instead of boxed divs. ### 2. WHY (what's weak about the current version) - **It doesn't read as a map.** It's a grid of labelled boxes; there's no sense of space, distance, or adjacency. The Fallout Pip-Boy map is iconic — this is a spreadsheet. - **Name-matching is a heuristic** (`scoreZoneForLoc`, `ui-render.js:1269`): exact→word→ substring scoring with a single-winner tiebreak. It's clever but fragile and only exists because the render has no stable per-zone identity to key off. - **Abbreviation table (`_mapAbbrev`) is a maintenance tax** — long names get hand-mapped ("Ranger Station Foxtrot" → "R.S. Foxtrot") purely so text fits a box. - **The `[?]` pip is generic** — you can't tell a bobblehead from a Lincoln artifact. - **No CRT motion.** The map is the one big surface with no scanline/sweep, so it feels the least "alive" of all the panels despite the app's heavy CRT identity. ### 3. HOW (in-stack) - One new render path inside the **existing `renderWorldMap()`** — build an SVG string with `map().join('')` (never `innerHTML +=` in a loop, per the Prohibited-Patterns table) and assign once to `#worldMapDisplay.innerHTML`. SVG is inline markup, no library, no build. - Node positions come straight from existing `gridRow`/`gridCol` → `cx = col*step`, `cy = row*step`. Zero new data; the registry is untouched (Protocol 23: registry stays read-only). - Status → class on each `<circle>`/`<text>`: reuse the existing `--current`/`--visited`/ `--empty` semantics as SVG-targeted CSS. - The sweep is a pure CSS `@keyframes` rotation on one `<line>`, **gated by the existing `prefers-reduced-motion` block** (`css/terminal.css`) exactly like `vats-scan`/`map-blink`. - Keyboard nav: a `keydown` handler mapping Arrow keys to the nearest node by coordinate, Enter = zoom (reuse `zoomMapToZone`). Focus ring via the existing `:focus-visible` rule. - Collectible glyphs become **typed** (`◆` bobblehead / `★` snowglobe / `▲` Lincoln) driven by the collectible's own category field — data-driven, so a new game's collectible type needs no map code (Protocol 38). ### 4. BETTER - **UX:** an actual spatial map you can scan at a glance; adjacency and "where am I" become obvious. Route lines turn `locationHistory` into a visible exploration trail. - **Accessibility:** SVG `<title>` per node + real arrow-key traversal + focus ring — today the grid is click/tap-only with `title=` tooltips (hover, which Protocol 17 discourages). - **Maintainability:** deletes the `_mapAbbrev` table and softens the reliance on `scoreZoneForLoc` (nodes are keyed by coordinate identity, not fuzzy name matching). - **Authenticity:** the radar sweep + phosphor node-glow is the single biggest CRT win available for the least code — it makes the map match the boot/VATS/limb surfaces. - **Perf:** one SVG string, one assignment, ~36 nodes — lighter than 36 flex divs plus compass header cells. ### 5. FIT - **Visual:** phosphor glow via existing `--robco-glow`; sweep reuses the `vats-scan`/ `refresh-bar` keyframe idiom; blink reticle reuses `map-blink` (`step-end`); colors from the per-game `THEMES` table so FO3's duller green vs FNV's `#14fdce` carry through. - **Architectural:** this edits `css/terminal.css` + `js/ui-render.js` (served files) → **`CACHE_NAME` bump required** (Protocol 1). No new precached asset (SVG is inline). No network/IO, so no kill-switch needed. Gate + both runners updated; existing Suites 74/114/126 must still pass, plus new SVG-contract guards. ### 6. TRADEOFF - **Backward-compat:** `state.mapView` ('auto'/'core'/'full') loses meaning — an SVG map can show all 36 nodes at once without box-crowding, so the CORE/FULL toggle would be retired or repurposed to zoom. Migration is trivial (ignore the stale value) but it's a UX change to muscle memory (Protocol 25) and needs a migration note. - SVG text at 360px can get cramped; mitigated by showing glyph nodes + a name-on-focus label rather than always-on labels. - Slightly harder to eyeball exact `[VISITED]` counts than a labelled grid — add a small text legend/counter beneath the SVG to preserve that. ### 7. EFFORT / RISK - **Size:** ~2 files (`ui-render.js`, `terminal.css`) + `index.html` untouched (same mount point) + `sw.js` cache bump. ~4–6 new tests (SVG node count == zone count, sweep is reduced-motion-gated, keyboard handler present, glyph is data-driven). - **Risk: LOW.** Pure presentation swap over an unchanged data model and unchanged fog-of-war single-source. Protocol 10 render-check at 360/412/desktop is the gate. ### 8. PIPELINE **Ship-direct** (single Sonnet pass) is defensible given low risk — but because it touches mobile layout + a `prefers-reduced-motion` animation, run the **Protocol 10 render-check** explicitly. Recommend the **light Opus→Sonnet→Opus** path only for the plan-audit of the `mapView` retirement (the one backward-compat wrinkle). --- ## ★ MEDIUM — V.A.T.S. SIMULATOR remake: "Targeting Silhouette" Remakes: **`showVATSOverlay()`** (`js/ui-core.js:1723`) and **`recomputeVATS()`** (`js/ui-core.js:1746`), the coefficient block in `GAME_DEFS` (`js/state.js:327–364` FNV / `424–451` FO3: `vats.regions` `{name,mod,ap}`, `apBase 65`, `apPerAgility 3` FNV / `2` FO3, `critBonus 0.05` FNV / `0.15` FO3, hit-% clamp 5–95, `combatSkills`, `ammoPerAttack`), `lookupWeaponStats` in both DB runners (`db_nv.js:852`, `db_fo3.js:506`), the melee-scope helpers (`_vatsResolveSkill`, `_vatsIsMelee`), and the `[VATS SIM]`/`[VS]`/`[VATS]` router tokens + the `V.A.T.S. CALCULATOR` button (`index.html:2197`). Guarded today by Suites 104 + 105. ### 1. WHAT Current VATS is a **numeric overlay in `#sysModal`**: you type a TARGET DT into `#vatsTargetDT` (`ui-core.js:1734`), and `recomputeVATS()` prints a **pre-wrapped ASCII table** — an INPUTS block, a per-region HIT-% estimate (base = PER×2 + AGI×1.5 + (skill+chem)/3, clamped 5–95, per-region mod applied) with a 10-block bar, and an AP-STRIKE OPTIMIZER (effective damage = `baseDamage − targetDT`, strikes = `floor(apPool/region.ap)`, best DMG/AP highlighted). All coefficients read from `GAME_DEFS` (no hardcoded table, GA-7). Melee/unarmed is exact; ranged hit-% is flagged as an estimate (WU-D4a-RANGED-GAP). **The remake:** a **body-silhouette targeting interface** — an inline SVG humanoid figure with tappable region hot-zones (head/torso/arms/legs/eyes/groin), each highlighting live with its hit-%, AP cost, and a queued-shot counter; an **AP budget bar** that drains as you queue a sequence of shots; a **weapon picker sourced from the player's actual equipped/carried weapons** (via `lookupWeaponStats`) rather than only the currently-equipped one; and **enemy DT auto-filled from the BESTIARY** (`lookupBestiaryEntry`, already used by THREAT) with the manual DT field kept as fallback. Same deterministic math and coefficients, rebuilt from a spreadsheet into an actual VATS panel. ### 2. WHY - **It's a table, not a targeting system.** VATS in-game is a body-part picker; here it's an ASCII list. The single most recognizable Fallout mechanic renders as the least game-like screen in the app. - **Manual DT entry is friction and a footgun** — the player guesses/looks up an enemy's DT and types it, when the app already ships a full BESTIARY (`lookupBestiaryEntry`). VATS and THREAT never talk to each other. - **AP is shown, not sequenced.** The optimizer already computes strikes-per-region from the AP pool (`apBase + apPerAgility*AGI`) — but you can't _queue_ a mixed shot sequence and watch the pool drain, which is the actual VATS decision. - **Weapon context is narrow.** Only the equipped weapon feeds the calc; you can't compare another carried weapon without equipping it first. ### 3. HOW (in-stack) - Rebuild the body of `showVATSOverlay()` to emit an **inline SVG silhouette** (built once, `map().join('')`), with `<path>`/`<g>` hot-zones carrying `data-region` keyed to `GAME_DEFS[ctx].vats.regions` — game-agnostic, so a new game's region table just works. - `recomputeVATS()` stays the math core (reused, not duplicated — Protocol 22); it now also reads a **selected** weapon from `lookupWeaponStats` and an enemy DT auto-filled from `lookupBestiaryEntry` when a bestiary target is chosen (manual `#vatsTargetDT` stays as fallback; a bestiary miss falls back cleanly — Protocol 3, never invent a DT). - AP-queue: a small array in overlay-local scratch state (not campaign `state` — it's a calc, so **no new persisted state field**, no Protocol 4 surface). Queue a region → subtract its AP from the pool → render the drain bar. Pure functions, fully offline. - Silhouette highlight + AP bar animate via existing keyframe idioms (`vats-scan`, `delta-rise`), all under `prefers-reduced-motion`. The existing `.vats-scanning` sweep class (already in CSS, currently unused) finally gets used. ### 4. BETTER - **UX:** tap a body part like real VATS; queue shots and see shots-per-turn from the AP pool; auto-DT from the bestiary kills the manual-lookup step and links two native terminals. - **Accessibility:** each SVG hot-zone is a focusable control with an `aria-label` ("Target head — 76% hit, 25 AP"); the result region already uses `aria-live="polite"` — today's table is read-only text. - **Authenticity:** a glowing green wireframe body with a targeting reticle is peak Pip-Boy. - **Correctness:** it reuses the exact same clamp/critBonus/AP formulas already gate-guarded by Suites 104/105 — the math doesn't change, so the deterministic guards keep protecting it. - **No AI, fully offline** (VATS is already a native terminal; this stays native, read-only, no `saveState`/`pushToCloud`). ### 5. FIT - **Visual:** phosphor wireframe via `--robco-glow`; AP bar reuses limb-bar `[████]` styling; reticle blink reuses `map-blink`; sweep reuses `.vats-scanning`. Per-game color from `THEMES`. - **Architectural:** edits `ui-core.js` + `terminal.css` + `index.html` overlay markup → **cache bump** (Protocol 1). No new asset (inline SVG). No network → no kill-switch. Scratch AP-queue is overlay-local → **no state migration**, keeping data-safety trivial. Both runners + gate updated; Suites 104/105 must stay green. ### 6. TRADEOFF - The overlay grows from a compact table to a taller silhouette panel — must be verified at 360px to avoid horizontal overflow (Protocol 17). Mitigate with a vertical silhouette that scales to viewport width. - Auto-DT from the bestiary introduces a soft coupling VATS→bestiary; if a monster isn't in `BESTIARY.CSV` it must fall back cleanly to manual DT (Protocol 3). - Slightly more code to maintain than a text table; offset by deleting nothing and reusing the math core. ### 7. EFFORT / RISK - **Size:** ~3 files (`ui-core.js`, `terminal.css`, `index.html`) + `sw.js`. ~6–8 new tests (SVG region keys match `GAME_DEFS.vats.regions`, AP-queue drain math, bestiary-auto-DT with manual fallback, reduced-motion gate, no new persisted state field, still read-only). - **Risk: MEDIUM.** Math is reused/proven; risk is layout at 360px + the bestiary coupling edge cases. Deterministic core means no AI-contract risk. ### 8. PIPELINE **Full Opus→Sonnet→Opus.** The plan-audit must enumerate every VATS entry path (macro button, `V.A.T.S. CALCULATOR` button + the three router tokens, empty-inventory case, melee-weapon scope gate `playstyle==='melee' || weaponIsMelee`, bestiary-miss fallback) and 360/412/desktop render. Medium-risk UI over a coefficient contract → worth the audit stage. --- ## ★ AMBITIOUS — BACKPACK INVENTORY remake: "Manifest & Loadout" Remakes: **`renderInventory()`** (`js/ui-render.js:64`), `addItem()` (`:1`), `delItem()` (`:48`), the category filter (`_invFilter`, `setInvFilter`, `:53`) — the item list renders in **insertion order** (no sort; `_origIdx` preserves indices across the filter); the AMMO RESERVES sub-panel (`index.html:691`, `addAmmo`; ammo is the only list that sorts, A–Z via `localeCompare` in `renderAmmo`, `:134`); the item database (`lookupItemInDb` in `js/db_nv.js:786` / `db_fo3.js` — `wgt`/`val`/`type` from `WEAPONS`/`ARMOR`/`AMMO`/`CHEMS`/`MISC`/`QUEST_ITEMS`/`WEAPON_MODS` CSVs); the equipped-slot display (`renderEquipped`, `:630`, set only via `[USE]`/`[EQUIP]` chat, not the UI); and the carry-weight readout (`updateMath`, `ui-core.js:2353` — `display_weight`, `maxWeight = 150 + s*10`, `weight-over`/`weight-critical`/`weight-heavy` body classes). Guarded today by Suites 40, 61, 75. ### 1. WHAT Current inventory is a **flat, insertion-order list** filtered by one active category (`_invFilter`: all/weapon/armor/aid/mod/misc; ammo lives in its own reserves sub-panel). Rows are `[USE]` + type tag + `qty×Name (wgt · val)` + delete; adding uses a text input that auto-fills weight/value/type from the DB (`lookupItemInDb`). Carry weight is computed and shown **only in BIO-METRICS** (`display_weight`) with a whole-screen "deformation" when over-encumbered (`weight-*` body classes) — but the inventory panel itself shows no running total, no per-item effect/stats, and no sort or search. **The remake:** a proper **item-manifest interface** — every row keeps `wgt · val` but gains a tap-to-open **inspect drawer** (full DB stats, effect text, equip/use/drop in one place); a **sort control** (name / weight / value / type); in-list **search**; a live **loadout header** (total weight vs `maxWeight`, total value, item count) rendered _in the inventory panel_ where you're actually managing gear; and equip/unequip surfaced per-row instead of only via the `[USE]`/`[EQUIP]` chat command. Same state model, same DB, same carry-weight math — rebuilt from a flat list into a real inventory manager. ### 2. WHY - **It's a flat list with one filter.** No sort options (items render in insertion order; only the ammo sub-panel sorts), no search, no way to see _why_ an item matters beyond the inline `wgt · val` — no effect/full-stats/inspect without leaving the panel. - **The DB is under-surfaced.** `lookupItemInDb` returns `wgt`/`val`/`type` (and armor DT, chem effects live in the CSVs) for every item, but the row shows only name + qty + wgt/val. The richest data source in the app is mostly invisible at point-of-use. - **Carry weight is disconnected from carrying.** The encumbrance readout lives in BIO-METRICS; the place you add/drop items shows no running total, so you over-encumber without seeing it happen where the decision is made. - **Equip UX is hidden** behind a chat command — there's no equip button in the item list, breaking the "manage my gear in one place" loop. ### 3. HOW (in-stack) - Rebuild `renderInventory()` to compose rows with DB-joined metadata (call `lookupItemInDb` per row, memoized) using `map().join('')` + single assignment (Prohibited-Patterns compliance). All text through `escapeHtml()` (XSS-1/2/3 guards). - **Sort/search are view-state, not campaign state** — module-level `_invSort` / `_invQuery` exactly like the existing `_invFilter`, so **no new persisted `state` field** and no Protocol 4 surface (nothing to migrate, nothing to sanitize on cloud pull). - Inspect drawer = an in-panel expander toggled per row (or reuse the `_openSysModal` focus-trap idiom if modal), populated from the same DB lookup. - Loadout header reuses the **existing** `maxWeight`/`curWt` math from `updateMath` (`ui-core.js:2353`) — call the same computation, don't fork it (Protocol 22). Equip actions call the existing equipped-slot setters used by `[EQUIP]`. - Fully game-agnostic: item types/categories come from the DB + `GAME_DEFS`, never hardcoded game literals (Protocol 38). ### 4. BETTER - **UX:** sort by weight to find what to drop; search a 60-item pack instantly; inspect before dropping; see total weight/value/count while you manage — the core inventory loop finally lives in one panel; equip without leaving to chat. - **Data safety:** view-state only → the campaign `state.inventory`/`state.ammo` shape is **unchanged**, so saves/cloud/round-trip (Suite 46) are untouched. Zero migration risk to user data — the single biggest reason to keep the state model fixed. - **Accessibility:** sort/search/inspect/equip are real `<button>`/`<input>` controls (Protocol UI-5) with labels; today's single filter bar becomes a full, keyboard-navigable toolbar. - **Performance:** memoize DB lookups per render; still one `innerHTML` assignment. Search filters an in-memory array (no re-fetch); gated by the existing dirty-check (`_isDirty`, Suite 91). - **Maintainability:** one render path that joins state + DB, replacing scattered filter/color logic. ### 5. FIT - **Visual:** rows keep the `.list-row-content` / `.tag` idiom (with the `white-space:nowrap` tag guard, Suite 92); inspect drawer uses panel styling + phosphor glow; over-weight rows can tint via the existing `weight-*` color vars. - **Architectural:** edits `ui-render.js` + `terminal.css` + `index.html` panel → **cache bump** (Protocol 1). No new asset. No network → no kill-switch. Because it's presentation over an unchanged state schema, cloud writes stay additive/untouched (Protocol 34). - **Tests:** Suites 40/61/75 stay green; add guards for sort determinism, search filtering, DB-join per row, no-new-state-field, and 360px no-overflow (Protocol 10/17). ### 6. TRADEOFF - **This is the migration-risk one to be honest about:** the _temptation_ is to enrich the state model (store per-item weight/condition/equipped-flags). **Don't** — that would touch Protocol 4 (4-file change), the sanitizer, and the cloud round-trip, and risk corrupting existing saves. The proposal deliberately keeps `state.inventory` as-is and derives everything from the DB at render time. The tradeoff: item data is only as rich as the CSVs (an item not in the DB shows name/qty/wgt/val only — must degrade gracefully, never blank). - Bigger panel → more vertical space; mitigate with collapsible inspect + compact rows. - More render work per frame (DB joins); mitigate with memoization + the existing dirty-check. ### 7. EFFORT / RISK - **Size:** ~3 files (`ui-render.js`, `terminal.css`, `index.html`) + `sw.js`. ~8–12 new tests. **State schema unchanged** — that's what keeps this Ambitious-not-MEGA. - **Risk: MEDIUM.** Contained because user data is untouched; risk is layout + DB-miss graceful degradation + not regressing the carry-weight/BIO-METRICS coupling. ### 8. PIPELINE **Full Opus→Sonnet→Opus.** The plan-audit must enumerate: every category filter state, empty-inventory, DB-miss item, equipped item, over-encumbered render, ammo-vs-item split, and 360/412/desktop. High reuse of existing math/DB means Sonnet implements, but the "resist touching state" discipline needs the Opus audit to enforce. --- ## ★ MEGA — AI COMM-LINK remake: "Streaming Two-Phase Narrator" Remakes the **core conversation engine**: `transmitMessage()` (`js/api.js:1308`) and `getSystemDirective()` (`js/api.js:23`), the Tri-Node contract (`narrative`/`state`/`modal`), `_validateTriNode` (`:219`) + `autoImportState()` (`:441`, explicit field-mapping), `appendToChat()` + `getTypewriterSpeed()` → `#chatDisplay` (`js/ui-core.js:2617`/`2607`), the retry engine (`_AI_RETRY_MAX`, `_AI_RETRY_DELAYS_MS`, 401/403/429 handling), the `responseMimeType:'application/json'` lock (`temperature 0.2`), the multi-turn history model (`chatHistory`, `CHAT_MAX=200`, `localStorage.robco_chat`), the `isFeatureEnabled('aiChat')` kill-switch gate (`:1341`), and the BYO-Gemini-key path. Guarded today by Suites 14, 53, 54. ### 1. WHAT Today the comm-link is **request → wait → parse whole JSON → typewriter-replay**. Because the model is locked to `responseMimeType:'application/json'`, `transmitMessage()` must receive the _entire_ Tri-Node object before anything appears: the narrative can't render until the full `{narrative, state, modal}` blob arrives and passes `_validateTriNode`. The app _does_ have a nice mood-aware typewriter (`getTypewriterSpeed` + `appendToChat` — faster on crisis words, slower on calm ones, with `playClack()` key sounds) — but it only starts **after** the whole response has been generated and received. So the user stares at a "TRANSMITTING…" state through the entire model-generation latency, and _then_ watches a purely local replay. History is multi-turn (full `chatHistory`, capped `CHAT_MAX=200`, persisted to `localStorage.robco_chat`) and the whole history is resent every request. **The remake:** a **two-phase streaming narrator** — split the single JSON call into (A) a **streamed narrative** that types out phosphor-cadence text token-by-token _as it is generated_ (true CRT teletype covering the wait), immediately followed by (B) a **structured state/modal delta** that is still strict-validated and field-mapped before it touches `state`. The AI still never mutates state without validation (Protocol 24) — only the human-facing prose is streamed. Plus: real **conversation-memory management** (a bounded, summarized context window instead of resend-everything), a **retry/latency HUD** so the user sees what the retry engine is doing, and a **graceful degraded mode** when the kill-switch is off or the key is missing. ### 2. WHY - **The JSON lock forces an all-or-nothing wait.** The single biggest UX weakness of the whole app: the narrative — the thing the user actually reads — is held hostage by the state JSON. On mobile with a slow model, that's multi-second dead air before anything prints. - **The typewriter is a local replay, not a live feed.** The cadence effect already exists and is good — but because it can't start until the full JSON is parsed, its charm is spent _after_ the wait instead of covering it. Streaming would make the same typewriter print text _as the model generates it_, turning dead air into live teletype (the whole point of a terminal). - **Context handling is coarse.** Long campaigns resend the full `chatHistory` (up to 200 turns) every request — token waste (Protocol 28) with no summarization or managed window; and once the 200-cap is hit, oldest turns are simply dropped (memory loss), never summarized. - **Retry is invisible.** `_AI_RETRY_MAX`/`_AI_RETRY_DELAYS_MS` do exponential backoff, and 401/403/429 have distinct messages — but mid-retry the user sees a generic wait. The robustness exists but isn't surfaced. - **Failure UX is thin.** Kill-switch-off / no-key / network-down should drop into a clearly labelled offline narrator mode, not a dead comm-link. ### 3. HOW (in-stack — and why the heavier parts are justified) - **Two-phase contract.** Phase A: a call that streams **plain narrative text** (SSE / chunked `fetch()` + `ReadableStream` reader — vanilla, no library, no build). Phase B: a second strict `responseMimeType:'application/json'` call (or a trailing JSON sentinel in the same stream) returning only `{state, modal}`, run through the **unchanged** `_validateTriNode` + `autoImportState` explicit mapping before persistence. **Protocol 24 is preserved** — state still never lands unvalidated; only the prose is streamed. - **Teletype render:** extend `appendToChat()` to feed streamed chunks into the **existing** `getTypewriterSpeed` typewriter (reuse, not rewrite — Protocol 22), cadence gated by `prefers-reduced-motion` (reduced → instant), keeping the `playClack()` key sounds. - **Context window:** a bounded rolling summary in `state` (one summarized-context string field) — one new state field, so it rides the **full Protocol 4 surface** (default + migrate in `state.js`, import in `autoImportState`, sanitize entry, cloud round-trip: serialized-whole, covered by Suite 46). Justified because managed memory is core to the remake and can't be view-state. - **Retry HUD:** surface the existing retry state (attempt N of `_AI_RETRY_MAX`, backoff from `_AI_RETRY_DELAYS_MS`, 401/403/429 labels) into the chat area — no new logic, just exposing what's already computed. - **Kill-switch + fallback:** streaming is a network feature → it stays behind `isFeatureEnabled('aiChat')` (Protocol 32), and the read is fail-safe (Protocol 33). When off/failing, degrade to a clearly-labelled deterministic offline narrator (reuse the native terminals) — the app stays fully usable offline. - **Why heavier is justified:** streaming and a managed context window are the only way to fix the all-or-nothing wait, which is the app's central interaction. Everything else stays vanilla/global-scope; no framework, no build, popup-auth untouched, BYO-key unchanged. ### 4. BETTER - **UX:** narrative starts appearing in ~1 network RTT instead of after full generation — the largest perceived-latency win in the app, most felt on mobile. - **Authenticity:** the existing teletype finally prints _live_ instead of replaying after the wait. - **Performance/cost:** bounded summarized context cuts token spend on long campaigns (Protocol 28) vs resend-all-200. - **Robustness/transparency:** retry HUD turns silent backoff into visible, trustworthy behavior; degraded mode makes failure a defined state, not a dead screen. - **Data safety unchanged:** state still only mutates through validated, explicitly-mapped fields (Protocol 24); cloud writes stay additive (Protocol 34). ### 5. FIT - **Visual:** live teletype reuses phosphor glow + the existing typewriter/`playClack`; retry HUD uses the existing status/warning styling; degraded mode reuses native-terminal framing. - **Architectural:** edits `api.js` + `ui-core.js` + `index.html` + one new `state` field → **cache bump** (Protocol 1), Protocol 4 four-file surface, Protocol 14 AI-contract test in the same commit (the contract shape changes — this is the mandatory guard), Suites 14/53/54 extended + new streaming/round-trip guards. No new precached asset. CSP already allows the Gemini origin (Suite 55) — streaming uses the same origin, so **no CSP change** (verify the stream endpoint matches the pinned origin). - **Offline/kill-switch:** fully covered above (Protocols 32/33). ### 6. TRADEOFF - **This is the highest-migration-risk remake and it's replacing the app's beating heart.** Splitting one JSON call into two changes the AI contract — Protocol 14 makes an AI-contract test mandatory in the same commit, and a silent schema break here is catastrophic (Protocol 24). The new context-summary state field must survive push→sanitize→migrate→apply (Suite 46) and migrate cleanly for existing saves (default empty). - **Two calls can mean more requests / cost** than one; mitigate by keeping Phase B minimal (state/modal only) and using the bounded context to shrink each call. - **Streaming parsing is fussier** than awaiting one blob (partial chunks, mid-stream errors); the retry engine must handle a dropped stream, and Phase B validation must still hard-gate state. Some providers/keys may not support streaming → **fall back to the current single-JSON path** (keep it as the degraded route, not deleted — Protocol 22/33). - Streaming + PWA needs real-device mobile verification (auth is untouched so Protocol 29 doesn't apply, but the streaming reader must be verified on a real phone). ### 7. EFFORT / RISK - **Size:** ~4 files (`api.js`, `ui-core.js`, `index.html`, `state.js`) + `sw.js` + heavy test work (AI-contract round-trip, streaming chunk assembly, context-summary migration, retry-HUD state, degraded-mode fallback, kill-switch gate) — easily 15–25 new tests across both runners at parity (Protocol 15). - **Risk: HIGH.** Touches the AI contract, adds persisted state, changes the central loop, and depends on provider streaming support. The single-JSON path must remain as a fallback so a streaming failure can never brick the comm-link. ### 8. PIPELINE **Full Opus→Sonnet→Opus, expect loops.** The plan-audit (Protocol 8) must enumerate every path: key present/absent, kill-switch on/off, network up/down, stream supported/unsupported, Phase-A-ok/Phase-B-fail, brand-new vs migrated save, reduced-motion, mobile vs desktop, and the fallback-to-single-JSON route. This is exactly the class of change the three-stage workflow exists for — the audit stage catches "narrative streams but state silently didn't apply" before it reaches the user. --- ## Cross-cutting notes - **No `APP_VERSION` bump** is implied by any of these on its own (they're feature remakes, not releases); each still requires a **`CACHE_NAME` -rN bump** because all touch served files (Protocol 1). - **Game-agnostic throughout** (Protocol 38): map nodes, VATS regions, inventory types/categories, and AI directives all stay data-driven from `GAME_DEFS`/DB/registry — a new Fallout title needs data, not feature-code edits. - **Every remake keeps the original as the fallback path where one exists** (single-JSON AI route, manual DT entry, box-grid legend counter) rather than a hard replacement — honoring Protocol 22 (extend, don't fork) and UX stability (Protocol 25). - **Tiers are deliberately spread and independent** — none depends on another; each is a standalone rebuild of a distinct existing feature.
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).