Automated summary
Performs a deep, dimension-by-dimension audit of the ECS layer, covering storage backends, queries, world and resource access, scheduling, systems, and lock ordering.
Strengths
- Structures the audit by blast radius and gives explicit paths and first commands for each dimension.
- Focuses on concrete high-risk ECS machinery, especially ABBA deadlocks, tracker scopes, lock ordering, and reentrancy.
- Includes delta-scoping guidance and ownership boundaries to avoid auditing gameplay logic.
Limitations
- Tightly coupled to the byroredux repository layout and depends on sidecar files (`_audit-common.md`, `_audit-severity.md`, `_audit-owners.md`) that are not included here.
- Requires a Rust toolchain, project-specific crates, and optionally GitHub Actions access for full verification.
- The supplied content is truncated mid-dimension, so the full checklist and later dimensions cannot be evaluated from this excerpt.
Use when auditing or reviewing the byroredux ECS internals for lock-ordering, storage, query, scheduling, or lifecycle risks.
Do not use for gameplay-system logic audits or for ECS codebases outside the referenced byroredux project.
Security analysis
SafeThe file is a pure read-and-report audit checklist for a Rust ECS codebase. It contains no destructive commands, no curl-pipe-to-shell, no base64 payloads, no secret/token handling, and no instructions to disable safety checks; its only instructions are to read code and run test suites and greps. The most powerful operations are local `cargo test`/Miri runs and a read-only `gh run view`, which are legitimate for an auditing skill, so the listing risk is minimal.
- •Low-severity note only: the workflow shells out to `cargo test`/`cargo miri` (compiles and executes repository code), `git log`, `rg`, and `gh run view` (authenticated network read). All are read-only, standard developer tooling operations with no destructive, obfuscated, or data-exfiltrating behavior, and no secrets or credentials are referenced.
Examples
Run an ECS audit of crates/core/src/ecs focused on lock ordering and deadlock machinery. Start with the lock_tracker tests and the BYRO_LOCK_ORDER_CHECK runs.Audit the ECS storage backends in crates/core/src/ecs/storage.rs, packed.rs, and sparse_set.rs, then review query.rs for lifecycle and access-declaration issues.Do a delta-scoped ECS audit since the last report across world.rs, query.rs, resource.rs, scheduler.rs, and scheduler registration in byroredux/src/boot/schedule/.description: "Deep audit of the ECS — storage backends, queries, world, systems, resources"
ECS Audit
Read _audit-common.md and _audit-severity.md for shared protocol.
Scope: crates/core/src/ecs/ (one file per concern: storage.rs traits, packed.rs / sparse_set.rs
backends, world.rs, query.rs, resource.rs, scheduler.rs, access.rs, lock_tracker.rs,
systems.rs transform propagation, hierarchy.rs), its components/ + resources/ trees, the
crates/core/src/animation/ runtime (Dim 9), and scheduler registration in byroredux/src/boot/schedule/.
Owner map: _audit-owners.md. Gameplay-system logic (combat, inventory, AI-package behaviors,
containers/loot, notifications) belongs to /audit-gameplay; this audit keeps only ECS shape — storage
class, lifecycle, lock/access declaration.
Dimensions run in order of blast radius. Per-dimension Paths: / First step: let you skip a
dimension whose Paths have no commits since the last report
(git log --since=<last-report-date> --format='%h %s' -- <Paths>).
Dimensions
1. Lock Ordering & Deadlock Machinery (HIGHEST blast radius)
Paths: crates/core/src/ecs/{world,lock_tracker,query,resource}.rs
First step: cargo test -p byroredux-core lock_tracker then BYRO_LOCK_ORDER_CHECK=1 cargo test -p byroredux-core
AND BYRO_LOCK_ORDER_CHECK=1 cargo test -p byroredux-scripting AND BYRO_LOCK_ORDER_CHECK=1 cargo test -p byroredux --bin byroredux
(rustc ≥ 1.96 toolchain — wasmtime 49's MSRV since 15a6b1d2d) — or read the last main run's ABBA lock-order detector
job conclusion via gh run view <id> --json jobs. Never skip this step on a delta-scoped run, even when
this dimension's Paths are unchanged: the process-wide graph is built by every caller, and the cycles live in
the binary's graph, not core's — core stayed green through the whole 2026-09-22..28 red streak (#4985, #4982).
A &mut World function still records edges the detector cannot tell apart from &World ones, so hoisting
several read guards into one block for speed (#4616) reddens the lane. The -p byroredux lane went red again
on three production inversions — a gather taking GlobalTransform before Transform (every mover takes
Transform first), a hair head-mount holding MeshHandle + Children across a Parent walk, a seated
restore holding Furniture across a GlobalTransform read — and was re-greened by #5535 1cb12fb3d; a new
multi-storage gather is the regression shape. It recurred within two days: #5713's cfabdfd6a called
collect_subtree (takes Children) while a MeshHandle guard was alive in common_shape_rotation, re-recording
the MeshHandle → Children edge #5535 removed; #5954 7a4f6c1a2 gathers first, then filters. Test bodies feed the
same graph: #5032 3bc60e0be made cinematic_trio_survives_save_load_round_trip (save_io/round_trip_tests.rs)
and quest_alias_collection_fill_type_still_declines_not_binds (crates/scripting/src/scene/quest_alias_tests.rs)
drop their first guard before the second acquisition — a test that holds a ComponentRef/ResourceRead across
another acquisition records a back edge exactly like production code.
A wrong lock order is HIGH. This dimension owns the machinery; system-level cycles, canonical-order
adherence and CI detector coverage are /audit-concurrency Dim 3 and Dim 5.
- Reentrancy: the thread-local check (write on a type already held, read while holding write)
panics in debug AND release. The global ABBA graph is debug-only and opt-in via
BYRO_LOCK_ORDER_CHECK=1(#313). Everyquery/query_mut/resource/resource_mutsite inworld.rsmust arm aTrackedRead/TrackedWritescope, defuse it only AFTER the real lock is taken, and untrack inDrop. - TypeId-sorted pairs:
query_2_mut,query_2_mut_mut,resource_2_mut,try_resource_2_mutacquire inTypeIdorder AND arm tracker scopes in the same order (arming in parameter order re-opens #313 — a caller spelling<B, A>then looks like ABBA). Eachassert_ne!sA == B; a silent self-deadlock is the regression.try_resource_2_mutchecks BOTH existences before taking EITHER lock (#465). - Tracker internals (each pinned by tests in
lock_tracker.rs— read them, do not re-derive):record_and_checkruns BEFORE the incomingLockStaterow is inserted, on the fresh-acquire AND recursive-read paths (#2384/#3696;is_clean()true after a caught ABBA panic);GRAPHlock poison is recovered withunwrap_or_else(|poison| poison.into_inner()), never.expect(#2385 — an.expectblinds the detector after its first correct catch); a recursive read is a deduplicatedlog::warn!, not a reject (#2386/#3249:recursive_read_warns_once_and_continues); theheld_otherssnapshot is gated oncfg(debug_assertions)ANDglobal_order::is_enabled()(#823/#3680) so a detector-off debug build allocates nothing. - Poison: every acquisition resolves
PoisonErrorthroughstorage_lock_poisoned/storage_lock_poisoned_erased/resource_lock_poisoned(world.rs) — re-panic loud with the type name.despawnuses the erased variant fed by thetype_namesside-table (#466); dropping either loses the type name in every panic.
2. Storage Correctness & Change Tracking
Paths: crates/core/src/ecs/{packed,sparse_set,storage}.rs, crates/core/src/ecs/components/{transform,global_transform,hierarchy,scene_flags,world_bound}.rs
First step: grep -rn 'type Storage = PackedStorage' crates byroredux --include='*.rs' | grep -v test
- PackedStorage census: production users are exactly
Transform,GlobalTransform,WorldBound,SceneFlags. Any otherPackedStoragecomponent is a finding unless it is read every frame by renderer/physics/animation. Everything sparse (actors, markers, events) isSparseSetStorage. - SparseSetStorage: swap-remove repoints the sparse slot of the entity moved into the gap;
removing the last element takes the no-swap path; re-insert overwrites in place (
swap_remove,remove_last,overwrite). - PackedStorage:
binary_searchkeeps the sorted-by-entity invariant on insert/remove;insert_bulkis append + one sort and the result stays sorted AND deduplicated (#467);World::insert_batchstill fires the per-itementity < next_entitydebug_assert. - Change tracking (
Component::TRACK_CHANGES, defaultfalse): ON for TEN components —Transform,GlobalTransform,Parent,Children, plusLocalBound(since ad012f9d6),MaterialandParticleEmitter(both since 1d56758ba / #3836),CollisionShape(crates/core/src/ecs/components/collision.rs) +RapierHandles(crates/physics), both since 88c23887b, andRigidBodyData(same file, #594344f3a4f35). The sparse ones get only astructural_generationbump and no dirty set — #3836'sSceneEffectSoftCache, the incremental world-bound propagation,physics_sync_system's unregistered-shape absence cache (sync.rs) andtrap_contact_system's Trap-layer body list (TrapLayerCache,byroredux/src/systems/trap_hits.rs) consume them. The generation moves on insert (overwrite included) and remove, never onget_mut— a cache keyed on a field (the trap list onhavok_layer) goes stale if that field is ever edited in place.PackedStoragekeeps a dirty set (may hold duplicates — consumers tolerate that);SparseSetStorage::structural_generationbumps on insert/remove.drain_dirty_intopreserves capacity (#1371);take_dirtyhands it away. TheGlobalTransformdirty set has ONE destructive drainer (make_world_bound_propagation_system,byroredux/src/systems/bounds.rs) — a secondtake_dirty/drain_dirty_intoconsumer steals its work; other systems must useget_mutonly where marking dirty is intended (billboards call this out). A mutation path that forgetsmark_dirtysilently breaks Dim 6's fast path. - Erased removal (per cell unload):
clear_erasedreleases capacity;remove_entities_erasedearly-outs on an empty victim set and, inPackedStorage, compacts in place with zero allocations (remove_entities_erased_does_not_reallocate, #3689) while keeping sort order + dirty marks (#2396). A regression is a per-streaming-cycle cost. EntityIdis monotonic and never recycled (World::spawnchecked_addpanic #36;despawnreclaims nothing, #372/#3375). Do not describe it as generational or "fix" it by reuse (danglingParentrefs go silent).
3. Query Borrow Safety & the ECS unsafe
Paths: crates/core/src/ecs/query.rs, .github/workflows/ci.yml (job ecs-query-miri)
First step: grep -n 'unsafe' crates/core/src/ecs/query.rs (expect exactly 4 derefs)
- The only
unsafeinecs/is the four cached-pointer derefs inquery.rs(QueryRead::storage,QueryWrite::storage/storage_mut,ComponentRef::Deref; #1367). Each needs a SAFETY comment tying the pointer to the live guard field; the#[allow(dead_code)] guardmust not drop early;&mut *self.storagestays gated by&mut self. A new unsafe block without a comment is MEDIUM. World::getreturnsComponentRef(owns its guard), never a raw pointer to dropped storage (the unsound #35 pattern = CRITICAL UAF if it returns).- Guard: CI job
ecs-query-mirirunscargo miri test -p byroredux-core --lib ecs::world::testsskipping onlyresource_visible_to_system_via_scheduler(crossbeam-epoch TLS vs Miri). Confirm the skip list has not widened and that any new cached-pointer site has a test underecs::world::tests. query/query_mutreturnNonefor never-created storage (no lazy creation);register::<T>()guarantees success before first insert.HierarchyTraversalGuard(crates/core/src/ecs/hierarchy.rs) bounds every parent/children walk toentities + child refs + 1steps; transform propagation and bounds use it. A new hierarchy walk without it, or without a visited set, is an unbounded-loop hazard on a cyclicParentgraph. The shared guarded collectorcollect_subtree(same file; visited set +HierarchyTraversalGuardbudget,collect_subtree_terminates_on_a_cycle) landed with #55139da80470d:retry_cinematic_readoption,loading_screen.rs,cinematic_retained_entities/ the unload purge (cell_loader/unload.rs),release_finished_tethersandsubtree_entities_underall route through it, and #5713cfabdfd6amoved the last five hand-rolled whole-subtree walks onto it (player_owned_entitiesincell_loader/unload.rs,player_body.rs's descendant walk,subtree_has_skinned_mesh+common_shape_rotationinnpc_spawn/resumable/runtime.rs, the water-plane scan incell_loader/water.rs). Nothing mechanical forces a new whole-subtree walk to use it — grep for hand-rolledChildrenstacks; the guarded walks still outside it (propagation insystems.rs,systems/bounds.rs,ragdoll.rs, the subtree name map inanim_convert.rs) carry their own visited set + budget. Call the collector before taking any other storage guard (#5954 above).
4. Resources & World-Level State
Paths: crates/core/src/ecs/{resource.rs,resources/,game_profiles.rs,debug_load.rs,metrics.rs}, world.rs resource half
First step: git log --since=<last-report-date> --format='%h %s' -- crates/core/src/ecs/resources
resource()/resource_mut()panic with the type name when never inserted;try_*returnNone.ResourceRead/ResourceWritedowncast perDeref(not cached — not the #1367 hot path);insert_resourcereturns the prior value;remove_resourceresolves poison;init_resource::<R>()insertsR::default()only when absent, leaving an existing (e.g. save-restored) value untouched (#5806d0572ed94— it replaced the hand-spelledtry_resource/insert_resourceidiom in scripting'sregister).- Resource vs. component choice: mode flags and singleton state are
Resources (HardcoreMode,SelectedRef, the*Bridge/*Telemetrystructs); per-actor state is aSparseSetStoragecomponent (ActorVitals,TimedRestorations). A new per-entity fact modelled as a global map keyed byEntityIdis the drift (it defeatsdespawncleanup and save capture). Every newResource/Componentis also subject to the save registry gate (every_component_or_resource_impl_is_saved_or_explicitly_allowlisted,byroredux/src/save_io/,/audit-save) and the debug-server registry. SkinSlotPool(resources/skin_slot_pool.rs): every collection isFxHashMap/FxHashSet(_audit-common.mdhot-path hashing rule);pose_dirtyanddrain_pendingare the renderer hand-off. Overflow/retry semantics are/audit-safetyDim 7.OwnershipTracker/ReclaimPolicy(resources/ownership.rs): the EX-08 soak-gate accounting. Check each owner class's policy is honest —Monotoniconly for identity watermarks (entity ids, mesh/texture slot-vector lengths),Exactfor anything that must return to baseline after unload,Boundedfor documented caches,Occupancyonly for per-frame camera-pose samples (the two ground-cover classes; a strictly increasing series must also rise pastOCCUPANCY_GROWTH_TOLERANCE_PERMILLEof its first sample, #6058adaa7a263). A leaking class reclassifiedBounded/Monotonic/Occupancyto turn the gate green is the finding.DeltaTimeis stamped once per frame by the main loop (byroredux/src/app_events.rs); a system that writes it orTotalTimeis a finding.
5. System & Scheduler Wiring, Declared Access
Paths: crates/core/src/ecs/{scheduler,access,system}.rs, byroredux/src/boot/schedule/, byroredux/src/boot/registries.rs, byroredux/src/scheduler_access_tests.rs
First step: cargo test -p byroredux -- system_access_declaration_tests scheduler_access then rg -n '^\s*scheduler\.add_to\(' byroredux/src/boot/schedule/ (must be empty)
Stages: Early → Update → PostUpdate → Physics → Late (Stage, discriminants 0..=4, ordered by
BTreeMap); "exclusive" is a phase inside every stage (StageData.exclusive, serial after that
stage's parallel batch), not a stage. Registered per stage in register_{early,update,post_update,physics,late}_systems.
- The
Accessmodel:Access::new().reads::<T>().writes::<U>().reads_resource::<R>()…; declared / declared-empty / undeclared (None).AccessConflicthas exactlyNone,Unknown { left_undeclared, right_undeclared },Conflict { pairs }(noParallelvariant);analyze_pairreturnsUnknownwhen either side is undeclared. Closures/bare fns cannot overrideSystem::access, so they declare at registration viaadd_to_with_access/add_exclusive_with_access. Pins:undeclared_closure_pairs_show_as_unknown,exclusive_systems_are_listed_but_not_paired,all_five_stages_run_in_order(reordering or inserting a stage without updating it is the regression). - Guard (mechanical declaration completeness):
byroredux/src/boot/schedule/mod.rs::system_access_declaration_tests—every_parallel_system_declares_everything_it_acquiresscans each parallel system's body (same-file callees to depth 3, plus the explicit cross-file hops inPARALLEL_SYSTEMS) and fails if an acquired type is missing from itsAccess;the_parallel_system_table_covers_every_parallel_registrationfails when anadd_to_with_accesslands outside the table; five exclusive fns are covered too (npc_combat_ai_system,papyrus_provider_system,legacy_obscript_load_order_system,equipment_appearance_system— its mid-life gear-import hop intonpc_spawn/loot_appearance.rs, #50699355fcb5c— andnpc_dialogue_selection_system— #5307, plus itsplay_line_voice/raise_hello_story_eventcross-file hops since #54149d8fd4cc9and the cross-crateevaluate_functioncondition-evaluator hop since #5649197a9e245, which added 18 acquisitions to its row),footstep_system'sPhysicsWorld/RapierHandlesreads (footstep_declares_its_physics_reads, #5556ac710964e), andcross_file_hops_and_get_forms_reach_their_acquisitionspins that the table's cross-file hops and thegetforms actually surface types (#4994). Sibling gates inscheduler_access_tests.rs:scheduler_access_invariants_hold_on_the_real_schedule(non-vacuous floors: ≥9 parallel systems, ≥7 analysed pairs, then 0 undeclared / 0 conflicts / 0 unknown),contract_bearing_exclusives_declare_their_access,p2_gameplay_exclusives_declare_non_empty_access,late_telemetry_declarations_read_all_their_resources. Boot enforces the same three counts as RELEASEassert_eq!s ininstall_runtime_registries(#1394/#1602/#2690) — notdebug_assert!, not a log line. Confirm the guards are live:rg -n '#\[ignore' byroredux/src/scheduler_access_tests.rs byroredux/src/boot/schedule/mod.rsreturns nothing, andPARALLEL_SYSTEMS.len()equals theadd_to_with_access(count. Theplayer_controller_systemrow's cross-file hops includeraise_kill_story_event(the drowning death'sStoryEventwrite + cell-context reads, #564117a84f442). What the guard cannot see (audit these by hand for every parallel system and any exclusive being promoted to parallel): it matches only single-type turbofishquery/query_mut/resource/resource_mut(+try_),.get/.get_mut/.has(#4994) and the genericremove_component/insert_componenthelpers (#4821) — NOTquery_2_mut::<A, B>,resource_2_mut, or types inferred without a turbofish (world.get(e)into a typed binding); it does not follow hops into a different file unless listed in the table; closures and macro bodies are opaque. (#4573 closed the mode/substring/comment blind spots: read-vs-write IS compared, names match whole declared types, and//lines in the registration block no longer satisfy the scan. The forms gap above remains.) A same-session precedent: an undeclared same-frameGlobalTransformwrite added tofly_camera_systemmade the bootknown_conflict_count() == 0proof unsound until the registration declared it (commit ac1d44f5c). - Exclusive declarations are optional: most
add_exclusiveregistrations are undeclared by design (undeclared_exclusive_count()non-zero); the analyzer never pairs exclusives. Flag only a parallel system that lost its declaration, or an exclusive that a test above says must declare. Enumerate live counts withrg -con the fouradd_*forms; never quote a total. - Panic policy is fail-fast (#1412): do not report a missing
catch_unwind.Scheduler::runtakes&mut selfandScheduleris deliberately not aResource(re-entry impossible, #868). The one other entry point isScheduler::run_exclusive_named(#5141,00115f7a5): the engine's paused frame path runs exactlydebug_drain_systemthrough it (its name is a shared constant asserted against the live schedule, #52928a9989070) while the pause menu gatesrun(pinned byrun_exclusive_named_runs_only_the_named_system). It bypasses stage order and per-system timing, so any new caller must name a system with no same-frame producer/consumer dependency.add_*warns on duplicate names;try_add_*returnsErr(name)across the flat name space (#312). - Cross-stage sequencing (a consumer in an earlier stage than its producer) is invisible to the
analyzer — see
/audit-concurrencyDim 4 for the pinned cases and the by-hand rule. The same blind spot covers same-stage order against a one-frame marker:HitEventproducers (melee, NPC strike, force-greet,Weapon.Fireprojectile flight, the VM'sProcessTrapHitflush inside the reference/alias script systems) must register before both consumerscombat_damage_systemandhit_event_system, orevent_cleanup_systemdrains the hit unseen — dart-trap and projectile damage never landed untilee6468fd1, now pinned byhit_event_consumers_run_after_every_producer. A new producer outside that list needs a name in the test.
6. Hot-Path Performance Invariants (regression guards)
Paths: crates/core/src/ecs/systems.rs, byroredux/src/systems/{animation,audio,bounds}.rs, byroredux/src/components/audio.rs
First step: cargo test -p byroredux-core ecs::systems
- Transform propagation fast path (#825/#1371,
make_transform_propagation_system): the cachedrootsset is keyed on(Transform::len(), Parent len-or-0, next_entity_id())plus theParent/Childrenstructural_generation()values and the drainedTransformdirty set; the BFS is skipped when nothing changed. Usesdrain_dirty_into, nottake_dirty. A path that stops bumpingstructural_generation/mark_dirtyyields a wrongGlobalTransform— a correctness bug, escalate. - PostUpdate/Late ordering contract (comments in
boot/schedule/post_update.rs): transform propagation, then bound propagation (drains theGlobalTransformdirty set), and noStage::Latesystem may writeGlobalTransformon aLocalBound-bearing entity (itsWorldBoundlags a frame; billboards are the one accepted exception). A new LateGlobalTransformwriter must make that call. Same rule forTransformwriters feeding propagation:player_body_facing_systemmoved Late → Update (#4995,player_body_facing_runs_in_update_before_propagation). A mover writesTransform, neverGlobalTransformalone, on a propagation root — the next propagation rebuilds the global from the unmoved local and erases the step (#5373f0c683ef1: the force-greet and eat/sleep walk arms turned in place forever; pinned per system byforcegreet_walk_survives_transform_propagation/eat_walk_survives_transform_propagation). Nothing mechanical guards a new mover — check it by hand. - Animation scratch (
byroredux/src/systems/animation.rs): theNameIndex.maprefill is in place (clear+ reserve + reinsert; a fresh map costs a ~3 ms stream-in spike, #824);SubtreeCacheclears only when theNamecount changes (#278);events/seen_labelsscratch is hoisted andcloned notmem::taken (#828). Lock order inside channel apply is content-determined (#2399) — do not reintroduce a macro that hides the acquisition order. FootstepScratch(byroredux/src/components/audio.rs;components.rsis now a thin re-export over per-domain files, #54676ef8976f2):mem::take+ restore soVeccapacity survives; a per-frameVec::newregresses (#932). Placement (Late exclusive aftercamera_follow_system) is pinned byfootstep_runs_after_camera_follow_in_late. Without aPhysicsWorldthe surface list is oneNoneper trigger, so each stride still plays the default sound (#571477eea4311; an empty list used to truncate the one-shot loop), pinned bystride_threshold_fires_one_default_footstep_without_physics_world.
7. Component Lifecycles (load/unload, transient, idempotency)
Paths: byroredux/src/{streaming,npc_spawn}*, byroredux/src/cell_loader/{unload,load,exterior}.rs, byroredux/src/systems/cinematic.rs, crates/core/src/ecs/components/, crates/scripting/src/{events,timer,cleanup}.rs, crates/core/src/animation/registry.rs
First step: cargo test -p byroredux --bin byroredux -- rapier_release persistent_cell_stamp_tests detach_tests cinematic_retention_tests (rustc ≥ 1.96) then read git log --since=<last-report-date> -- crates/core/src/ecs/components
- Cell load/unload symmetry (
streaming/,cell_loader/unload.rs): every component/resource row a cell load attaches is removed on unload (no orphanCharacterController,LightFlicker,RapierHandles, animation players,SeatReservationsclaims whose furniture or claimant is gone). Spawn dispatch is idempotent — one REFR FormId never spawns twice. CellRootstamp discipline (stamp_cell_root_range,cell_loader/load.rs): every resumable applier stamps only the[first, last)range captured inside its ownadvance; a cursor that spans frames claims whatever spawned in between. #54835a68e983e: the persistent-CELL job seeded once and gave the camera, player rig and foreground cell to the persistent root — it now reseeds per advance (begin_advance,entities_spawned_between_advances_are_not_claimed). Defence in depth:unload_cell_innerrefuses any victim inplayer_owned_entities(PlayerEntity,ActiveCamera, thePlayerBodyRootEntitysubtree), stripping the strayCellRootwith a warn — that warn is always a producer bug.- Cinematic re-adoption (
retry_cinematic_readoption): only parentless released-convoy members queue onCinematicReAdoption; a root's adoption stamps its wholeChildrensubtree with the sameCellRoot(#5384faf8e5682,parented_subtree_rides_the_roots_adoption), and the player subtree is never queued or adopted (#5379203be9ed4,player_rider_is_never_queued_or_cell_adopted). The session-replace purge (purge_cinematic_retention_state(world, ctx)) collects each pending root'sChildrenclosure (minus the player rig) and releases it throughrelease_entities, so no subtree orphans and GPU/Rapier handles go with it (#549099cd9fd6c). - Detach pass:
detach_victims_from_surviving_parentsreads every victim'sParentunder one guard (dropped before theChildrenwrite) and tests membership by binary search over a sorted slice (#54189bbe9304e, source-pinned bydetach_victims_takes_one_parent_guard_for_the_whole_sweep); a per-victimworld.get::<Parent>is the regression. - Behavior components stay sparse: every AI-procedure
*Behavior/*State/ terminal marker isSparseSetStorage. The roster is pinned in the debug registry byroster_tests::every_ai_procedure_behavior_component_is_registered(crates/debug-server/src/registration.rs, #4063); teardown completeness (clear_ambient_behavior,npc_spawn/ai_package.rs) and behavior semantics are/audit-gameplay. - Transient markers (two-pattern contract, #2672,
crates/scripting/src/cleanup.rscontract_tests): Pattern A markers with no single owning consumer (ActivateEvent,HitEvent,SplashEvent,RippleEvent,TimerExpired,AnimationTextKeyEvents, the*Batch/ command markers, …) are removed byevent_cleanup_system(Late exclusive, registered after every consumer — pinned bytransient_cleanup_is_the_last_late_exclusive; a second cleanup site would double-free); Pattern B markers (StoryEvent, the scene-package batches, …) drain in their one consumer and must not also be listed in the cleanup. The cleanup system also resets the PapyrusVmFrameBudget(#5763). Late readers now includeequipment_appearance_systemandnpc_dialogue_selection_system.timer_tick_systemnever accumulates negative time. Known-open #5647 (MEDIUM):StoryEventis one slot per entity with several producers (CLOC, KILL, scripted kills), so two player-keyed events in one frame overwrite each other. AnimationClipRegistry(animation/registry.rs): interns by ASCII-lowercased path (#790) so streaming does not grow it;release()clears a slot but never returns it — no free list, by design (#2689), because a released handle may still sit on anAnimationPlayer/AnimationLayer. Every evict/reload strands one empty stub (stub_slot_count(), visible not closed). Do not propose a free list without addressing that aliasing hazard.- No animation controller layer:
AnimationStackis the whole sequencing surface; audit its lifecycle (no dangling clip refs after unload) rather than looking for a controller above it. - Retained
EntityIdfields (never recycled, so a stale id is a failed lookup, not aliasing): new per-actor carriersNpcSkeletonBones(bone-name → entity map, retained on living actors and mirrored onto the process-lifetime player capsule for mid-life gear import),AmbientEngagement.target,PlayerBodyRootEntity. Check every consumer tolerates a despawned target, and that a cell unload which despawns a skeleton also drops or invalidates the map holding it. - Emitters:
apply_emitter_params(byroredux/src/systems/particle.rs) fillsParticleEmitterfromImportedEmitterParams; size isinitial_radius × base_scale.unwrap_or(1.0)and colour is not clobbered (apply_emitter_params_size_defaults_base_scale_to_one,apply_emitter_params_overrides_kinematics_and_size_not_color). Mapping correctness is/audit-nifal. - Physics/audio handle lifetimes: Rapier release on unload is
/audit-safetyDim 3; kira spatial sub-track handles drop before the listener;OneShotSoundmarkers are pruned once kira reportsStopped(/audit-audio).
8. NIFAL Canonical Material in the Component Layer
Paths: crates/core/src/ecs/components/material.rs, byroredux/src/material_translate.rs
First step: cargo test -p byroredux-core resolve_pbr
Material is the landing zone of the NIFAL boundary (upstream is /audit-nifal).
- Plain-
f32contract:metalness/roughnessare resolvedf32, notOption<f32>(a regression re-introduces per-draw classification — HIGH). - Single mutation site:
material_translate.rs::translate_materialis the soleImportedMesh → Materialboundary;Material::resolve_pbris the only fill-the-gap helper (sharedclassify_pbr_keyword, fills only the unset slot). No per-drawclassify_pbrfallback survives inbyroredux/src/render/static_meshes.rs. - Pinned by
resolve_pbr_is_idempotent,resolve_pbr_preserves_upstream_translator_values,resolve_pbr_fills_only_missing_slot,resolve_pbr_clamps_authored_out_of_range; clobbering authored scalars or breaking idempotency is a finding. - Other producers (Starfield CDB via
crates/sfmaterial/) go throughtranslate_material/resolve_pbr;crates/debug-ui/must not register or mutate gameplay components.
9. Animation Runtime (crates/core/src/animation/)
Paths: crates/core/src/animation/, byroredux/src/systems/animation.rs, byroredux/src/anim_convert.rs
First step: cargo test -p byroredux-core --features inspect animation
Import is /audit-nif, the NIF→clip boundary is /audit-nifal Dim 7; this dimension owns sampling,
blending, root-motion split and text-key dispatch, driven by an ECS system.
- Clip-handle validity:
AnimationPlayer.clip_handle/AnimationLayerindex the registry; a stale handle after unload is a no-op, never a panic or OOB read. Same forNamedSequences(animation/named_sequences.rs, M47.3f3380f6c8): a placed object's sequence-name → clip-handle table, shared viaArcacross every placement of the model and re-stamped at spawn (unsaved), read by ObScriptPlayGroup/IsAnimPlaying. - Time advance:
advance_time(player.rs) /advance_stack(stack.rs) handledt == 0, negative/NaNdtand a zero-length clip without divide-by-zero or an endless loop;CycleTypeis applied per clip. - Blend weights:
AnimationLayer::effective_weight,play,cleanup_finished, with the live ramp written byadvance_stack(weight = blend_in_target * progressevery tick, #3701). Blend timers tick ahead of theplayinggate so a paused blend-out layer still retires (#3702,advance_stack_ticks_blend_out_on_a_paused_layer_so_it_can_be_retired). Check thatcleanup_finishednever removes a layer still contributing weight and thatplayper tick with a nonzero blend time cannot grow the stack unboundedly. sample_blended_transformis the per-bone-per-frame hot path: no allocation, single-layer short-circuit bit-identical to the general path (#3706,single_layer_short_circuit_matches_two_pass_output). Cost belongs to/audit-performance.- Layer masks:
AnimationLayer::bone_weights+AnimationStack::bone_index(runtime-only,serde(skip), rebuilt by the owning behavior graph) scale a layer per bone throughbone_layer_weight;Noneis the unmasked path, and a bone the mask does not index counts in full. All three weight sites insample_blended_transform(single-layer short-circuit included) must go through it (bone_weights_mask_a_layer_per_bone). - Root motion:
split_root_motionapplies once per tick and is drained — an undrainedRootMotionDeltaintegrates every frame. A blended stack's per-tick accumulation-root delta is the weight-averagedsampled_root_motion_deltaacross every layer whose clip carries that root, not the heaviest layer's alone (animation_stack_root_motion_is_the_weighted_mix_of_its_layers). - Clip sinks: the loose-NIF, KF and cell-REFR spawn sites attach a clip's non-transform channels and read
its start phase through
anim_convert::attach_clip_sinks/clip_start_phase(#5590d58e7dbad, pinned by a source-scan test) — a hand-rolled fourth copy is the drift. - Text keys:
visit_stack_text_events(stack.rs) emits each key exactly once across a loop wrap and still emits when one largedtspans several key times. - Interpolation (
interpolation.rs):find_key_pairat t < first / t > last key; quaternion shortest-path (missing dot-sign flip = bone spinning the long way). B-splines occur on FNV/FO3 too.
Process
- Scope: run each dimension's
First step:; skim dimensions whose Paths are unchanged. cargo test -p byroredux-core --features inspectandcargo test -p byroredux(counts live in ROADMAP.md; do not pin a number). Every-p byroreduxinvocation here needs the rustc ≥ 1.96 toolchain recipe in CLAUDE.md (#4466); the default toolchain gives no feedback.- Save the report to
docs/audits/AUDIT_ECS_<TODAY>.md.
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/deep-ecs-audit-mvhm5lzw/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/deep-ecs-audit-mvhm5lzw' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/deep-ecs-audit-mvhm5lzw/download' --output '.skill-downloads/deep-ecs-audit-mvhm5lzw/SKILL.md.part' && mv '.skill-downloads/deep-ecs-audit-mvhm5lzw/SKILL.md.part' '.skill-downloads/deep-ecs-audit-mvhm5lzw/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
This file does not provide a usable name/description header for this native procedure. It remains available to download and inspect; use the manual method below.
Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/deep-ecs-audit-mvhm5lzw/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.