Synthèse automatique
Audite le sous-système Rust de sauvegarde/chargement M45/M45.1 pour détecter les pertes de données et corruptions de sauvegarde, en combinant garde-fous existants et revue ciblée par dimensions.
Points forts
- Structure l’audit en dimensions avec orchestration d’agents et priorisation CRITICAL/HIGH selon le risque de perte de données.
- Impose d’exécuter d’abord la garde-ledger (tests cargo) et vérifie que chaque garde-fou est réellement actif.
- Couvre les points critiques du format : snapshot ECS complet, registre typé, discipline de version, écriture atomique et remap FormId.
- Délimite explicitement les responsabilités avec l’audit gameplay pour éviter les doublons.
Limites
- Fortement couplé aux chemins, modules et documents internes du dépôt byroredux.
- Suppose l’existence préalable de `_audit-common.md`, `_audit-severity.md` et de la documentation de conception.
- Ne juge pas la sémantique de jeu (inventaire, butin, consommables), laissée à un autre audit.
À utiliser avant de modifier ou relire le code de sauvegarde/chargement, ou lors d’un soupçon de corruption ou de perte de progression.
À éviter sur un autre projet que byroredux ou pour décider quels états de jeu doivent persister.
Analyse de sécurité
SûrThe skill is a read-mostly audit orchestrator for a specific Rust repository. Its executable actions are limited to local, legitimate developer operations: `cargo test`, `git log`/`git show`, `mkdir -p /tmp/audit/save`, `gh issue list` against the project's own repo, and writing markdown reports under `docs/audits/`. Cleanup is a scoped `rm -rf /tmp/audit/save` on a directory the skill itself creates. There is no curl|sh, no base64 payloads, no credential/token handling, no exfiltration, no disabling of safety mechanisms, and no instruction-injection text embedded in the content. Only minor note is `gh issue list` requires network/GitHub auth, but it is used for read-only dedup, not data egress.
Aucun point d'attention détecté
Comment cette analyse est faiteExemples
Run a deep save/load audit focused on schema/format-version guards and atomic disk writes for the M45 subsystem.Audit the M45.1 live load-apply path for FormId remap, cell reload, and player-pose restore bugs before we ship.Check the save/load guard ledger, then audit snapshot encode/decode and registry registration for silently dropped columns.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_DESIGNlist, 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;PlayerNotificationsandPickedUpare correctly transient — the durable half of a pickup is thePersistentReferenceStatestombstone). Recent false reasons, since fixed:StoryEventAliasFill("quests restart through fresh events" — now registered, #539403e51dfad),EatSleepState("resolve_destinationis idempotent" — restated per PLDT anchor, #539142aab4c09),CinematicReAdoption("the teardown handles them" — the purge now despawns them, #5379203be9ed4); plus the Eat/Sleep rows' "registered Seated restore" (Seatedis not overlaid —restore_world-only; restated and pinned by theseated_without_pathcheck at the tail of the completeness guard, #54580aa844dbb) andStoryLocationCursor("a load legitimately re-fires" —execute_pending_save_loadsnow resets it on every in-process load, #5435557ead21d, pinned inlive_reload_tests.rs). - Removed-from-allowlist stays registered:
CharacterController(fractional breath/drowning carry),RigidBodyData(registered viaregister_component_preserving: itsserde(skip)part_surface_materialsis re-derived by NIF import, so the overlay'spreservehook keeps the live slice instead of wiping it with the row's empty default — #56864e0c962c4; a newserde(skip)import-derived field on an overlaid column needs the same hook),Material,RumbleOnActivate,FragmentExecutionQueue, the cinematic trio — verify none regressed out ofbuild_save_registry, and no re-addedNOT_SAVED_BY_DESIGNentry 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 itsregister_componentsite:Material(blast radius),AnimationPlayer/AnimationStack(staleroot_entity/clip_handle),FollowState/EscortState/Seated/cinematic pair (EntityId, registry handles),ActorVitals(write-once FormID key). A column with anEntityId/handle must also be covered byvalidate_saved_entity_references(Dim 4). - Replacing columns:
Perks,TimedRestorations,Dead,ActorControlStateuseregister_replacing_component(the last two since #5027/#50527aa1d7741— the process-lifetime player outlives the reload, so an additiveDead/restraint row survived a load into an alive save; pinned bydead_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 sessionEntityIds takes theStoryEventAliasFillshape (#5394):StoryEventFillserde(skip)s itsEventDataSlotsand saves the R1/R2 reference FormIds, which the alias refresh re-resolves. The overlay emits noEquipmentEventBatch: presentation derived from an overlaid column is re-derived byreconcile_worn_gear(byroredux/src/npc_spawn/loot_appearance.rs, #503420717d5b7) — run on the player afterapply_deltasand on NPCs fromreference_state::restore; it diffs live roots against restored slots (reveal / hide / queuePendingGearImport) and synthesizes no event batch. A new presentation fact derived from an overlaid column needs the same treatment. Re-derived-not-saved player state:FactionRanksis reset to the record byreset_player_factions_to_recordafter the overlay (#5058be3cd9468— 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 byclear_player_gear_handoff_scratch, #52556b494d002,PendingInventoryActions) and NPC dialogue (NpcDialogueTopic,DialogueSurfaceState) are allNOT_SAVED_BY_DESIGN;ObjectiveHudCacheis anAppfield, not a Resource. Verify each reason (body subtree entities have noFormIdComponent: captured bysave_world, never remapped — the basis ofRigidBodyData's "NoReconcilerNeeded" row). - Determinism (the
Snapshotdoc claims reproducible CRCs at equal state):Snapshotmaps areBTreeMapand component rows are sorted by entity id (registry.rs), but saved resources serialize as-is — at 2026-09-19Globals(HashMap<u32, f32>),QuestStageState,ReferenceEnableState(HashSet),ReferenceLockStateandPersistentReferenceStates::pair_rowsall emit hash-iteration order (since joined byDialogueSpokenInfoForms(HashSet) andStoryEventAliasFill(HashMap)), so two saves of equal state can differ in bytes/CRC. #4748 is closed as a doc fix: theSnapshotdoc insnapshot.rsnow says the payload "is not guaranteed deterministic for equal logical state" (Globalsis still aHashMap, 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_entityis saved verbatim and replayed viaset_next_entitybefore inserts;insert_batch'sentity < next_entityis adebug_assertonly (release inserts at an unspawned id silently — MEDIUM);StringPool::dump/from_dumppreserves symbol order (a reordered dump = everyNamewrong = 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_MAJORdoc comment insnapshot.rs: 104 as of 2026-10-15, with onevN -> vN+1entry per bump from v22 on; the ledger still lacks the v32→v33 entry — #536714cff35ae, new saved resourceDialogueSpokenInfoForms— known-open #5459. Recent anchors: v36 Wave 4RigidBodyData.surface_material, v37 #5542 widened itu8→u32, v38 #5300 re-keyedDialogueSpokenInfoFormsto a (speaker ref, INFO) pair, and v39–v104 are almost entirely M47.2 scripting state — new saved resource columns,Effect/FragmentGuard/ValueExprvariants and fields reachable through queued fragment continuations (FragmentExecutionQueue),CharacterLevelv46 (kill XP ended #2947's exemption),Bleedoutv51,LevelUpPointsv53; do not hardcode the number elsewhere): intra-type shape changes need a bump becauseschema_fingerprinthashes only column keys (+ replacing policy). A new required field, retyped field, or newOptionin 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-taggedserde_jsonenum (keyed by variant name, never ordinal — #4140 corrected the old "discriminant shift" rationale), removing a field (nodeny_unknown_fieldson any saved type), renaming withserde(alias). Changing an existing variant's field shape is not compatible. Adding/removing a registered column changes the fingerprint and rejects old saves viaSchemaMismatchwith 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_FINGERPRINTchange 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, includingNOT_SAVED_BY_DESIGNtypes sharing it (e.g.AnimatedTextureFlip; inspect-only files such asWaterMaterial's left the scan in 573170e1c) — those move the hash with no snapshot impact; (b) a tuple struct sweeps the followingimplblock 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 andFORMAT_MAJORwere 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-deriveregistered type (thebyroredux/srcpattern) defined in another file is outsidesave_type_sources()unless added to its explicit list (#5059 fixed in945be7dc6forLightSource.emitterand thecrates/sdkFormRef/ScriptValuepayloads — the next such nested payload is the residue; the extension-manifest declaration types sharingscript_function.rsare exempt from theserde(default)scan, with a staleness test); the shape scan once skipped a save derive that was not the last#[…]attribute (#5403 fixed ine19cb0491, coverage markers assertStoryManagerNodeState/SmNodeRuntimecontribute shapes); a newOption<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 byextensions::capture_extension_state, typeExtensionStateSnapshotincrates/sdk/src/component.rs): it is outside the registry, the completeness guard, and both serde guards (sdk types carry a plainderive, sosave_type_sources()drops them), and follows its own policy — aformat_versionrange (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 inpreflight_extension_state(before teardown); every shape change to its rows bumpsEXTENSION_STATE_FORMAT_VERSION(nothing enforces it — no shape baseline); rows without stable authored identity refuse the save rather than dropping;decode_saved_statetreats a missing key as empty but a present-and-malformed one as an abort. ValidateFnparity: everyregister_*variant builds avalidateclosure that decodes the SAME typeloaddoes (Vec<(u32, T)>for columns, bareTfor resources) — a missing/wrong one exempts a column from the preflight so it fails only after teardown.- Feature chain:
crates/save→byroredux-corefeatures=["save"](→inspect) must still hold, or a non-default build compiles away serde impls and columns serialize tonull. - FormId handle vs pair:
register_form_id_componentsaves the stableFormIdPair(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 explicitis_form_idflag, never "firstapply: Noneentry". FnvHasheruses 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 withcrates/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 insidestage_and_rename) → parent-directory fsync. Any failure up to the rename removes the temp (#516369fd54fb2;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 (#51432e95f0bbf). The three non-save writers pin that withassert_no_clobber_fallback(production text only, viacore::source_scan);write_slotis pinned bywrite_slot_has_no_clobber_fallbackand stages through the uniqueatomic_temp_path(#5247eb08e14fd; the old fixedsave_<slot>.ess.tmpraced two processes sharing a save dir) aftercreate_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::newmust build the ring viaSaveRing::resume(notnew), and the cursor advances only after a committed write (quicksave_ring_cursor_does_not_advance_on_validation_abort). - Minor-version + default-fill:
decodeaccepts 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.SetInChargenabDisableSaving; plus the context-invariant refusal #525327562f8ae—LoadedCellIndexpresent but neitherCurrentCellContextnorCurrentExteriorContextmeans an unloadable snapshot even when the synced flag still readsfalse, as in the console ingresses that dispatch outside the drain), thenvalidate_world+validate_form_ids+validate_cinematic_entity_refs(abort, up to 20 lines, nothing written), thensave_world→capture_extension_state→encode→write_slot; ring advance and theSaveCompletesession event only after the commit. Any alternate save path that bypasses the gates (console, player action, SDK) is HIGH; input adapters must enqueue throughqueue_player_save_action(post-scheduler drain —save_worldtakes ~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 debuginput.press quicksave|quickload, which resolves throughActionBindingsto 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_worldcurrently runs hierarchy, equipment, saved-entity references (session-localEntityIdfields of columns excluded from the overlay — grep the function for the live column list), animation (AnimationPlayer,AnimationStackroot + layer clips,Seated.animation_restore), inventory instances vsItemInstancePool, andMaterialfiniteness (the progression checkvalidate_progression_stateretired indbe562c1fwhenCharacterLevelbecame a saved replacing column, v46); the binary adds FormId resolvability and cinematicEntityIdrefs. 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 bothrestore_worldandexecute_pending_save_loads; the typed preflight aborts (before teardown). - Transient state: saves must not depend on transients —
PlayerNotifications, pending action queues,CellTransitionInFlight, parkedStreamStateSnapshotsare 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; neverItemInstancePool— teardown'srelease_victim_item_instancesneeds the live pool, #4135) →without_parked_statewraps the reload (outgoing session'sPersistentReferenceStatesandStreamStateSnapshotsare set aside and restored on failure) → teardown + reload (validate_cell_loadablepreflight first, so a missing ESM/cell keeps the live session; thenpurge_cinematic_retention_statedrops everyActorCinematicState/HorseTetherStaterow so the convoy is not retained across the teardown as a ghost twin sharing aFormIdPair— #5056a197e8563, both reload arms + both debug-load paths, never ordinary cell transitions (purge_wiring_tests) — and despawns + clears the unsavedCinematicReAdoptionpending list, whose un-rooted entities theCellRootIndexteardown cannot enumerate (#5379203be9ed4). Since #5384 that list holds parentless roots only, so the purge (now taking theVulkanContext) collects each root'sChildrenclosure and hands it torelease_entities— subtrees no longer orphan (which madevalidate_worldrefuse every later save) and GPU/Rapier handles are released, #549099cd9fd6c. The teardown never despawns the player rig:unload_cell_innerrefusesplayer_owned_entities, #54835a68e983e) →reset_story_location_cursor+discard_pending_story_events(the unsaved CLOC cursor and any straddlingStoryEventmarkers describe the outgoing session; every load behaves like a fresh boot, #5435557ead21d) →restore_extension_state→ wholesalerestore_resourcesagain (idempotent; re-assertsCurrentCellContext/PlayerPose; the second call is not redundant) →restore_resident(parked rows for resident placements, #4695) →drop_entity_bound_continuationsonPapyrusProviderContinuationQueue(session-localEntityRefhandles must not resume, #4139) →reseat_ambient_packages_after_restore(re-pick packages against the restored clock before the overlay, #4815; its order againstrestore_residentand the overlay is pinned bypost_reload_drain_steps_keep_their_order, #506002fa8358c) →build_form_id_remap→ park the snapshot rows whoseFormIdPairdid not resolve (unresolved_form_id_pairs→park_unresolved_snapshot_rowsintoPersistentReferenceStates, #505417e720430— the exterior reload streams onlyradius_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 ina8b3d1902and the count pinned by #5460bda5a1167),reset_player_factions_to_record→ diagnosticvalidate_world→apply_player_poseLAST (after the overlay ofCharacterController, 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 fromexecute_pending_save_loads(id collision = CRITICAL).clear_entitiesdoes not free GPU/physics handles — the live path'sunload_current_interior/drain_streaming_statemust. - Remap:
build_form_id_remapmatches savedFormIdPair→ 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 carriesPLAYER_FORM_ID_PAIRfrom 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_actionsdrains a FIFO afterScheduler::run; remote console runs in the exclusiveDebugDrainSystem);capture_player_posereads post-propagationTransformbefore the drain;PlayerNotificationsdrain inrender_one_frameis unconditional (bounded ring of 8, oldest dropped). The load drain lives instep_save_loadswhere the App owns&mut World+&mut VulkanContextand is skipped while an interior transition or foreign loading screen is active; the singlePendingSaveLoadSlotis 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 #5591946310eb9: bodyTransform+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:
LoadCommandaccepts interior OR exterior context and rejects only neither;SaveInfoCommandmirrors the three arms (#3500);quickload_latestwalksslots_by_recencyand falls back past a corrupt newest slot;command_output_is_failurescans 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).
Du fichier au premier essai
Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.
Consulter la source et ses fichiers annexesCe téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.
1. Télécharger le fichier
Télécharger SKILL.mdDepuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/m45-save-load-subsystem-audit-mveraoih/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.
Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.
mkdir -p '.skill-downloads/m45-save-load-subsystem-audit-mveraoih' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/m45-save-load-subsystem-audit-mveraoih/download' --output '.skill-downloads/m45-save-load-subsystem-audit-mveraoih/SKILL.md.part' && mv '.skill-downloads/m45-save-load-subsystem-audit-mveraoih/SKILL.md.part' '.skill-downloads/m45-save-load-subsystem-audit-mveraoih/SKILL.md'2. Vérifier les prérequis
Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.
Aucune section de prérequis reconnue dans ce fichier. Cela ne signifie pas qu’il est autonome : les bibliothèques, outils et accès peuvent être cités ailleurs dans le contenu ou dans le dépôt source.
Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.
3. Charger la skill
Le fichier ne fournit pas un en-tête name/description exploitable pour cette procédure native. Le téléchargement reste disponible pour lecture ; utilisez le mode manuel ci-dessous.
Mode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.
4. Faire un premier essai
Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :
Lis .skill-downloads/m45-save-load-subsystem-audit-mveraoih/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.
Bonnes pratiques React Hook Form
Developpement
Maîtrisez React Hook Form : évitez les anti-patterns, gérez les abonnements, et traitez correctement reset, dirty state et entrées contrôlées.
Migration Emotion vers Mantine
Developpement
Migrer les styled-components Emotion vers des composants Mantine avec style props et modules CSS. Pour convertir .styled.tsx ou supprimer les imports @emotion.
Détecteur de dérive de périmètre
Developpement
Analyse les diffs git par rapport à une intention pour détecter la dérive de périmètre, fichiers non liés et PR trop larges, puis suggère de garder, diviser ou justifier.