LIBRARY
library/CODE_MAP.md
sha256 eaa673642d001e97 · 33310 bytes ·
original held in the private archive
# CODE_MAP.md — "where does X live"
> ## ⛓ BASELINE PIN — read this first
>
> | | |
> | --- | --- |
> | **Commit** | `9970cc8` (`9970cc85f1bad85a83a661208fce3100a1ea9568`) |
> | **Branch** | `dev` |
> | **Date** | 2026-07-20 (R4 re-pin) |
>
> **This map is accurate ONLY as of that commit.** Compare the hash above against
> `git rev-parse dev` — if they differ, the repo has moved and every concrete claim here is
> **degraded**: a reconstruction, not current truth. Re-verify against the code before relying on
> anything specific. That comparison is the whole point of the stamp; it replaces guessing.
>
> Nothing derivable is written here on purpose. If a fact can be computed — a cache rev, a test
> count, a line number, a file count — this doc points at the command instead of copying the
> answer, because a copied answer is a future lie. Derived from source, so **verify rather than trust**: where this map and the code disagree, the code wins (Protocol 3) — flag the drift.
**Class:** LIVE (Protocol 46). Gitignored, local-only — never committed (see `.gitignore` and the 3-class model in `rules/docs-and-library.md`). `library/MANIFEST.txt` is the one committed pointer to this file's existence, so the doc-reference-integrity gate (Suite 220) can verify `CLAUDE.md` never points at a filename that isn't real, even on a CI checkout where this file itself is absent.
**Purpose:** a navigation index, not a tutorial. It tells a session which file (and, loosely, which function) owns a piece of behavior, so it can go read exactly that instead of loading a whole 8000-line file to find one thing. Organized by **subsystem/responsibility**, not by today's filename — the 2.8.5 restructure (`planning/2.8.5/plans/CODE_HEALTH_PLAN.md`) is going to move a lot of this code, and a map keyed on filename would go stale on day one of that work. Where a split is planned but not yet built, this doc says so explicitly and still points at today's real location.
**Derivation:** every reference below was pulled directly from source (grep + read) on `dev`, 2026-07-11 — not copied from another doc. Re-verified and updated for the 2.8.5 U-A1 `ui-core.js` split, then for the 2.8.5 U-A2 reorg (`js/` split into `data/`/`core/`/`ui/`/`services/`/`dev/` subfolders; `css/terminal.css` split into order-prefixed files, `05-base.css` … `99-mobile.css` — note the files went to numeric-prefix names in commit `9274c75`, NOT the `terminal-*.css` names an earlier draft used), then for the 2.8.5 U-A3 `api.js` split (`api.js` stays the network-layer hub; `api-directive.js`/`api-import.js`/`api-router.js` split out by responsibility) — every path below now names the real on-disk file. **Line numbers were REMOVED at R4 (2026-07-20) and must not come back.** They were "approximate anchors, not a contract" — but a re-verification found them drifted by up to **+685 lines** (`state.js`'s `migrateState()` was listed at ~1929 and is at 2733), which is not an anchor, it is a wrong answer wearing an anchor's clothes. The symbol name was always the authoritative pointer and the number always the shortcut, so the shortcut is gone: **grep the function name.** Re-adding line numbers here re-creates a rot surface that has already failed once — the same reasoning that retired Protocol 2a's hand-synced counts. **If this doc and the code disagree, the code wins** — flag the drift, don't trust the doc silently (same rule as `library/BRAIN_DUMP.md`).
**Where the RULES live (R2 restructure, 2026-07-20):** the agent rulebook is no longer one file. `CLAUDE.md` holds the **universal contract** (the rules that apply to all work) plus a **retrieval map**; every surface-scoped protocol lives in one of ten `rules/*.md` subsystem notes — `state-and-save`, `deploy-and-cache`, `auth-and-cloud`, `ui-and-mobile`, `audio`, `game-data`, `ai-contract`, `file-layout`, `testing-and-gates`, `docs-and-library`. Each protocol number exists in exactly one file (gate-guarded: Suite 220.13), and `CLAUDE.md` reaches every note (220.14). Full landing table: `planning/2.8.5/plans/R2_RESTRUCTURE_SUMMARY.md`.
**Maintenance (Protocol 46):** update this file in the same commit as any file split/add/move/remove or major function relocation. Add/remove the filename from `library/MANIFEST.txt` in the same commit a file is added to or removed from `library/` itself (not the app's `js/` — this manifest only tracks `library/`'s own contents).
---
## Boot Lifecycle
- **Entry point:** `window.onload` — `js/ui/ui-core.js` (the hub file). A single async function with a fixed, ordered call sequence; nothing reads `state`/globals before this fires (Governing constraint, `planning/2.8.5/plans/CODE_HEALTH_PLAN.md` §2). window.onload itself stayed whole in the hub during the 2.8.5 U-A1 split — only the callee functions it invokes moved into the `ui-core-*.js` family (global scope, so the split changes nothing about call resolution). Order, as of this snapshot:
1. Event-bus subscriber wiring (six `_wire*EventBusSubscribers`/`_wire*Subscriber` calls — see Event Bus below) — always first, RobcoEvents is guaranteed loaded.
2. `_hydrateMetaFromIdb()` → `_reconcileMetaFromIdb()` (hub) — P2: IndexedDB↔localStorage device-pref reconciliation, **meta store only** (two-store boundary below).
3. `_restoreLiveContainerFromIdb()` (hub, bounded `Promise.race` wrapper → `window.restoreLiveContainerFromIdb()` in `js/core/state.js`) — **P8 live-container recovery**: if localStorage lost `robco_v8` but IndexedDB survived, restore it from the `'campaign'/'live'` mirror BEFORE hydration; recovery-only (no-ops when localStorage still holds it, so it can never clobber a newer value). Then `_hydrateStateFromStorage()` (SAVE_LAYER3: split-try quarantine of an unreadable container via `_quarantineCorruptContainer()`, fail-soft migration helpers, the READ FAULT / EVICTION banner triggers at its tail — see Save Integrity below), cold-store→IDB migration (P3, fire-and-forget), `_restoreApiKeyAndChatHistory()`.
4. `loadUI()` (Render Pipeline below), `initTabs()` (`js/ui/ui-core-nav.js`), `_initBezelChrome()` (`js/ui/ui-core-nav.js`).
5. Interaction wiring: HP/XP/RAD bar drag, bio-harness zone taps, fader-ladder drag (`js/ui/ui-core-cmd.js`).
6. Ambient audio arm (`_armAmbientAudio`, autoplay-safe first-gesture pattern), registry autocomplete, wake lock, haptic, Overseer's Log, high-lumen, immersion dial, radio.
7. Standby/ambient-experience wiring, `initOverseerScope()` (`js/ui/ui-core-overseer.js`), `initAmbientRuntime()`, `initChassisCore()` (`js/ui/ui-core-chassis.js`), `initTestConsole()` (no-ops on production — Diagnostic Shell).
8. Panel persistence, Tool Deck wiring, optics/device-pref restore, keyboard shortcuts (`js/ui/ui-core-nav.js`).
9. `_runBootSequenceAndBriefing()` (hub) — the boot animation + AI briefing.
10. Ambient timers, input-history nav, unload-flush wiring.
11. `routeLaunchShortcut()` (`js/ui/ui-core-nav.js`) — PWA shortcut deep-link routing. **Must run last**, after `initTabs()`.
- **Boot-order gate:** Suite 56 (file-exists + in-`ASSETS` + `<script>` tag + load-order-before-`api`/before-`ui-core` guards) and Suite 220/Protocol 45 (the `LOAD-ORDER-GUARD` block in `rules/file-layout.md`/`ARCHITECTURE.md` is mechanically diffed against `index.html`'s real tag order). `scripts/check-boot-chain.js` (U-A0) additionally cross-checks index.html/sw.js/disk/docs/test.html mutual consistency — run it after any file split.
- **Script load order narrative:** `rules/file-layout.md` — the canonical ordered list (moved out of `CLAUDE.md` at the R2 rules restructure, 2026-07-20).
## State & the Two-Store Boundary
Two stores, never conflated (Protocol 23):
- **Campaign state** — `let state = {...}` (`js/core/state.js`). The single mutable campaign-data object (stats, inventory, quests, factions, …). `saveState()` persists it debounced (500ms) to `localStorage['robco_v8']`, flushed on `beforeunload` **and** `visibilitychange → hidden` (`_flushUnload` in `js/ui/ui-core.js` `_wireUnloadFlush()`). **P8 durability shadow:** every debounced write + every flush also fires `window.mirrorLiveContainer()` (state.js) — a fire-and-forget IDB-only put to `'campaign'/'live'`; recovered on boot by `window.restoreLiveContainerFromIdb()` (recovery-only, checksum+shape gated, localStorage-always-wins — Protocol 34). Guarded by Suite 239. `migrateState(version, s)` runs on load. `generateSyncPayload()` / `exportSaveFile()` build the save envelope; `verifyBundleChecksum()` / `isValidBundleShape()` guard import integrity; `_writeImportedContainer()` is the single write path for an imported/migrated container.
- **Device prefs** — `MetaStore` + `META_MANIFEST`, the registered key list (audio mute, optics color, immersion tier, input mode, panel-open state, …). IndexedDB-shadowed via `_idbShadow()`. **Never** part of `state` or `robco_v8` — a device pref that leaks into campaign state breaks cloud-sync semantics (a device choice would sync as if it were campaign data).
- **Per-game data:** `GAME_DEFS` (`js/core/state.js`) — the Protocol-38 sanctioned per-game table, including the `identity` design-data block (DO-K). Read accessors: `getGameContext()`, `_activeDef()`, `getFactionRegistry()`, `getSkillKeys()`, `getIdentity(ctx)`.
- **Immersion/input-mode device settings:** `getImmersionTier`/`setImmersionTier`/`immersionAllows` and `getInputMode`/`setInputMode`/`otherInputMode` (`js/core/state.js`) — both MetaStore-backed, not campaign state.
- **Transcript event ledger (AI_OVERSEER Finding 5, 2026-07-19):** `let transcriptEvents = []` + `recordTranscriptEvent(kind, title, lines)` + `_persistTranscriptEvents()` (`js/core/state.js`, declared beside `chatHistory`). Records the on-screen events `chatHistory` does NOT hold — an AI `modal` node (recorded in `js/services/api.js` where it is rendered into `#sysModal`) and every `confirmAction()` dialog plus the answer given (recorded in `js/ui/ui-core.js`). Entries anchor to `chatHistory.length`; `appendToChat()` rebases every anchor when the history is capped. Persisted to `localStorage['robco_transcript_events']`, restored at boot, cleared with the chat. Consumed by `_transcriptRecords()` / `_transcriptEventLines()` / `_transcriptSpeaker()` (`js/ui/ui-saves.js`), the ONE assembly point all three export formats (txt/md/html) share, so a downloaded log represents what was actually on screen. Guarded by Suite 240.
## Save Integrity (Layers 1–3)
- **Layer 1 (write-side fail-loud):** `saveState()` quota warnings (`js/core/state.js`); the save-survival behavioral gate (`tests/save-survival.mjs`, full push gate only).
- **Layer 2 (storage survival):** `_requestPersistentStorage()` + `_showStorageWarningBanner(coldStoreOffline)` (`js/ui/ui-core.js`) — `navigator.storage.persist()` at boot, denied → banner (template `storageWarningBannerTemplate`, `index.html`). SAVE_LAYER3 tail rider: denied + no IdbStore records `robco_storage_persisted='denied-noidb'` and compounds the banner copy.
- **Layer 3 (read-side fail-loud, 2026-07-16):** all in `js/ui/ui-core.js` unless noted —
- `_quarantineCorruptContainer(sourceKey, rawStr, err)` — capture-then-remove: corrupt `robco_v8`/`robco_v7` bytes go whole into `localStorage['robco_v8_quarantine']` (never overwriting an earlier unresolved one) + a fire-and-forget IDB `'campaign'` copy (`quarantine` / stamped `quarantine_<ts>` overflow, swept to newest 3) + `window._quarantinedEnvelope` in-memory fallback. Records `robco_read_fault` pref + `_recordError`.
- `_showReadFaultBanner('corrupt'|'evicted')` (~ after `_requestPersistentStorage`) — clones `readFaultBannerTemplate` (`index.html`), fills `#readFaultBannerMsg`. Exposed as `window._showReadFaultBanner` (Diagnostic Shell).
- Eviction signature: `window._bootMarkerRecovered` set inside `_reconcileMetaFromIdb()`'s recovery loop (key `robco_booted_before` only); checked at `_hydrateStateFromStorage()`'s tail as `!v8Str && !v7Str && recovered` → EVICTION banner + `robco_eviction_detected` pref.
- Recovery affordances: `_readQuarantineEnvelope()` / `window.exportQuarantinedRecord` / `window.confirmPurgeQuarantine` → `_purgeQuarantineApply()` (`js/ui/ui-saves.js`); the `[FAULT] QUARANTINED RECORD` row in `renderSavesList()` (`js/ui/ui-account.js`).
- Degraded-write notice: `_coldWriteObj()` (`js/core/state.js`) returns `{ ok, idbOk, lsOk }` (⚠ always-truthy object — callers check `.ok`; test seam `window.__robcoForceColdWriteMode`); `_maybePostDegradedWriteNotice(res)` + once-per-session latch in `js/ui/ui-saves.js` `saveToSlot()`.
- Guards: Suite 233 (static + VM behavioral) in `tests/robco-diagnostics.js`; the LAYER3 browser sections in `tests/save-survival.mjs`; 7 `SAVE INTEGRITY` tools in `js/dev/test-console.js`.
- **P8 live-container durability mirror (2026-07-19):** `window.mirrorLiveContainer()` / `window.restoreLiveContainerFromIdb()` + `const LIVE_MIRROR_IDB_KEY='live'` (`js/core/state.js`, in the "LIVE CONTAINER DURABILITY MIRROR" section before FACTION REGISTRY). The live `robco_v8` container — the only cold-store datum with no IDB shadow before this — is mirrored fire-and-forget into `'campaign'/'live'` on every debounced save (post dirty-check) and on the `_flushUnload` beforeunload/visibilitychange-hidden path. Restore is a bounded, awaited boot phase (`_restoreLiveContainerFromIdb()` in `js/ui/ui-core.js`, `_LIVE_RESTORE_BUDGET_MS`) run before `_hydrateStateFromStorage()`; recovery-only (returns false without reading the mirror while localStorage holds `robco_v8`), shape+checksum gated (same idiom as `_reconcileMetaFromIdb`), so a stale mirror can never overwrite a newer local value (Protocol 34). Fail-safe: no IndexedDB ⇒ mirror no-ops, restore yields nothing, boot is byte-identical (eviction banner still fires). Diagnostic Shell: `save-simulate-eviction-recovery` → `_dshSimulateEviction()`. Guarded by Suite 239 (10 static + 6 behavioral incl. RED→GREEN recovery + anti-clobber).
## Event Bus
- **Definition:** `RobcoEvents` (`js/core/state.js`), exposed as `window.RobcoEvents`. `.emit(type, payload)` / `.on(type, handler)`.
- **Subscriber wiring** — each called exactly once, from `window.onload`, always before any state hydration:
| Function | File |
| --- | --- |
| `_wireCoreEventBusSubscribers` | `js/ui/ui-core-cmd.js` (2.8.5 U-A1: moved from ui-core.js) |
| `_wireAudioEventBusSubscribers` | `js/ui/ui-audio.js` |
| `_wireApiEventBusSubscribers` | `js/services/api-import.js` (2.8.5 U-A3: moved from api.js) |
| `_wireChassisCoreEventBusSubscribers` | `js/ui/ui-core-chassis.js` (Living Core; 2.8.5 U-A1: moved from ui-core.js) |
| `_wireFeedbackEchoSubscribers` | `js/ui/ui-core-cmd.js` (Status Annunciator; 2.8.5 U-A1: moved from ui-core.js) |
| `_wireLocationCardSubscriber` | `js/ui/ui-core-cmd.js` (arrival toast; 2.8.5 U-A1: moved from ui-core.js) |
- **Emitted event names** (as of this snapshot — grep `RobcoEvents\.emit(` across `js/*.js` for the exhaustive current list, this list is a snapshot not a contract): `level.up`, `limb.state`, `effect.applied`, `item.added`, `quest.status`, `effect.expiring`, `faction.threshold`, `collectible.acquired`, `sleep.completed`, `data.write`, `location.visited`, `stat.change`, `location.current`, `karma.tier`, `weight.seized`, `hp.critical`, `rad.tier`, `item.equipped`, `craft.completed`, `craft.scrapped`, `trade.bought`, `trade.sold`, `runtime.state`.
- **Emitters are not centralized** — they fire wherever the underlying state change happens: `js/services/api-import.js` (AI-driven changes, via `autoImportState()`), the `js/ui/ui-render-*.js` family (manual panel edits), `js/ui/ui-core-cmd.js` (bar drags, level-ups), `js/services/cloud.js` (`data.write` on push/pull/backup-restore), `js/core/runtime.js` (`runtime.state`), `js/core/state.js` (`location.visited`), plus `js/services/api-router.js`, `js/ui/ui-core.js`, `js/ui/ui-core-chassis.js` and `js/ui/ui-saves.js`. **Treat this list as non-exhaustive** — it went stale once already by omitting those last four. `grep -rn 'RobcoEvents.emit(' js/` is the authoritative list.
- **`js/dev/test-console.js` is a replay surface, not a real emitter** — it re-emits every event above on demand as a Diagnostic Shell "fire-anim-`<event>`" tool (Protocol 44), for testing hard-to-trigger reactive behavior. Don't count its emit call sites as production emitters.
- **Reactive auto-log subscriber:** seven of the events above have a `js/core/state.js`-owned subscriber that appends to `state.eventLog` on every fire — read the actual subscriber body before assuming an emit is inert (this is the Protocol 44 tiering trap: firing one of these seven via the Diagnostic Shell writes campaign state indirectly, so those tools are `tier: 'staging'` + `destructive: true`, not `'prod'`).
## AI Contract (Tri-Node)
- **`getSystemDirective()`** (`js/services/api-directive.js`) — builds the system prompt from 7 top-level section builders (`_directiveConstraints` is called one level down, from inside `_directivePersonaAndContract`), all in `js/services/api-directive.js`: `_directiveConstraints`, `_directivePersonaAndContract`, `_directiveCoreTracking`, `_directiveSkills`, `_directiveFactions`, `_directiveSystems`, `_directiveTrackers`, plus `_directiveInjectionBoundary`. 2.8.5 U-A3: split out of `api.js` into its own file, guarded byte-identical by the Suite 131 golden-master.
- **`transmitMessage()`** (`js/services/api.js`) — the network call lifecycle: retry/abort, `_resetTransmitUI()` resets the composer UI on completion or failure. Stays in the `api.js` hub (2.8.5 U-A3).
- **`_validateTriNode(parsed)`** (`js/services/api.js`) — schema validator for the `{narrative, state, modal}` response shape. The app is locked to `responseMimeType: 'application/json'`.
- **`autoImportState(jsonString)`** (`js/services/api-import.js`) — the ONLY path that writes AI output into `state`; explicit field mapping (Protocol 24), never recursive key transforms. `sanitizeImportedContainer(container)` is the shared sanitizer also used by the file-import (`js/ui/ui-saves.js` `handleFileUpload`) and cloud-pull (`js/services/cloud.js` `loadCloudSave`) paths — one sanitizer, three entry points. 2.8.5 U-A3: split out of `api.js` into its own file.
- **Golden-master regression test:** Suite 131 (directive-shape). Changing the directive schema or the Tri-Node shape requires a same-commit test update (Protocol 14).
- **AI_OVERSEER batch 2 (2026-07-19) — the post-sync surface.** `autoImportState()`'s tail no longer navigates: the changed-category pass calls `expandPanelForCategory(cat, { navigate: false })`, so the panel still expands but the tab never switches away from the terminal (Finding 6, owner directive). Each change is surfaced in place instead via `_syncChangeCardsShow(lines)` (`js/ui/ui-core-cmd.js`), fed from the SAME diff that builds the `[DELTA]` chat line — one change-detector, two surfaces. `expandPanelForCategory(categoryKey, opts)` (`js/ui/ui-core.js`) gained `opts.navigate`, DEFAULT TRUE, so every other caller (`#go=` deep links, the native router, LOOT/ammo hand-offs) is unchanged. **Directive authority sweep (Finding 8):** four now-native authority claims were removed from `_directiveCoreTracking` (Consumable Purge, Financial Metrics/Vendor Base_Cap, Skill Point Math, and the quadratic-XP formula — which also DISAGREED with the native curve) and the example schema's all-eleven-factions block was cut to one changed faction; chars (tokens) saved per call. Guarded by Suite 240 + the 11 regenerated Suite 131 hashes.
## Native Command Router
- **`NATIVE_COMMAND_ROUTER`** (`js/services/api-router.js`) — the command-token → handler map. `_routeNativeCommand(userText)` dispatches it; `_routePanelNav(raw)` handles panel-jump tokens. 2.8.5 U-A3: split out of `api.js` into its own file, along with every offline/native routing helper below.
- **Quick-log grammar** (offline shorthand that mutates state without a network round-trip): `_routeQuickLog`/`_routeQuickLogMulti` dispatch to `_quickLogKill`, `_quickLogCaps`, `_quickLogLocation`, `_quickLogFaction`.
- **Native stat-token setters:** `_resolveStatToken`, `_readStatCurrent`, `_applyStatToken`, `_statTokenLabel`, `_quickLogStatSet`, `_quickLogStatDelta`.
- **Command-line entry:** `submitCommandInput()` → `_resolveCommandInput()` → routes through the router or quick-log dispatch (or `transmitMessage()` in `js/services/api.js` for OVERSEER mode). Autocomplete: `_commandSuggestions`, `_quickLogContentSuggestions`, `_statTokenSuggestions`.
## Audio Model
- **Cache object:** `AudioSettings` (`js/ui/ui-core.js`, the hub file) — every audio-hot-path read goes through this cache, never `localStorage.getItem()` directly (Prohibited Patterns table in `CLAUDE.md`).
- **Context bootstrap:** `ensureAudioCtx()` (`js/ui/ui-audio.js`).
- **Ambient loops** (start/stop pairs, `js/ui/ui-audio.js`): `startCrtHum`/`stopCrtHum`, `startTinnitus`/`stopTinnitus`, `startReactorHum`/`stopReactorHum`, `startThermalLoad`/`stopThermalLoad`, geiger via `scheduleGeiger`/`setGeigerRate`/`playGeigerClick`.
- **One-shots:** `playClack`, `playWakeTone`, `playSyncTone`, `playLimbCrippleSound`, `playLimbRestoreSound`, `playHeadCrippleSound`.
- **Haptics:** `triggerHaptic`, `isHapticEnabled`, `toggleHaptic`, `initHaptic`, `_hapticSupported`, `_hapticReducedMotion`.
- **Optics/theme color:** `changeOpticsColor`, `_resolveOptic`, `_resolveDefaultOptics`, `_applyThemeVars`, `_expandOpticsFamily`/`_collapseOpticsFamily`.
- **Mute-guard pattern (Protocol 7):** every trigger function checks `AudioSettings.masterMute` first, then its own per-source settings key, before doing anything audible.
## Render Pipeline
- **Master fan-out:** `loadUI()` (`js/ui/ui-core.js`, the hub file) — calls every panel's `render*()` once per full refresh; `_isDirty` gates redundant repaints.
- **Panel render functions — split by responsibility (2.8.5 U-A4, landed)** across nine `js/ui/ui-render-*.js` siblings (grep `^function` across the family for the exhaustive current list), leaving `js/ui/ui-render.js` as a thin hub holding only `_updateContextPanels()`:
- `js/ui/ui-render-inventory.js` — Cargo Manifest & Ammo: `addItem`/`delItem`/`adjItemQty`/`toggleEquipItem`, `nativeUseItem`, `renderInventory`, `renderAmmo`/`addAmmo`/`removeAmmo`
- `js/ui/ui-render-character.js` — Character & Field Status: `renderSquad`/`addSquadMember`/`adjustAffinity`, the game clock/calendar (`formatGameTime`/`getGameDate`/`calendarToTicks`), `FACTION_THRESHOLDS`/`getFactionStanding`, `renderStatus`/`addStatusEffect`, `renderPerks`/`addPerk`, `renderQuests`/`cycleQuestStatus`
- `js/ui/ui-render-record.js` — Personal Record: `renderSessionStats`, `renderEquipped`, `renderCollectibles`/`toggleCollectible`, `renderLincolnMemorabilia`/`toggleLincolnItem`, `renderTraits`/`toggleTrait`
- `js/ui/ui-render-ledger.js` — Field Ledger: the shared `_renderReadTracker()` helper, `renderSkillBooks`/`toggleSkillBook`, `renderMagazines`/`toggleMagazine`, `renderCampaignNotes`/`addCampaignNote`, `renderCampaignStatus`/`_recordLine`
- `js/ui/ui-render-map.js` — Cartography Table: `renderWorldMap`, `setMapView`, `zoomMapToZone`, `resetMapZoom`, `travelToLocation`, `markLocationVisited`, `_mapNodeKeyNav`
- `js/ui/ui-render-factions.js` — Faction Reputation & Karma: `renderFactionRep`, `adjustFaction`, `setFactionChannel`, `renderKarmaCenter`
- `js/ui/ui-render-economy.js` — Resource Economy: `renderCraft`/`renderCraftCard`/`renderScrapCard`, `renderTrade`/`renderTradeBuyList`/`renderTradeSellList`
- `js/ui/ui-render-loot.js` — Item Acquisition: `renderLoot`/`renderLootList`, `_lootAdd`; the Visual Upload OCR apply flow — `applyVisualParse`, `_visualParseInventoryMerge`, `renderVisualParsePreview`, `_confirmVisualParse`
- `js/ui/ui-render-databank.js` — Native Databank Tools: `renderThreat`, `renderConsult`/`renderDatabankPanel`, `renderEligiblePerks`, `renderBioScan`
- **`renderHolster()`** lives in `js/ui/ui-render-loot.js` (item-acquisition adjacent — the Tool Deck's Quick-Draw Holster sockets).
- **Shared cross-panel updater:** `_updateContextPanels()` (`js/ui/ui-render.js` — the hub's sole remaining function; cross-cutting glue not owned by one sibling).
- **Panel wiring points (the Protocol 5 authority — R3, 2026-07-20).** `rules/ui-and-mobile.md`'s Protocol 5 used to carry a hand-written checklist that named `ui-render.js`; it went stale at the U-A4 split and now points here instead. A new `<details class="panel">` in `index.html` must be wired at each of these, all verified from source on `dev`, 2026-07-20:
1. **Render function** — a `render*()` in the responsibility-matching `js/ui/ui-render-*.js` sibling above (NOT `ui-render.js`, which is a thin hub).
2. **Fan-out** — called from `loadUI()` (`js/ui/ui-core.js`).
3. **Badge, if it shows a count** — an entry in `_updatePanelBadges()` (`js/ui/ui-core.js`).
4. **AI auto-expand, if AI changes should open it** — a key in `expandPanelForCategory()`'s map (`js/ui/ui-core.js`).
5. **Autocomplete, if it has a registry-backed text input** — a `wireInput()` call inside `initRegistryAutocomplete()` (`js/ui/ui-saves.js`) — see § Registry.
6. **Sub-panel persistence** — a unique `data-sub-id` on every `<details class="sub-panel">` (Protocol UI-2).
- **CRUD naming convention** (Protocol 6): `addX`/`delX`/`toggleX` — e.g. `addItem`/`delItem`, `addPerk`, `addSquadMember`, `addStatusEffect`, `addCampaignNote`/`removeCampaignNote`, `toggleCollectible`, `toggleLincolnItem`, `toggleTrait`, `toggleSkillBook`, `toggleMagazine`.
- **2.8.5 U-A1 (landed):** `js/ui/ui-core.js`'s *non-render* subsystems were split by responsibility into `js/ui/ui-core-nav.js` (bezel nav — `selectSubsystem`/`switchTab`/`_syncBezelNav`/`SHORTCUT_ROUTES`; FO3 Pip-Boy build U1 added the second nav axis mechanism here too — `_applyRailGrouping()`/`selectSubtab(name)`/`_applyRails(subsystem)`, all no-ops without `identity.rails`, Protocol 38; U2 added `_renderFo3TopStrip()`/`_fo3StripFieldValue(key)`, hooked into `_refreshBezelTelemetry()`, all no-ops without `identity.statusStrip`; the U2 owner-feedback pass added `_applyFo3NavLabels()` — re-labels the 3 lamp keycaps from `identity.navLamps`, no-op without it, called from `window.onload` alongside `_applyRailGrouping()` — and extended `_scrollElFor()` to target `#uiPanel` when `identity.orientation === 'landscape-primary'` AND the orientation:landscape media query matches, mirroring the existing desktop-pointer branch), `js/ui/ui-core-overseer.js` (Director Uplink — `setOverseerState`/scope canvas/composer wiring/Tool Deck launcher), `js/ui/ui-core-chassis.js` (Living Core — `_coreRefresh`/`initChassisCore`/System Status/Service & Fault Console), `js/ui/ui-core-modulebay.js` (Module Bay — `renderModuleBay`/wake-lock/immersion-dial/campaign-config board), and `js/ui/ui-core-cmd.js` (command layer — native stat setters/`COMMAND_REGISTRY`/the core event-bus subscriber wiring), leaving `js/ui/ui-core.js` as a slimmed hub (`AudioSettings`, `loadUI()`, `updateMath()`, `appendToChat()`, `window.onload` + its boot-phase functions, `_isStagingEnv()`, `expandPanelForCategory()`, `_updatePanelBadges()`). All six load as plain global-scope `<script>` tags (the `ui-core-` stem lets both test runners' `readGroup('ui-core')` helper glob the whole family — U-A0).
## Cloud Path
- **Module:** `js/services/cloud.js` — the one `type="module"` file (ES module), loads last in the static-tag group.
- **Entry points:** `window.saveCurrentToCloud`, `window.loadCloudSave` — the real manual push/pull globals. (The names `pushToCloud`/`pullFromCloud` were documented for months but never existed — the Suite 220/Protocol 45 regression guard, test 220.5, locks this fixed.)
- **Additive-write discipline (Protocol 34):** saves are created with `addDoc`, modified by id with `updateDoc`. `setDoc` is banned outright for `saves` documents; any `setDoc` to a mutable config doc must pass `{merge: true}`.
- **Kill-switch/fallback bookkeeping (Protocol 32/33):** `_recordFeatureFailure()`, `_autoDisabled`, `_failCounts`, `FAIL_THRESHOLD`, `_isOfflineError()`.
- **Save payload construction:** `_buildSavePayload(label)`.
- **UI side:** `renderAccount()`, `renderSavesList()` (`js/ui/ui-account.js` — this superseded the old `renderCloudSavePicker()`, which showed cloud saves only and **no longer exists**; two comments in that file still name it as history), `undoLastSync()` (`js/ui/ui-account.js`).
## OCR Path
- **Lazy-loaded only — never at boot:** `_ensureTesseract()` (`js/services/ocr.js`) is the gate; Tesseract.js is not fetched until a visual upload actually happens.
- **Pipeline:** `routeVisualUpload` → `_loadImageFromFile` → `_preprocessImageToCanvas` → Tesseract recognize → `_parseOcrText(text, ctx)` → `_tryParseStatLine` / `_tryParseInventoryLine` → `_looksLikeItemName`/`_cleanOcrLine` clean the candidates.
- **AI-vision fallback** (when OCR confidence is low or Tesseract is unavailable): `_aiVisionAvailable()`, `_tryAiVisionFallback()`, `_visualUploadDeadEnd()`.
- **Applies into state via `js/ui/ui-render-loot.js`:** `applyVisualParse(parsed)`, `_visualParseInventoryMerge()`, previewed by `renderVisualParsePreview()` and confirmed by `_confirmVisualParse()`.
## Diagnostic Shell
- **Registry:** `DIAGNOSTIC_SHELL_TOOLS` (`js/dev/test-console.js`) — the single source of truth `initTestConsole()` renders as the shell's tool list.
- **Tiering (Protocol 44):** every tool declares `tier: 'prod'` (visible on both staging and production; must be safe/non-destructive) or `tier: 'staging'` (staging/dev-only, may carry `destructive: true` if it mutates campaign state — directly, or indirectly via a reactive event-bus subscriber, see Event Bus above).
- **Environment gate:** `_isStagingEnv()` (`js/ui/ui-core.js`, the hub file) — the same environment detection the Protocol-21 environment-aware changelog viewer keys off (fail-safe: defaults to hiding staging-only surfaces).
- **Cross-reference enforcement:** a gate suite asserts every `RobcoEvents.emit()` name and every view-once MetaStore flag (`robco_bay_opened`, `robco_last_seen_version`, `robco_booted_before`) has a `triggers: [...]` entry somewhere in `DIAGNOSTIC_SHELL_TOOLS`, with a small curated allowlist for deliberately-internal events (Protocol 44 enforcement).
## Registry (read-only reference data)
- **Engine:** `registrySearch(category, query)` (`js/data/registry-core.js`) — read-only, never touches `state` (Protocol 23).
- **Data:** `FALLOUT_REGISTRY`, injected per-game by `js/data/reg_nv.js` / `js/data/reg_fo3.js` via the `GAME_FILES` boot manifest in `index.html`.
- **Wiring:** `initRegistryAutocomplete()` (`js/ui/ui-saves.js` — re-verified 2026-07-20; the earlier anchor had drifted), `wireInput(inputId, categoryOrFn)` (nested inside it) — every autocomplete text input in `index.html` routes through this. **This section is the Protocol 6 authority**: a new category must be added to BOTH per-game files (`reg_nv.js` and `reg_fo3.js`) or it silently returns nothing in the other game (Protocol 38), and the handler follows the `addX`/`delX`/`toggleX` convention in § Render Pipeline.
## Persistence Substrate
- **IndexedDB durable-shadow engine:** `js/core/idb.js`, exposed as `window.IdbStore`. First static `<script>` tag — loads before everything else, including the per-game `GAME_FILES` manifest.
- **CSV item lookups:** `databaseCSVs`, `lookupItemInDb()` — `js/data/db_nv.js` / `js/data/db_fo3.js`, injected per-game (FNV fail-safe default).
## Test System
- **Single canonical runner (Protocol 15 retired, 2.8.5 U-B3):** `tests/robco-diagnostics.js` (Node) is the one test runner — run on every commit (no count is tracked; Protocol 2a retired). The former PowerShell mirror (`tests/robco-diagnostics.ps1`) was deleted; it caught nothing the Node runner cannot, at× the cost.
- **Suite declaration:** `header('Suite N — title')` in Node, each followed by a run of `assert(...)` calls.
- **Runtime mirror:** `tests/test.html` — actually executes the live import contract in a real browser (Protocol 40), headless-run by `tests/test-html-check.mjs` in the full gate.
- **Gate orchestration:** `scripts/gate.js` (lint → format → boot-chain preflight → the Node runner → browser checks). `scripts/check-boot-chain.js` (U-A0, landed) cross-checks index.html/sw.js/disk/docs/test.html consistency. `scripts/gen-test-catalog.js` is **proposed, not yet built** — `library/TEST_CATALOG.md` is still hand-maintained today.
- **Per-suite coverage detail:** `library/TEST_CATALOG.md`.
---
_Snapshot: derived from `dev`, 2026-07-11 — updated for the 2.8.5 U-A1 `ui-core.js` split, the 2.8.5 U-A2 `js/`+`css/` reorg, and the 2.8.5 U-A3 `api.js` split. LIVE doc (Protocol 46) — keep current in the same commit as any file split/add/move/remove or major function relocation. Function/line-number references are approximate jump anchors, not a contract; grep the function name to confirm. Where code and this doc disagree, the code wins._
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).