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).