---
description: "Deep audit of the M45 save/load subsystem — full-ECS-snapshot capture, type-erased registry, format-version discipline, atomic disk write + ring, save-side refusal/validation gates, and the M45.1 live load-apply (cell reload + FormId-keyed deltas + player-pose restore)"
argument-hint: "--focus <dimensions> --depth shallow|deep"
---

# Save / Load Subsystem Audit (M45 + M45.1)

Audit `crates/save` (full-ECS-snapshot format) and its sole live caller `byroredux/src/save_io.rs`
(+ `save_io/*_tests.rs`) for **data loss and save corruption**. The subsystem exists to remove Bethesda's
slow-corruption tail by making the live ECS the single source of truth; verify the CODE delivers that.
A silently dropped column, a stale schema guard, a torn capture, or a botched FormId remap **loses player
progress** — frame as CRITICAL/HIGH per `_audit-severity.md` (data loss is CRITICAL).

**Architecture**: Orchestrator; each dimension is a Task agent (max 3 concurrent). Read
`.claude/commands/_audit-common.md` (layout, methodology, dedup, finding format) and
`.claude/commands/_audit-severity.md` first; do not duplicate them here.

## Scope

**Crate** `crates/save/src/` (~3.2k LOC — read all): `lib.rs` (design intent docstring, `SaveError`),
`snapshot.rs` (`Snapshot`, container header, `FORMAT_MAJOR` doc = the bump ledger, `encode`/`decode`),
`registry.rs` (`SaveRegistry`; `register_component` / `register_replacing_component` /
`register_component_preserving` / `register_resource` / `register_form_id_component`; `ValidateFn`; `schema_fingerprint`), `driver.rs` (`save_world`,
`restore_world`, `validate_snapshot_types`, `restore_resources`, `restore_resources_subset`,
`build_form_id_remap`, `apply_deltas`), `disk.rs` (`write_slot`, `slots_by_recency`, `SaveRing`),
`validate.rs` (`validate_world` + 6 sub-checks). Tests: `crates/save/tests/round_trip.rs`.

**Engine side** (`byroredux/src/save_io.rs`): `build_save_registry` (the curated type set),
`MUTABLE_DELTA_COLUMNS` (**second** hardcoded list driving the live overlay), `PRE_RELOAD_RESOURCES`,
`SaveCommand`/`SaveInfoCommand`/`LoadCommand`, `PendingPlayerSaveActions` (F5/F9/pause-menu ingress),
`quickload_latest`, `execute_pending_save_loads` (+ `reload_interior_session`/`reload_exterior_session`),
`capture_player_pose`/`apply_player_pose`, `validate_form_ids`, `validate_cinematic_entity_refs`.
Cross-cut: `byroredux/src/boot/registries.rs` (installs registry/state), `app_events.rs` (per-frame
order: `capture_player_pose` → `step_player_save_actions` → `step_save_loads`), `app_step.rs`,
`byroredux/src/notifications.rs` (`PlayerNotifications`: bounded, transient, unsaved; drained in
`app_frame.rs::render_one_frame`), `byroredux/src/extensions/` (SDK extension-state capture/preflight/
restore layered around the save/reload — the extension *API* is `/audit-tooling`),
`byroredux/src/cell_loader/reference_state.rs` (`PersistentReferenceStates`, `without_parked_state`),
`cell_loader/{transition,spawn,unload}.rs` (`unload.rs`: `purge_cinematic_retention_state`, the
`player_owned_entities` teardown refusal), `crates/core/src/{atomic_file.rs,string/mod.rs,ecs/world.rs}`,
`crates/physics/src/sync.rs`. Companion doc: `docs/engine/save-load-roundtrip.md` (numbers the extension preflight/restore
steps as 1b/3b).

**Ownership split**: this skill owns schema, atomicity, validation, and load-apply *mechanics*. What loot /
inventory / consumable state *should* persist (gameplay semantics) → `/audit-gameplay`; reconcile-after-
overlay code lives in gameplay files but its **existence per removable column** is Dim 1's.

## Guard ledger (E2) — run these first; aim dimensions at what they cannot see

`cargo test -p byroredux-save` and `cargo test -p byroredux --bin byroredux save_io` (plus
`app_step` / `boot::schedule` sources named below). Only the four real-master tests in
`save_io/consumable_tests.rs` are `#[ignore]`d. Confirm each guard is live: not `#[ignore]`d, its scan
finds >0 items, and it fails when the invariant is broken.

