matiaszanolli

Fallout 4 Compatibility Audit

"Per-game audit of Fallout 4 compatibility — BA2, half-float verts, BGSM materials, M49 precombines (CSG)"

matiaszanolli 28 3 Updated 1w ago
GitHub

Install

npx skillscat add matiaszanolli/byroredux/claude-commands-audit-fo4

Install via the SkillsCat registry.

SKILL.md

Fallout 4 Compatibility Audit

Deep audit of ByroRedux readiness for Fallout 4 content.

Architecture: Orchestrator. Each dimension runs as a Task agent (max 3 concurrent).

Read .claude/commands/_audit-common.md (layout, game data, dedup, finding format) and .claude/commands/_audit-severity.md. This file carries only FO4-specific context.

Scope: this skill owns FO4's data through the shared mechanisms. Defects in the mechanism itself go to the owner: BA2/CSG/BGSM/.uvd reader discipline → /audit-parsers; NIF block parsing → /audit-nif; ESM walker → /audit-esm; canonical-material invariants → /audit-nifal; Scaleform HUD (--hud, byroredux/src/scaleform_hud.rs, docs/smoke-tests/m48-7-fo4-hud.sh, AVM2/BGSCodeObj) → /audit-ui; runtime telemetry (.claude/audit-baselines/runtime/fo4-InstituteBioScience.tsv) → /audit-runtime — run it after any precombine / SCOL / spawn change (#5484's skip-set fix, 777351ec1, moved its draw / entity / BLAS rows; that recapture is still owed).

Everything below is shipped — audit as regression guard, never re-propose as pending: NIF BSVER 130+ incl. Next-Gen, BA2 v1/v7/v8, the BGSM/BGEM parser, SCOL/PKIN expansion, M49 CSG precombines, BSConnectPoint attach graph.

Game Context

