Our review
Deep audit of the byroredux save/load subsystem covering full-ECS-snapshot capture, type-erased registry, atomic disk writes, validation gates, and the live load-apply path, detecting data-loss and corruption risks.
Strengths
- Covers the entire save pipeline from snapshot encoding through live load-apply
- Explicitly treats data loss as CRITICAL and maps findings to the project's severity scale
- Cross-checks the crate against its sole engine-side consumer, avoiding integration blind spots
- Uses integration tests and frame ordering to verify real invariants rather than docstring claims
Limitations
- Requires access to the byroredux repository and audit support documents to run properly
- Assumes familiarity with ECS, FormId remapping, and cell-loading concepts
- Does not cover original Bethesda save formats or external migrations outside the audited code
Use when verifying or hardening the save/load subsystem against corruption and data loss.
Not for feature development or general code review without the audit infrastructure and project context.
Security analysis
SafeThe skill is a code audit instruction that directs an AI agent to read and analyze source files, trace data flow, and identify correctness issues. It contains no shell commands, no destructive actions, no exfiltration, and no obfuscation. The declared allowed-tools list is empty, and the skill does not instruct any risky operations.
No concerns found
Examples
Run the M45 save/load subsystem audit on crates/save and byroredux/src/save_io.rs, focusing on data-loss and corruption risks with deep depth.Audit only the M45.1 live load-apply code path: cell reload, FormId-keyed deltas, and player-pose restore. Flag any CRITICAL data-loss issue.Use the save/load audit to examine disk.rs write_slot and SaveRing for torn writes or missing fsync/read-back verification.description: "Deep audit of the M45 save/load subsystem — full-ECS-snapshot capture, type-erased registry, atomic disk write + ring, pre-save 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 the byroredux-save crate (M45 — full-ECS-snapshot save format) and its
sole engine-side consumer (byroredux/src/save_io.rs, M45.1 live load-apply) for
data-loss and save-corruption correctness. The whole subsystem exists to
remove Bethesda's slow-corruption tail by making the live ECS the single source
of truth; the audit's job is to verify the CODE actually delivers that, not to
take the docstring's word for it. A silently-dropped component column, a stale
schema fingerprint, a torn frame-boundary capture, or a botched FormId remap each
loses player progress — frame those as CRITICAL/HIGH per
.claude/commands/_audit-severity.md (Data loss is CRITICAL on that scale).
Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).
See .claude/commands/_audit-common.md for project layout, methodology,
deduplication, context rules, and finding format. See
.claude/commands/_audit-severity.md for the severity scale. Do NOT duplicate
those here.
Scope
Crate (crates/save/src/, ~1.2k LOC — read ALL of it before auditing):
crates/save/src/lib.rs— module docstring (design intent: full snapshot, atomic write, ring, validation gate, load-off-frame),SaveErrorenum, public re-exports.crates/save/src/snapshot.rs—Snapshotstruct, binary container layout (FORMAT_MAGIC/FORMAT_MAJOR/FORMAT_MINOR/HEADER_LEN),encode/decode(magic / version / schema-fpr / CRC32 / payload-len gates).crates/save/src/registry.rs—SaveRegistry, the type-erasedSaveFn/LoadFn/ApplyFnclosures,register_component/register_resource/register_form_id_component,schema_fingerprint(FNV-1a),form_id_column.crates/save/src/driver.rs—save_world,restore_world,restore_resources,build_form_id_remap,apply_deltas.crates/save/src/disk.rs—write_slot(tmp → fsync → read-back-verify → rename),read_slot,list_slots,SaveRing.crates/save/src/validate.rs—validate_world,ValidationError,ValidationKind, the three sub-checks (hierarchy / equipment / animation).crates/save/tests/round_trip.rs— the crate-level integration tests; read to learn which invariants are already guarded.
Engine-side consumer (byroredux/src/save_io.rs — the ONLY live caller of
the crate; the crate audit is incomplete without it):
build_save_registry— the curated type set (the authoritative completeness list).MUTABLE_DELTA_COLUMNS— the second hardcoded column list that drives the live overlay; must stay in lockstep withbuild_save_registry.SaveCommand/SaveInfoCommand/LoadCommand(console commands),SaveState,PendingSaveLoadSlot,PlayerPose.capture_player_pose,apply_player_pose,execute_pending_save_loads,snapshot_cell_context,snapshot_player_pose.
Cross-cut ground truth — read before auditing the relevant dimension:
byroredux/src/boot.rs— registry/state install at boot (~line 1137);byroredux/src/app_events.rs— the per-frame ordering ofcapture_player_poseTHENstep_save_loads(inabout_to_wait, ~line 658; moved out of main.rs by the #2731 split);byroredux/src/app_step.rs—step_save_loadsbody (~line 291).byroredux/src/cell_loader/transition.rs—CurrentCellContext(the saved cell identity),reposition_camera(FlyCam restore target).crates/core/src/ecs/world.rs—insert_batch(theentity < next_entitydebug_assert, NOT a release-mode guard),clear_entities,set_next_entity,next_entity_id.crates/core/src/string/mod.rs—StringPool::dump/from_dump(symbol-order round-trip contract).crates/physics/src/sync.rs—set_kinematic_translation(returnsfalse/ no-ops when no Rapier handle).
Confirmed-shipped surface (verify against live code, do not assume):
- Container is binary-framed JSON payload: 32-byte header (
magic8 /major2 /minor2 /schema_fpr8 /crc324 /payload_len8) + serde_jsonSnapshot. Snapshot { next_entity, strings, components: BTreeMap, resources: BTreeMap }.- Disk slots are
<dir>/save_<slot>.ess; ring is in-memory round-robin. - Live load = reload saved cell via
load_cell_with_masters→restore_resources→build_form_id_remap→apply_deltas(MUTABLE_DELTA_COLUMNS)→apply_player_pose. restore_world(clear + full repopulate) is the test/loose path; the LIVE load path usesapply_deltasoverlay, NOTrestore_world— two divergent restore code paths.
Doc-rot check: docs/feature-matrix.md:189 already carries an explicit
TD3-002 comment noting Save/load (M45/M45.1) shipped 2026-06-21 — the
"unstarted" row is gone. Do not re-flag this as doc-rot; confirm it still reads
correctly before reporting anything here.
Parameters (from $ARGUMENTS)
--focus <dimensions>: Comma-separated dimension numbers (e.g.,1,3,6). Default: all 6.--depth shallow|deep:shallow= check container/API contracts;deep= trace full capture → encode → disk → decode → reload → delta-apply data flow + the frame-boundary / off-frame drain ordering. Default:deep.
Extra Per-Finding Fields
- Dimension: Snapshot Completeness & Determinism | Registry & (De)serialization | Disk Format & Durability | Validation Gates | Frame-Boundary Capture & Off-Frame Apply | M45.1 Live Load-Apply
- Data-Loss Class: silent-drop | corruption-on-load | irrecoverable-write | reference-break | none — every finding that can lose progress MUST name its class.
Phase 1: Setup
- Parse
$ARGUMENTSfor--focus,--depth. mkdir -p /tmp/audit/save- Fetch dedup baseline:
gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/save/issues.json - Read the most recent
docs/audits/AUDIT_SAVE_*.mdreport (sort by date — do not hardcode a filename here, it rots every cycle). Diff direction against it: findings it already closed are regression checks, not new findings — verify the fix is still in place before reporting anything as NEW. Also scandocs/audits/for any save/load mention in other reports and grepissues.jsonforsave,load,snapshot,corrupt,FormId. - Read the
crates/save/src/lib.rsmodule docstring and thecrates/save/src/snapshot.rscontainer-layout doc-comment. They state the design intent (atomic write, ring, validation gate, off-frame load). For each claim, the matching dimension must verify the CODE delivers it — a docstring promise the code doesn't keep is itself a finding. - Run the registry-completeness guard before Dimension 1 starts:
cargo test -p byroredux every_component_or_resource_impl_is_saved_or_explicitly_allowlisted(SAVE-D1-12, #2295,byroredux/src/save_io.rs). It source-scans everyimpl Component for/impl Resource forline undercrates/core/src/ecs/components/,crates/scripting/src/, andcrates/physics/src/and asserts each type is registered inbuild_save_registryXOR listed in the test's ownNOT_SAVED_BY_DESIGNallowlist with a one-line reason. A green run IS the completeness ledger — Dimension 1 should consume itsNOT_SAVED_BY_DESIGNlist rather than re-deriving completeness from scratch, spot-checking a sample of reasons for staleness (the guard enforces a reason exists, not that it's still true).
Phase 2: Launch Dimension Agents
Ordered by data-loss risk: completeness + registry first (silent-drop is the worst class), durability + validation next, frame-boundary + live-apply last.
Dimension 1: Snapshot Completeness & Determinism (highest risk)
Entry points: byroredux/src/save_io.rs — build_save_registry,
MUTABLE_DELTA_COLUMNS; crates/save/src/driver.rs — save_world;
crates/save/src/snapshot.rs — Snapshot.
Why highest risk: a persistent component that nobody registered is silently
absent from every save — invisible until the player notices their progress is
gone. Data-Loss Class = silent-drop.
Checklist:
- The registry IS the completeness contract. Enumerate every component/resource
in
build_save_registryand cross-check against the full game-state component set (inventory, equipment, lights, animation, scripting, form id, plus theItemInstancePool/CurrentCellContext/PlayerPose/GameTimeRes/QuestAliasInjectionStateresources — the M34 day/night clock and the QUST alias inventory-grant ledger, both registered 2026-08-07). For EACH persistent component type in the codebase that carries player-mutable state, confirm it is either registered OR documented as reconstruct-on-load (derived data:GlobalTransform,WorldBound; GPU handles:MeshHandle,TextureHandle,SkinnedMesh; transient event markers). An unregistered mutable component = HIGH silent-drop finding. Don't re-derive this list by hand — run the SAVE-D1-12 guard (Phase 1 step 6) and start from itsNOT_SAVED_BY_DESIGNallowlist. Building that allowlist on 2026-08-05 surfaced 7 genuine gaps, all now fixed and registered:RigidBodyData(#2379),RumbleOnActivate(#2382),Material(#2378),FragmentExecutionQueue(#2381), and the MQ101 cinematic trioActorCinematicState/HorseTetherState/CinematicPresentationState(#2380). Verify none of the seven regressed back out ofbuild_save_registry. - Two lists, one truth (drift hazard).
MUTABLE_DELTA_COLUMNSinbyroredux/src/save_io.rsis a SEPARATE hardcoded&[&str]from theregister_componentcalls inbuild_save_registry. The live load only overlays columns named in BOTH the registry ANDMUTABLE_DELTA_COLUMNS. A component registered (so it's SAVED) but absent fromMUTABLE_DELTA_COLUMNSis captured to disk yet never replayed on a live load — its post-spawn changes are silently lost. Verify every mutable column inbuild_save_registryappears inMUTABLE_DELTA_COLUMNS(or is deliberately structural/identity:Name,Parent,Children, the form-id key). Flag any registered-but-not-overlaid mutable column as HIGH (silent-drop on load). Guard:delta_columns_carry_only_session_stable_fields(#1720,47dad578) tripwires any future addition toMUTABLE_DELTA_COLUMNSagainst embedding aFixedString/EntityId/session-local handle.Material(#2378) and theActorCinematicState/HorseTetherStatepair (#2380) are current, deliberate instances of this exact pattern — registered but NOT inMUTABLE_DELTA_COLUMNS(blast-radius andEntityId-hazard reasons respectively, documented at eachregister_componentcall site inbyroredux/src/save_io.rs). Verify they stay documented as intentional rather than silently drifting into the HIGH bucket above. - Determinism.
Snapshot.components/.resourcesareBTreeMap(sorted keys) andsave_worldskips empty columns / null resources. Confirm the CRC is reproducible at equal state: column ROW order comes fromWorld::queryiteration — verify that order is stable across runs (storage iteration order) or that determinism is only claimed at the column-key level, not row level. A per-run-varying row order breaks the "reproducible CRC" claim in the docstring (MEDIUM doc/contract mismatch, not data loss). next_entityround-trip.save_worldrecordsworld.next_entity_id(); restore replays it viaset_next_entityBEFORE inserts so original (sparse) ids passinsert_batch'sentity < next_entityguard. Verify the high-water mark is saved verbatim (a too-low value silently drops every row at/above it via the debug_assert — and in RELEASE the assert is COMPILED OUT, so the row inserts at an unspawned id with no diagnostic). Flag the release-mode silence as MEDIUM.- StringPool symbol-order contract.
save_worlddumps viaStringPool::dump(symbol order); restore re-interns viafrom_dump. EveryFixedStringin a saved component is a symbol index into this pool. Verifydump/from_dumppreserve index identity (re-interning the exact sequence reproduces every symbol). A reordered or de-duplicated dump = everyName/ interned string points at the wrong symbol = CRITICAL corruption-on-load. Confirm againstcrates/core/src/string/mod.rs. - Empty-column omission vs. delete-on-load.
save_worldomits empty columns;restore_worldonlyloads columns present in the snapshot. The live overlay is additive-only (can insert/update, never remove) — documented atapply_deltas(#1847/SAVE-04,cec3b9ab). Confirmed inert today (no enable/disable/delete persistence exists to leak orphaned rows). Re-flag only once such a component lands without the promised companion despawn/hide pass — until then this is DEFERRED-DOCUMENTED, not a live finding. Output:/tmp/audit/save/dim_1.md
Dimension 2: Registry & (De)serialization Fidelity
Entry points: crates/save/src/registry.rs — register_component,
register_resource, register_form_id_component, the SaveFn/LoadFn/ApplyFn
closures, schema_fingerprint, form_id_column, FnvHasher.
Checklist:
- Serde availability is feature-gated. Components serialise only with
serde::Serialize + DeserializeOwned; these derives are behind#[cfg_attr(feature = "inspect", …)]on the core types, andcrates/savedepends onbyroredux-corewithfeatures = ["save"](which pullsinspect). Confirm thesave→inspectfeature chain incrates/save/Cargo.tomlandcrates/core/Cargo.tomlso a non-default build can't compile away the serde impls and ship a save crate that round-trips nothing. A registry that builds but whose columns serialise tonullis a silent-drop trap. - Schema fingerprint = coarse drift only.
schema_fingerprintis FNV-1a over ordered, kind-tagged column KEYS — it catches add/remove/rename of a TYPE, NOT an intra-type field change. Confirm the doc-comment's stated limitation matches reality and that an intra-type field change is caught at load byserde_json::from_valuefailing (aSaveError::Serde, not a silent default-fill). The danger case: a field ADDED with#[serde(default)]would load OLD saves silently. A guard test,serde_default_on_saved_struct_requires_format_major_bump(byroredux/src/save_io.rs, #1714,806ba7af), source-scans every save-participating type (top-level + nested:ItemStack,AnimationLayer,FormIdPair, …) for a newly-added#[serde(default)]whileFORMAT_MAJOR == 1. TheOption-widening half is still uncaught statically (legitimateOptions already exist) — verify this residual is still documented, not silently dropped. - Fingerprint stability across builds.
FnvHasheris hand-rolled specifically becauseDefaultHasheris unspecified across std versions. Verify the FNV constants (0xcbf2_9ce4_8422_2325offset basis,0x100_0000_01b3prime) are the canonical 64-bit FNV-1a values and that the hash depends ONLY on registered names + order — not on any address/TypeId (which would vary per run and reject every save). form_id_column(regression guard, #1845,326fcb44).form_id_column()is keyed off an explicitEntry::is_form_idflag (not the oldapply.is_none()heuristic), with a registration-time assert against a second form-id column. Guard:form_id_column_resolves_the_flagged_entry. Verify a futureregister_*variant can't silently reintroduce the old first-apply:None-wins heuristic (that pre-fix behavior would let any futureapply: Nonecomponent hijack the live-load remap key).- FormId handle vs. pair.
register_form_id_componentsaves the stableFormIdPair(resolved throughFormIdPool), NOT the session-localFormIdhandle. Save skips (with WARN) any handle that doesn't resolve in the pool; load re-interns the pair to a fresh handle. Verify: (a) save never panics on an unresolvable handle, (b) load'sresource_mut::<FormIdPool>()can't deadlock / panic if the pool resource is absent, (c) the re-interned handle is internally consistent with every OTHER re-interned reference in the same load. A handle saved verbatim instead of the pair = CRITICAL reference-break across loads. - Round-trip fidelity.
crates/save/tests/round_trip.rsand thesave_io.rstests (binary_registry_round_trips_including_scripttimer,player_pose_survives_snapshot_round_trip) are the guards. Verify the cross-crateScriptTimerand a stable form id round-trip; flag any registered type with no round-trip coverage (LOW test-gap unless the type has tricky serde). Output:/tmp/audit/save/dim_2.md
Dimension 3: Disk Format & Durability
Entry points: crates/save/src/disk.rs — write_slot, read_slot,
list_slots, parse_slot_filename, SaveRing; crates/save/src/snapshot.rs —
encode / decode header gates.
Checklist:
- Atomic write dance.
write_slotdoescreate_dir_all→ write.tmp→flush→sync_all→ READ-BACK-VERIFY (readback != bytes→ delete tmp + error) →rename. Verify the ordering is exactly that: therenameis the LAST step and only runs after a byte-exact read-back. A rename-before-fsync, or a read-back that compares lengths only, is a HIGH durability hole (a lying/short write can replace a good save). Confirm the failed read-back removes the tmp and returnsSaveError::Iorather than proceeding to rename. - Directory durability gap.
sync_allfsyncs the FILE; on most filesystems therenameitself is not durable until the DIRECTORY is fsynced. A power cut afterrenamereturns but before the dir entry is flushed can lose the rename. Check whether the parent dir is fsynced after rename — if not, flag as MEDIUM (the read-back + tmp pattern still protects against half-written content; this is the residual rename-durability gap). - Ring never clobbers the last good save.
SaveRing::advanceis round-robin over0..size(size floored to ≥1);SaveCommandwith no arg callsring.advance(). Verify a quicksave spreads across slots so the previous good save survives (the explicit design goal vs. Bethesda's "F5 ate my save"). Regression guard (SAVE-D3-02): the cursor itself is in-memory only (SaveRingis not persisted), soSaveState::newbuilds it viaSaveRing::resume, which scans on-disk slot mtimes (cursor_after_newest) and starts one past the newest — not via a bareSaveRing::new, which would restart at slot 0 every launch and let the first quicksave of a new session clobber whichever slot is newest on disk. VerifySaveState::newstill callsresume, notnew. - Header gate ordering in
decode. Must be: length ≥HEADER_LEN→Truncated; magic →BadMagic; major mismatch →UnsupportedVersion; schema_fpr mismatch →SchemaMismatch; thenpayload_lenbounds (checked_addoverflow →Truncated,bytes.len() < payload_end→Truncated); then CRC over the payload →CrcMismatch; thenfrom_slice. Verify ALL gates precedeserde_json::from_sliceso a corrupt/truncated/skewed file fails before any parse. A CRC check AFTER parse, or a missingpayload_lenbounds check (slice panic), is HIGH. - CRC scope.
encodeCRCs the PAYLOAD only (not the header). Confirmdecoderecomputes over the same payload slice[HEADER_LEN..payload_end]. A header edit (e.g. version bump) deliberately does NOT trip CRC — verify the version gate catches it instead (guarded byrejects_major_version_skew). A CRC that covered the header would make the version-skew error unreachable. parse_slot_filenamestrictness. Confirmsave_42.ess.tmpandsave_x.essare rejected so a stray tmp or garbage file never registers as a slot (guard:parse_slot_names). A loose parse would surface a half-written tmp as a loadable slot.minorversion is advisory. A newer MINOR still loads (serde default-fills missing fields). Confirmdecodedoes NOT reject on minor skew — but cross-check Dimension 2's#[serde(default)]concern: advisory-minor + default-fill is the exact path that can silently load a downgraded save. Output:/tmp/audit/save/dim_3.md
Dimension 4: Validation Gates (the slow-corruption-tail defense)
Entry points: crates/save/src/validate.rs — validate_world,
validate_hierarchy, validate_equipment, validate_animation,
ValidationKind; byroredux/src/save_io.rs — SaveCommand::execute (the gate
caller).
Why this dimension: the whole format's thesis is "refuse to persist an
inconsistent world rather than seed a corruption tail." This dimension verifies
the gate actually exists, runs before write, and covers the references that matter.
Checklist:
- Gate is enforced on the write path.
SaveCommand::executecallsvalidate_worldand, on a non-empty result, ABORTS the save (prints up to 20 issues, never writes). Verify the abort precedessave_world/encode/write_slot— a validation that runs but doesn't block the write is theatre (HIGH: the corruption-tail defense is a no-op). Confirm there is NO alternate save path that bypasses the gate. - Coverage vs. claim (regression guard, #1700,
380ea4c4).validate_worldnow checks FOUR reference classes — Hierarchy (Parent⇄Childrenbidirectional agreement + dangling-id), Equipment (EquipmentSlotsoccupant indexes a liveInventoryrow), Animation (AnimationPlayer.clip_handleresolves inAnimationClipRegistry,root_entityis spawned), and ItemInstance (validate_inventory_instances—Inventoryrows resolve againstItemInstancePool) — plus a FIFTH the binary layers on top:validate_form_ids(byroredux/src/save_io.rs) checks cross-plugin FormId resolvability, run inSaveCommand::executebefore every save. Verify all five still run pre-write. Regression: 6 tests split core/binary (dangling/ no-pool rejected, resolvable passes). Enumerate any newly-added inter-entity reference type not yet covered by one of these five as a MEDIUM defense-in-depth gap. - Dangling-id semantics.
validate_hierarchy/validate_animationflag any referenced id>= next_entityasDanglingEntity. Verify this catches never-spawned ids but does NOT false-positive on legitimately sparse-but-spawned ids (an id< next_entitythat has no live components is still "spawned" by the high-water-mark model). Confirm the check is>= next_entity, not "id has no components." - Equipment occupant bounds.
validate_equipmentresolves the occupant index against the SAME entity'sInventory.items.len(). Verify theinv.iter().findper-occupant is O(equip×inv) but correct; flag the None-Inventory and out-of-bounds cases produce distinct errors. An off-by-one (>vs>=) here passes a save that loads an out-of-bounds equip → corruption-on-load. - Load-side validation (regression guard, #1844,
dc89ff68).decodevalidates the CONTAINER (magic/CRC/version/schema);log_validation_warningsnow ALSO re-runsvalidate_world(+validate_form_ids) post-load, wired into bothrestore_world(crates/save/src/driver.rs) andexecute_pending_save_loads(byroredux/src/save_io.rs, right afterapply_deltas) — diagnostic-only (WARN-log, no abort; a load can't cleanly revert). Verify it stays diagnostic-only and covers both restore paths. Regression: theround_trip.rstest provingrestore_worldneither aborts nor silently repairs broken-but-decodable data. Output:/tmp/audit/save/dim_4.md
Dimension 5: Frame-Boundary Capture & Off-Frame Apply
Entry points: crates/save/src/driver.rs — save_world (read-only capture),
restore_world (&mut World); byroredux/src/save_io.rs —
SaveCommand (read-only), LoadCommand (queues), execute_pending_save_loads
(the &mut World drain), capture_player_pose; byroredux/src/app_events.rs
run-loop ordering (the about_to_wait arm, ~line 658 — post-#2731; do not look
for it in main.rs).
Checklist:
- Capture is read-only and consistent.
save_worldtakes&World(queries +try_resource), so it can run as a console command without&mut. Verify the capture reads a CONSISTENT world — it must run at a frame boundary, NOT mid-system with some storages already mutated this tick.SaveCommandruns through the console drain; confirm the console drain executes at a point where the scheduler is between ticks (no system holds a storage write lock). A capture interleaved with a running system would snapshot torn state (e.g. half-propagated transforms) — CRITICAL if a system can be mid-mutation during the capture. capture_player_poseordering. It runs inapp_events.rs(about_to_wait) AFTER the scheduler's camera systems published this frame'sTransform/GlobalTransformand BEFOREstep_save_loads, every frame. Verify the pose source is post-propagation (readsTransform.translationof the body in Character mode, camera in FlyCam), not stale interpolation state. A pre-propagation read saves last-frame's pose (MEDIUM, position-off-by-one-frame; not data loss).- Load is off-frame, drained between ticks.
restore_world/apply_deltasneed&mut World, which a system can't get.LoadCommandonly decodes + pushes toPendingSaveLoadSlot;execute_pending_save_loadsdrains it instep_save_loadswhere the App owns&mut World+&mut VulkanContext. Verify the load NEVER runs inside the scheduler (it would alias the world). This mirrorsPendingDebugLoadSlot; confirm the draintake()s the slot (load runs once) and no-ops on an empty slot. clear_entitiesdoes NOT tear down GPU/physics handles.restore_worlddrops component data but the docstring (andworld.rs) explicitly state GPU/ physics handles are the CALLER's responsibility. The live path (execute_pending_save_loads) usesunload_current_interior+drain_streaming_stateBEFORE the reload to release those handles — but it usesapply_deltas(overlay), NOTrestore_world. Verify the live path's teardown fully releases GPU/physics handles before reload so no leaked BLAS/texture/Rapier body survives the load (HIGH resource leak per load otherwise). Confirm therestore_worldclear-path is ONLY reached in tests / loose mode where there are no GPU handles to strand.- Two restore paths, divergent semantics.
restore_world(clear + full repopulate at saved ids) vs. the liverestore_resources+apply_deltas(overlay onto a freshly-reloaded cell, id-remapped). They are NOT interchangeable:restore_worldreuses SAVED entity ids;apply_deltasremaps to the reloaded cell's FRESH ids. Verify the live load never accidentally callsrestore_world(which would resurrect the saved cell's ids on top of the reloaded cell's ids = id collision / CRITICAL corruption). Confirmexecute_pending_save_loadscalls ONLYrestore_resources+apply_deltas, neverrestore_world. Output:/tmp/audit/save/dim_5.md
Dimension 6: M45.1 Live Load-Apply (cell reload + FormId deltas + pose)
Entry points: byroredux/src/save_io.rs — execute_pending_save_loads,
build_form_id_remap (in crates/save/src/driver.rs), apply_deltas,
apply_player_pose, snapshot_cell_context, snapshot_player_pose;
byroredux/src/cell_loader/transition.rs — CurrentCellContext,
reposition_camera; crates/physics/src/sync.rs — set_kinematic_translation.
Companion doc: docs/engine/save-load-roundtrip.md (cross-cutting trace of this
exact flow, verified against the tree 2026-07-15).
Checklist:
- Strict apply ordering.
execute_pending_save_loadsmust run: drain slot → resolveCurrentCellContext→ teardown (drain_streaming_state+unload_current_interior) →load_cell_with_masters→ apply lighting +signal_temporal_discontinuity+ recordLoadedPluginSet→restore_resources→build_form_id_remap→apply_deltas(MUTABLE_DELTA_COLUMNS)→apply_player_pose. Verifyrestore_resourcesprecedesapply_deltassoItemInstancePoolids thatInventoryrows reference resolve against the RESTORED arena (a delta-before-resource order would dangle every item instance — HIGH reference-break). Verify pose-restore is LAST (after the cell reload places the player at the default door spawn). - Remap correctness & identity.
build_form_id_remapmatches savedFormIdPair→ live entity carrying the same pair in the RELOADED cell, producingsaved-id → live-id. Verify: (a) entities WITHOUT a form id (NIF child nodes, particles) are absent from the map and their deltas silently skipped (correct — they're respawned identically by the loader); (b)apply_deltas/ApplyFnfilter_maps out rows whose saved id isn't in the remap (no panic, no wrong-entity write); (c) aFormIdPairpresent in the save but NOT in the reloaded cell (record removed from a plugin, or cell content changed) is dropped with the delta lost — flag whether this is logged so a silently-vanished moved object is diagnosable (MEDIUM; data-loss class = reference-break, but arguably correct behaviour — the target no longer exists). The player body itself now carries a reservedFormIdComponent(PLAYER_FORM_ID_PAIR, #1846,91b8c5df,crates/core/src/form_id.rs, attached at spawn inbyroredux/src/scene.rs) so it participates in this remap like any NPC instead of being invisible to it — verify it stays attached at spawn. - Idempotency. A
loadisapply_deltasOVERLAY onto a freshly reloaded cell. Loading the SAME slot twice must yield the same world (the teardown + reload resets to a clean cell each time). Verify the teardown is unconditional (if streaming.is_some()drain +unload_current_interioralways) so a second load doesn't stack deltas on a world that already has the first load's deltas. - Cell-resolve failure (regression guard, #1697,
3043ffdc).validate_cell_loadableruns a non-destructive pre-flight (parse + cell-lookup,byroredux/src/cell_loader/load.rs) BEFORE teardown inexecute_pending_save_loads, covering the two named failure modes (missing/corrupt ESM, unresolvable cell id) — the current cell survives on either. Residual: a failure after cell-resolve (mid spawn/GPU-setup) still tears down first; that narrower window remains MEDIUM. Confirm the snapshot'sCurrentCellContextis re-validated (it was already verified present byLoadCommand, butexecute_pending_save_loadsre-reads it and errors if it vanished — a defensive double-check; confirm it's there). AnimationPlayer/AnimationStackexclusion (regression guard, #1696,92f8f663). Deliberately excluded fromMUTABLE_DELTA_COLUMNS— the reloaded cell owns their post-spawn state instead of overlaying a stale savedroot_entity/clip_handle. Verify they stay excluded; regression test incrates/save/tests/round_trip.rsasserts both the hazard and the fix.- Player-pose restore correctness.
apply_player_pose: yaw/pitch always go toInputState(the source of truth both camera modes rebuild rotation from — a savedTransform.rotationalone wouldn't survive a tick). Character mode + live body → set bodyTransform+GlobalTransformtranslation, zero theCharacterControllermomentum (vertical_velocity/is_grounded/wants_jump), andset_kinematic_translationto sync the Rapier KCC. Verify: (a)set_kinematic_translationno-ops cleanly without a Rapier handle (returnsfalse, no panic — guarded byplayer_pose_character_tracks_body); (b) the Character-saved-but-no-live-body fallback drops the CAMERA at the saved spot viareposition_camera(FlyCam reload of a Character save still honours look dir); (c) momentum is CLEARED so the body doesn't carry stale free-fall velocity into the reloaded cell. A missing momentum-clear = player launches/falls on every load (MEDIUM, gameplay correctness). - Pose capture/restore mode mismatch.
PlayerPose.character_moderecords the SAVE-time mode; restore branches oncharacter_nowalone (#2018,SAVE-D6-03) — a live Character-mode session always relocates the body, converting the saved camera position to a body position via eye-height when the pose was captured in FlyCam mode. Verify a mode change saved-FlyCam/loaded-Character still relocates the body correctly, that saved-Character/loaded-FlyCam falls through to the camera-reposition branch, and that a body Transform is never written when no body is live. - Schema/cell-context guards.
LoadCommandrefuses a save with noCurrentCellContext("loose/exterior save — live load needs an interior cell"). Verify exterior/loose saves are rejected at queue time, not silently half-applied at drain time. Confirmsnapshot_player_posereturningNone(pre-refinement save) is handled — but note schema-fingerprint drift would reject such a save first; confirm that's actually true (aPlayerPose-less save has a different fingerprint, sodecoderejects it before pose-restore is reached). Output:/tmp/audit/save/dim_6.md
Phase 3: Merge
- Read all
/tmp/audit/save/dim_*.mdfiles. - Combine into
docs/audits/AUDIT_SAVE_<TODAY>.mdwith structure:- Executive Summary — M45 (crate: snapshot/registry/disk/validate) + M45.1
(live load-apply, player-pose restore) shipped status, verified against the
crates/save/src/lib.rsdocstring's claimed design (full snapshot / atomic write / ring / validation gate / off-frame load) — for each claim, state CODE-CONFIRMED or DRIFTED. Findings count by severity AND by Data-Loss Class (silent-drop / corruption-on-load / irrecoverable-write / reference-break). - Data-Loss Class Matrix — each finding × class × dimension, so the reader sees the silent-drop / corruption surface at a glance.
- Completeness Ledger — the two parallel lists (
build_save_registryregistrations ×MUTABLE_DELTA_COLUMNS), marking each registered column SAVED-only vs SAVED+OVERLAID vs structural-identity, to expose any save-but-never-replay drift. Cross-check the registered side against the SAVE-D1-12 guard'sNOT_SAVED_BY_DESIGNallowlist (Phase 1 step 6) instead of re-deriving it — anything in neither list is the guard's job to catch, not this report's. - Findings — grouped by severity (CRITICAL first), deduplicated.
- Regression Guards Discovered — the existing tests
(
crates/save/tests/round_trip.rs, thesave_io.rstest module, thesnapshot.rs/disk.rs#[cfg(test)]modules) and which invariant each pins, so a future change knows what it'd break.
- Executive Summary — M45 (crate: snapshot/registry/disk/validate) + M45.1
(live load-apply, player-pose restore) shipped status, verified against the
- Remove cross-dimension duplicates: the two-list drift is owned by Dim 1
(pointer from Dim 6); the
form_id_columnheuristic trap is owned by Dim 2 (pointer from Dim 6's remap checklist); the GPU/physics-handle teardown is owned by Dim 5 (pointer from Dim 6's ordering checklist).
Phase 4: Cleanup
rm -rf /tmp/audit/save- Inform user the report is ready.
- Suggest:
/audit-publish docs/audits/AUDIT_SAVE_<TODAY>.md(domain label:save-load; addtest-gapfor coverage findings anddoc-rotfor drifted save/load docs).
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.