| Guard | Pins |
|---|---|
| `registry_completeness_tests::every_component_or_resource_impl_is_saved_or_explicitly_allowlisted` | every `Component`/`Resource` impl in any `crates/*/src` (roots **discovered**, not listed) + `byroredux/src` is registered XOR in `NOT_SAVED_BY_DESIGN` with a reason; `PlayerNotifications`, `PickedUp` etc. are classified there |
| `serde_default_guard_tests::serde_default_on_saved_struct_requires_format_major_bump` | any `#[serde(default)]` on a save-participating type (skip-only fields exempt) |
| `serde_default_guard_tests::saved_type_shape_changes_require_format_major_bump` | hash of every `#[derive(..Serialize..)]` struct/enum body in `save_type_sources()` — discovered files that carry a `cfg_attr(feature = "save"` derive or define a registered type, plus seven explicit nested-payload files (since #5059 `945be7dc6`: `lighting.rs`, `crates/sdk` `identity.rs` / `script_function.rs`) — vs `BASELINE_SHAPE_FINGERPRINT`/`BASELINE_MAJOR`; the attribute scan reads the whole contiguous attribute block, so a save derive above a plain `#[derive(..)]` is seen (#5403 `e19cb0491`) |
| `serde_default_guard_tests::set_in_chargen_renames_still_decode_v23_keys` | `serde(alias)` rename path stays decodable |
| `round_trip_tests::delta_columns_carry_only_session_stable_fields` | no `FixedString`/`EntityId`/session handle in a `MUTABLE_DELTA_COLUMNS` type |
| `round_trip_tests::delta_columns_removed_at_runtime_have_a_load_reconciler` | every delta column with a production removal site has a declared reconciler or "NoReconcilerNeeded" reason |
| `round_trip_tests::npc_spawn_stamped_components_are_saved_or_intentionally_rederived` | spawn-stamped components registered or in `REDERIVED_NOT_SAVED` |
| `crates/save` `snapshot.rs`/`disk.rs`/`registry.rs` unit tests | header gates (`rejects_bad_magic`/`_truncated`/`_payload_truncation`/`detects_crc_corruption`/`rejects_schema_mismatch`/`rejects_major_version_skew`), atomic write, slot parsing, ring resume, recency tie-break, `form_id_column_resolves_the_flagged_entry`, replacing-column semantics |
| `tests/round_trip.rs::typed_snapshot_preflight_rejects_bad_column_without_world_mutation` | typed preflight precedes `clear_entities` |
| `live_reload_tests::{saved_resources_are_restored_before_the_cell_reload, pre_reload_restore_must_not_install_saved_item_instance_pool_early}` | pre-reload resource subset, #4135 |
| `command_queue_tests::{a_save_taken_mid_cell_transition_is_refused_not_written, a_save_taken_while_chargen_disables_saving_is_refused_not_written, quicksave_ring_cursor_does_not_advance_on_validation_abort, player_save_actions_wait_for_the_quiescent_fifo_drain, quickload_empty_errors_and_corrupt_newest_falls_back}` | save-side refusal gates, ring, quiescent drain |
| `live_reload_tests::post_reload_drain_steps_keep_their_order` | source-order pin inside `execute_pending_save_loads`: `restore_resources` → `restore_resident` → `reseat_ambient_packages_after_restore` → `build_form_id_remap` → `apply_deltas`, each called exactly once (#5060 `02fa8358c`; the `reconcile_worn_gear` single-call pin joined it, #5460 `bda5a1167`) |
| `app_step.rs::the_save_drain_publishes_the_transition_flag_before_draining` | `CellTransitionInFlight` published before the drain |
| `*_survives_save_load*` (`round_trip_tests.rs`, `consumable_tests.rs`) | per-type round trips incl. lock/enable ledgers, fragment/provider queues, cinematic trio, `Perks`/timed restorations, SM node state + `StoryEventAliasFill` reference FormIds |

Not covered by any guard (the audit's real work): the **two-list drift** between registry and
`MUTABLE_DELTA_COLUMNS`; staleness of `NOT_SAVED_BY_DESIGN` *reasons*; whether a baseline refresh
without a bump was *justified*; manual `impl Serialize` types (invisible to both serde guards);
the extension-state payload's own versioning (Dim 2); semantic correctness of load-apply ordering beyond
the two pinned pre-reload cases. (Path-qualified `impl byroredux_core::ecs::Resource for X` impls were
invisible to `impl_target_type` until #5255 `6b494d002`: it now parses the trait path's final segment, the nine
formerly unclassified types are allowlisted, and the guard also asserts every `NOT_SAVED_BY_DESIGN` row matches a
discovered production impl — a renamed/deleted type can no longer keep a stale row. Still judge the *reasons*.)

## Parameters / Extra Fields

`--focus <dims>` (default all 5) · `--depth shallow|deep` (`shallow` = container/API contracts; `deep` =
trace capture → encode → disk → decode → reload → delta-apply). Finding fields: **Dimension**: Snapshot
Completeness | Format & Schema Discipline | Container & Disk | Save-Side Gates | Live Load-Apply ·
**Data-Loss Class**: silent-drop | corruption-on-load | irrecoverable-write | reference-break | none
(required for any finding that can lose progress).

## Phase 1: Setup

`mkdir -p /tmp/audit/save`; dedup: `gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/save/issues.json`;
read the newest `docs/audits/AUDIT_SAVE_*.md` (fixed findings are regression checks — verify the fix is
in place); read the `crates/save/src/lib.rs` docstring and `snapshot.rs` container doc — each design claim
must be CODE-CONFIRMED or DRIFTED; run the guard ledger. Delta-first: for each dimension
`git log --since=<last report> --format='%h %cs %s' -- <Paths>`, skim unchanged ones.

## Phase 2: Dimensions

### Dimension 1: Snapshot Completeness & the Two Lists (highest risk — silent-drop)
Paths: `byroredux/src/save_io.rs` (`build_save_registry`, `MUTABLE_DELTA_COLUMNS`, `PRE_RELOAD_RESOURCES`), `save_io/registry_completeness_tests.rs`, `crates/save/src/{registry,driver}.rs`
First step: `git log -p -S'register_' --since=<last report> -- byroredux/src/save_io.rs`; then `git log --since=<last report> --format='%h %cs %s' -- 'crates/*/src' byroredux/src | grep -i 'component\|resource\|state'` to find new persistent state.
- **The registry IS the completeness contract**; start from the guard's `NOT_SAVED_BY_DESIGN` list, not from a
  hand enumeration. A persistent, player-mutable component/resource that is neither registered nor
  allowlisted is a HIGH silent-drop; a *stale allowlist reason* (the guard checks a reason exists, not that
  it is true) is the auditable residue — spot-check a sample every run, and re-check any reason that names a
  "no production mutator" claim after new gameplay commits land (P3 loot/consumables/pickups, perks, timed
  restorations, hardcore mode, persistent reference state were all new saved state; `PlayerNotifications`
  and `PickedUp` are correctly transient — the durable half of a pickup is the `PersistentReferenceStates`
  tombstone). Recent false reasons, since fixed: `StoryEventAliasFill` ("quests restart
  through fresh events" — now registered, #5394 `03e51dfad`), `EatSleepState` ("`resolve_destination` is
  idempotent" — restated per PLDT anchor, #5391 `42aab4c09`), `CinematicReAdoption` ("the teardown handles
  them" — the purge now despawns them, #5379 `203be9ed4`); plus the Eat/Sleep rows' "registered Seated restore"
  (`Seated` is not overlaid — `restore_world`-only; restated and pinned by the `seated_without_path` check at the tail
  of the completeness guard, #5458 `0aa844dbb`) and `StoryLocationCursor` ("a load legitimately re-fires" —
  `execute_pending_save_loads` now resets it on every in-process load, #5435 `557ead21d`, pinned in
  `live_reload_tests.rs`).
- **Removed-from-allowlist stays registered**: `CharacterController` (fractional breath/drowning carry),
  `RigidBodyData` (registered via `register_component_preserving`: its `serde(skip)` `part_surface_materials`
  is re-derived by NIF import, so the overlay's `preserve` hook keeps the live slice instead of wiping it with
  the row's empty default — #5686 `4e0c962c4`; a new `serde(skip)` import-derived field on an overlaid column
  needs the same hook), `Material`, `RumbleOnActivate`, `FragmentExecutionQueue`, the cinematic trio — verify none
  regressed out of `build_save_registry`, and no re-added `NOT_SAVED_BY_DESIGN` entry re-excludes them.
- **Two lists, one truth**: the live load overlays only columns in BOTH the registry and
  `MUTABLE_DELTA_COLUMNS`. A registered mutable column absent from the overlay is captured but never
  replayed → HIGH silent-drop, unless deliberately excluded and documented at its `register_component`
  site: `Material` (blast radius), `AnimationPlayer`/`AnimationStack` (stale `root_entity`/`clip_handle`),
  `FollowState`/`EscortState`/`Seated`/cinematic pair (`EntityId`, registry handles), `ActorVitals`
  (write-once FormID key). A column with an `EntityId`/handle must also be covered by
  `validate_saved_entity_references` (Dim 4).
- **Replacing columns**: `Perks`, `TimedRestorations`, `Dead`, `ActorControlState` use
  `register_replacing_component` (the last two since #5027/#5052 `7aa1d7741` — the process-lifetime player
  outlives the reload, so an additive `Dead`/restraint row survived a load into an alive save; pinned by
  `dead_and_restraint_cleared_on_live_player_by_saved_absence`) — saved absence is
  authoritative for FormID-matched entities (empty column kept in the snapshot; a *missing* column is not a
  tombstone). Ordinary columns are additive-only, so **every runtime removal of an overlaid component needs a
  reconciler** (`reconcile_dead_actor_runtime_state`, `reconcile_player_equipped_weapon`); the guard enforces
  disposition, this dimension judges whether "NoReconcilerNeeded" reasons are true (carrier destroyed by
  the cell reload?) and whether a new persisted fact (disable, pickup, lock) chose the marker-plus-
  reconciler or FormID-keyed-ledger model (`ReferenceEnableState`, `ReferenceLockState`,
  `PersistentReferenceStates` — resources keyed by FormID survive cell unload; a component would not). A saved
  resource that needs session `EntityId`s takes the `StoryEventAliasFill` shape (#5394): `StoryEventFill`
  `serde(skip)`s its `EventDataSlots` and saves the R1/R2 reference FormIds, which the alias refresh re-resolves.
  The overlay emits no `EquipmentEventBatch`: presentation derived from an overlaid column is re-derived by
  `reconcile_worn_gear` (`byroredux/src/npc_spawn/loot_appearance.rs`, #5034 `20717d5b7`) — run on the player after
  `apply_deltas` and on NPCs from `reference_state::restore`; it diffs live roots against restored slots
  (reveal / hide / queue `PendingGearImport`) and synthesizes no event batch. A new presentation fact derived
  from an overlaid column needs the same treatment. Re-derived-not-saved player state: `FactionRanks` is reset
  to the record by `reset_player_factions_to_record` after the overlay (#5058 `be3cd9468` — alias-injected
  ranks otherwise outlived the load).
- **P3/P4 runtime state (2026-09-29)**: player body (`PlayerBodyRoot`, `PlayerBodyRootEntity`, `HiddenFirstPerson`,
  `PlayerCameraView`), mid-life gear import (`NpcSkeletonBones`, `ActorBodyClass`, `PendingGearImport`,
  `PendingGearRelease` — both dropped together on load by `clear_player_gear_handoff_scratch`, #5255 `6b494d002`,
  `PendingInventoryActions`) and NPC dialogue (`NpcDialogueTopic`, `DialogueSurfaceState`) are all
  `NOT_SAVED_BY_DESIGN`; `ObjectiveHudCache` is an `App` field, not a Resource. Verify each reason (body subtree
  entities have no `FormIdComponent`: captured by `save_world`, never remapped — the basis of `RigidBodyData`'s
  "NoReconcilerNeeded" row).
- **Determinism** (the `Snapshot` doc claims reproducible CRCs at equal state): `Snapshot` maps are
  `BTreeMap` and component rows are sorted by entity id (`registry.rs`), but saved **resources** serialize
  as-is — at 2026-09-19 `Globals(HashMap<u32, f32>)`, `QuestStageState`, `ReferenceEnableState` (`HashSet`),
  `ReferenceLockState` and `PersistentReferenceStates::pair_rows` all emit hash-iteration order (since joined by
  `DialogueSpokenInfoForms` (`HashSet`) and `StoryEventAliasFill` (`HashMap`)), so two saves
  of equal state can differ in bytes/CRC. #4748 is closed as a doc fix: the `Snapshot` doc in `snapshot.rs` now
  says the payload "is not guaranteed deterministic for equal logical state" (`Globals` is still a `HashMap`, by
  design of that resolution). A new claim of reproducible CRCs, or a consumer that diffs/hashes saves, reopens
  it (MEDIUM doc/contract mismatch, not data loss). Do not claim determinism at the row level without checking this.
- **`next_entity`** is saved verbatim and replayed via `set_next_entity` before inserts; `insert_batch`'s
  `entity < next_entity` is a `debug_assert` only (release inserts at an unspawned id silently — MEDIUM);
  `StringPool::dump`/`from_dump` preserves symbol order (a reordered dump = every `Name` wrong = CRITICAL).
**Output**: `/tmp/audit/save/dim_1.md`

### Dimension 2: Format & Schema Discipline (registry fidelity + `FORMAT_MAJOR`)
Paths: `crates/save/src/{snapshot,registry}.rs`, `save_io/serde_default_guard_tests.rs`, `crates/save/Cargo.toml`, `crates/core/Cargo.toml`
First step: `cargo test -p byroredux --bin byroredux serde_default_guard` ; `git log --since=<last report> --format='%h %cs %s' -- crates/save/src/snapshot.rs`
- **Bump rule** (read the `FORMAT_MAJOR` doc comment in `snapshot.rs`: 104 as of 2026-10-15, with one `vN -> vN+1`
  entry per bump from v22 on; the ledger still lacks the v32→v33 entry — #5367 `14cff35ae`, new saved resource
  `DialogueSpokenInfoForms` — known-open #5459. Recent anchors: v36 Wave 4 `RigidBodyData.surface_material`, v37 #5542
  widened it `u8`→`u32`, v38 #5300 re-keyed `DialogueSpokenInfoForms` to a (speaker ref, INFO) pair, and v39–v104
  are almost entirely M47.2 scripting state — new saved resource columns, `Effect` / `FragmentGuard` / `ValueExpr`
  variants and fields reachable through queued fragment continuations (`FragmentExecutionQueue`), `CharacterLevel`
  v46 (kill XP ended #2947's exemption), `Bleedout` v51, `LevelUpPoints` v53; do not hardcode the number elsewhere): intra-type shape changes need a bump because
  `schema_fingerprint` hashes only column keys (+ replacing policy). A new required field, retyped field, or new `Option` in a saved
  type bumps; `#[serde(default)]` is forbidden as a compatibility mechanism (guard) — even where the default
  would be correct for every old save. Read-compatible changes move the shape baseline **without** a bump:
  adding an enum variant to an externally-tagged `serde_json` enum (keyed by variant *name*, never ordinal —
  #4140 corrected the old "discriminant shift" rationale), removing a field (no `deny_unknown_fields` on any
  saved type), renaming with `serde(alias)`. Changing an existing variant's field shape is not compatible.
  Adding/removing a registered column changes the fingerprint and rejects old saves via `SchemaMismatch`
  with no bump. An audit proposing to relax the blanket rule for a "safe" default reopens #1714.
- **Baseline refresh triage** (the guard cannot judge this): every `BASELINE_SHAPE_FINGERPRINT` change must
  carry a justification comment stating whether a registered column's shape changed. Known false-positive
  classes: (a) the guard hashes *every* serialized-derive type in a scanned file, including `NOT_SAVED_BY_DESIGN`
  types sharing it (e.g. `AnimatedTextureFlip`; inspect-only files such as `WaterMaterial`'s left the scan in
  573170e1c) — those move the hash with no snapshot impact; (b) a **tuple struct** sweeps the following `impl` block into its span (`VisibilityMask`) — method
  edits move the hash; (c) the hash includes the file-relative path, so moving a save-participating type
  between files moves it. Verify baseline and `FORMAT_MAJOR` were updated in the same commit, and that a
  refresh-without-bump was not hiding a real field change to a registered type.
- **Guard blind spots**: manual `impl Serialize` (no derive) types; a nested payload type of a plain-`derive`
  registered type (the `byroredux/src` pattern) defined in another file is outside `save_type_sources()` unless
  added to its explicit list (#5059 fixed in `945be7dc6` for `LightSource.emitter` and the `crates/sdk`
  `FormRef` / `ScriptValue` payloads — the next such nested payload is the residue; the extension-manifest
  declaration types sharing `script_function.rs` are exempt from the `serde(default)` scan, with a staleness
  test); the shape scan once skipped a save derive that was not the *last* `#[…]` attribute (#5403 fixed in
  `e19cb0491`, coverage markers assert `StoryManagerNodeState` / `SmNodeRuntime` contribute shapes); a new `Option<T>` field is hashed but
  looks like a routine baseline bump — confirm it got the bump; `serde(alias)` must never coexist with a
  dropped value semantic change.
- **Second payload — extension state** (`Snapshot.resources["ByroExtensionState"]`, written by
  `extensions::capture_extension_state`, type `ExtensionStateSnapshot` in `crates/sdk/src/component.rs`):
  it is outside the registry, the completeness guard, and both serde guards (sdk types carry a plain
  `derive`, so `save_type_sources()` drops them), and follows its **own** policy — a `format_version` range
  (`MIN_EXTENSION_STATE_FORMAT_VERSION..=EXTENSION_STATE_FORMAT_VERSION`) with `#[serde(default)]` fields, which
  the blanket rule forbids for registry types. Verify: the range gate runs in `preflight_extension_state`
  (before teardown); every shape change to its rows bumps `EXTENSION_STATE_FORMAT_VERSION` (nothing enforces
  it — no shape baseline); rows without stable authored identity refuse the save rather than dropping;
  `decode_saved_state` treats a missing key as empty but a present-and-malformed one as an abort.
- **`ValidateFn` parity**: every `register_*` variant builds a `validate` closure that decodes the SAME type
  `load` does (`Vec<(u32, T)>` for columns, bare `T` for resources) — a missing/wrong one exempts a column
  from the preflight so it fails only after teardown.
- **Feature chain**: `crates/save` → `byroredux-core` `features=["save"]` (→ `inspect`) must still hold, or a
  non-default build compiles away serde impls and columns serialize to `null`.
- **FormId handle vs pair**: `register_form_id_component` saves the stable `FormIdPair` (skips unresolvable
  handles with WARN, never panics); load re-interns to fresh handles and errors cleanly without a pool.
  `form_id_column()` is keyed by the explicit `is_form_id` flag, never "first `apply: None` entry".
- `FnvHasher` uses canonical FNV-1a 64 constants and hashes only names/order/policy (no TypeId/address).
**Output**: `/tmp/audit/save/dim_2.md`

### Dimension 3: Container & Disk Durability
Paths: `crates/save/src/{snapshot,disk}.rs`, `crates/core/src/atomic_file.rs`, `crates/save/tests/round_trip.rs`
First step: `cargo test -p byroredux-save disk:: snapshot::`
Guarded (see ledger): header gate order (length → magic → major → schema fingerprint → `payload_len`
checked bounds → CRC over the payload only → `serde_json::from_slice`), advisory `minor`, slot-name strictness
(`save_42.ess.tmp` rejected), `slots_by_recency` newest-first with slot-number tie-break, ring wrap/resume.
Residual checks:
- **`atomic_write`** (`crates/core/src/atomic_file.rs`; shared with `crates/settings-io`,
  `crates/boot-request`, `crates/game-detect/src/overrides.rs`) order is exactly: create tmp → `write_all` →
  `flush` → `sync_all` → byte-exact read-back → `rename` (all inside `stage_and_rename`) → parent-directory
  fsync. **Any** failure up to the rename removes the temp (#5163 `69fd54fb2`; `failed_rename_removes_the_temp`),
  and no caller may fall back to copying/renaming the temp over the target on error — the old "Windows rename"
  fallback clobbered good files on disk-full (#5143 `2e95f0bbf`). The three non-save writers pin that with
  `assert_no_clobber_fallback` (production text only, via `core::source_scan`); `write_slot` is pinned by
  `write_slot_has_no_clobber_fallback` and stages through the unique `atomic_temp_path` (#5247 `eb08e14fd`;
  the old fixed `save_<slot>.ess.tmp` raced two processes sharing a save dir) after `create_dir_all`. Rename-before-fsync, a length-only read-back, or a new fallback branch is a HIGH
  durability hole.
- **Ring never clobbers the last good save**: `SaveState::new` must build the ring via `SaveRing::resume`
  (not `new`), and the cursor advances only after a committed write (`quicksave_ring_cursor_does_not_advance_on_validation_abort`).
- **Minor-version + default-fill**: `decode` accepts newer minors and serde default-fills; that is the exact
  path `#[serde(default)]` would exploit — cross-check Dim 2's guard is live.
- `SAVE_DIR_ENV` (`BYROREDUX_SAVE_DIR`) redirects the ring; smoke gates depend on the spelling.
**Output**: `/tmp/audit/save/dim_3.md`

### Dimension 4: Save-Side Gates (refusal + validation)
Paths: `byroredux/src/save_io.rs` (`SaveCommand::execute`, `validate_form_ids`, `validate_cinematic_entity_refs`), `crates/save/src/validate.rs`, `byroredux/src/app_step.rs` (`step_player_save_actions`)
First step: `grep -n 'fn validate_\|validate_[a-z_]*(world' crates/save/src/validate.rs byroredux/src/save_io.rs`
- **Order in `SaveCommand::execute`**: refusals first (`CellTransitionInFlight` — a mid-transition save would be
  written and permanently unloadable; `CinematicPresentationState.disable_saving` — `Game.SetInChargen`
  `abDisableSaving`; plus the context-invariant refusal #5253 `27562f8ae` — `LoadedCellIndex` present but neither
  `CurrentCellContext` nor `CurrentExteriorContext` means an unloadable snapshot even when the synced flag still
  reads `false`, as in the console ingresses that dispatch outside the drain), then `validate_world` + `validate_form_ids` + `validate_cinematic_entity_refs`
  (abort, up to 20 lines, nothing written), then `save_world` → `capture_extension_state` → `encode` →
  `write_slot`; ring advance and the `SaveComplete` session event only after the commit. Any alternate save
  path that bypasses the gates (console, player action, SDK) is HIGH; input adapters must enqueue through
  `queue_player_save_action` (post-scheduler drain — `save_world` takes ~30 storage + resource read locks, so
  a live scheduler lane needs the ABBA analysis re-derived; `/audit-concurrency`). Current ingresses: the
  winit F5/F9 intercept, the pause menu, and the debug `input.press quicksave|quickload`, which resolves
  through `ActionBindings` to the same F5/F9 edges (`queue_debug_action_press`, `byroredux/src/interaction.rs`,
  `bae84e54b`; `quicksave_press_joins_the_player_save_queue_through_the_f5_binding`).
- **Coverage**: `validate_world` currently runs hierarchy, equipment, saved-entity references (session-local
  `EntityId` fields of columns excluded from the overlay — grep the function for the live column list),
  animation (`AnimationPlayer`, `AnimationStack` root + layer clips, `Seated.animation_restore`),
  inventory instances vs `ItemInstancePool`, and `Material` finiteness (the progression check
  `validate_progression_state` retired in `dbe562c1f` when `CharacterLevel` became a saved replacing column, v46);
  the binary adds FormId resolvability and cinematic `EntityId` refs. Do
  not restate the list elsewhere — enumerate **inter-entity reference fields in any newly registered type
  not covered** (MEDIUM defense-in-depth gap) and any check that flags a legitimately sparse-but-spawned id
  (dangling = `>= next_entity`, not "no components").
- **Post-load validation is diagnostic-only** (`log_validation_warnings`, WARN, no abort) in both
  `restore_world` and `execute_pending_save_loads`; the **typed preflight aborts** (before teardown).
- **Transient state**: saves must not depend on transients — `PlayerNotifications`, pending action queues,
  `CellTransitionInFlight`, parked `StreamStateSnapshots` are unsaved by design; a save taken while one is
  non-empty must still round-trip (check new transient resources land on the allowlist, not the registry).
**Output**: `/tmp/audit/save/dim_4.md`

### Dimension 5: Live Load-Apply & Frame Boundary
Paths: `byroredux/src/save_io.rs` (`execute_pending_save_loads`, `reload_*_session`, `apply_player_pose`), `crates/save/src/driver.rs`, `byroredux/src/{app_events,app_step,app_frame}.rs`, `byroredux/src/cell_loader/{transition,reference_state,unload}.rs`
First step: read `execute_pending_save_loads` top to bottom against the sequence below; `git log --since=<last report> --format='%h %cs %s' -- byroredux/src/save_io.rs`
- **Strict apply sequence**: drain slot → `validate_snapshot_types` (abort, session kept) →
  `preflight_extension_state` (abort) → `restore_resources_subset(PRE_RELOAD_RESOURCES)` (only resources
  read by spawn-time code during the reload: `ReferenceEnableState`, `ReferenceLockState`, `ReferenceScriptState`; **never**
  `ItemInstancePool` — teardown's `release_victim_item_instances` needs the live pool, #4135) →
  `without_parked_state` wraps the reload (outgoing session's `PersistentReferenceStates` and
  `StreamStateSnapshots` are set aside and restored on failure) → teardown + reload (`validate_cell_loadable`
  preflight first, so a missing ESM/cell keeps the live session; then `purge_cinematic_retention_state`
  drops every `ActorCinematicState`/`HorseTetherState` row so the convoy is not retained across the teardown
  as a ghost twin sharing a `FormIdPair` — #5056 `a197e8563`, both reload arms + both debug-load paths, never
  ordinary cell transitions (`purge_wiring_tests`) — and despawns + clears the unsaved `CinematicReAdoption`
  pending list, whose un-rooted entities the `CellRootIndex` teardown cannot enumerate (#5379 `203be9ed4`).
  Since #5384 that list holds parentless roots only, so the purge (now taking the `VulkanContext`) collects each
  root's `Children` closure and hands it to `release_entities` — subtrees no longer orphan (which made
  `validate_world` refuse every later save) and GPU/Rapier handles are released, #5490 `99cd9fd6c`. The teardown never
  despawns the player rig: `unload_cell_inner` refuses `player_owned_entities`, #5483 `5a68e983e`)
  → `reset_story_location_cursor` + `discard_pending_story_events` (the unsaved CLOC cursor and any straddling
  `StoryEvent` markers describe the outgoing session; every load behaves like a fresh boot, #5435 `557ead21d`)
  → `restore_extension_state` → wholesale
  `restore_resources` **again** (idempotent; re-asserts `CurrentCellContext`/`PlayerPose`; the second call is
  not redundant) → `restore_resident` (parked rows for resident placements, #4695) → `drop_entity_bound_continuations` on `PapyrusProviderContinuationQueue` (session-local
  `EntityRef` handles must not resume, #4139) → `reseat_ambient_packages_after_restore` (re-pick packages
  against the restored clock *before* the overlay, #4815; its order against `restore_resident` and the overlay is
  pinned by `post_reload_drain_steps_keep_their_order`, #5060 `02fa8358c`) → `build_form_id_remap` → park the snapshot rows
  whose `FormIdPair` did not resolve (`unresolved_form_id_pairs` → `park_unresolved_snapshot_rows` into
  `PersistentReferenceStates`, #5054 `17e720430` — the exterior reload streams only `radius_load`, so the
  save's hysteresis-band rows were dropped and their loot duplicated; `unresolved_snapshot_rows_are_parked_for_the_next_respawn`)
  → `apply_deltas(MUTABLE_DELTA_COLUMNS)` → dead/equipped-weapon reconcilers, `reconcile_worn_gear` (called
  once; the duplicate was dropped in `a8b3d1902` and the count pinned by #5460 `bda5a1167`), `reset_player_factions_to_record` → diagnostic `validate_world` →
  `apply_player_pose` LAST (after the
  overlay of `CharacterController`, whose motion fields pose-restore then zeroes; a new field on either
  side needs an explicit decision). Any failure after the reload returns immediately, never falling through
  into pose-restore on a partial overlay. Idempotency: teardown is unconditional, so a second load of the same
  slot does not stack deltas.
- **Two restore paths**: live load uses `restore_resources` + `apply_deltas` (saved ids remapped to the
  reloaded cell's fresh ids); `restore_world` (clear + repopulate at saved ids) is test/loose only and must
  never be reachable from `execute_pending_save_loads` (id collision = CRITICAL). `clear_entities` does not
  free GPU/physics handles — the live path's `unload_current_interior`/`drain_streaming_state` must.
- **Remap**: `build_form_id_remap` matches saved `FormIdPair` → live entity; rows without a form id are
  skipped (respawned by the loader); unresolved pairs are logged (WARN, first 20) — a moved object vanishing
  must stay diagnosable; the player body carries `PLAYER_FORM_ID_PAIR` from spawn so it participates.
  Persistent-reference rows: resident rows arrive via the component overlay, nonresident via the resource
  restore; rows are consumed on respawn (semantics → `/audit-gameplay`).
- **Frame boundary**: capture runs post-scheduler (`step_player_save_actions` drains a FIFO after
  `Scheduler::run`; remote console runs in the exclusive `DebugDrainSystem`); `capture_player_pose` reads
  post-propagation `Transform` before the drain; `PlayerNotifications` drain in `render_one_frame` is
  unconditional (bounded ring of 8, oldest dropped). The load drain lives in `step_save_loads` where the
  App owns `&mut World` + `&mut VulkanContext` and is skipped while an interior transition or foreign
  loading screen is active; the single `PendingSaveLoadSlot` is last-writer-wins and reports supersession.
- **Pose**: yaw/pitch always to `InputState`; Character mode + live body → `place_character_body`
  (`byroredux/src/systems/character.rs`, the one teleport helper since #5591 `946310eb9`: body `Transform` +
  `GlobalTransform` + `CharacterController::clear_momentum` — `vertical_velocity`/`is_grounded`/`wants_jump` —
  + kinematic Rapier sync, no-op without a handle); restore branches on the *live* mode (#2018), so
  FlyCam-saved/Character-loaded relocates the body and the reverse repositions the camera.
- **Context guards**: `LoadCommand` accepts interior OR exterior context and rejects only neither;
  `SaveInfoCommand` mirrors the three arms (#3500); `quickload_latest` walks `slots_by_recency` and falls back
  past a corrupt newest slot; `command_output_is_failure` scans every output line.
**Output**: `/tmp/audit/save/dim_5.md`

## Phase 3: Merge

Combine `/tmp/audit/save/dim_*.md` into `docs/audits/AUDIT_SAVE_<TODAY>.md`: **Executive Summary** (each
`lib.rs` design claim CODE-CONFIRMED/DRIFTED; findings by severity and Data-Loss Class) · **Data-Loss
Class Matrix** · **Completeness Ledger** (registry × `MUTABLE_DELTA_COLUMNS`: SAVED-only / SAVED+OVERLAID /
structural; cross-check against the guard's allowlist rather than re-deriving) · **Findings** (CRITICAL
first, deduplicated; two-list drift owned by Dim 1, GPU/physics teardown by Dim 5) · **Guards
Verified** (which ledger guards were confirmed live, which reasons were spot-checked).

## Phase 4: Cleanup

`rm -rf /tmp/audit/save`; tell the user the report is ready; suggest
`/audit-publish docs/audits/AUDIT_SAVE_<TODAY>.md` (domain label `save-load`; `test-gap` for coverage
findings, `doc-rot` for drifted save/load docs).
