RELEASE
planning/2.8.0/plans/MAINREVERT_COMPAT_PLAN.md
sha256 f828f6772e1a573b · 10110 bytes ·
original held in the private archive
# MAIN-REVERT ↔ CLOUD-SAVE COMPATIBILITY — Investigation + Safe Plan
> READ-ONLY investigation. Nothing was changed, committed, or pushed. This doc is the
> only file written (planning/ is gitignored). Implement nothing without owner approval.
>
> Scenario: owner will later revert **`main`** back to the clean **v2.6.0** baseline
> (tag `v2.6.0` = commit `bc8c2eb`), dropping the unreleased 2.7.0 work from production
> (that work stays on `dev`). Concern: CLOUD saves (Firestore project `nv-overlord`)
> created while running the in-progress 2.7.0 build will then be loaded by the reverted
> v2.6.0 production code — will the older code drop/break newer save fields, or treat
> them as a "future version"?
---
## TL;DR VERDICT — **SAFE. No data loss. No "future version" prompt. No save surgery needed.**
The owner's cloud saves are **fully compatible** with reverted v2.6.0 and survive a
load → re-save round-trip with **zero field loss**. The three feared failure modes
**cannot fire**:
1. **"Future version" rejection** — does NOT trip. Every cloud save carries
`version: "2.6.0"` (APP_VERSION never changed during the whole 2.7.0 build). v2.6.0's
future-check is `schemaVersion||version > APP_VERSION` → `2.6.0 > 2.6.0` is **false**.
2. **Field dropping on load** — does NOT happen. v2.6.0's import path is **passthrough**
at every stage (sanitize copies all fields, migrate only *adds* missing ones, boot
hydrates with a spread merge). Nothing whitelists or deletes unknown fields.
3. **Round-trip strip on re-save** — does NOT happen. Re-save serializes the full
in-memory `state`, which already holds every field loaded.
**The decisive fact:** **v2.6.0 IS the Phase-6 release.** The "newer 2.7.0 fields" the
owner worried about (`collectibles`, `traits`, `skillBooks`, `magazines`, `lincolnItems`,
faction `fame`/`infamy`, `campaignMode`, `playthroughType`, `mapView`) all already exist
in v2.6.0's `state` defaults + `migrateState`. The unreleased 2.7.0 work added **zero new
state fields** — it was hardening (XSS escaping, perf caching, game-agnostic refactor,
more-defensive import coercion).
---
## EVIDENCE
### Git topology
| Ref | Commit | Note |
|-----|--------|------|
| `v2.6.0` tag | `bc8c2eb` | "Release: v2.6.0 — Phase 6 (content, crafting, trackers, fixes)" — the revert target |
| `main` (now) | `505276f` | ~16 unreleased commits **ahead** of v2.6.0 (WU-A series + a11y + fixes) |
| `dev` (now) | `44542d9` | 7 further commits ahead of main (WU-B series) — the furthest "2.7.0 build" tip |
"Revert main to v2.6.0" = move `main` back to `bc8c2eb`. The owner's saves were made on
the 2.7.0 build; comparing **v2.6.0 → dev** (the furthest tip) covers everything the saves
could possibly carry.
### 1. Save envelope + version stamp
- `APP_VERSION === '2.6.0'` on **v2.6.0, main, AND dev** (confirmed in `js/state.js:2`).
It never moved during the 2.7.0 build.
- Cloud payload (`js/cloud.js`, both `saveCurrentToCloud` and `syncLocalSavesToCloud`)
stamps **`version: "2.6.0"`** and `schema: 2`. It writes **no `schemaVersion`** and
**no `checksum`** field (it writes `contentHash`, a dedup hash, not the envelope
`checksum` that the integrity check looks for).
- `verifySaveEnvelope` (`js/state.js`) on a cloud doc:
- `sv = schemaVersion || version` → `"2.6.0"`. `_semverGt("2.6.0","2.6.0")` = false →
**not** `future_version`.
- No `checksum` field present → returns **`{status:'legacy'}`** → loads normally, **no
warning, no confirm**. (The `checksum_mismatch` path also cannot fire.)
### 2. What the cloud saves contain
Payload = `{ schema:2, version:"2.6.0", savedAt, updatedAt, label, gameContext,
contentHash, robco_v8, chat, playstyle }`.
`robco_v8` is the full multi-campaign container; each campaign is
`JSON.parse(JSON.stringify(state))` — the **entire** state object, no field whitelist.
Every field below already exists in v2.6.0:
- Core: `lvl, xp, hpCur, hpMax, s/p/e/c/i/a/l, caps, rads, karma, ticks, loc, limbs`
- Arrays/objects: `inventory, ammo, quests, perks, squad, status, equipped,
campaign_notes, locationHistory, stats, factions`
- Phase-6 trackers: `collectibles[], traits[], skillBooks[], magazines[],
lincolnItems{}`
- Mode flags: `campaignMode, playthroughType, mapView, gameContext`
**`js/state.js` diff v2.6.0 → dev = only 2 hunks:** a `seedInventory` constant added to
`GAME_DEFS` (not a state field) and a `_loadingSave` guard in `saveState()`. The
`let state = {…}` defaults block and `migrateState` are **byte-identical**. ⇒ no new
state fields exist in the 2.7.0 build.
### 3. v2.6.0 baseline load path (does it drop / reject?)
`loadCloudSave(docId)` in v2.6.0 `js/cloud.js`:
1. `verifySaveEnvelope` → `legacy`/`ok` (never future for these saves; see §1).
2. `snapRollingBackup()` — backs up current local state before loading.
3. `sanitizeImportedContainer(data.robco_v8)` — **passthrough**: `Object.assign({}, s)`
copies *all* fields, then coerces only the known ones (numbers, limbs, quests,
inventory, squad, perks, status, equipped). It **never deletes** unknown fields and
`return`s the full object. (`equipped` is the only sub-object rebuilt, to exactly
`{weapon,armor,headgear}` — the only sub-keys that exist; no loss.)
4. `migrateState(version, campaign)` — **additive**: only adds missing fields and
migrates legacy keys (old flat faction keys, `macros`); never strips unknown fields.
5. writes `robco_v8` to localStorage, sets `playstyle`, reloads.
Boot hydration (`js/ui-core.js`, **unchanged** v2.6.0 ↔ dev):
`state = { ...state, ...activeCampaign }` — **spread merge**, so every saved field lands
in `state`.
⇒ Load **preserves every field**. Reject-as-future and field-drop both impossible.
### 4. THE REAL RISK — load → re-save round-trip
After load, `state` holds **all** fields (spread merge). On any subsequent save/push,
`saveCurrentToCloud` does `robco_v8.campaigns[ctx] = JSON.parse(JSON.stringify(state))` —
serializing the **full** state. No field is stripped going back out. **Round-trip is
lossless.** This is the permanent-data-loss path the owner feared, and it is closed.
### One real (harmless) difference
dev's `sanitizeImportedContainer` gained extra *defensive coercion* for the Phase-6
fields (drop nulls in `collectibles/traits/skillBooks/magazines`, whitelist `lincolnItems`
dispositions, clamp faction `fame`/`infamy` to non-negative ints). v2.6.0 lacks that
coercion but still **carries the fields through untouched**. Because the saves were
written by the app itself (well-formed values), the absence of extra coercion changes
nothing on load. (Only relevance: a *hand-corrupted* save could pass a malformed value
into v2.6.0 that dev would have cleaned — not the owner's case.)
---
## SAFE ONE-TIME PROCEDURE
### (a) Back up the cloud saves first — YES, do this (belt-and-suspenders)
Reverting `main` is a **git/deploy** operation; it does **not** touch Firestore. The cloud
documents (`users/{uid}/saves/{docId}` in project `nv-overlord`) persist unchanged
regardless of which code is deployed. Still, take a cold backup before the revert:
- **Easiest (in-app, while still on the 2.7.0 build):** for each cloud save, load it, then
use **EXPORT SAVE FILE** to write a local `.json` to disk. Keep those files.
- **Or (no code dependency):** Firebase console → Firestore → project `nv-overlord` →
`users/{uid}/saves` → export/copy each save doc's JSON. (A `gcloud firestore export`
to a bucket also works if configured.)
This backup is insurance only — analysis shows the saves load cleanly as-is.
### (b) Normalize the saves to a "v2.6.0-clean" shape? — **NO. Not needed.**
The saves are **already** v2.6.0-shaped (identical state schema). v2.6.0's import is
already preserve-on-passthrough. No save surgery, no reverted-code patch, no migration
script. Touching the saves would add risk for zero benefit. All real game data
(caps/level/inventory/skills/factions/perks/quests/trackers) is preserved automatically.
### (c) Version stamp so v2.6.0 reads them as current, not future — **NO change needed.**
Saves already carry `version: "2.6.0"`, which equals v2.6.0's `APP_VERSION`, so the
future-version branch never executes. Do **not** rewrite the stamp.
> Contingency only (does not apply here): if any save somehow carried `version` > 2.6.0,
> v2.6.0 would show a **warning + "Force-load anyway?" confirm** — and even force-loading
> still runs the same passthrough sanitize + additive migrate, so **even then there is no
> data loss**, just a scary prompt. Since every stamp is exactly 2.6.0, the prompt won't
> appear.
### Irreversible steps needing owner approval
- **None on the save/data side.** No destructive Firestore writes or deletes are required;
do not delete or overwrite any cloud doc.
- The **`main` revert itself** (revert/reset `main` → `v2.6.0`, bump `CACHE_NAME`, push)
is the owner's separate deploy action — out of scope here and unrelated to save safety.
Per Protocol 1/16 it needs a cache bump so cached clients pick up the reverted build;
that does not affect the saves.
---
## POST-REVERT SANITY CHECK (optional, after the owner deploys v2.6.0)
1. Hard-reload prod + "REBOOT TERMINAL" so the reverted build is actually live.
2. Open ACCOUNT → cloud saves list; load one 2.7.0-era save.
3. Confirm **no** "future version" prompt appears and it loads silently.
4. Spot-check a Phase-6 field survived: trackers (snow globes/bobbleheads, traits, skill
books, magazines), faction fame/infamy, caps/level/inventory.
5. Re-save to cloud, reload, load again — confirm the round-trip kept everything.
---
## ONE-LINE ANSWER FOR THE OWNER
Your cloud saves are safe — v2.6.0 *is* the Phase-6 release, the version stamp never left
2.6.0, and every load/save stage is passthrough, so reverting `main` to v2.6.0 will read
your saves as current and lose nothing. Take a quick EXPORT-SAVE backup as insurance, then
revert with confidence. No save editing or version-stamp surgery required.
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).