Aspect State
NIF BSVER 130 + Next-Gen patches (FALLOUT4..FO4_DLC_UPPER = 130..=139 carries the DLC trailing fields, crates/nif/src/version.rs)
Shader flags typed u32 pair at BSVER 130 only; BSVER 131 (FO4_SHADER_GAP) has neither; BSVER ≥ 132 uses CRC arrays. Guard: bsver_shader_flag_band_tests (version.rs), gates carries_typed_shader_flags / carries_crc_shader_flags
Materials PBR params live in external .bgsm/.bgem; the NIF carries a path stub. crates/bgsm/ parses; merge_external_material folds into ImportedMesh.material before translate_material
Precombines crates/bsa/src/csg.rs → crates/nif/src/import/precombine.rs → byroredux/src/cell_loader/precombined.rs; per-REFR skip set = the CELL XCRI pair tail, never XPRI (#5484). Spec docs/engine/fo4-csg-format.md
Data /mnt/data/SteamLibrary/steamapps/common/Fallout 4/Data/; bench --cell MedTekResearch01 (ROADMAP)

Parameters / Setup

--focus <dimensions> (comma-separated, default all 5). Setup: parse $ARGUMENTS; mkdir -p /tmp/audit/fo4; gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/issues.json; confirm Fallout 4/Data/ exists (else note dimensions losing real-data validation).

Dimensions

Dimension 1: M49 Precombined Geometry — Decode + Spawn (highest FO4 risk)

Subagent: general-purpose
Paths: crates/nif/src/import/precombine.rs, byroredux/src/cell_loader/precombined.rs, byroredux/src/cell_loader/load.rs + exterior.rs + partial.rs, byroredux/src/cell_loader/spawn/{mesh_instance,precombined}.rs, byroredux/src/streaming/pre_parse.rs (worker pre-parse), crates/plugin/src/esm/cell/helpers.rs (XCRI decode), crates/bsa/src/csg.rs, crates/bsa/src/uvd.rs
First step: git log --since=<last report> --format='%h %cs %s' -- crates/nif/src/import/precombine.rs byroredux/src/cell_loader/precombined.rs crates/bsa/src/csg.rs
Guards (confirm live, not #[ignore]d/vacuous): precombine.rs tests decode_keeps_every_lod_band + lod_bands_skip_empty_bands_and_bound_the_read (#4234); csg.rs oversized_read_len_is_rejected_before_it_is_reserved (#3758 — read_psg must bound its length before Vec::with_capacity; reader discipline is /audit-parsers); precombined.rs tests resolve_precombine_owner_follows_form_id_mod_index, csg_paths_index_every_plugin_in_the_load_order, dedup_requires_both_a_cache_key_and_a_matching_length (#3510), oc_nif_paths_are_already_canonical_cache_keys; finish_partial_tests.rs::finish_partial_import_builds_precombine_geometry_entry; blend restore on both routes (#5101): precombined.rs::merge_precombine_materials_restores_blend_but_keeps_bgsm_flags + finish_partial_tests.rs::finish_partial_import_precombine_route_applies_the_material_merge; zero origin (#5228): exterior_apply_advances_precombine_at_zero_origin; #[ignore]d real-data dlc_rebake_of_a_master_owned_cell_decodes_from_the_dlc_csg + exterior_precombine_instances_are_world_absolute. The runtime baseline is an interior, so no telemetry gate sees exterior placement.
Checklist (what the guards cannot see):

  • Stride / transform — psg_vertex_stride(vertex_desc) vs the BSPackedGeomObject layout (off-by-N shifts every vertex silently; cross-check the spec doc). into_imported_mesh applies Z-up→Y-up + instance transform once. cell_origin must be Vec3::ZERO on both routes (#5228, fixed f3e1bba62): exterior _oc.nif instance transforms are world-absolute (same frame as the exterior REFR DATA positions), so adding cell_grid_to_world_yup places every tile at ≈2× its world position — the "else precombines stack at origin" rationale was the disproved #1222 premise.
  • LOD bands (#4234) — the three bands are disjoint sub-meshes whose union is the model (12,096/12,096 BSMeshLODTriShape); every populated band is decoded. Picking one band deletes geometry (12.45% of MeshesExtra.ba2's baked triangles) and is not z-fighting. Distance LOD may drop tail bands, never pick one. An empty band's offset is untrusted.
  • Which CSG blob — the _oc.nif filename keys off the cell's owning plugin (form-id mod byte; DLC bakes under <plugin>.esm\), but the *geometry blob* is chosen by each object's BSPackedGeomObject::filename_hash → csg_paths_by_name_hash (#2369: DLCs re-bake ~460 master-owned cells into their own blob). A blob no loaded plugin answers to, or a missing CSG, must fail closed to per-REFR (0 spawns), never read another plugin's PSG space or panic.
  • REFR de-dup gate (#1188 / #5484, fixed 777351ec1) — the skip set is the XCRI pair tail, (remapped REFR form id, bake hash) pairs on CellData::absorbed_ref_bakes (crates/plugin/src/esm/cell/helpers.rs); XPRI is the disjoint previs participant list, validated and never stored. Pre-fix the two were inverted (every baked STAT/SCOL drawn twice, ~9.9k XPRI statics deleted); census 959,238 XCRI refs, 100% STAT/SCOL, XCRI ∩ XPRI = 0 — which also settles #2699's furniture question. effective_absorbed_refs(bakes, baked_hashes) (shared by interior load.rs and exterior exterior.rs) suppresses a REFR only when its own bake drew geometry, so one missing _oc.nif un-skips only its refs (no holes, no double-draw with SCOL expansion). Guards: effective_absorbed_refs_un_skips_only_the_missing_bakes_refs; parse_cell_xcri_pairs_remap_and_keep_their_bake_hash + parse_wrld_exterior_cell_captures_precombined_xcri_xpri (XPRI populates nothing) in crates/plugin/src/esm/cell/tests/.
  • Texturing — each mesh takes its material from the owning shape's shader/alpha properties (collect_precombine_geom_refs + precombine_material_from_shape), then the BGSM merge; no REFR overlay on this path (pre_merge_materials empty by design, #4290). Regression = untextured/checker precombines.
  • Opaque-architecture blend guard — on this path only, merge_precombine_materials (called by both PrecombinedSpawnJob and the streaming drain in partial.rs) keeps the BGSM merge's two_sided/decal/alpha_test/texture flags but restores the pre-merge alpha-blend triple (has_alpha, src/dst_blend_mode): FO4 authors the "Standard" blend identically on lab glass and opaque Institute metal, so forwarding the merged has_alpha makes walls MATERIAL_KIND_GLASS (see-through, mirror-hazy).
  • Archive chain precedence (#3637) — --bsa / --materials-ba2 chains resolve last-listed-wins (a DLC's re-baked _oc.nif must not be shadowed; 1,871 DLC mesh entries were shadowed pre-fix per the #3637 title). Provider rebuilds now share opened handles (#5288, 5402b2530: Archive::open_shared, a process-wide registry validated by file length + mtime) and skip the shadow-count walk for a reused handle — precedence is unchanged, only that diagnostic count is. Guard: byroredux/src/asset_provider/tests/archive_precedence.rs (real-data, #[ignore]d — run it).
  • Upload once per object (#3510) — CachedNifImport::geometry_dedup maps every instance to its object's representative; keying the cache on the flattened sub-mesh index reintroduces one upload + BLAS per instance (1.37x VRAM, 4.6x worst tile).
  • Exterior streaming — PrecombinedSpawnJob yields between hashes / meshes / BLAS batches, and PrecombinedPlacement (spawn/precombined.rs) between geometry groups, keeping the placement root + submesh numbering across yields; the synchronous spawn_precombined_meshes wrapper must stay behaviourally identical. The cell-stream worker pre-decodes each cell's precombine_oc_nif_paths via decode_precombine_csg (per-plugin CsgHandleCache) and the drain caches a geometry_only_cached entry under the same key so the job only spawns — the two routes must agree on owner resolution, cache key, fail-closed fallback and the blend restore (divergence = different spawn counts per route). Regression guard (#5522, fixed 2ca9bc313): lookup_precombine_cache separates Hit / KnownFailure / Unseen, so the worker's negative cache entry (Some(None): missing file, parse error, contained panic) is a terminal miss, never a main-thread re-parse outside catch_unwind; pinned by negative_registry_entry_is_a_known_failure_not_unseen.
  • .uvd previs — envelope only (parse_uvd_header, incl. exterior_cell_grid; PVS payload uncracked, no occlusion consumer, #3810). Verify nothing pretends to consume the payload.
    Output: /tmp/audit/fo4/dim_1.md

Dimension 2: BGSM / BGEM → ImportedMaterial (merge + roles)

Subagent: general-purpose
Paths: crates/bgsm/src/, byroredux/src/asset_provider/material/{merge,mod,provider}.rs, byroredux/src/cell_loader/spawn/mesh_instance.rs (REFR overlay), byroredux/src/asset_provider/tests/{bgsm_merge,material_flags}.rs
First step: git log --since=<last report> --format='%h %cs %s' -- crates/bgsm byroredux/src/asset_provider/material
Guards: bgsm_merge.rs (bgsm_merge_forwards_alpha_blend_mode, bgsm_blend_to_gamebryo_is_identity_narrowing, the neutral-roughness pair, bgem_glass_forwards_authored_optics_and_overlay_roles); material_flags.rs::bgsm_metalness_*; crates/bgsm/src/template.rs resolve_breaks_*_cycle (#1148). Parser discipline for crates/bgsm is /audit-parsers.
Checklist:

  • Narrowed signature — merge_external_material takes &mut ImportedMaterial, never &mut ImportedMesh, and is the only exported fn in merge.rs (test merge_external_material_is_the_only_exported_fn_in_this_file); a widened signature is a NIFAL boundary violation. It runs before translate_material.
  • Roles, not slots — BGSM texture paths and REFR/TXST overlays land in MaterialTextureSet (22 named roles + decals: [T; 4], crates/nif/src/import/types.rs); FO4 wire-slot numbers must not survive past the import boundary. Overlay output is carried as named roles, not wire slots (#4434). Texture-set slot 7 is the _s.dds smooth-spec map and routes to the smooth-spec role, not Specular (#4424: the BC5 map fed to the specular-colour multiply tinted every FO4 highlight warm); the gloss-sampler channel semantics are still an open question — do not guess.
  • Metalness from saturation, not luminance (#1476) — legacy spec-glossiness BGSMs (pbr == false): bgsm_metalness = (max-min)/max of specular_color (white spec → 0). Luminance for the non-PBR branch makes vanilla concrete read chrome; luminance is correct only for pbr == true.
  • Authored enable bits gate their blocks — specular_enabled == false zeroes specular and derives no metalness / conductor tint in either branch (#4654, #4836; bgsm_specular_disabled_*); the envmap fill requires the chain's environment_mapping bit (#4428; *_skips_envmap_fill_when_env_mapping_disabled); a dead NIF texture path yields to a resolvable BGSM-chain path (#4636; dead_nif_texture_path_yields_to_resolvable_bgsm_chain_path). Reading a disabled block's values is the regression.
  • smoothness→roughness exactly once at the merge ((1 - smoothness) clamped to the near-mirror floor); flag any downstream re-inversion.
  • Neutral-roughness fallback — smoothness == 1.0 with no resolvable gloss map → neutral 0.5 (NEAR_MIRROR_NEUTRAL_ROUGHNESS): #3639 at the merge (authored-nowhere; fires on the specular_enabled arm only — a spec-off BGSM keeps the NIF-side roughness, #5012 / e80f7e854; must not fire when a template parent supplies smooth_spec — pinned by bgsm_merge_keeps_the_floor_when_the_gloss_map_comes_from_the_template_parent, #5234 / 460c55a41), #3905 at spawn via material_translate::resolve_unresolved_gloss_neutral_roughness (authored-but-unresolvable; gated on the shader's glossMapIndex != 0u). The arms cover disjoint populations — collapsing them regresses. Regression guard (#5230, fixed 9f0a8a7cc): the spawn pass infers "hit the floor" from roughness <= 0.04, which the glass classifier's forced mirror pane (0.04, metalness 1.0) also hits; classify_glass_into_material_with_provenance now reports the forced mirror out through translate_material_with_provenance and resolve_unresolved_gloss_neutral_roughness skips it (a_forced_mirror_pane_is_exempt_even_sitting_at_the_floor, a_bgsm_mirror_pane_keeps_its_mirror_through_the_spawn_pass_gate in material_translate.rs). Both spawn sites must feed the provenance flag.
  • Blend factors pass through (#1823) — BGSM/BGEM src_blend/dst_blend are already Gamebryo-native (Standard (6,7), Additive (6,0), Multiplicative (4,1)); any 0↔1 swap (or a gl_to_gamebryo_blend) corrupts Additive/Multiplicative while leaving Standard untouched.
  • BGEM merge — merge_bgem_arm deliberately leaves metalness/roughness as NaN so resolve_pbr's classifier runs; effect data merges into the NIF payload (#4425). Glass is glass_enabled-driven and must not misclassify opaque architecture with a stuck flag.
  • Lookup — .bgsm/.bgem/.mat dispatch on content magic, not extension (#758); last-listed-wins across --materials-ba2 (#3637); template resolve is cycle-aware and depth-limited (DEPTH_LIMIT = 16).
    Output: /tmp/audit/fo4/dim_2.md

Dimension 3: NIF BSVER 130 Geometry, Shader Flags, Collision (FO4 slice)

Subagent: legacy-specialist
Paths: crates/nif/src/blocks/tri_shape/bs_tri_shape.rs, crates/nif/src/import/mesh/{bs_tri_shape,bs_geometry,skin}.rs, crates/nif/src/blocks/shader/lighting.rs, crates/nif/src/import/material/dedicated_shader.rs, crates/nif/src/import/collision/shape.rs
First step: git log --since=<last report> --format='%h %cs %s' -- crates/nif/src/import/material crates/nif/src/blocks/shader
Guards: fo4_shader_flag_tests.rs, alpha_flag_tests.rs (crates/nif/src/import/material/); bsver_shader_flag_band_tests; parse_rate_fo4_all_meshes (Dim 5). Parser mechanics are /audit-nif; the canonical handoff (single translate_material boundary, resolve-once metalness/roughness, glass classified after resolve_pbr) is /audit-nifal — file there.
Checklist:

  • VF_FULL_PRECISION default-half unless set; binary16 decode incl. denormals/NaN. BSSubIndexTriShape segments walked (actors lean on it); skinned indices/weights honour the packed layout. Regression guard (#5389, fixed cf9dec258): a per-vertex bone index >= the skin's own bone count declines the whole weight set (bind pose), never clamps — decline_unbounded_packed_indices (skin.rs) covers the inline arrays, the SSE global-buffer payload and classic NiSkinData; pinned by bs_tri_shape_skin_with_out_of_range_bone_index_declines / bs_tri_shape_skin_last_bone_index_is_in_range (bs_tri_shape_partition_remap_tests.rs). Vanilla census: 0 out-of-range over 23,099 FO4 skinned meshes (largest list 141/144). The FO4 BsSkinInstance arm is pinned too (#5524, fixed 656e2bdc3): bs_skin_instance_out_of_range_bone_index_declines / bs_skin_instance_last_bone_index_is_in_range, and the decline warning is arm-neutral. Inline tangents when VF_TANGENTS | VF_NORMALS (shared with Skyrim SE) — consolidating with the NiBinaryExtraData path is a regression of #795/#796.
  • Render-affecting flags consumed, not just parsed (#1592) — dedicated_shader.rs ORs Model_Space_Normals (F4SF1 bit 12) and Alpha_Test (F4SF2 bit 25) into MaterialInfo, gated by carries_typed_shader_flags (BSVER 130 only). Glow_Map (F4SF2 bit 6) is not gated: the (Fallout4, 2) arm in slot_role.rs routes slot 2 to Emissive regardless — closed docs-only (#4430, 1a12670ae; routing unchanged on purpose — census: gating would strip 116 genuine _g.dds glow maps, not gating masks 20 with a _d.dds; the BGSM glowmap bool is parsed, never read). The rule still needs an FO4 source; do not re-file or guess. These are lower priority than the BGSM merge (vanilla is BGSM-backed; only inline/loose/modded NIFs exercise them).
  • Skin/Hair tint BSLightingShaderType variants sample different slots than the default path — the per-type map must match what triangle.frag reads (#563). A BGSM material must never fall back to Lambert (FO4's inverse of the FNV regression); bare BSShaderPropertyBaseOnly stubs route through MaterialInfo (#1244).
  • Parsed-then-dropped is FO4's recurring finding class (texture_clamp_mode on 7.9% of lit materials #3507; the greyscale→palette enable bit): every decoded FO4 shader/BGSM field must reach an ImportedMaterial sink and the canonical Material, with one default across the three material tiers (#3515). The palette row ("Color Remapping Index") has two carriers (#5497): the precombine per-instance grayscale_to_palette_scale is stamped by apply_instance_palette_row and restored across the BGSM merge in merge_precombine_materials (guards merge_precombine_materials_keeps_the_instance_palette_row, apply_instance_palette_row_stamps_finite_rows_only); the REFR-side MSWP CNAM (color_remap_index in mswp.rs) is applied as the palette row for a swapped placement, stamped on the raw-tier ImportedMaterial before translate_material (0b2ffed89, #5497).
  • Collision — BhkMultiSphereShape and BhkConvexListShape translate to CollisionShape in import/collision/shape.rs (two downcast_ref arms); an arm reverting to "unsupported + drop" removes power-armor / settlement / debris collision. Packed-Havok (BhkSystemBinary) collision is blocked on the blob decode (PHYSAL, /audit-physics); FO4 architecture uses synthesized ghost trimesh colliders. The precombine collision sibling is <cell_formid:08x>_physics.nif (not _precomb.nif, which appears in no archive) — forward scope.
    Output: /tmp/audit/fo4/dim_3.md

Dimension 4: ESM Architecture Records + Cell Expansion

Scope split with /audit-esm: it owns the parser as a parser (GRUP walk, SubReader accounting, FormID remap); this dimension owns FO4's data through it. Shared-mechanism defects → /audit-esm.
Subagent: general-purpose
Paths: crates/plugin/src/esm/records/{scol,movs,pkin,mswp,parse}.rs, crates/plugin/src/esm/records/actor/npc.rs, crates/plugin/src/equip.rs, crates/plugin/src/esm/cell/{mod,support,walkers,wrld}.rs, byroredux/src/cell_loader/{refr,references/,spawn.rs,nif_import_registry.rs}
First step: git log --since=<last report> --format='%h %cs %s' -- crates/plugin/src/esm/records crates/plugin/src/esm/cell byroredux/src/cell_loader/refr.rs
Guards: cell_loader/scol_expansion_tests.rs, pkin_expansion_tests.rs, attach_points_spawn_tests.rs, refr_texture_overlay_tests.rs; the real-data ESM harness (#819) before any parse-rate claim.
Checklist:

  • SCOL/PKIN expand into per-instance synthetic REFRs with composed (parent × child) transform; recursion bounded by MAX_PKIN_DEPTH = 4 (vanilla has zero nesting; the cap guards modded cross-recursion); SCOL-of-SCOL has its own gate. Known open, don't re-file: #5523 (SCOL CM path taken verbatim from MODL, 114 stale DLCRobot MODLs; the expansion gate is model_path emptiness only, so a set-but-absent CM is an extract_mesh miss and renders nothing — the documented absent-CM fallback does not exist). Regression guards: SCOL/PKIN FLTR is the CK object-window filter path, a zstring decoded with read_zstring into filter: String and never remapped (#5493, fixed 136048a95 — pre-fix 2,843 false malformed-FormID warns across the FO4 DLCs buried real remap failures; the *_expansion_tests.rs pins use real string payloads plus a raw-under-remap test); NPC_.TPTA decodes onto NpcRecord::template_actors: [u32; 13] (xEdit wbTemplateFlags order, 0 = use the TPLT default) and walk_inherited_records (crates/plugin/src/equip.rs) takes the per-flag entry for that flag's edge when non-zero (#5498, fixed 99c22a8db; 763 Fallout4.esm records resolved the wrong template before). MOVS is parse-only (physics runtime unwired). The TXST group arm is in records/parse.rs and its decode in esm/cell/support.rs — an unreachable_patterns warning there means b"TXST" matches earlier than intended. The FO4-architecture maps must surface in categories() (records/index.rs).
  • TXST DODT (DecalData) has zero consumers — 303 vanilla payloads parse onto TextureSet.decal_data and are never read (grep decal_data finds only the parser and its tests); DNAM is consumed (TXST_FLAG_MODEL_SPACE_NORMALS in refr.rs). Known and deferred — #3638 closed documentation-only (a decal-rendering feature build, not a parse bug); forward scope, don't re-file.
  • PKIN CNAM is a template CELL, not a content base — fixed d63131c57 (#5231): every vanilla PKIN CNAM resolves to a CELL (the census spans Fallout4.esm + four DLCs) and vanilla FO4 places 0 PKIN REFRs (the CK bakes contents into ordinary REFRs) — an FO4-only premise: Starfield places 97,981, so the skip is live there (open #5489, /audit-starfield). The expander now skips CELL-typed CNAMs with a warn (instancing a template CELL's references under the outer transform is unimplemented) and returns None for an all-CELL list so the outer REFR reaches the default miss accounting; base-record children remain the mod-authored shape. Guards: expand_pkin_cell_typed_cnam_is_an_explicit_miss, expand_pkin_skips_the_cell_child_but_fans_out_base_children, #[ignore]d fo4_pkin_cnam_targets_are_template_cells (crates/plugin/tests/parse_real_esm.rs).
  • Authored actor state — ACHR "Starts Unconscious" (0x2000, FO4/FO76/SF only) decodes onto PlacedRef::starts_unconscious and spawns the actor dormant (#5017, 3c08cfe50); XRGD corpse poses decode onto PlacedRef::ragdoll_pose (260afc33f) with no consumer — open #5015, forward scope.
  • ARMA female→male biped fallback is Skyrim-only (open sourcing question, not a bug) — resolve_armor_meshes (crates/plugin/src/equip.rs) falls back to the male biped model for a female wearer only when game == GameKind::Skyrim (#5510, fixed d678dc578: the Skyrim CK documents the rule; 172 of Skyrim.esm's 766 addons are male-only). FO4 deliberately keeps the strict reading because the FO4 CK page documents no such rule and none is sourced locally — a FO4 male-only addon renders nothing on a female wearer. Needs an FO4 source (xEdit/CK/engine) before any change; do not extend the fallback by analogy and do not file it as a defect.
  • BSConnectPoint — the import lifts BSConnectPoint::Parents/Children into AttachPoints / ChildAttachConnections (crates/core/src/ecs/components/attach_points.rs); spawn must materialise them (modular weapons / power-armor frames otherwise spawn base-receiver only).
    Output: /tmp/audit/fo4/dim_4.md

Dimension 5: Archives + Real-Data Validation + Forward Scope

Subagent: general-purpose
Paths: crates/bsa/src/ba2.rs, crates/nif/tests/parse_real_nifs.rs, crates/nif/examples/nif_stats.rs, ROADMAP.md, docs/feature-matrix.md
First step: BYROREDUX_FO4_DATA=… cargo test -p byroredux-nif --test parse_real_nifs -- --ignored fo4
Checklist:

  • BA2 (FO4 slice; reader discipline is /audit-parsers) — version dispatch is an exhaustive match (v1/v7/v8 FO4 → zlib; v2/v3 Starfield → /audit-starfield Dim 1); unknown majors bail at open() and a cascading if v == 1 … else with a silent v1 fallback is the regression (#811). GNRL byte-exact; DX10 reconstructs the DDS header from width/height/dxgi_format/mips (BC1/BC3/BC5/BC7, mip0 → mip_last).
  • Parse rate — parse_rate_fo4_all_meshes covers all 8 mesh-bearing archives (base pair + six DLC Main.ba2; DLCUltraHighResolution is textures-only), 235,082 NIFs incl. .bto/.btr (widened 2026-08-29 by #3466; re-measured 2026-10-09, 8/8 archives), 100.00% clean. Report the measured number, not ROADMAP's. Regression guard (#5232, fixed 3970c8d19): open_all_mesh_archives / open_ba2_by_name (crates/nif/tests/common/mod.rs) panic on a present-but-unopenable BA2 (None is reserved for an absent file) and run_all_meshes_gate asserts walked > 0; a reversion turns the gates into green no-ops — still confirm the archive count actually walked, not just the pass. DX10 records with chunk_hdr_len != 24 are now a hard InvalidData (#5008, cf2a36920; vanilla census: all 24).
  • Trace import_nif_scene on a workshop item, a creature (deathclaw / super mutant), a power-armor frame (heavy skinning + BSConnectPoint) and a modular weapon: mesh count, material_path, skinned vs rigid, connect points.
  • Forward scope (do not file as blockers): precombine _physics.nif collision and packed-Havok decode, .uvd PVS payload / occlusion consumer, MOVS physics runtime, DecalData consumer (above), XRGD pose consumer (#5015), PKIN template-CELL instancing (skipped with a warn since #5231), BSBehaviorGraphExtraData parse-only (verify nothing pretends to drive it), FaceGen NIF truncation tail, deeper cell coverage (LIGH power state / CONT leveled items / NPC_ face morph).
    Output: /tmp/audit/fo4/dim_5.md

Phase 3: Merge

  1. Read /tmp/audit/fo4/dim_*.md; combine into docs/audits/AUDIT_FO4_<TODAY>.md:
    • Executive Summary — measured parse rate; what is shipped vs pending (above).
    • Dimension Findings — grouped by severity per dimension.
    • BGSM Consumption Table — BGSM field × merged-into-ImportedMaterial by merge_external_material / surfaced-on-canonical-Material. Texture columns name the MaterialTextureSet role, never an FO4 slot number.
    • Forward Scope Chain — precombine collision / .uvd payload → MOVS physics → DecalData → deeper REFR/LIGH/CONT/NPC_ coverage.
  2. Remove cross-dimension duplicates.

Suggest: /audit-publish docs/audits/AUDIT_FO4_<TODAY>.md
(label every finding game:fo4 + legacy-compat, plus its own domain label.)