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.
The ECS core is crates/core/src/ecs/. Since Session 34's split the module
is one-file-per-concern: storage.rs holds only the Component /
ComponentStorage / DynStorage traits + EntityId; the two backends live in
packed.rs (PackedStorage) and sparse_set.rs (SparseSetStorage).
world.rs owns the RwLock-per-storage World; query.rs the guard-owning
query wrappers; resource.rs the resource guards; scheduler.rs the stage
scheduler; access.rs the declared-access conflict analyzer; lock_tracker.rs
the deadlock / ABBA detector; systems.rs the transform-propagation system. Dimension 10 (added 2026-08-13)
extends this skill past ecs/ into the sibling crates/core/src/animation/
runtime, which had no owner dimension anywhere.
Dimensions are ordered by ECS blast radius: lock ordering / deadlock first, then storage correctness, query borrow safety, scheduler declarations, resource lifetimes, then the cross-cutting lifecycle and hot-path guards.
Dimensions
1. Lock Ordering & Deadlock (HIGHEST blast radius)
A wrong lock order is a HIGH (per _audit-severity: "ECS deadlock potential").
- Same-thread reentrancy:
lock_tracker(lock_tracker.rs) panics with a clear message when a thread takeswriteon a type it already holds (read or write), orreadwhile holdingwrite. The thread-local check runs in BOTH debug and release; the global lock-order graph (ABBA, #313) is debug-only AND opt-in viaBYRO_LOCK_ORDER_CHECK=1. Verify everyquery/query_mut/resource/resource_mutsite inworld.rsarms aTrackedRead/TrackedWritescope, defuses it only AFTER the real lock is acquired, and that the wrapper'sDropuntracks. - TypeId-sorted multi-lock acquisition:
query_2_mut/query_2_mut_mut(world.rs) andresource_2_mut/try_resource_2_mut(world.rs) acquire inid_a < id_border — and set up the tracker scopes in the same order (the #313 fix: pre-fix the scopes were armed in generic-parameter order, which looked like ABBA to the graph when the caller spelled<B, A>). A regression that arms scopes in parameter order instead of TypeId order re-opens #313. - Same-type double-lock panics, never deadlocks:
query_2_mut/query_2_mut_mut/resource_2_mutassert_ne!onA == Bwith a clear message. A silent self-deadlock is the regression. - ABBA across rayon workers: the global graph generalizes the pair guarantee
to any N-lock hold pattern across the parallel scheduler. Two single-type
queries acquired in opposite orders on two workers must trip the graph (when
run under
BYRO_LOCK_ORDER_CHECK=1), not deadlock. Pin: this is the only protection for ad-hoc N>2 lock holds. - Poison-on-panic resolution: every lock acquisition resolves
PoisonErrorthroughstorage_lock_poisoned::<T>()/storage_lock_poisoned_erased()/resource_lock_poisoned::<R>()(world.rs) — a post-panic access re-panics loud with the type name, never silently reads torn state.despawnuses the type-erased variant fed by thetype_namesside-table (#466). Removing a poison-resolve site is a finding.
2. Storage Correctness
- SparseSetStorage (
sparse_set.rs): swap-remove fixes the sparse pointer for the entity moved into the gap (self.sparse[moved_entity] = Some(dense_idx)); removing the last element takes the no-swap path; insert into an existing entity overwrites in place (no duplicate, len unchanged). Pinned byswap_remove,remove_last,overwritein the file's test module. - PackedStorage (
packed.rs):binary_searchmaintains the sorted-by-entity invariant on every insert/remove;insert_bulkuses the append + single-sort fast path (#467) instead of O(n) per-insert shift; a bulk insert that re-sorts must keep the set sorted AND deduplicated. - Change tracking (
Component::TRACK_CHANGES): opt-in per-entity dirty set (PackedStorage, viamark_dirtyon insert/get_mut/remove) and a monotonicstructural_gencounter (SparseSetStorage::structural_generation, bumped on insert/remove incl. reparent overwrite). The const isfalseby default so non-tracked components pay nothing (branch folds away). Enabled forTransform/GlobalTransform. Audit:drain_dirty_intoclearsoutthen drains while preservingself.dirtycapacity (#1371);take_dirtyhands capacity away (0-cap regrow). The dirty set MAY contain duplicates — consumers must tolerate that. A storage that forgets tomark_dirtyon a mutation path silently breaks transform propagation's fast path (dim 8). insert_bulkdebug guard:World::insert_batch(world.rs) wraps the iterator so theentity < next_entitydebug_assertstill fires per item — a bulk path that skips it lets unspawned IDs in.
3. Query Borrow Safety
- Guard-owning wrappers:
QueryRead/QueryWrite/ComponentRef(query.rs) hold theRwLock*Guardfor the wrapper's lifetime and cache a raw pointer downcast ONCE innew()(#1367 hot-path fix). The SAFETY argument: the cached*const/*mut T::Storagepoints into the box the guard keeps locked + pinned; no writer can move it while the lock is held. Re-verify eachunsafe { &*self.storage }/&mut *self.storagestill has the guard field alive (the#[allow(dead_code)] guardmust not be dropped early). ComponentRefis the sound replacement for the unsound #35 pattern — it retains the guard rather than returning a raw pointer to dropped storage. A regression that drops the guard and hands back a pointer is CRITICAL (UAF).- Deref soundness:
QueryWrite'sDeref/DerefMutroute throughstorage()/storage_mut();DerefMutrequires&mut self, so the borrow checker forbids a live&and&mutinto the same storage simultaneously. query/query_mutreturnNonefor never-created storage (no lazy empty-storage creation on the read path).register::<T>()is the way to guarantee a query succeeds before first insert.
4. Resource Lifetimes
resource()/resource_mut()panic with the type name when the resource was never inserted;try_resource()/try_resource_mut()returnNone.ResourceRead/ResourceWrite(resource.rs) downcast through the guard on eachDeref(NOT cached — these are not the #1367 hot path); verify the downcastexpectcan't fire (TypeId keys the map).- Resources are usable from systems via
&selfinterior mutability. insert_resourcereturns the prior value (downcast back out of the old lock);remove_resourceresolves poison viaresource_lock_poisoned.try_resource_2_mutdoes BOTH existence checks before acquiring EITHER lock (#465) — a regression that checks-then-locks-then-checks reintroduces a partial-acquire deadlock window.
5. System & Scheduler Wiring
- Blanket
Systemimpl forFn(&World, f32)(system.rs); closures and bare fns can't overrideSystem::access, so they declare via the scheduler's registration-site override (dim 5b). - Mutations from a system are visible to later systems in the same
run()(pinned bymutation_visible_across_stages). - Empty scheduler and empty intermediate stages run without panic
(
empty_scheduler_runs_cleanly,empty_stages_skipped). system_names()returns stage-order then within-stage (parallel first, then exclusive); duplicate names warn onadd_*buttry_add_*rejects withErr(name)across the flat name space (#312).- Panic policy is fail-fast by design (TS-08 / #1412): a panicking system
aborts the frame and the process; do NOT report "missing
catch_unwind" as a bug — see theScheduler::rundoc comment.runtakes&mut selfandScheduleris intentionally NOT aResource(re-entry is structurally impossible, #868).
5b. Scheduler Access Declarations (R7 / M27, closed 2026-05-23)
The stages are Early → Update → PostUpdate → Physics → Late
(Stage enum, scheduler.rs, discriminants 0..=4, iterated via
BTreeMap<Stage, _> Ord). There is no ParallelUpdate or LateExclusive
stage — "exclusive" is a phase within every stage (StageData.exclusive),
not a stage. Exclusive systems run serially after the stage's parallel batch.
Access(notSystemAccess) is the declaration type (access.rs):Access::new().reads::<T>().writes::<U>().reads_resource::<R>()…. A system's declaration isSome(Access)orNone(undeclared). Three states: declared- empty ("touches no ECS state"), declared-with-claims, or undeclared (None). The default for bothSystem::access()and the per-entry override isNone.- M27 Phase 1+2 (
a9810d40): every parallel-stage system on the engine binary declares reads/writes viaScheduler::add_to_with_accessat the registration site inbyroredux/src/boot.rs(build_scheduler; 13 such calls as of 2026-08-16 — closures can't implSystem::access). Any parallel system registered via plainadd_to(no declared access) is a regression. Count this fresh rather than quoting the number — it has drifted twice (10 → 13) between skill refreshes. - M27 Phase 3 (
05fe2bac): 4 analyzer-visible conflicts were resolved two ways — one dispatcher merge plus two exclusive re-stages.player_controller_system(Stage::Early) stays parallel and declares the union offly_camera+character_controlleraccesses because it branches onPlayerModeper frame;audio_system(Late) andspin_system(Update) were the two moved to exclusive.sys.accessesreports 0 unknown / 0 conflicts. AccessConflictlives inaccess.rs(re-exported viaecs::mod) and has EXACTLY three variants:None,Unknown { left_undeclared, right_undeclared },Conflict { pairs }. There is noParallelvariant (the #1521 wording fix).analyze_pairreturnsUnknownwhen one/both sides are undeclared. #1394 (a7e1502b) added theundeclared_parallel_count()accessor onAccessReport— the migration KPI counting parallel-stage systems still atNone— NOT a reclassification. Drivingundeclared_parallel_count() == 0drivesunknown_pair_count()to 0 because every parallel pair then has both sides declared. Pin:undeclared_closure_pairs_show_as_unknown(scheduler.rs) asserts two undeclared closures yieldunknown_pair_count() == 1.- Exclusive declarations are OPTIONAL and mostly absent (#1236/#1237,
94e78b9f):add_exclusive_with_access/try_add_exclusive_with_accessEXIST so closures/fns can declare on the exclusive phase, but the live schedule still registers most exclusives via plainadd_exclusive(e.g.event_cleanup_system,audio_system,spin_system, the DLC dispatchers), soundeclared_exclusive_count()is non-zero by design. The analyzer (access_report) only pairs parallel-stage systems — exclusives are listed but never paired (exclusive_systems_are_listed_but_not_paired). Do NOT report undeclared exclusives as a conflict; flag only a regression where a parallel system loses its declaration. - #1238 stage-order chain (
54ea11c0):all_five_stages_run_in_order(scheduler.rs) registers out of order and asserts theBTreeMapOrdrunsEarly..=Lateexactly once. Reordering / merging / inserting a stage without updating this test is the regression pattern. (Correct chain:Early → Update → PostUpdate → Physics → Late.) - Regression guard:
byroredux/src/boot.rs(install_runtime_registries) runsdebug_assert_eq!(scheduler.access_report().undeclared_parallel_count(), 0)after building the schedule (#1394) — this is the boot guard, NOT a log line. #1602 added two sibling asserts on the same snapshot:known_conflict_count()andunknown_pair_count()must also be 0 (the old undeclared-only guard let a declared WriteWrite conflict through — #1601). Operators inspect contention at runtime via thesys.accessesconsole command (reads theSchedulerAccessReportresource). A non-zeroundeclared_parallel_count/known_conflict_countis an audit finding.
6. Unsafe Code Review
- The only
unsafein the ECS core is the four cached-pointer derefs inquery.rs(QueryRead::storage,QueryWrite::storage/storage_mut,ComponentRef::Deref) — all #1367. Each MUST have a SAFETY comment tying validity to the live guard. Verify no new unsafe block lacks one (MEDIUM min per_audit-severity). World::spawnuseschecked_addand panics onEntityIdoverflow (#36);despawndoes NOT reclaim IDs (no generational tagging — #372) — document, do not "fix" by reusing IDs (silent corruption on danglingParentrefs).
7. Component Lifecycles (load/unload, transient, idempotency)
- M40 streaming (
byroredux/src/streaming.rs): cell-load attaches components, cell-unload removes them — verify no orphaned components after a load/unload cycle. - M41 NPC spawn (
byroredux/src/npc_spawn.rs): ACHR/REFR → entity dispatch is idempotent (same REFR FormId never spawns twice). - M42 AI-package behavior components (
byroredux/src/systems/{sandbox,wander,travel,follow,escort,guard,patrol}.rs,crates/core/src/ecs/components/{sandbox,furniture,wander,travel,follow,escort,guard,patrol}.rs): seven procedure runtimes now exist —SandboxBehavior/Seated(M42),WanderBehavior/WanderState(M42.3),TravelBehavior/TravelState/Traveled(M42.4),FollowBehavior/FollowState(M42.5),EscortBehavior/EscortState/Escorted(M42.6),GuardBehavior/GuardState(M42.7),PatrolBehavior/PatrolState(M42.8) — ALLSparseSetStorage(only actors running that procedure carry them). Verify a growing actor population doesn't force any of them ontoPackedStorage. An NPC's active package is always a single winningPackRecord(active_package'sfindincrates/plugin/src/esm/records/misc/pack.rs), so at most one Behavior component lands per actor at spawn — a regression that lets two of these seven land on the same entity is a correctness bug in thenpc_spawn.rsspawn-tail'sif runs_*chain, not a storage issue.- One-shot terminal markers:
Seated(Sandbox) andTraveled/Escorted(Travel/Escort) are one-shot gates — once tagged, the corresponding system must skip the entity on every later frame (never re-enter seat search / re-walk to an already-reached destination). - Indefinite, non-terminal state:
WanderState/PatrolState(oscillate forever) andGuardState(holds a post, walking back if the actor drifts pastradius— no terminal marker, since guarding never ends) are read and written every tick by their system, unlike the one-shot markers above. - Live vs. frozen resolution:
FollowState/EscortState(mid-collect) re-read their target'sGlobalTransformfresh every tick;TravelState/EscortState(once leading)/GuardStatefreeze a resolved-or-picked position exactly once and never re-track it, even if the resolvedNearReferenceentity later moves. A system that blurs this line (freezes a Follow target, or re-tracks a Travel destination) is a finding. - Shared logic, separate storage:
patrol_systemcallswander_system'sstep_oscillating_wander(a plain-value, component-agnostic function insystems/wander.rs) directly rather than duplicating the phase-transition state machine — verify a future edit to one path doesn't silently diverge from the other without updating bothwander_systemandpatrol_system's call sites.PatrolStatereusesWanderPhasedirectly (not a second enum).travel_system::resolve_destination(pub(crate), generic over primitive fields) is the second instance of this pattern —escort_system's lead phase calls straight into it.guard_system::resolve_anchordoes NOT: it reaches the sameNearReferenceFormID resolution through the sharedresolve_entity_by_global_form_idprimitive, because its no-target fallback is deliberately the actor's own position, NOT Travel's hash-picked point — reusing Travel's fallback here was tried and reverted because it trivially satisfies Guard's own leash check on the first tick). - Seat claims in
SeatReservationsmap each(furniture entity, marker index)to its claimant actor.prune_seat_reservations(cell_loader/references/mod.rs) runs per cell-reference load and keeps a claim only while the furniture is live and the claimant still carries aSeatedcomponent naming that furniture. Verify both liveness halves stay intact: dropping the furniture check leaks unloaded seats; dropping the claimant/Seatedcheck strands a live cross-cell seat after its actor despawns. Entity IDs are monotonic and never recycled, so do not justify cleanup with an ID-reset premise. - All seven systems are opt-in and NOT in the default scheduler — gated by
BYRO_SANDBOX_SIT/BYRO_WANDER/BYRO_TRAVEL/BYRO_FOLLOW/BYRO_ESCORT/BYRO_GUARD/BYRO_PATROLrespectively (boot.rs). A regression that registers one unconditionally (or drops its env-var check) changes default engine behavior silently.
- One-shot terminal markers:
- Scripting transient markers (
crates/scripting/src/events.rs):ActivateEvent/HitEvent/TimerExpiredare removed byevent_cleanup_system(registeredadd_exclusive(Stage::Late, …)) — verify single-frame lifetime. - Gameplay slice (P2, added 2026-08-15/16 — no owner audit, so it is in scope
here): three
add_exclusive(Stage::Update, …)registrations inbyroredux/src/boot.rs, in this order —interaction::interaction_system, thencombat::combat_input_system, thencombat::combat_damage_system. Ordering is load-bearing:interaction_systemis the canonical producer of the action edges (ActionState/InputAction) both combat systems consume, and it must stay ahead of everyOnActivateconsumer. Check:combat_damage_systememits the canonicalHitEvent(crates/scripting/src/events.rs) and relies on the Late-stageevent_cleanup_systemabove for teardown — a combat-local cleanup would double-free the marker, and a missed Late registration leaks it. Do not report the Late-stage cleanup as combat's leak.- The alive→dead transition inserts
Deadand tears down the AI-behavior component set (SandboxBehavior/WanderBehavior/TravelBehavior/FollowBehavior/EscortBehavior/GuardBehavior/PatrolBehavior+ their*State/Seated/Traveled/Escortedsiblings). A behavior component surviving death re-animates a corpse — verify the teardown list against the live seven-procedure roster above, since it must grow with it. CombatState(aResource) holds cooldown plus aCombatTraceEntrytrace used as smoke evidence bydocs/smoke-tests/p2-melee-core.sh— unbounded trace growth across a long session is a real leak.inventory.rsmust not become a second source of truth: canonical state isInventory+EquipmentSlots(crates/core), andInventoryCatalogis a rebuilt-on-plugin-install metadata cache keyed by form id. Stale catalog entries after a load-order change are the failure mode to look for.
- ScriptTimer (
crates/scripting/src/timer.rs):timer_tick_systemdecrements per-frame, firesTimerExpiredon hit — verify no negative-time accumulation. - Animation controller (
crates/core/src/animation/controller.rs): controller vsAnimationPlayerlifecycle — no dangling clip refs after unload. - AnimationClipRegistry (
crates/core/src/animation/registry.rs): #790 dedupes by lowercased path so cell streaming doesn't grow it unboundedly — losing case-folding interning leaks one keyframe set per cell load (steady RAM growth across exterior streaming). - DebugDrainSystem (
crates/debug-server/src/system.rs): registeredadd_exclusive(Stage::Late, …)(crates/debug-server/src/lib.rs) — verify no World mutation outside the drain (per-client TCP threads enqueue commands, never mutate). - AudioWorld (
crates/audio/src/lib.rs, M44):audio_systemrunsadd_exclusive(Stage::Late, …);OneShotSoundmarkers are pruned once kira reachesPlaybackState::Stopped— verify no infinite-marker leak. Spatial sub-track handle drop must precede listener handle drop (kira invariant). - Particle emitter (NIFAL typed-block path):
byroredux/src/systems/particle.rs::apply_emitter_params(registeredadd_exclusive(Stage::PostUpdate, particle_system)) populatesParticleEmitter(crates/core/src/ecs/components/particle.rs) fromImportedEmitterParams(crates/nif/src/import/types.rs, built byextract_emitter_params/extract_emitter_rateincrates/nif/src/import/walk/mod.rsfrom the typedNiPSysEmitter/…Ctlr/…CtlrData/NiPSysGrowFadeModifierblocks incrates/nif/src/blocks/particle.rs). Pin the override semantics: authored size isinitial_radius × base_scale.unwrap_or(1.0)(Oblivion has nobase_scale) and color is NOT clobbered — seeapply_emitter_params_size_defaults_base_scale_to_oneandapply_emitter_params_overrides_kinematics_and_size_not_color. Regression: zero-sizing the emitter or overwriting the preset color. See/audit-nifal. - Character / light-anim (
byroredux/src/systems/character.rs,byroredux/src/systems/light_anim.rs):character.rsowns KCC state viabyroredux_physics::CharacterController(+RapierHandles);animate_lights_systemreadsLightFlicker(crates/core/src/ecs/components/light.rs) againstLightSource. Verify no orphanedCharacterController/LightFlickerafter a cell load/unload cycle, matching thestreaming.rsorphan invariant.
8. Hot-Path Performance Invariants (regression guards)
- Lock-tracker held-set collection is
cfg(debug_assertions)-gated (#823): theheld_others: Vecbuilt beforerecord_and_checkinlock_tracker.rs(track_read) is gated as one block — release builds skip the alloc entirely. Re-enabling for release rebuilds ~100 small allocs/frame for a no-op. NameIndex.mapin-place refill (#824):animation_system(byroredux/src/systems/animation.rs, theidx.map.clear()block) refills theHashMapin place (clear+reserve+ reinsert) instead ofnew()+swap. The fresh-map pattern costs a ~3 ms cell-stream-in spike.- Transform-propagation change detection (#825 + #1371):
make_transform_propagation_system(crates/core/src/ecs/systems.rs) keys a cachedrootsset on(Transform::len(), Parent-len-or-0, next_entity_id())AND tracksParent/Childrenstructural_generation()plus the drainedTransformdirty set. The FAST PATH skips the whole BFS when the dirty set is empty and the full state is unchanged — a static cell with a moving camera touches ~1 subtree, not all entities (~250 µs/frame regression at Megaton if recomputed every frame). Usesdrain_dirty_into(&mut transform_dirty)to keep the scratch capacity across frames (#1371), NOTtake_dirty. Any path that stops bumpingstructural_gen/mark_dirtysilently breaks this fast path (escalate — wrongGlobalTransformis a correctness bug, not just perf). animation_systemscratch hoisting (#828):events/seen_labelsscratches are hoisted out of the per-entity loop and useclone(notmem::take) so capacity persists; helpersensure_subtree_cache/write_root_motion/apply_bool_channels+ thewrite_lazy!macro (5 color-target arms) were factored out by2bdbc36— DRY-undo drift there is a finding.footstep_systemscratch (#932):byroredux/src/systems/audio.rswrites aFootstepScratch: Resourceviamem::take+ restore to preserve Vec capacity; per-frameVec::newis the regression. (Registeredadd_exclusive(Stage::PostUpdate, footstep_system).)- Poison side-table (#466):
World::despawnnames the offending component via thetype_namesside-table; removing it loses the type name in panic messages (10× harder bisects).
9. NIFAL Canonical Material in the Component Layer
The NIFAL tier resolves PBR scalars once, at the single ImportedMesh → Material
boundary, so the renderer never re-classifies per draw. The ECS-owned Material
component is the landing zone for that contract. See /audit-nifal for the
upstream boundary.
- Plain-
f32contract:Material(crates/core/src/ecs/components/material.rs) carriesmetalness: f32/roughness: f32— fully resolved, NOTOption<f32>. A regression toOption/Nonere-introduces per-draw classification (HIGH). - Single mutation site:
byroredux/src/material_translate.rs::translate_materialis the SOLEImportedMesh → Materialboundary;Material::resolve_pbr(crates/core/src/ecs/components/material.rs) is the only fill-the-gap helper (runs the sharedclassify_pbr_keyword, fills only the unset slot). No per-drawclassify_pbrfallback survives inbyroredux/src/render/static_meshes.rs. resolve_pbridempotent + preserves translator values: pinned byresolve_pbr_is_idempotent,resolve_pbr_preserves_upstream_translator_values,resolve_pbr_fills_only_missing_slot,resolve_pbr_clamps_authored_out_of_rangein thematerial.rstest module. Clobbering authored scalars or breaking idempotency is a finding.- ECS-adjacent producers: Starfield CDB output (
crates/sfmaterial/) must flow throughtranslate_material/resolve_pbr;crates/debug-ui/(egui overlay) must not register or mutate gameplay components.
10. Animation Runtime (crates/core/src/animation/, added 2026-08-13)
crates/core/src/animation/ is a byroredux-core subsystem with no owner
dimension anywhere: /audit-nif owns the NIF/KF import, /audit-nifal
Dim 7 owns the NIF→AnimationClip translation boundary, and nothing owns
what happens after — sampling, layer blending, root-motion split, text-key
dispatch. It lands here because AnimationPlayer / AnimationStack are ECS
components driven by an ECS system (byroredux/src/systems/animation.rs), and
AnimationClipRegistry is a Resource.
- Clip-handle validity:
AnimationPlayer.clip_handle/AnimationLayerindex intoAnimationClipRegistry(crates/core/src/animation/registry.rs). A stale handle after a cell unload must be a no-op, never a panic or an out-of-bounds read. Verify unload clears or invalidates players alongside the registry (same lifecycle class as Dimension 7). - Time advance:
advance_time(crates/core/src/animation/player.rs) andadvance_stack(crates/core/src/animation/stack.rs) must handledt == 0, a negative/NaNdt, and a zero-length clip without dividing by zero or looping forever. VerifyCycleType(loop / clamp / reverse) is applied per clip, not globally. - Blend weights:
AnimationLayer::effective_weight+play+cleanup_finisheddefine the crossfade. Verify weights are normalized (or documented as additive), thatcleanup_finishedcannot remove a layer still contributing weight, and that an unbounded layer stack cannot grow per frame —playon every tick with a nonzero blend time is the leak shape. sample_blended_transformis the hot path (per bone, per skinned entity, per frame). Verify it allocates nothing and short-circuits the single-layer case; cross-reference/audit-performanceDim 1 for cost, report the allocation here.- Root motion:
split_root_motion(crates/core/src/animation/root_motion.rs) separates the delta applied to the entity from the residual left on the bone. Verify the split is applied exactly once per tick and drained — an undrainedRootMotionDeltaintegrates every frame (the same failure mode/audit-scriptingDim 8 checks on the cinematic path). - Text keys:
visit_stack_text_events/collect_stack_text_events(crates/core/src/animation/stack.rs) must not emit an event twice when a clip loops across the frame boundary, and must emit it at all when a single frame spans multiple key times (a largedtafter a stall). - Interpolation (
crates/core/src/animation/interpolation.rs):find_key_pairboundary behaviour at t < first key and t > last key, plus quaternion shortest-path (a missing dot-sign flip is a bone spinning the long way). B-splines reach FNV/FO3 too — do not assume Skyrim+ (feedback_bspline_not_skyrim_only).
Process
- Read each file in
crates/core/src/ecs/(paginate the >1000-line ones:world_tests.rs,scheduler.rs,resources/mod.rs). - Run
cargo test -p byroredux-coreandcargo test -p byroredux— verify the scheduler/storage/query suites are green (test counts live in ROADMAP, not here; do not pin a number). - Check each dimension top-down (lock ordering first).
- Save report to
docs/audits/AUDIT_ECS_<TODAY>.md.
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.