Synthèse automatique
Audite le parseur binaire NIF en Rust pour vérifier sa précision octet par octet sur l'ensemble des versions de jeux Bethesda, en ciblant notamment les dérives de position de flux et la couverture des blocs.
Points forts
- Organise l'audit en six dimensions classées par risque, avec la dérive de position de flux comme priorité principale.
- Prend en charge des paramètres de focus, de jeu et de corpus, avec un filtrage par version allant d'Oblivion à Starfield.
- S'appuie sur des données de jeux réelles et des tests de corpus optionnels pour mesurer les taux de parsing.
- Référence une documentation vérifiée dans le code et la spécification nif.xml comme source d'autorité.
Limites
- Périmètre volontairement limité au parseur NIF : archives, matériaux, SpeedTree et physique sont explicitement exclus.
- Les tests de corpus sont optionnels et ne semblent pas exécutés automatiquement, donc un cargo test vert ne prouve pas la qualité du parsing.
- Dépend de fichiers de protocole partagés, de chemins de données de jeux et de références externes qui doivent être disponibles localement.
À utiliser pour auditer ou déboguer la justesse du parsing binaire NIF entre versions de jeux Bethesda, en particulier les désalignements de flux ou les lacunes de dispatch de blocs.
À ne pas utiliser pour les lecteurs d'archives, la traduction de matériaux, SpeedTree ou le comportement physique, ni en l'absence de code du parseur NIF ou de données de jeux locales.
Analyse de sécurité
PrudenceThis is a long, repo-specific audit playbook. I found no malicious payloads: no curl|sh, no deletion, no credential/secret handling, no encoded blobs, nothing that disables safety mechanisms. It does drive shell commands, cargo builds/tests, and repo writes for legitimate auditing purposes, and it defers some behavior to sibling instruction files and an absolute reference path that are not part of the submitted content, so a human should confirm those exist and are benign. Rated caution rather than safe on tool-surface grounds, not on intent.
- •No destructive, exfiltration, or obfuscation behavior: the skill only runs local developer tooling (`mkdir -p /tmp/audit/nif`, `git log`, `cargo test`, `cargo run --example nif_stats`) and writes markdown reports under `/tmp/audit/nif/` and `docs/audits/`.
- •Behavior is delegated to files not included in this submission: `.claude/commands/_audit-common.md`, `.claude/commands/_audit-severity.md`, `docs/engine/nif-parser.md`, and the absolute path `/mnt/data/src/reference/nifxml/nif.xml`. Those transitively control output format and data locations and cannot be vetted here.
- •`cargo test`/`cargo run` compile and execute code from the target repository, and the skill relies on environment variables (`BYROREDUX_<GAME>_DATA`, `BYROREDUX_REQUIRE_GAME_DATA`) pointing at installed game data — legitimate for the task but a broad execution surface with no declared `allowed-tools` allowlist.
- •Mentions `BYROREDUX_REGEN_BASELINES=1`, which rewrites checked-in test baselines; misuse could silently mask parser regressions. The text does warn to investigate deltas before regenerating, which mitigates this.
- •Instructs the agent to execute shell commands (`mkdir -p /tmp/audit/nif`, `cargo test`, `cargo run ... --example nif_stats`, `git log`) and to spawn up to 3 concurrent Task agents; all are legitimate build/audit operations but require shell/tool access in the target repo.
- •May mutate repository state: `BYROREDUX_REGEN_BASELINES=1` regenerates checked-in baseline TSVs and Phase 3 writes `docs/audits/AUDIT_NIF_<TODAY>.md`, so runs are not purely read-only.
- •Reads arbitrary user-supplied corpus paths (`--corpus <path>`) and external reference path `/mnt/data/src/reference/nifxml/nif.xml`; no validation or restriction on those paths is described.
- •References a repo-local `/audit-publish` follow-up command, delegating further action to another skill.
Exemples
/audit-nif --focus 1 --game oblivion --corpus /games/oblivion/Data/MeshesAudit the NIF parser for Skyrim, focusing on version gating and block dispatch coverage. Use the extracted meshes in /data/skyrim/meshes./audit-nif --focus 1,4 --game fnv --corpus /games/fnv/Data/Meshesdescription: "Deep audit of NIF parser — stream position, version gating, block dispatch coverage, geometry handoff" argument-hint: "--focus <dimensions> --game <fnv|fo3|skyrim|oblivion|fo4|fo76|starfield> --corpus <path>"
NIF Parser Audit
Audit the NIF binary-format parser (crates/nif/src/) for byte-accurate correctness
across the Oblivion → Starfield version span. Real game data is used when available.
The recurring failure mode is stream-position drift: a block over/under-reads its
payload, the consumed-byte count diverges from the header block_sizes entry, and either the
reconciliation masks it (parse "succeeds" with silent corruption) or — on Oblivion-era files
with no block_sizes table — every later block is misaligned and the scene truncates.
Dimensions are ordered by that risk.
Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).
Read .claude/commands/_audit-common.md and .claude/commands/_audit-severity.md for
shared protocol (layout, game-data locations, dedup, finding format). Do not duplicate them.
docs/engine/nif-parser.md is the code-verified reference (module map, version handling,
per-game coverage matrix, block coverage) — prefer it over re-deriving. The authoritative
block spec is /mnt/data/src/reference/nifxml/nif.xml (reference_nifxml); Gamebryo 2.3
source is the tiebreaker.
Not in scope: BSA/BA2/CSG archive-reader discipline and the FO4 previs header
(/audit-parsers); per-game material translation and the canonical tier (/audit-nifal);
SpeedTree .spt (/audit-speedtree); Havok→Rapier behaviour (/audit-physics — only the
constraint CInfo decode is a NIF seam).
Parameters (from $ARGUMENTS)
--focus <dimensions>: comma-separated numbers (e.g.1,2). Default: all 6.--game <name>:fnv,fo3,skyrim,oblivion,fo4,fo76,starfield. Default: all detected.--corpus <path>: directory of extracted.niffiles for bulk testing.
Extra Per-Finding Fields
- Dimension: Stream Position | Version Gating | Block Dispatch Coverage | Geometry Handoff | Collision/Shader Parsing | Allocation Hygiene
- Game Affected: which
NifVariant(s), citing thebsverband.
Phase 1: Setup
- Parse
$ARGUMENTS;mkdir -p /tmp/audit/nif; fetch the dedup baseline (_audit-common.md). - Note which game-data directories exist. Skim
docs/engine/nif-parser.md§ "Per-game NIF coverage" so findings are measured against current clean/recoverable rates. cargo test -p byroredux-nif(default lane); record the pass count.- Corpus gates are opt-in (
#[ignore], need installed data):parse_real_nifs,per_block_baselines,block_coverage_baselines,oblivion_stream_drift_corpus,constraint_drift_corpus,normal_synthesis_corpus,np_ragdoll_corpusundercrates/nif/tests/. They are wired per title in.github/workflows/real-data-gates.yml(BYROREDUX_REQUIRE_GAME_DATA=1, so an absent corpus is a failed job). Locally: one title at a time,BYROREDUX_<GAME>_DATA=<Data dir> cargo test -p byroredux-nif --test per_block_baselines <game> -- --ignored --nocapture. The workflow parses since d98769436 (#4619, closed), but nobyroredux-game-dataself-hosted runner is registered, so the lane is manual-only (workflow_dispatch; the nightly schedule was removed in fde8feec2, #5905) — no automated result exists (the corpus jobs run--no-fail-fast, fa2af0e89, #5660, so one red test binary no longer hides the ones after it). A green defaultcargo testsays nothing about parse rates; run the gate locally.
Phase 2: Dimensions
Dimension 1: Stream Position Integrity (PRIMARY)
Paths: crates/nif/src/{lib,stream,header,scene}.rs, every blocks/**/*.rs parse()
First step: git log --since=<last report> --format='%h %s' -- crates/nif/src/blocks crates/nif/src/lib.rs
Flow: parse_nif → parse_nif_with_options → dispatch_blocks (block loop, no_block_sizes
branch) with parse_header / finalize_scene as sibling phases.
Guards: the NifScene::recovered_by_guess zero-assert in the corpus walk
(tests/common, surfaced by parse_real_nifs: a size-cache/oblivion_skip_sizes skip is
inferred, not file-declared — absolute zero, not a rate; a size-cache skip that lands
inside the failed block's own consumed bytes is refused, #3926);
every_tail_capturing_block_reports_it_and_parse_nif_records_it (blocks/shader_tests/starfield.rs;
source-shape scan of blocks/shader/*.rs + blocks/node.rs: every struct declaring a
starfield_tail field overrides opaque_tail_len; a tail-capturing block in a file not in
its list is invisible to it); blocks/controller/sequence_pre_10_1_0_106_tests.rs;
tests/oblivion_stream_drift_corpus.rs (no-block_sizes detector has zero false positives;
every sizeless Oblivion NIF must land on its root footer via parse_nif_with_options_and_consumed,
eight measured stale-tail LOD files allowlisted — 6ad0c0214, #5782; extraction / parse failures
are counted and fail the sweep, 7869a93fd #5781; a present-but-unopenable Oblivion DLC archive
fails, c2d3914bf #5780).
Checklist (what the guards cannot see):
- Each block consumes exactly its
block_sizesentry. A finding is a block whose drift the reconciliation is silently absorbing, not the reconciliation itself. - Oblivion-era files (v20.0.0.4/5 and the v10.x NetImmerse family) ship no
block_sizes: one wrong field cascades. Audit those parsers for unconditional reads that assume a later-game layout; version-gated trailing fields must be guarded, never always-read. - Bool width:
read_bool(version-dependent 1 or 4 bytes) vsread_byte_bool(always 1) must match the nif.xml type per field — a phantom/missing bool is the classic 1-/3-byte drift. - An opaque tail captured to
block_sizeis invisible todrift_histogram(consumed == declared by construction). It needs its own telemetry:NiObject::opaque_tail_lenfeedingNifScene::opaque_tail_histogram(#2625). Regression = a new tail-capturing block that doesn't wire it. Also confirm capture-to-block_sizeis used, never a hardcoded length (StarfieldBSLightingShaderPropertytail is 0 B on most content — 480,757 of 483,837 blocks — with a 30 B tail on the remaining 3,080). - Pre-10.1.0.106
NiSequence/ControlledBlock/NiControllerSequence: exactly one of eachuntil="10.1.0.103"/since="10.1.0.106"pair is present for any version (a read that fires both, or drops theuntilhalf, loses text keys and binds no channel); below-bandcycle_typedefault isCYCLE_CLAMP = 2, andimport_sequence(crates/nif/src/anim/sequence.rs) collapses a non-finite/non-positive duration to0.0. NiDynamicEffectcarries two affected-nodes groups (until="4.0.0.2"andsince="10.1.0.0"); both must be read (NiLight/NiTextureEffect, no anchor on files that old).- Starfield
BSGeometryMeshData's meshlet/cull trailer isMeshTrailer::OptionalAtEndonly for a standalone.meshbody (parse_from_bytes); an inline (flag 0x200) body reads itRequired(#4269) —remaining() == 0says nothing inside a NIF stream. - Starfield
BSFaceGenNiNodehas its own type with an opaquestarfield_tail(aliasing it to plainNiNode::parseunder-reads 2 B on every instance);BSWeakReferenceNodedeclines an unfittable payload intostarfield_tailrather than overrunning. - Regression pins (verify still fixed, do not re-report): Oblivion v10.x family (#1509,
#1337, #1329, #1310/#1301/#1302 — method: extract →
trace_block→ byte-decode; memory nif_v10x_stride_drift_resolved); Starfield shader over-read (#1510: gate FO76-tail fields onbsver < STARFIELD, since#BS_F76#is stream 155 only); root-NiNode / inline-type-name truncation recovers rather than hard-Err(#688/#698). - If a corpus is available: report consumed-vs-
block_sizemismatches by block type with counts; any non-zero on a known type is drift; separately anyno_block_sizestruncation. Output:/tmp/audit/nif/dim_1.md
Dimension 2: Version Gating (highest report yield)
Paths: crates/nif/src/{version,shader_flags}.rs, all stream.bsver() / stream.version() sites in blocks/
First step: git log --since=<last report> --format='%h %s' -- crates/nif/src/version.rs crates/nif/src/shader_flags.rs, then for every parser changed since, compare each since=/until=/vercond in nif.xml with the gate written; grep -rn 'V10_1_0_106\|V10_1_0_103' crates/nif/src/blocks shows raw uses of constants a NifVersion helper already encodes.
Guard: the detect_* tests in version.rs; crates/nif/src/version_literal_tests.rs (72769b95a, #5119) fails on any bare NifVersion(0x…) literal or bare decimal bsver comparison in production src/ outside version.rs. The scan blanks each #[cfg(test)] module individually (a first-occurrence cut once hid every later production item, #5258). Nothing guards a named constant used where a NifVersion helper already encodes the gate.
Checklist:
NifVariant::detectcovers every(version, user_version, user_version_2)combination of the seven titles + the FO3-dev edge case.impl NifVariantis minimal (detect,bsver); the live named-helper surface is onNifVersion(has_mopp_offset,has_havok_strips_scale,has_object_group_id,has_skin_data_partition_ref,has_skin_data_vertex_weights_flag,has_keyframe_controller_data,has_quat_transform_trs_valid,has_interp_controller_manager_controlled,uses_old_rigid_body_layout,has_ni_sequence_prologue,has_controller_sequence_fields) — verify againstversion.rsbefore citing (it has been pruned and grown). A new parser that hardcodes a raw version literal for a feature a helper covers is the regression; a new helper needs a consumer in the same change (dead helpers were pruned). Doctrine:docs/engine/nif-parser.md"Version handling".bsverbands are named constants (OBLIVION = 11,FO3_FNV = 34,RIGID_BODY_FLAGS16 = 76,SKYRIM_LE = 83,SKYRIM_SE = 100,FALLOUT4 = 130,FO4_DLC_UPPER = 140,FO76 = 155,STARFIELD = 172, …): check operator and constant (off-by-one band membership silently corrupts a neighbouring game). FO76 (155) and Starfield (172+) differ — keep apart.shader_flags.rs: FO3/FNVBSShaderFlags+BSShaderFlags2u32 pair (fo3nv_f1/f2) vs Skyrim+ single-word storage (skyrim_slsf1/2,fo4_slsf1/2) and the FO76/Starfieldbs_shader_crc32arrays; each game reads its own storage shape. Import reads them viais_decal_from_legacy_shader_flags/is_decal_from_modern_shader_flags/is_two_sided_from_modern_shader_flags(crates/nif/src/import/material/mod.rs).- Oblivion specifics: u16 flags below
FLAGS_U32_THRESHOLD = 26; inline strings belowSTRING_TABLE_THRESHOLD;NiPSysEmitterrouted through the nif.xml gate (bsver < 26);NiTextureEffect's embeddedNiDynamicEffectbase gatedbsver < FALLOUT4. Output:/tmp/audit/nif/dim_2.md
Dimension 3: Block Dispatch Coverage
Paths: crates/nif/src/blocks/mod.rs, crates/nif/tests/{per_block,block_coverage}_baselines.rs, crates/nif/tests/data/, crates/nif/src/corpus.rs, crates/nif/src/kfm.rs
First step: cargo run -p byroredux-nif --release --example nif_stats -- <archive-or-dir> --tsv — but do not diff it directly against tests/data/per_block_baselines/<game>.tsv: the two TSVs use different keys (#4265 — nif_stats keys parsed on the dispatched Rust struct name, the baselines on the header-advertised wire name), so they legitimately disagree on alias-collapsed families. Use nif_stats --tsv for the unknown/parsed split per struct, and the per_block_baselines gate itself (--unknown-only for the short view) for wire-name comparison against the checked-in baselines
Checklist:
- Dispatch is a hand-written
match type_nameinparse_block_inner(theimpl_ni_object!macro only generates trait impls). Count top-level arms fresh — nestedmatches (type_name_staticRTTI arms,type_name_arc_hint) inflate a naivegrep -c '=>'; never quote a stale number or copy one fromdocs/engine/nif-parser.md§ "Block coverage". - From a corpus or archive listing, enumerate block types that fall through to
NiUnknown; count per game; flag any that cascade (a missing block on Oblivion truncates the scene → Dim 1). - A block that parses but is dropped downstream is a Dim 4/5 finding, not coverage.
- Baseline harness invariants: baselines key on wire RTTI via
header.block_type_indices(record_scene_blocks,tests/common/mod.rs), not the parsed struct name (several arms parse multiple wire types into one struct —BhkRigidBody.is_t,NiPSysBlock.original_type); the corpus definition is shared incorpus.rs(NIF_ENTRY_EXTENSIONSincludes.bto/.btr— renamed NIFs; a second private copy of the rule is the regression; every corpus filter routes throughcorpus::is_nif_entry, pinned by a source scan, #5259); the gates walk every mesh-bearing archive (open_all_mesh_archivesall-or-nothing,open_optional_mesh_archivespresent-only — a present-but-unopenable archive panics and an all-absent corpus fails, #5232;run_all_meshes_gatewith a per-archivelimit);parse_real_nifsasserts a per-archive clean-rate floor (min_clean, 0.995) plus recoverable 100% — truncation counts as recoverable, so the floor is what catches silent clean-rate collapse. New coverage findings should land as a baseline-test extension;BYROREDUX_REGEN_BASELINES=1regenerates after an intentional change.run_baselinecompares each TSV's owntotal=header (baseline_corpus_total) against the walked corpus before the per-type comparator, so an install that grew or shrank fails naming the corpus drift rather than reading as parser loss (#4628). The #5260 file-count pin:diagnose_corpus(tests/block_coverage_baselines.rs) compares the NIF file count against that sametotal=header, so atotal_blocksmove on a matching install reports as a parser delta (fewer / more — investigate before regenerating), and only a file-count mismatch reads as corpus drift (92ad8247d, #5438).open_present_archive(tests/common/mod.rs) owns the present-but-unopenable panic foropen_mesh_archive,open_all_mesh_archivesand the Oblivion drift corpus, which now resolves its directory throughgame_data_dir(5f414d681, #5439;open_optional_mesh_archivesalone still logs and continues, by design).nif_stats' directory mode routes throughcorpus::is_nif_entryand the #5259 scan also rejectseq_ignore_ascii_case("nif")and== / != "nif"compares (ffa85a0b4, #5440; pinned bythe_corpus_scan_knows_both_hand_rolled_spellings). Regression guards, not open gaps. kfm.rs(KFM binary catalog, v1.2.0.0–2.2.0.0, transcribed fromNiKFMTool::ReadBinary) has no engine consumer today — audit for version-gate fidelity andallocate_vecbounds only (itsread_cstringbounds thei32length against remaining bytes andMAX_SINGLE_ALLOC_BYTES, #4631); "unused" is not a finding. Output:/tmp/audit/nif/dim_3.md
Dimension 4: Geometry Extraction & Import Handoff
Paths: crates/nif/src/import/{mod,types,transform,coord,precombine,units}.rs, import/mesh/, import/walk/, import/material/{walker,dedicated_shader}.rs
First step: git log --since=<last report> --format='%h %s' -- crates/nif/src/import/mesh crates/nif/src/import/walk
This is the parse → ECS handoff; per-game material classification is /audit-nifal.
Guards: import/mesh/sse_skin_index_space_tests.rs (#[ignore], needs Skyrim SE data),
bs_tri_shape_partition_remap_tests.rs, sse_skin_geometry_reconstruction_tests.rs,
tangent_convention_tests.rs, tests/normal_synthesis_corpus.rs (ignored), import/walk/tests.rs.
Checklist:
- All
NiAVObjectfields via the.av.*sub-struct. Coordinate conversion (Z-up → Y-up,coord.rs) applied consistently to positions, normals, rotations.compose_transformsneutralises an overflowed (finite-input, non-finite-product) result through the #4549 sanitiser (#4633,compose_transforms_overflowing_products_are_neutralized). - Walker bounds:
NiNode.childrenholds rawBlockRefs never verified to form a tree, so all five scene-graph walkers (hierarchical, flat, and the lights / texture-effect / particle-emitter satellites) thread one per-walk visited bitset (new_visited) throughrevisit_guard(import/walk/mod.rs): a repeat reference is skipped and depth is capped atMAX_NIF_NODE_DEPTH(128). #1269's depth cap alone left self-reference / diamond fan-out exponential (fixed in cf8730adc, #5485; pinned by the*_walker_survives_a_self_referencefamily +walkers_survive_a_mutual_cycleinimport/walk/tests.rs). A new walker that recurses without the guard is the regression; vanilla has zero multi-parent children. - Per-game geometry path: classic
NiTriShape(Oblivion/FO3/FNV) vs Skyrim SE+ packed-halfBSTriShapevs StarfieldBSGeometry(bulk-read viaread_u16_array+ unpack; import extractor inimport/mesh/bs_geometry.rs). Each decodes its own stride and index format. - Tangents: FO4+
BSTriShapeinline whenVF_TANGENTS | VF_NORMALSset; Oblivion/FO3/FNVNiBinaryExtraData"Tangent space (binormal & tangent vectors)" — must honor the[tangents…, bitangents…]swap (the field holds ∂P/∂V, #786); Starfield UDEC3tangents_rawwith W normalized to ±1 at import (#2246); otherwise synthesis — Lengyel accumulation + Gram-Schmidt (niflyCalcTangentSpace, not MikkTSpace, d81f7501f #5963) in the single producersynthesize_tangents_yup; the Z-upsynthesize_tangentsis a wrapper that converts its inputs and calls it (89a0b6ccd, #5793 — a second accumulation copy is the regression). Distinct paths — don't cross-wire. - Skyrim SE skinning index spaces (#3355/#3360, then the 2026-09-17 correction): the
NiSkinPartition::Trianglesare already global vertex indices on theStream() == 100band (do not push them throughvertex_map); the packedBSTriShape/ SSE global-buffer bone indices already address the skin's bone list and are only widened (widen_packed_bone_indices); only the separateNiSkinPartition.bone_indiceschannel is partition-local. Regression = re-applying a partition palette to the packed channel (the deleted remap_bs_tri_shape_bone_indices behaviour) orvertex_mapto triangles.sse_recon::try_reconstruct_sse_geometryandskin::*consume the right counts. Every skin producer declines the whole weight set (bind pose, never a clamp) when any bone index >= the skin's own bone count: StarfieldBSGeometryinconvert_bs_geometry_skin_weights(#4268, 0abc86c8b), and theBSTriShapeinline + SSE global-buffer packed channel and classicNiSkinDataviadecline_unbounded_packed_indices(import/mesh/skin.rs; fixed in cf9dec258, #5389 — #4268's "siblings already bound" premise was false). The palette bound inrender/skinned.rsis a producer-upheld invariant, so a new producer without the decline is the regression. #5524 (fixed in 656e2bdc3) gave the FO4BsSkinInstancearm its decline +bone_count - 1boundary tests (mesh/bs_tri_shape_partition_remap_tests.rs), made the decline warning arm-neutral, and restoredwiden_packed_bone_indices' doc comment. - Particle emitters:
NiPSysEmitter/NiPSysEmitterCtlr/NiPSysEmitterCtlrData/NiPSysGrowFadeModifierare typed (blocks/particle.rs); params flowextract_emitter_params/extract_emitter_rate(import/walk/emitter.rs) →apply_emitter_params(byroredux/src/systems/particle.rs, overrides kinematics + size, not colour). The rate walk is scoped to the system's own controller chain; when a siblingNiPSys*controller parses to the opaqueNiPSysBlockmarker (which discardsnext_controller_ref) the fallback resolves bybase.target_refto the ctlr targeting the system whosecontroller_refequals the chain head — per-instance exact, never a whole-scene first-match, and gated on a non-NULL chain head (#4467; opt-in gatereal_archive_torch_meshes_surface_particle_emitters). The legacy tier follows the ctlr's ownNiPSysEmitterCtlr.data_ref(until 10.1.0.103, #4560); an unlinked data block is attributed to nobody. The geometry APP_CULLED + live-NiVisControllergate lives once inshape_is_culled(node arms:node_is_culled; ef78a0eec, #5792, source-count guard inimport/tests/app_culled_visibility.rs— a per-arm copy is the regression). Both walkers skip APP_CULLED / editor-marker particle systems (particle_system_is_culled, #4561) unless aNiVisController— found on the chain or bytarget_ref— un-hides them. Regression = a hardcoded preset replacing an authored birth rate, or the fallback claiming another system's ctlr. - Starfield units (
import/units.rs):bsver >= FO76_STARFIELD_BOUNDARYcontent is authored in meters and scaled once to 70 units/m (length_scale) across meshes, hierarchy, emitters, force fields and animation;BSGeometryvertices also divide outHAVOK_SCALE(69.969). Rotations, weights and authored scales stay dimensionless; pre-Starfield imports are bit-identical (import/tests/units.rs). A second scale site downstream is the regression. - FO4 precombined geometry (
import/precombine.rs, M49) reusesdecode_bs_vertex_streamwithfull_precision = false(PSG positions are half even when the descriptor sets full-precision); container read isbyroredux_bsa::CsgArchive(/audit-parsers); specdocs/engine/fo4-csg-format.md. - FO4 model-space normals + alpha-test:
dedicated_shader.rsORsModel_Space_Normals(F4SF1 bit 12, or theMODELSPACENORMALSCRC forbsver >= 132) andAlpha_Test(F4SF2 bit 25) intoMaterialInfo; parsed-but-dropped bits render object-space normals as tangent-space. The NIF flag ranks below the later BGSM merge. Output:/tmp/audit/nif/dim_4.md
Dimension 5: Collision & Shader Block Parsing
Paths: crates/nif/src/blocks/collision/, crates/nif/src/blocks/shader/, crates/nif/src/import/collision/
First step: cargo test -p byroredux-nif -- dispatch_coverage_tests bhk_ hk_packed
Guards: import::collision::dispatch_coverage_tests::every_dispatched_bhk_shape_has_resolve_arm
(a new bhk*Shape dispatch arm needs a resolve_shape_inner downcast_ref arm — else it
parses then silently drops collision; nif_shape_dispatch_resolve_parity; the scan reads only the
resolve_shape_inner body, #5573; a body's per-leaf Havok surface materials are pushed by
resolve_shape_into in the same walk as the shape, #5550, and a packed-strips table is chained
from the data block's sub_parts, #5541);
import::collision::dispatch_tests::havok_motion_type_maps_full_enum;
blocks/collision/hk_packed_ni_tri_strips_data_tests.rs;
tests/constraint_drift_corpus.rs (opt-in; decoded_constraints_consume_their_whole_block —
drift must be exactly 0 on Oblivion, FO3, FNV, Skyrim LE and SE);
tests/np_ragdoll_corpus.rs (opt-in FO4: every ragdoll blob decodes with consistent joint
pivots, every body's shape volume × density equals its mass).
Checklist:
bhk*field-for-field: rigid-body flag width (uses_old_rigid_body_layout,RIGID_BODY_FLAGS16 = 76,RIGID_BODY_EXTRA_FLOATS = 9); MOPP offset / strips scale gated (has_mopp_offset,has_havok_strips_scale); FO3+hkSubPartDatais decoded (filter + material), neverskip(12).bhkRigidBodytakes the CInfo2014 layout atbsver >= FALLOUT4, a deliberate widening of nif.xml's== 130(which sends FO76/Starfield to CInfo2010); no vanilla content decides it (0bhkRigidBodyon FO4/FO76/Starfield) — absver > 130body in the wild is the evidence to re-check (#4627). The PHYSAL per-game seam is only the constraint CInfo decode.- Constraint CInfo: typed decoders exist for hinge (into
LimitedHingeCInfo), limited hinge (the Oblivion layout authors noPerp Axis In B1; it is derived asPerpB2 × AxisBfrom nif.xml's own identity, never zeroed — 526cb07d9, #5537, pinned byoblivion_limited_hinge_derives_perp_b1_from_perp_b2_cross_axis_b), prismatic, ragdoll, ball-and-socket, stiff-spring and the ball-socket chain (BhkConstraintData;BhkBreakableConstraintand malleable wrappers decode the inner CInfo).bhkGenericConstrainthas no dispatch arm (nif.xml gives only its name), so it lands asNiUnknownwith an exactblock_sizeskip. There is no drift-suppression list any more (#4626 removed is_havok_constraint_stub / stubbed_drift_histogram); any constraint drift lands in the realdrift_histogram— suppression is the mechanism that hidbhkHingeConstraint's +128, so do not reintroduce it. The trailingbhkConstraintMotorCInfo(type byte + 0/25/18/17 B) of FO3+ Ragdoll / LimitedHinge / Prismatic is now consumed — in the bare dispatch and the Malleable wrapper (motor, thenStrengthf32), leniently, via the oneBhkBreakableConstraint::consume_motortable; the breakable wrapper consumes it exactly once (a071ed217, #5960). The old motor-tail allowlist (is_known_constraint_motor_tail_drift) is deleted — any non-zero constraint drift is a finding. Decoded is not imported: the ragdoll importer (import/collision/ragdoll.rs) still declines ball-and-socket / spring / chain until a canonical joint kind exists (docs/engine/physal.md). BSLightingShaderProperty::parseis a thinbsverdispatcher →parse_skyrim(83–129) /parse_fo4(130–154) /parse_fo76_plus(≥155); each reads only its own field set; per-shader_typetrailing count (0–7) matches nif.xml (parse_shader_type_data_fo4handles only the FO4-specific types and delegates every "same as Skyrim" arm toparse_shader_type_data, b99db0f11 #5737 — a duplicated arm is the regression). Starfield capturesstarfield_tailtoblock_size. The material-reference stub gate is!name.is_empty()forbsver >= STARFIELD(stubs are.matnames; the only suffix-less ones are the bareMaterials\directory, not content hashes — #4439) but the suffix-awareis_material_referencefor FO76 (155..171 — the stopcond isbsver >= FO76, not the 152FO76_SF2_CRCSshader-flag threshold; theeffect.rscomment was corrected in 2be7c7c0b, #5082);BSEffectShaderPropertyandBSLightingShaderProperty::parse_fo76_plusmust stay in lockstep (parse_bs_effect_starfield_suffixless_name_stubs).- Starfield shader-type translation is keyed at the parser boundary
(
parse_with_sizeroutes everybsver >= 155throughparse_fo76_plus), not the slot-table layout tag;normalize_shader_typemasks types 4/5 — a Starfield FaceTint (3) must not reach the slot table as Skyrim Parallax (#3364). BhkNPCollisionObjectis "approximated", not "decoded": the FO4/FO76/StarfieldBhkSystemBinaryblob does not resolve to aCollisionShape.blocks::collision::havok_packfile::parse_havok_packfiledecodes the container (header, sections, three fixup tables → typed object graph); what stays opaque ishknpCompressedMeshShapeData's bit-packed layout (docs/engine/physal.md). Do not report it as decoded. The FO4 skeleton ragdoll blob is decoded (blocks/collision/np_ragdoll.rs,decode_np_ragdoll:hknpRagdollDatacapsule / sphere /hknpConvexPolytopeShape/hknpDynamicCompoundShapebodies, ragdoll + limited-hinge joints,hknpBreakableConstraintDataunwrapped — e1bd40bcf, #5015 (closed); fc4a15104, #6067 (open: 64 joints whose bind-pose pivots disagree are only characterised)) into the classicRagdollCInfo/LimitedHingeCInforecords; its offsets were recovered from the corpus (no SDK) and are pinned by independent physical checks, so a layout claim needs a corpus measurement, not nif.xml.summarize_collision_authoringscans every collision block and feedsCachedNifImport.collision_authoring(byroredux/src/cell_loader/nif_import_registry.rs);examine_collision_kindkeepsNone / Classic / NewPhysicsStub / Phantom / Unrecogniseddistinct (a Skyrim+ phantom must not be force-translated as a rigid body). A per-game detail (rawbsver, block-type string) escapingimport/collision/is a finding → cross-link/audit-nifal.hkMotionTypebyte → canonicalMotionType(havok_motion_type):1..=5 | 8Dynamic,6Keyframed,7Static,9CharacterKinematic, else Static; the zero-mass "Dynamic" reclassification lives beside it — see/audit-physics, do not re-derive. Output:/tmp/audit/nif/dim_5.md
Dimension 6: Allocation Hygiene (PERF)
Paths: crates/nif/src/stream.rs, blocks/**/*.rs callers, crates/nif/src/anim/bspline.rs, crates/nif/tests/heap_allocation_bounds*.rs, byroredux/src/streaming/pre_parse.rs (pre_parse_cell)
First step: cargo test -p byroredux-nif --features dhat-heap --test heap_allocation_bounds (CI job nif-heap-allocation-bounds runs the three heap files, each its own process)
Guards: the dhat-gated heap_allocation_bounds.rs (a harness = false sequential main since
#5050 — libtest's own threads polluted its exact total_blocks == 1 pin; single node, FO4 packed vertices,
SSE geometry+particle, skin blocks), heap_allocation_bounds_geometry.rs,
heap_allocation_bounds_import.rs; stream.rs unit tests
(allocate_vec_sized_*, allocate_vec_min_bytes_uses_the_supplied_minimum_not_size_of);
blocks/tri_shape_tangent_presize_tests.rs + the SSE-recon sibling (tangent Vec pre-sized
exactly for a VF_TANGENTS | VF_NORMALS descriptor and not otherwise, both packed decoders, #4617).
Checklist:
allocate_vec/allocate_vec_sized/allocate_vec_min_bytesare#[must_use]; a call that only bound-checks and drops the Vec is a no-op. Bulk arrays go throughread_pod_vec<T>(single allocation); allocate-then-loop-fill is the regression.read_pod_vecis bounded by the crate-localunsafe trait AnyBitPatternwith an explicit per-type impl list (stream.rs;bytemuckis not a dependency — check before reporting it) and a big-endiancompile_error!gate.allocate_vec_min_bytes's minimum must be the smallest legitimate on-disk encoding of one emitted element, notsize_ofof the in-memory type (#3918): a generated element (e.g. de-stripped triangles, ≥ 2 B each on disk, 6 B in memory) with asize_of-derived bound rejects valid data near EOF and fails silently downstream (a demotedNiUnknownNiSkinPartitionun-hides skin partitions → NPCs render bare skin through armor). Known siblings over-demand by 12–18% (InterpBlendItem20 vs 17,NiAgdDataStream28 vs 25).- Per-block loop counters use the
entry().get_mut() / insertsplit, notor_insert(name.to_string()); block names are internedArc<str>;ragdoll.rsusesallocate_vec(not the oldcheck_alloc). pre_parse_cell: each stream-pool task extracts (archive read + inflate) and parses its own input, admitted first againstSTREAM_PARSE_INPUT_BYTES(64 MiB) by the archive-declared size (mesh_declared_size) so nothing is allocated before admission; cells with < 8 fresh inputs keep a serial fast path. A coordinator-side extract-all barrier returning is the regression (its doc comment was corrected to the per-task extract in 3deda6bb7, #5063). Regression guard: #1171's compile-timePartialNifImport: Sendassertion (restored beside the struct inbyroredux/src/streaming/mod.rs, 7de61c2c2, #5442, after the #5092 split dropped it).NifStreamcaps any single file-driven allocation (256 MB); confirm new readers route through the capped helpers, not rawvec![0; n].- B-spline resampling (
anim/bspline.rs) is import-side allocation the stream cap never sees: each interpolator span is intersected with the owningNiControllerSequence's span, capped per channel atBSPLINE_MAX_SAMPLES_PER_CHANNEL(108 k = 30 Hz × 1 h), and charged against oneBsplineSamplingbudget (BSPLINE_IMPORT_KEY_BUDGET, 1 M keys) perimport_kfpass (import_embedded_animationsgets its own, with no sequence span); a spent budget takes the static-pose fallback with a warn, the degraded channels counted and logged once per pass (BsplineSampling::degraded/log_degraded, a076ca7fb #5577) (fixed in 0c4d4af08, #5486; pins inanim/tests/bspline.rs). The sequence clamp only decides which times are emitted:BsplineWindow(reserve()) keeps the interpolator's own span as theudomain (6fef6bd57, #5572), and only a finite, positive-width sequence span clamps (usable_sequence_span, 356a74b0f, #5575 — the pre-10.1.0.106f32::MAX/f32::MINdefaults must not). Sampling from the interpolator's ownstop_timeunclamped, or a per-channelBsplineSampling::new(), is the regression — the allocation abort it allows is not caught byparse_one_nif'scatch_unwind. Output:/tmp/audit/nif/dim_6.md
Phase 3: Merge
- Read all
/tmp/audit/nif/dim_*.md; combine intodocs/audits/AUDIT_NIF_<TODAY>.md:- Executive Summary — clean/recoverable rate per game (vs the
nif-parser.mdmatrix), stream-position mismatches, critical coverage gaps. - Block Type Coverage Matrix — block types × games (parsed / skipped / NiUnknown).
- Findings — by severity (
_audit-severity.mdNIF rows: hard parse failure = HIGH; a mismatch theblock_sizereconciliation covers = MEDIUM). - Prioritized Fix Order — rendering-blocking blocks, then animation, then collision.
- Executive Summary — clean/recoverable rate per game (vs the
- Remove cross-dimension duplicates; material translation →
/audit-nifal.
Suggest: /audit-publish docs/audits/AUDIT_NIF_<TODAY>.md
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/nif-parser-audit-mvhm5lzl/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/nif-parser-audit-mvhm5lzl' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/nif-parser-audit-mvhm5lzl/download' --output '.skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md.part' && mv '.skill-downloads/nif-parser-audit-mvhm5lzl/SKILL.md.part' '.skill-downloads/nif-parser-audit-mvhm5lzl/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/nif-parser-audit-mvhm5lzl/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.