[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JZVPY3SWF48TGGCSPSDFG6":3,"skill-same:01M6JZVPY3SWF48TGGCSPSDFG6":63},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":34,"linkOnly":35,"markdownContent":36,"markdownBody":37,"frontMatter":38,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":43,"repositoryHtmlUrl":44,"repositoryHomepageUrl":6,"ownerHtmlUrl":45,"ownerBlogUrl":46,"ownerDescription":47,"updatedAt":48,"install":49,"resources":62},"01M6JZVPY3SWF48TGGCSPSDFG6","audit-parsers",null,"Parser-discipline audit of the non-NIF, non-ESM readers of untrusted game-archive input — BSA\u002FBA2\u002FCSG archives, BGSM\u002FBGEM, Starfield CDB, Havok HKX packfiles, FaceGen sidecars, MenuXml parse side, Steam VDF\u002FACF","matiaszanolli\u002FByroRedux","matiaszanolli","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F2379093?v=4","01M6JZJDYMPY01793W4XK157YG","Matias Zanolli","01M6JZJE2KE2BXSDPAYPQC6BSH",36,"MIT","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.claude\u002Fcommands\u002Faudit-parsers\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33],"audit","parsers","audit parsers","esm","game","archive","parser","discipline","steam","dimensions","focus","input","detect","parse","csg","2026-11-10 10:12:30",false,"---\ndescription: \"Parser-discipline audit of the non-NIF, non-ESM readers of untrusted game-archive input — BSA\u002FBA2\u002FCSG archives, BGSM\u002FBGEM, Starfield CDB, Havok HKX packfiles, FaceGen sidecars, MenuXml parse side, Steam VDF\u002FACF\"\nargument-hint: \"--focus \u003Cdimensions> [--crate bsa|bgsm|sfmaterial|hkx|facegen|menuxml|game-detect]\"\n---\n\n# Parser Discipline Audit (archives, materials, packfiles, sidecars)\n\nRead `_audit-common.md` and `_audit-severity.md` for shared protocol.\n\nArchives and mod files are attacker-controllable. This skill audits *reader discipline* for formats no other audit owns; per-game content claims (parse rates, rendering) stay with the per-game audits; NIF blocks `\u002Faudit-nif`, ESM `\u002Faudit-esm`, `.spt` `\u002Faudit-speedtree`, `.pex`\u002F`.psc` `\u002Faudit-papyrus`; HKX *playback* (cinematic systems, root motion, completion events) stays `\u002Faudit-scripting` Dim 5 — this skill owns the HKX *decode*.\n\n| Crate | Reads | Consumer |\n|---|---|---|\n| `crates\u002Fbsa\u002Fsrc\u002F` (`archive\u002F`, `ba2.rs`, `csg.rs`, `uvd.rs`, `safety.rs`, `naming.rs`, `read_at.rs`, `lod_family.rs` — pure path classifier, no byte parse) | BSA v103\u002Fv104 zlib, v105 LZ4 **frame**; BA2 v1\u002F2\u002F3\u002F7\u002F8 GNRL + DX10 (DDS header reconstructed; v3 LZ4 **block**); FO4 `.csg`, `.uvd` header (`docs\u002Fengine\u002Farchives.md`, `docs\u002Fengine\u002Ffo4-csg-format.md`) | `byroredux\u002Fsrc\u002Fasset_provider\u002Farchive.rs`, `byroredux\u002Fsrc\u002Fcell_loader\u002Fprecombined.rs` |\n| `crates\u002Fbgsm\u002Fsrc\u002F` | BGSM\u002FBGEM v1–v22 + template chain | `byroredux\u002Fsrc\u002Fasset_provider\u002Fmaterial\u002F{provider,merge}.rs` |\n| `crates\u002Fsfmaterial\u002Fsrc\u002F` (`reader.rs`, `index.rs`, `settings.rs`) | Starfield `materialsbeta.cdb` — generic object tree + the streaming per-material `MaterialIndex` | `byroredux\u002Fsrc\u002Fasset_provider\u002Fmaterial\u002Fcdb.rs` (header probe + `MaterialIndex::build`\u002F`lookup`; each valid CDB's index builds on a named worker thread started at `discover_starfield_cdbs`, single-flight `OnceLock` slot installed synchronously before the worker is queued, and every build — prefetch or lazy — serialised behind one `CDB_BUILD_GATE` — dd759b762, #5291; e16a624e9, #5666, `lookup_waits_for_a_queued_prefetch_slot`), `material\u002Fmerge.rs` (`apply_cdb_material`) |\n| `crates\u002Fhkx\u002Fsrc\u002F` (`packfile.rs`, `animation.rs`, `behavior.rs`, `expression.rs`) | Havok 2010 packfile (v8), 32-bit LE (Skyrim LE) + 64-bit LE (SE); Havok 2014 packfile (v11, Fallout 4: 0x40-byte section headers after a predicate area, spline type 3, THREECOMP48 rotations, `hkaDefaultAnimatedReferenceFrame`, `hkaLosslessCompressedAnimation` — layout measured from 856 FO4 clips, not sourced, `decode_animation` dispatches both kinds, 2649febc7 #5897); spline + lossless clips, skeletons, behavior graphs \u002F character \u002F project files and their condition-expression text | `byroredux\u002Fsrc\u002Fasset_provider\u002F{animation,actor_graph,archetype_idle,object_graph}.rs`, `byroredux\u002Fsrc\u002Fsystems\u002Fgraph_locomotion.rs` |\n| `crates\u002Ffacegen\u002Fsrc\u002F` | `.egm` \u002F `.egt` \u002F `.tri` | `byroredux\u002Fsrc\u002Fnpc_spawn\u002Fresumable\u002Fruntime.rs` (EGM only) |\n| `crates\u002Fmenuxml\u002Fsrc\u002F{parse,tex,font}.rs` | Oblivion\u002FFO3\u002FFNV menu XML (lenient scanner), its own DDS decoder (BC1\u002F2\u002F3\u002F4\u002F5 + DX10-header BC1–5 \u002F RGBA8 \u002F BGRA8 \u002F R8 + masked 8\u002F16\u002F24\u002F32-bpp; BC4\u002FBC5\u002FDX10 added in 5017a64c2 for terrain height maps) and `.fnt` fonts — parse robustness only, the rest is `\u002Faudit-ui` | `crates\u002Fmenuxml\u002Fsrc\u002Fmenu.rs`; `Rgba8::decode_dds` also feeds `byroredux\u002Fsrc\u002Fasset_provider\u002Fterrain_height.rs` and `tex.dump` (`commands\u002Fassets.rs`) |\n| `crates\u002Fgame-detect\u002Fsrc\u002F{vdf,steam,catalog}.rs` | Steam `libraryfolders.vdf` \u002F `appmanifest_*.acf` (policy: `\u002Faudit-tooling` Dim 5) | launcher, `byro-detect` |\n\n**Contract every reader must meet** (a finding violates one): (a) a file-controlled size is bounded *before* memory is reserved; (b) malformed bytes give `Err` with field context — never a panic, abort or unbounded loop; (c) a short\u002Fdegraded decode is a documented `Ok` + warning or an error, never silent; (d) a real-data sweep exists and cannot go green vacuously.\n\n## Parameters (from $ARGUMENTS)\n\n- `--focus \u003Cdimensions>`: dimensions to run (default all 6). `--crate \u003Cname>`: one crate.\n- Delta-first: run each `First step`, then `git log --since=\u003Clast AUDIT_PARSERS date> -- \u003CPaths>`; deep-audit only dimensions whose Paths changed.\n\n## Extra Per-Finding Fields\n\n- **Dimension**: Size Discipline | Error Semantics | Version Gating | Corpus Gates | Decode-Consumer Wiring | I\u002FO & Paths\n- **Trigger Input**: the byte-level condition (field, value, file). Severity per `_audit-severity.md`: abort\u002FOOM\u002Fstack overflow reachable from an archive or mod file = HIGH (CRITICAL if UB); vanilla content failing to load = HIGH; silently wrong bytes = MEDIUM–HIGH; missing gate\u002Ftest = LOW–MEDIUM.\n\n## Phase 1: Setup\n\n1. `mkdir -p \u002Ftmp\u002Faudit\u002Fparsers`; `gh issue list --repo matiaszanolli\u002FByroRedux --limit 200 --json number,title,state,labels > \u002Ftmp\u002Faudit\u002Fparsers\u002Fissues.json`; dedup per `_audit-common.md`.\n2. Read the newest `docs\u002Faudits\u002FAUDIT_PARSERS_*.md` if any.\n3. `cargo test -p byroredux-bsa -p byroredux-bgsm -p byroredux-sfmaterial -p byroredux-hkx -p byroredux-facegen -p byroredux-menuxml -p byroredux-game-detect`; record counts. Real-data suites, data present, one crate at a time: `BYROREDUX_REQUIRE_GAME_DATA=1 cargo test -p \u003Ccrate> -- --ignored`. Never `ComponentDatabaseFile::parse` the vanilla CDB (~9.19 GiB tree, #4274); `real_cdb.rs` streams.\n\n## Phase 2: Dimensions (max 3 concurrent agents)\n\n### Dimension 1: Untrusted-size discipline\nPaths: `crates\u002Fbsa\u002Fsrc\u002F`, `crates\u002Fhkx\u002Fsrc\u002F`, `crates\u002Ffacegen\u002Fsrc\u002F`, `crates\u002Fsfmaterial\u002Fsrc\u002F{reader,index}.rs`, `crates\u002Fbgsm\u002Fsrc\u002Freader.rs`, `crates\u002Fmenuxml\u002Fsrc\u002F{parse,tex,font}.rs`, `crates\u002Fgame-detect\u002Fsrc\u002Fvdf.rs`.\nFirst step: `grep -n 'with_capacity\\|vec!\\[0\\|read_to_end\\|as usize' \u003CPaths>`; trace each hit to the bound before it.\nGuards (confirm live, not `#[ignore]`d): `entry_count_rejects_attacker_u32_max`, `over_ratio_payload_is_rejected_at_the_ceiling` (`crates\u002Fbsa\u002Fsrc\u002Fsafety.rs`); `malicious_bsa_folder_count_u32_max_rejected` (`crates\u002Fbsa\u002Fsrc\u002Farchive\u002Ftests.rs`); `malicious_file_count_u32_max_rejected_before_allocation`, `lz4_flex_is_pinned_to_the_safe_decoder` (workspace `lz4_flex` 0.14 keeps `safe-decode` — feature list unchanged by the 20ad630a6 bump; off = heap overflow no `catch_unwind` catches, HIGH), `lz4_decompress_is_panic_guarded` (`crates\u002Fbsa\u002Fsrc\u002Fba2.rs`); `oversized_read_len_is_rejected_before_it_is_reserved` (`crates\u002Fbsa\u002Fsrc\u002Fcsg.rs`); `decode_spline_animation_rejects_a_sample_count_bomb`, `decode_spline_animation_rejects_a_block_claiming_its_whole_clip` (`crates\u002Fhkx\u002Fsrc\u002Fanimation.rs`); `rejects_morph_count_over_cap` (`crates\u002Ffacegen\u002Fsrc\u002Fegm.rs`); `parse_with_limits_rejects_object_tree_before_materialising_it` (sfmaterial).\nBeyond the guards:\n- One implementation of the ceilings — `MAX_ENTRY_COUNT` 10 M, `MAX_CHUNK_BYTES` 1 GiB, `MAX_RECORD_TOTAL_BYTES` 2 GiB, `inflate_bounded` \u002F `inflate_bounded_zlib`. A reader passing a file-controlled length to `Vec::with_capacity`, or `read_to_end` on a decoder with no `declared + 1` cap, is the regression; so is a second copy of the constants.\n- Record reservations are also file-relative (#4661): `capacity_hint(count, remaining_bytes, min_record_bytes)` bounds `read_general_records`, `read_dx10_records` and the BSA folder\u002Ffile tables by the bytes left. A new table reader reserving on the absolute cap alone is the regression.\n- `declared_size` (BSA, BA2, `asset_provider\u002Farchive.rs`) hands file-controlled sizes to the streaming budget: BSA runs them through `checked_chunk_size`; BA2 sums raw `unpacked_size`s with no ceiling — check no consumer allocates from it.\n- Sizes computed from data *outside* the archive (NIF-carried counts into `CsgArchive::read_psg`, #3758) need their own overflow-checked bound.\n- File-value arithmetic (`count * stride`, `offset + len`) is `checked_*` or bounded by a preceding cap (hkx `bone_count > 4096` precedes its multiplies).\n- Nesting: menuxml (tiles `depth > 48`, expressions `> 64`, `MAX_READ_DEPTH` 32, include-cycle set; `include_cycles_terminate`; `MenuFileSource` providers must stay archive-backed); bgsm templates (`DEPTH_LIMIT` 16 + visited set, `resolve_breaks_self_reference_cycle`; a self-reference reached through a path alias is canonicalised and cached, multi-node cycles reparse — de034a53d, #5907). sfmaterial value nesting `MAX_NESTING_DEPTH` 64 → `Error::NestingTooDeep` (#4657, `self_referential_struct_is_rejected_instead_of_overflowing_the_stack`); `vdf::parse_entries` `MAX_VDF_DEPTH` 32 (#4673, `pathological_nesting_hits_the_depth_cap`); menuxml includes re-enter at `depth + 1` and are capped by `MAX_INCLUDE_SPLICES` 256 (#4650) *plus* document-wide byte\u002Ftile budgets, since splice count alone let a 64 KiB fragment x 256 splices materialise 2.4 M tiles (#5314, 23fd95055: `MAX_INCLUDE_BYTES` 256 KiB, `MAX_INCLUDE_FRAGMENT_BYTES` 64 KiB checked before the per-document cache, `MAX_DOCUMENT_TILES` 16,384); a fragment that cannot fit the splice budget is refused *before* it is cached (caches `None`), and every fetch, spliced or refused, is charged against `MAX_INCLUDE_FETCHED_BYTES` (2x the splice budget) before any probe (fixed in 6a132a967, #5399: `refused_includes_are_bounded_by_the_fetch_budget`). Decoded menu textures are a second unbounded-retention site: `MenuRenderer` keys decodes by resolved archive path (one fetch + decode + charge for every spelling of a file) and charges each against `MAX_DECODED_TEXTURE_BYTES` (1 GiB, from the DDS header via `Rgba8::decoded_len` before allocating); over budget the filename resolves to nothing, cached, with one warning (5dce486b6, #5658 — vanilla totals 980 MiB Oblivion all-resolution-sets, so it never trips on shipped content); refusals are remembered per *resolved* path and consulted before the fetch, and a refused candidate falls through to a smaller resolution-set sibling (4c979297b, #5693); a lower-priority set's copy substituting for an undecodable preferred texture warns once per spelling (64bca63af, #5670). hkx strings are borrowed, `read_cstr` capped at 256 B, and the absolute sample cap `MAX_TRANSFORM_SAMPLES` is 1 M (spline and lossless paths) — 8x Skyrim's census max of 124,821 but only 1.7x FO4's (voice lip-sync clips reach 584,155, pinned since a3574d65e #5896) (#5316, d8295d77c; the old 16 M admitted a 597 MiB decode) — and the spline frame count is bounded relative to the file's blocks (#4648\u002F#4655) with `max_frames_per_block` capped at `MAX_FRAMES_PER_BLOCK` 256 (fixed in a65595815, #5006 — the one-block bypass; `skyrim_se_spline_dimensions_census_stays_under_the_gate_ceilings` is the real-data guard) — a new per-entry owned copy of a shared string is the regression. Spline scalar bounds whose finite span overflows f32 flag the track like NaN bounds instead of pushing ±inf through the sampler (c3b578ae5, #5691, `overflowing_finite_span_drops_only_that_track`); the FO4 reference-frame twin — neighbouring finite samples whose difference overflows, non-finite \u002F non-positive duration — is refused by `check_reference_samples`, and a carried-but-unusable frame is reported in `HkxAnimation::reference_frame_dropped` (warned with the clip path) rather than reading as no frame (74733c655, #5894); its field offsets derive from the packfile pointer width (3674a045a, #5901). Lossless-clip rotations (`decode_lossless_in`) go through the same `normalize_quaternion` policy as the spline decoder: a zero-length \u002F overflowing quaternion drops the track into `dropped_bones` (like a non-finite value, #5662) and a non-unit one is normalised in the decoder (18ff2c3a0, #6242; `a_lossless_rotation_the_spline_policy_refuses_drops_its_track_and_others_are_normalised`) — a second quaternion policy per decoder is the regression. A lossless clip with no dynamic series (every channel absent or static) ties its declared frame count to nothing in the file, so it is capped at `MAX_STATIC_CLIP_SAMPLES` 65,536 (33x the FO4 census maximum of 1,974 samples across 11 such clips) and a zero duration over more than one frame is refused, as the spline path does (e4f72f3e5, #6306; `a_lossless_clip_with_no_dynamic_data_cannot_claim_a_sample_budget_of_frames`, `a_lossless_clip_cannot_stretch_many_frames_over_zero_duration`). The behavior-graph decoder (`behavior.rs`) bounds every file-driven table: `MAX_EVENTS` 4096, `MAX_STATES` 1024, `MAX_TRANSITIONS` 4096, `MAX_TRIGGERS` 1024, `MAX_VARIABLES` 4096, `MAX_BONES` 1024, generator recursion `MAX_GENERATOR_DEPTH` 32, modifier nesting `MAX_MODIFIER_DEPTH` 16, reference chasing `MAX_REFERENCE_HOPS` 8 \u002F `MAX_INLINED_REFERENCES` 64, inlined machine height `MAX_MACHINE_NESTING` 33 (event\u002Fvariable name merge is hashed, 3ce34ed28 #5893). `inline_references` spends one `InlineBudget` shared by the whole recursion: the 64 resolves *plus* a `MAX_INLINED_WEIGHT` (= `MAX_GRAPH_STEPS`) weight budget (`machine_weight`: states, transitions, clips, sequences, modifiers, **weighed by what each holds** — a clip's triggers and every modifier's leaf tables (assign bindings, event ranges, expressions) counted recursively through lists \u002F modify-once \u002F event-driven wrappers, the same entries the decode charges a step each; a list of 1,024 expression modifiers once weighed 3 while owning ~a million strings, so 64 references inlined 64 deep copies, +1.2 GiB, ec8f8af6f #6479, `a_modifier_list_of_expression_tables_is_weighed_by_what_it_holds`) charged as soon as a sub-behavior resolves and before anything recurses into it, and a reference already on the inlining stack (self or a cycle) is cut before it is resolved — a self-reference loads once, where it used to stack up to 64 live deep clones (1,990 MiB peak in the probe); the nesting height is checked before the recursion as well as after, and a sub-behavior whose modifier ids would wrap `u32` is left out (5696d0947, #6212; `mutually_referencing_behaviors_are_each_loaded_once`, `inlined_volume_is_bounded_by_one_shared_weight_budget`; the shared weight is `graph_weight`, which counts every entry a resolve deep-copies — bone-binding tables and bare reference trees included, each walked field named with no `..` so a new table does not compile until weighed — fixed in 9a6dd51dd, #6593, pinned by `a_blender_childs_bone_bindings_are_weighed_by_what_they_hold`, `a_tree_of_references_to_itself_is_weighed_by_what_it_holds`, `a_graphs_weight_counts_every_table_a_resolve_copies`), and the FO4 reference-frame sample table 65,536 — a new table reader without a cap is the regression. Depth caps alone do not bound work: a `Walk` guard carries an on-stack ancestor set (a pointer back to an ancestor is refused) and a per-decode `MAX_GRAPH_STEPS` 2,000,000 budget charged per object entered, per child-array entry walked (e730f9a77, #5891) and per leaf-table entry read inside an object — clip triggers, variable bindings (once per member looked up), bone weights, transitions, event ranges, expressions, assign bindings (#6211; `a_fan_out_onto_a_clip_with_full_tables_is_refused_by_the_step_budget`). FO4's shipped max is 245,831 steps (Skyrim SE 3,329), asserted ≥ 8x by the corpus census; a new table reader that does not charge `walk` is the regression. File-driven integer arithmetic uses `checked_*` — `bone_of_binding`'s `offsetInArrayPlusOne - 1` overflowed on `0x80000000` (49895b2df, #5895). `expression.rs` is a recursive-descent parser over graph-authored condition text (`!`\u002F`-`\u002Fparenthesis recursion has no depth counter of its own; it is bounded only by the 256 B `cstr` cap that feeds it — a longer string source would make it a stack-overflow surface). MenuXml include budget + cycle checks run *before* the archive fetch and probes are memoised per resolved path (fixed in 2465740cc, #5007: `self_including_fragment_costs_one_fetch`, `budget_exhausted_includes_never_fetch`; fixed in ba252c482, #5329 — the cycle check now tests only the *resolved* key, after the cached fetch, so a lower-priority spelling on the stack no longer rejects a distinct higher-priority include; a real cycle still resolves from the cache and never re-fetches). Every fetch is charged `max(len, INCLUDE_FETCH_FLOOR_BYTES)` (1 KiB) so the per-document extract count is bounded at 512, and the exhausted-budget warnings (`budget` \u002F `bytes` \u002F `fetched` \u002F `tiles`) fire once per *document* rather than once per path (547d3c385, #5668). `MaterialIndex::build` parses the schema with `ParseLimits::unlimited()` and streams the `DBFileIndex` collections element-by-element — its bound is the per-instance decode, not `parse_with_limits`. A degraded join on a non-vanilla CDB now errors instead of yielding an empty\u002Fwrong index (#5320, 6489ae225: `MissingDbFileIndex`, `RowInstanceMismatch`, `DuplicateDbFileIndex`, `UnsupportedDbFileIndexChunk`; plus `NoKeyedObjects`, a0da96c0f #5899); the skip path walks `Class::read_order` like the read path (#5323, 258ba2b90); `cdb_material_index` warns before memoizing a failed archive fetch (#5319, 8be8c3d3a).\n- MenuXml's own DDS decoder (`tex.rs`) rejects dimensions > 8192 and sizes the uncompressed payload against `width × height × bpp` *before* allocating the RGBA canvas; every BC path (BC1–5, incl. via the DX10 header) checks the full block payload with checked arithmetic in `bc_output` before allocating (0b7501e70, #5667); a zero channel mask is an absent channel and sub-8-bit masks replicate by shift-OR (the old `2*bits - 8` underflowed — dev panic, release mis-decode), L8 keys off `DDPF_LUMINANCE` (fixed in 015e10549, #5377: `truncated_payload_is_rejected_before_allocation`, `l8_grey_decodes_as_luminance_not_red`). 16-bpp A4R4G4B4 \u002F R5G6B5 decode through the same masks and the loader names \"present but undecodable\" separately from \"not found\" (fixed in 5c53ad9c7, #5400; the opt-in `menu_texture_header_census_decodes` pins zero undecodable vanilla menu headers).\n- `&str` byte-range slicing of disk-derived names panics mid-char (#3391); use `as_bytes()`.\n\n### Dimension 2: Truncation, EOF and error semantics\nPaths: Dimension 1's, plus `byroredux\u002Fsrc\u002Fasset_provider\u002F{archive,animation,material\u002Fprovider}.rs`, `byroredux\u002Fsrc\u002Fnpc_spawn\u002Fresumable\u002F` (consumer `Err` arms).\nFirst step: list non-test `unwrap()` \u002F `expect(` \u002F `[idx]` on file-derived indices in Paths; read each consumer's `Err` arm.\n- Short decode is a deliberate `Ok` + `log::warn!` (shipped padding deltas #622\u002F#812); over-run is `Err(InvalidData)`. Guards: `short_decode_stays_ok_for_the_shipped_padding_deltas`, `decompress_chunk_zlib_short_stream_returns_actual_length`, `decompress_chunk_lz4_under_run_returns_actual_length_not_declared`. A short **non-final** DX10 chunk is an `Err` naming the entry (#4662, `extract_dx10_rejects_a_short_non_final_chunk_naming_the_entry`); only the final chunk stays lenient. A DX10 `chunk_hdr_len != 24` is a hard `InvalidData` in every build, and no `debug_assert!` sits on a file-controlled field (fixed in cf2a36920, #5008: `dx10_open_rejects_a_non_24_chunk_hdr_len_as_invalid_data`, `dx10_open_tolerates_non_monotonic_start_mip_without_panicking`). An empty compressed BSA record (vanilla zero-byte `.txt` placeholders: 35 Oblivion, 1 FO3, 1 FNV) is decided in `BsaArchive::extract` *before* any decoder — size header 0 with no stream → `Ok(empty)` on the zlib and LZ4-frame arms, a non-zero declared size with no stream → `InvalidData` naming the entry — because flate2 1.1.10 reports an empty zlib stream as `UnexpectedEof` where 1.1.9 returned `Ok(0)` (fixed in fa2af0e89, #5073\u002F#5660; `extract_returns_empty_for_a_zero_size_compressed_placeholder`, `extract_rejects_a_nonzero_size_with_no_compressed_stream`; `safety.rs` pins the decoder semantics the readers rely on). \"Short decode stays `Ok`\" holds for complete streams only; a stream cut mid-block is an error.\n- A checksum-only zlib failure retries as raw DEFLATE; a corrupt body must still error (`corrupt_adler32_trailer_recovers_via_raw_deflate`, `corrupt_deflate_body_still_errors`).\n- Errors carry context (bgsm `Error::UnexpectedEof { offset }`, hkx labels, `checked_*` label). Consumer fall-through loops go through `extract_first` \u002F `Archive::extract_or_warn` (`asset_provider\u002Farchive.rs`, #4658): a non-`NotFound` error warns once per (archive, entry) instead of reading as \"missing\" and silently falling back to the vanilla copy — a raw `.extract(..).ok()` loop is the regression. The debug-load and `--mesh`\u002F`--tree` NIF paths (`byroredux\u002Fsrc\u002Fdebug_load.rs`, `scene\u002Fnif_loader.rs`) now resolve through `TextureProvider::extract_mesh` (BA2 dispatch, last-listed-wins, `open_shared`), not a raw first-wins `BsaArchive::open` (fixed in 44219e881, #5009; pinned by the hygiene test `no_production_code_opens_archives_outside_the_provider_layer` — bin-crate production code may not open `BsaArchive`\u002F`Ba2Archive` directly). `Archive::open_shared` (`asset_provider\u002Farchive.rs`, 5402b2530 #5288) is the process-wide opened-handle registry (validated by an `ArchiveStamp` — length + mtime everywhere, plus `(dev, ino, ctime)` on Unix so a same-length in-place rewrite that restores the mtime is caught, 04036a6fb #5663 — capped at 1024) that the texture \u002F material pools and the CDB index build now open through — a replaced archive must be re-read, not served stale (`open_shared_reuses_an_unchanged_archive_and_rereads_a_replaced_one`, `open_shared_rereads_a_same_length_rewrite_that_restores_the_mtime`); the path-keyed Starfield CDB caches are documented process-lifetime and never re-stamped.\n- A bad asset costs the asset, not the cell, and a partial result must be signalled: hkx skips one bad annotation and keeps the clip (`read_annotations_skips_an_out_of_range_time_and_keeps_the_rest`); menuxml truncates an over-deep subtree with a warning. Flag any silent partial result.\n- File-byte-reachable panics: NIF parse runs under `catch_unwind` in `byroredux\u002Fsrc\u002Fstreaming\u002Fpre_parse.rs`; archive extract, hkx, bgsm, facegen and menuxml callers may run unguarded on the main thread. `crates\u002Ffacegen\u002Fsrc\u002Fegm.rs` indexes `bytes[offset]` after its exact-size check — verify that precondition is unconditional.\n\n### Dimension 3: Version and generation gating\nPaths: `crates\u002Fbsa\u002Fsrc\u002Farchive\u002Fopen.rs`, `crates\u002Fbsa\u002Fsrc\u002Fba2.rs`, `crates\u002Fbgsm\u002Fsrc\u002F{base,bgsm,bgem}.rs`, `crates\u002Fsfmaterial\u002Fsrc\u002Freader.rs`, `crates\u002Fhkx\u002Fsrc\u002Fpackfile.rs`, `crates\u002Ffacegen\u002Fsrc\u002F`.\nFirst step: `grep -n 'version' \u003CPaths>`; every gate keys off the file's *own* version field, never a `GameKind` sniff.\n- BSA: allowlist {103,104,105}, unknown = `Err`; folder record 16 vs 24 B and `u64` offsets on v105; embed-name flag 0x100 only for >= 104; codec zlib v103\u002Fv104, **LZ4 frame** v105 — never `lz4_flex::block` (`synthetic_v105_block_codec_payload_is_rejected_by_frame_reader`); size-word bit 31 has no sourced meaning and is not acted on (#3367).\n- BA2: exhaustive `match` over {1,2,3,7,8} (`unknown_version_rejected`); v2 header +8 B, v3 +12 B carrying `compression_method` (0 zlib, 3 LZ4 **block**, else `Err`: `v3_unknown_compression_method_rejected`); v1\u002Fv2\u002Fv7\u002Fv8 always zlib; per-chunk `packed_size == 0` = stored raw regardless of codec; DX10 DDS header 148 B (`build_dds_header_is_148_bytes`) with cubemap, mip clamp and per-DXGI pitch\u002Flinear-size flag. `Ba2Archive::dx10_layout` (record-table read, no extract) and `extract_dx10_mips(path, first_mip)` (inflates only chunks reaching the range; `first_mip` 0 equals `extract`; GNRL refused; M39 TXS-SRC-001, 36b55c728 \u002F f70d041de) are file-controlled-range readers: `dx10_layout_reports_header_and_chunk_mip_ranges`, `dx10_extract_mip_range_reads_only_tail_chunks`.\n- BGSM\u002FBGEM: per-field `version >=` \u002F `\u003C` gates; a version above `NEWEST_KNOWN_VERSION` (22) still decodes with the newest layout but surfaces `version_over_ceiling` + `unconsumed_bytes` (#4664); strings decode lossily (#4672). `material\u002Fprovider.rs` logs those diagnostics on the BGEM path, and the BGSM template resolver (`crates\u002Fbgsm\u002Fsrc\u002Ftemplate.rs`) now parses every leaf and template through `parse_bgsm_diag`, reporting each via the `TemplateResolver::parsed` hook that the provider routes to the same `warn_parse_diagnostics` (746e0858e, #5010, pinned by `resolve_reports_per_file_parse_diagnostics`; cache hits are not re-reported). A new parse site that calls the non-diagnostic `parse_bgsm` \u002F `parse_bgem` is the regression.\n- CDB: header errors (`BadMagic`, `UnsupportedVersion`, `BigEndianUnsupported`); vocabularies pinned by `chunk_type_recognized_set_is_pinned`, `builtin_type_recognized_set_is_pinned`, `class_flags_known_mask_is_pinned`; `probe_header` tolerates unknown chunk FourCCs (#4273), the object-tree parse must reject them. User-class inline bytes are read in field-*offset* order (`Class::read_order`; XMCOLOR declares r,g,b,a at offsets 2,1,0,3 — fixed in 224a19372, `read_order_reorders_xmcolor_shape`); a declaration-order read is the regression.\n- HKX: the spline-clip dimension gate ties `mask_size` to the file's own track counts as `pad(4*transform_tracks + float_tracks, 4)` (Havok pads the mask table to 4 B; the unpadded equality rejected 229 SE \u002F 167 LE vanilla clips, #5659, `decode_spline_animation_rejects_a_mask_size_off_the_padded_value`). `Packfile::parse` gates magic, pointer width 4|8, little-endian, `fileVersion` 8 (Havok 2010, `hk_2010*`) or 11 (Havok 2014, `hk_2014*`; the contents string must match the version), reuse-padding 0 (#4332, `rejects_packfiles_that_are_not_havok_2010_msvc_layout`); a v11 file's section table starts after the `m_predicateArraySizePlusPadding` area and the spline `animation_type` is 3 (v8: 5), both keyed off the file's own version; offsets derive from pointer size (`layout_walk_reproduces_the_skyrim_se_offsets`); a track binding a bone the skeleton lacks is dropped with a warning in `convert_hkx_clip` (#3013), never zip-truncated.\n- FaceGen: magics `FREGM002` \u002F `FREGT003` \u002F `FRTRI003`; EGM demands *exact* file size (`rejects_size_count_mismatch`), so trailing bytes on a modded head are refused — intended? EGM deltas are `scale × i16`, not half-floats (#4653); EGT is planar signed chars (#4668); EGT header offset 8 is R = rows = `height`, 12 is C = `width` (fixed in 8f7acf3a7, #5014: `non_square_header_maps_rows_to_height_and_columns_to_width`).\n\n### Dimension 4: Corpus and baseline gates\nPaths: `crates\u002F*\u002Ftests\u002F*real*.rs`, `crates\u002Fbgsm\u002Ftests\u002Fparse_all.rs`, `crates\u002Fsfmaterial\u002Ftests\u002F`, `crates\u002Fmenuxml\u002Ftests\u002F`, `crates\u002Fhkx\u002Fsrc\u002Fanimation.rs` (test module), `crates\u002Fhkx\u002Fsrc\u002Fcorpus_tests.rs`, `.github\u002Fworkflows\u002Freal-data-gates.yml`.\nFirst step: `grep -rn '#\\[ignore' crates\u002F{bsa,bgsm,sfmaterial,hkx,facegen,menuxml}` vs `grep -n 'cargo test' .github\u002Fworkflows\u002F*.yml`; diff the two.\n- Strict lane (#3850): `BYROREDUX_REQUIRE_GAME_DATA=1` makes an absent corpus panic and a set `BYROREDUX_\u003CGAME>_DATA` override binding. Implemented in `crates\u002Fbsa\u002Ftests\u002F{bsa_real,ba2_real,csg_real}.rs`, `crates\u002Fbgsm\u002Ftests\u002Fparse_all.rs`, `crates\u002Fsfmaterial\u002Ftests\u002Freal_cdb.rs`, `crates\u002Ffacegen\u002Ftests\u002Fparse_real_facegen.rs`.\n- Vacuous-green sites (#4660 closed the known ones; verify no regression): the menuxml corpora (`crates\u002Fmenuxml\u002Ftests\u002F{vanilla,fnv,fo3}_corpus.rs`) are `#[ignore]`d and resolve through `tests\u002Fcommon` (binding override, strict on a missing install\u002Farchive, panic on an open failure); hkx's `animations_archive`, bgsm's `open_archive_at`, facegen's `Game::extract`, the bsa `require_archive` helpers and the in-crate `archive_missing` gate all fail under `REQUIRE` instead of skipping, and treat a present-but-unopenable archive as a failure. All six parser crates' `--ignored` suites run in `real-data-gates.yml`'s `parsers` job (#4659), which targets a `byroredux-game-data` self-hosted runner that is not registered — the workflow is now **manual-only** (*workflow_dispatch*; the nightly schedule was removed in fde8feec2, #5905 closed), so no automated parser real-data result exists and the closed-HIGH pin list (`.github\u002Freal-data-pins.txt`, 9ee74b6e7 #6183) carries no parser-crate lane; run the suites locally (the job runs `--no-fail-fast`, fa2af0e89, #5660, so one red binary no longer hides the rest of the crate); the Skyrim LE hkx parity test is `--skip`ped there with a `::warning::` unless `BYROREDUX_SKYRIMLE_DATA` is set.\n- Assertion strength: `real_cdb.rs` pins exact counts (97 classes \u002F 1,438,780 values); bgsm holds `MIN_SUCCESS_RATE` 1.0 over FO4 and FO76 `Materials.ba2` (FO76's two JSON exports are a named, counted bucket). #4665 added FO76 BA2\u002FDX10, FO3 BSA, FNV MenuXml and Oblivion EGM sweeps. Tabulate (format x game) sweeps and list what is still missing. The #5326 hardening landed (d4352d477): the HKX census pins the SE clip count (6,126), bounds skips, and runs every clip through `decode_spline_animation` against a reject baseline (the first run measured 288 vanilla rejects — a real decoder bug, then fixed: #5659 `mask_size` is Havok's 4-byte-padded size, eca0238e7, and #5662 a non-finite prop-bone track is dropped into `HkxAnimation::dropped_bones` rather than failing the clip, 9707cb367; the census now rejects 0 of 6,126), and `material_index_dlc_cdb_resolves_the_base_corpus` asserts the five Nightmare texture slots under the strict lane. #5671 (390241376) made the census exact — 6,126 clips \u002F 1,573 skipped \u002F 59 dropped-track clips \u002F 1,471 max frames \u002F 15 max blocks, rejects must be the empty set. The #5011 hardening landed (e8676b097): the Oblivion EGM test goes through `require_game_data` \u002F `require_archive` and pins its exact byte, vertex and morph counts, and the FO4 BGSM\u002FBGEM and FO76 sweeps parse through `parse_*_diag` and fail on unconsumed bytes or an over-ceiling version (the other (format x game) gaps the issue listed are not addressed). Havok 2014 is gated too (a3574d65e, #5896): `corpus_tests.rs` pins `fallout_4_animations_archive_census_is_exact` (14,370 clips — 13,514 spline \u002F 856 lossless, 848 additive), `fallout_4_behavior_graph_census_is_exact` and `skyrim_se_behavior_graph_census_is_exact` (three named non-decoders), and the default lane now builds version-11 packfiles (`PackfileBuilder` v11 mode: header, spline type gate, THREECOMP48, reference frame, modifier cycle\u002Ffan-out guards). Archive and FaceGen sweeps pin counts and extract every vanilla entry across FO3\u002FFNV\u002FSkyrim SE\u002FFO4\u002FFO76, Skyrim LE optional behind `BYROREDUX_SKYRIMLE_DATA` (ce008af1c, #5903: `fo3_fnv_and_skyrimse_bsa_corpus_census`, `fo4_ba2_vanilla_corpus_full_extract_zero_errors`, `vanilla_facegen_sidecar_census_is_exact`); the bgsm sweep pins vanilla corpus sizes (c5a0b98c6, #5904) — except FO76, a live-service corpus: its BGSM material count and BA2 archive\u002Fentry census are **floors** (29,991 materials, 40 archives, 679,240 entries) asserted *after* the clean-parse \u002F drift \u002F extract-everything checks, with a re-measure line on change (regression guard: fixed in a2fb1bb12, #6482, closed; last measured 2026-10-24: 30,078 materials, 43 archives, 688,096 entries). The menu DDS census is exact per resolution set (`textures\\menus*` 1,975 \u002F 1,712 \u002F 1,789) and covers the FO3\u002FFNV `textures\\interface\\` art (1,157 \u002F 1,767; 065c9d31d, #5669). The HKX dimension-gate fixtures isolate their own clauses (regression guard: fixed in c78e9241e, #5446 — every fixture carries the exact padded `mask_size` and a sample product that only its named clause can refuse, so deleting any one of the three clauses reddens exactly its test; probe by mutation, not by reading).\n- ROADMAP compat-matrix rates are NIF-level; any BGSM\u002FBA2\u002FCDB rate claim in `ROADMAP.md` or `docs\u002Ffeature-matrix.md` needs a dated measurement + sweep command. CI has no game data, so each format branch needs an in-memory fixture (`build_v105_archive`, `PackfileBuilder`, `synth_egm`, `bgsm::tests::minimal_v2_bytes`).\n\n### Dimension 5: Decode \u003C-> consumer wiring\nPaths: `crates\u002Fbgsm\u002Fsrc\u002F`, `byroredux\u002Fsrc\u002Fasset_provider\u002Fmaterial\u002F{merge,cdb}.rs`, `crates\u002Ffacegen\u002Fsrc\u002Flib.rs`, `byroredux\u002Fsrc\u002Fnpc_spawn\u002Fresumable\u002Fruntime.rs`, `crates\u002Fhkx\u002Fsrc\u002Fanimation.rs`, `byroredux\u002Fsrc\u002Fasset_provider\u002Fanimation.rs`, `crates\u002Fbsa\u002Fsrc\u002Fuvd.rs`.\nFirst step: list `pub` fields of `BgsmFile` \u002F `BgemFile` \u002F `HkxAnimation` \u002F `EgmFile`; grep each in its consumer.\n- BGSM\u002FBGEM fields vs `merge_external_material` (`byroredux\u002Fsrc\u002Fasset_provider\u002Ftests\u002Fbgsm_merge.rs`): classify unconsumed fields renderer-relevant (finding) vs editor-only. The merge takes `&mut ImportedMaterial`; a widened signature is a NIFAL violation (`\u002Faudit-nifal`).\n- CDB (#3398 still OPEN for the rest; spike `docs\u002Faudits\u002FSF_CDB_PHASE2_SPIKE_2026-08-29.md`): `MaterialIndex` (224a19372) joins path → material → layer → texture set → `MRTextureFile` slots (`SLOT_COLOR`..`SLOT_TRANSMISSIVE`) plus per-slot `TextureReplacement`, alpha test, `IsGlass`, `UseSSS`, and (9bb6b7dce, #5277\u002F#5283) `EffectSettingsComponent.BlendingMode` and `EmissiveSettingsComponent` (Enabled + the inline nested `EmittanceSettings` tint \u002F luminous emittance). `apply_cdb_material` (`material\u002Fmerge.rs`, 978d25c19) forwards colour \u002F normal \u002F emissive slots, the `SLOT_COLOR` replacement only when no colour texture landed (never a tint), alpha test only when `HasOpacity = true` is authored on the `Parent` chain (an absent field is the measured class default `false`; regression guard: fixed in 0d698d16c, #5661, closed), `IsGlass` → `bgem_glass`, `\"AlphaBlend\"` → `has_alpha`, (enabled emissive settings are captured but parked, 4667b884b #5503 — no `emissive_color` \u002F `emissive_mult`); it stamps `PbrMaterial::NO_SIGNAL_NEUTRAL` scalars on a hit (#5197). Parked by design: opacity \u002F metal \u002F rough \u002F AO \u002F transmissive slots, slot 6 `_height` (a layer height-blend input, not parallax — 46dfdcc6c, #5381), `MaterialParamFloat`, non-colour replacements, `UseSSS` \u002F `TransmissiveScale` (captured off `TranslucencySettingsComponent.Settings` since 595110e3c, #5673 — the old arm keyed on the nested struct's class name, which never occurs top-level). The `Objects` decode counts rows whose `DBID` \u002F `Parent` \u002F `PersistentID` shape drifted and warns once per build (fa6666053, #5672; `PersistentID` misses counted and an all-miss table is the typed `NoKeyedObjects` error, a0da96c0f, #5899); parent-graph defects (cycles, objects inheriting through one, chains past `MAX_PARENT_DEPTH` 64) come from one O(n) census at build and warn once — the per-merge `Parent` walk (`ancestry`, bounded + cycle-safe) is silent (a0da96c0f, #5900); `MaterialIndex` reads the `PersistentID` columns by name, not map order (30bdbcc3b, #5465). `ObjectInfo.Parent` inheritance is resolved for the settings components (5c14ebdf8; the decode is pinned in the default lane since 7ceda787d, #5578) and, since 1d7e2c6bd, for texture-set slots and `TextureReplacement` flat colours too (nearest-set-wins down the chain; an authored `Enabled = false` on a nearer set stops inheritance; real-data no-texture count 2,049 → 217 of 10,152, pinned by `material_index_resolves_inherited_texture_slots`). #5502 is closed (5c14ebdf8 + 1d7e2c6bd; the glass-coverage decision is recorded in `nifal.md`: `IsGlass` routes to `bgem_glass` directly, the `AlphaBlend` forward only passes the classifier's coverage early-return); `Additive` \u002F `Multiply` blend modes are forwarded (8e30e66e1, `cdb_blend_mode_tests`); the soft variants and `TakeSmaller` stay parked by name in `nifal.md` (dbfae5a34; #5509 open). `MergeOutcome::PresenceOnly` now means an index *miss* (`apply_cdb_pbr_fallback`, `mat_path_lookup_miss_keeps_presence_only_and_no_textures`). A loose `.mat` JSON file that *decodes* wins over the CDB (`material\u002Floose_mat.rs`, #4277, c2f28e06c); the decoder reads the installed layout — `Objects[].Components[]` with properties under `Data`, string-typed bools and scalars, `Data\\` stripped from `FileName`, `Index` via `u8::try_from`, all four colour channels required (fixed in faf09c6e6, #5396; vanilla ships none, four Creation archives carry 20). An undecodable file falls through to the CDB lookup instead of pre-empting it (#5436); both arms record `ImportedTextureSource::Mat`. `parse_loose_mat` is a second `CdbMaterial` producer, but the Alpha \u002F Effect (`BlendingMode`) \u002F Emissive settings rule lives once in `capture_settings` (`crates\u002Fsfmaterial\u002Fsrc\u002Fsettings.rs`, over a `FieldSource` trait both decoders implement; loose folds first-writer-wins) — fixed in 442901646, #5525, pinned by `the_compiled_and_loose_representations_capture_identically`; the loose decoder still walks no `Parent` chain, and `TranslucencySettingsComponent` stays a per-decoder arm (no installed loose file authors it). Do not file the parked fields as new; a parked field leaking into a live slot is the finding.\n- FaceGen `.egt` \u002F `.tri` have no consumer — documented in `crates\u002Ffacegen\u002Fsrc\u002Flib.rs` (#3544, closed by doc correction); only EGM reaches `apply_morphs`. Re-file only if that claim changes.\n- HKX partial results reach `convert_hkx_clip` as data, not silence: `HkxAnimation::dropped_bones` (#5662), `reference_frame_dropped` (#5894) and `additive` (Havok 2014 `blendHint`, #5897) — the engine has no additive layering for Havok clips, so an additive clip (848 of FO4's 14,370 vanilla clips per the census) is converted as an absolute pose with a warning; it is a known gap, not a new finding, unless the warning goes missing.\n- UVD: envelope only, no consumer yet (`parse_uvd_header` has no production caller; `cell_loader\u002Fprecombined.rs` owns the future previs consumer; #3810, the envelope spike, is closed); payload undecoded by design.\n- Consumed-but-unparsed: a consumer `unwrap_or(default)` standing in for an unexposed field. ESM\u002FNIF cases (TXST DecalData #3638, SkinAttach) go to `\u002Faudit-esm` \u002F `\u002Faudit-nif`.\n\n### Dimension 6: I\u002FO and path robustness\nPaths: `crates\u002Fbsa\u002Fsrc\u002F{naming.rs,read_at.rs,archive\u002Fmod.rs,ba2.rs,csg.rs}`, `byroredux\u002Fsrc\u002Fasset_provider\u002F{archive,texture,material\u002Fprovider}.rs`, `byroredux\u002Fsrc\u002Fasset_provider\u002Ftests\u002F`, `crates\u002Fgame-detect\u002Fsrc\u002F{vdf,steam}.rs`, `crates\u002Fmenuxml\u002Fsrc\u002Fparse.rs`.\nFirst step: `cargo test -p byroredux --bin byroredux asset_provider`; read `archive_precedence.rs` and `archive_siblings.rs`.\n- Key: both readers and the loose tier share one exported `byroredux_bsa::normalize_path` (Unicode `to_lowercase().replace('\u002F', \"\\\\\")`; the BSA module's duplicate is gone, f26eb657f #6308); consumer normalisers `normalize_mesh_path`, `normalize_texture_path`, `normalize_material_path`, `strip_build_prefix`, `canonical_texture_key` — check open-time and lookup keys agree and non-ASCII names do not diverge from ASCII-only folds elsewhere.\n- Siblings: `numeric_sibling_paths` (pure; shared with the launcher validator): `Foo.bsa` -> 2..9, `Foo0.bsa` -> 1..9, `Foo01.ba2` -> 02..09, mid-series none; guards `siblings_*` in `archive_siblings.rs`; `open_with_numeric_siblings` de-dups by case-fold.\n- Provider order is **last-listed wins** across a chain (#3637); a loose `Data\\` tier (`LooseDir`, indexed once per directory per process from the `--esm` plugin's directory, `loose_data_dir`) is appended after every archive in the texture, mesh and material pools, so loose mod\u002Fuser files win (48e422a71, #6200; `asset_provider\u002Ftests\u002Floose_override.rs`); loose keys fold through `normalize_path` like the archives (`loose_keys_fold_like_the_archives`), and two loose files folding onto one key resolve to the lexicographically last path with the collision count warned once, not in `read_dir` order (f26eb657f, #6308; `loose_case_variant_duplicates_resolve_to_the_last_path`); `every_content_provider_resolves_collisions_last_wins` scans providers for a first-wins loop; `count_shadowed_entries` logs shadowing. Within one archive a duplicate normalised name is counted at open and warned, last record wins (#4671, BSA and BA2).\n- File model: no mmap, no `Mutex\u003CFile>` — positional reads via `read_at.rs::ReadAt` (`pread` \u002F `seek_read`), so extracts from one archive run concurrently on Unix (Windows serialises in the kernel, #4999); CSG's mutex guards only its chunk cache. A shared-cursor read reintroduced anywhere is the regression (`concurrent_extracts_read_their_own_entries`, `synthetic_v105_concurrent_extracts_read_their_own_bytes`, `concurrent_reads_get_their_own_chunk_bytes`, #5000). Bounds checks must fail on a replaced archive, not return garbage (`extract_rejects_compressed_payload_too_short`).\n- VDF\u002FACF: malformed input rejected, not half-read (`malformed_documents_are_rejected_rather_than_half_read`); `libraryfolders.vdf` from `steamapps\u002F` + `config\u002F` is unioned; ACF `installdir` is contained under `steamapps\u002Fcommon\u002F` in `installs_in_library` via the host-independent string predicate `is_bare_install_dir` (#4673; widened in aad5dfd75, #5016 — `\u002F`, `\\`, `:`, `.`, `..` rejected so Windows-rooted `\\X` \u002F `D:X` cannot escape; `only_a_bare_directory_name_is_a_valid_installdir`); unreadable manifests are logged.\n\n## Phase 3: Merge and Cleanup\n\nWrite `docs\u002Faudits\u002FAUDIT_PARSERS_\u003CTODAY>.md`: Executive Summary (findings by severity; crates swept; real-data suites run vs skipped), deduplicated Findings, and a Gate Matrix (crate x {size caps, error policy, version gate, corpus sweep, CI lane}). Cross-audit dedup: NIF\u002FESM sizes `\u002Faudit-nif` Dim 1 \u002F `\u002Faudit-esm` Dim 1; `unsafe` `\u002Faudit-safety`; MenuXml rendering `\u002Faudit-ui`. Then `rm -rf \u002Ftmp\u002Faudit\u002Fparsers` and suggest `\u002Faudit-publish docs\u002Faudits\u002FAUDIT_PARSERS_\u003CTODAY>.md` (labels: `import-pipeline` BSA\u002FBA2\u002FCSG\u002FFaceGen, `nifal` BGSM\u002FCDB, `animation` HKX, `ui` MenuXml, `test-gap` gates, `tech-debt` game-detect; `game:*` when title-specific).\n","# Parser Discipline Audit (archives, materials, packfiles, sidecars)\n\nRead `_audit-common.md` and `_audit-severity.md` for shared protocol.\n\nArchives and mod files are attacker-controllable. This skill audits *reader discipline* for formats no other audit owns; per-game content claims (parse rates, rendering) stay with the per-game audits; NIF blocks `\u002Faudit-nif`, ESM `\u002Faudit-esm`, `.spt` `\u002Faudit-speedtree`, `.pex`\u002F`.psc` `\u002Faudit-papyrus`; HKX *playback* (cinematic systems, root motion, completion events) stays `\u002Faudit-scripting` Dim 5 — this skill owns the HKX *decode*.\n\n| Crate | Reads | Consumer |\n|---|---|---|\n| `crates\u002Fbsa\u002Fsrc\u002F` (`archive\u002F`, `ba2.rs`, `csg.rs`, `uvd.rs`, `safety.rs`, `naming.rs`, `read_at.rs`, `lod_family.rs` — pure path classifier, no byte parse) | BSA v103\u002Fv104 zlib, v105 LZ4 **frame**; BA2 v1\u002F2\u002F3\u002F7\u002F8 GNRL + DX10 (DDS header reconstructed; v3 LZ4 **block**); FO4 `.csg`, `.uvd` header (`docs\u002Fengine\u002Farchives.md`, `docs\u002Fengine\u002Ffo4-csg-format.md`) | `byroredux\u002Fsrc\u002Fasset_provider\u002Farchive.rs`, `byroredux\u002Fsrc\u002Fcell_loader\u002Fprecombined.rs` |\n| `crates\u002Fbgsm\u002Fsrc\u002F` | BGSM\u002FBGEM v1–v22 + template chain | `byroredux\u002Fsrc\u002Fasset_provider\u002Fmaterial\u002F{provider,merge}.rs` |\n| `crates\u002Fsfmaterial\u002Fsrc\u002F` (`reader.rs`, `index.rs`, `settings.rs`) | Starfield `materialsbeta.cdb` — generic object tree + the streaming per-material `MaterialIndex` | `byroredux\u002Fsrc\u002Fasset_provider\u002Fmaterial\u002Fcdb.rs` (header probe + `MaterialIndex::build`\u002F`lookup`; each valid CDB's index builds on a named worker thread started at `discover_starfield_cdbs`, single-flight `OnceLock` slot installed synchronously before the worker is queued, and every build — prefetch or lazy — serialised behind one `CDB_BUILD_GATE` — dd759b762, #5291; e16a624e9, #5666, `lookup_waits_for_a_queued_prefetch_slot`), `material\u002Fmerge.rs` (`apply_cdb_material`) |\n| `crates\u002Fhkx\u002Fsrc\u002F` (`packfile.rs`, `animation.rs`, `behavior.rs`, `expression.rs`) | Havok 2010 packfile (v8), 32-bit LE (Skyrim LE) + 64-bit LE (SE); Havok 2014 packfile (v11, Fallout 4: 0x40-byte section headers after a predicate area, spline type 3, THREECOMP48 rotations, `hkaDefaultAnimatedReferenceFrame`, `hkaLosslessCompressedAnimation` — layout measured from 856 FO4 clips, not sourced, `decode_animation` dispatches both kinds, 2649febc7 #5897); spline + lossless clips, skeletons, behavior graphs \u002F character \u002F project files and their condition-expression text | `byroredux\u002Fsrc\u002Fasset_provider\u002F{animation,actor_graph,archetype_idle,object_graph}.rs`, `byroredux\u002Fsrc\u002Fsystems\u002Fgraph_locomotion.rs` |\n| `crates\u002Ffacegen\u002Fsrc\u002F` | `.egm` \u002F `.egt` \u002F `.tri` | `byroredux\u002Fsrc\u002Fnpc_spawn\u002Fresumable\u002Fruntime.rs` (EGM only) |\n| `crates\u002Fmenuxml\u002Fsrc\u002F{parse,tex,font}.rs` | Oblivion\u002FFO3\u002FFNV menu XML (lenient scanner), its own DDS decoder (BC1\u002F2\u002F3\u002F4\u002F5 + DX10-header BC1–5 \u002F RGBA8 \u002F BGRA8 \u002F R8 + masked 8\u002F16\u002F24\u002F32-bpp; BC4\u002FBC5\u002FDX10 added in 5017a64c2 for terrain height maps) and `.fnt` fonts — parse robustness only, the rest is `\u002Faudit-ui` | `crates\u002Fmenuxml\u002Fsrc\u002Fmenu.rs`; `Rgba8::decode_dds` also feeds `byroredux\u002Fsrc\u002Fasset_provider\u002Fterrain_height.rs` and `tex.dump` (`commands\u002Fassets.rs`) |\n| `crates\u002Fgame-detect\u002Fsrc\u002F{vdf,steam,catalog}.rs` | Steam `libraryfolders.vdf` \u002F `appmanifest_*.acf` (policy: `\u002Faudit-tooling` Dim 5) | launcher, `byro-detect` |\n\n**Contract every reader must meet** (a finding violates one): (a) a file-controlled size is bounded *before* memory is reserved; (b) malformed bytes give `Err` with field context — never a panic, abort or unbounded loop; (c) a short\u002Fdegraded decode is a documented `Ok` + warning or an error, never silent; (d) a real-data sweep exists and cannot go green vacuously.\n\n## Parameters (from $ARGUMENTS)\n\n- `--focus \u003Cdimensions>`: dimensions to run (default all 6). `--crate \u003Cname>`: one crate.\n- Delta-first: run each `First step`, then `git log --since=\u003Clast AUDIT_PARSERS date> -- \u003CPaths>`; deep-audit only dimensions whose Paths changed.\n\n## Extra Per-Finding Fields\n\n- **Dimension**: Size Discipline | Error Semantics | Version Gating | Corpus Gates | Decode-Consumer Wiring | I\u002FO & Paths\n- **Trigger Input**: the byte-level condition (field, value, file). Severity per `_audit-severity.md`: abort\u002FOOM\u002Fstack overflow reachable from an archive or mod file = HIGH (CRITICAL if UB); vanilla content failing to load = HIGH; silently wrong bytes = MEDIUM–HIGH; missing gate\u002Ftest = LOW–MEDIUM.\n\n## Phase 1: Setup\n\n1. `mkdir -p \u002Ftmp\u002Faudit\u002Fparsers`; `gh issue list --repo matiaszanolli\u002FByroRedux --limit 200 --json number,title,state,labels > \u002Ftmp\u002Faudit\u002Fparsers\u002Fissues.json`; dedup per `_audit-common.md`.\n2. Read the newest `docs\u002Faudits\u002FAUDIT_PARSERS_*.md` if any.\n3. `cargo test -p byroredux-bsa -p byroredux-bgsm -p byroredux-sfmaterial -p byroredux-hkx -p byroredux-facegen -p byroredux-menuxml -p byroredux-game-detect`; record counts. Real-data suites, data present, one crate at a time: `BYROREDUX_REQUIRE_GAME_DATA=1 cargo test -p \u003Ccrate> -- --ignored`. Never `ComponentDatabaseFile::parse` the vanilla CDB (~9.19 GiB tree, #4274); `real_cdb.rs` streams.\n\n## Phase 2: Dimensions (max 3 concurrent agents)\n\n### Dimension 1: Untrusted-size discipline\nPaths: `crates\u002Fbsa\u002Fsrc\u002F`, `crates\u002Fhkx\u002Fsrc\u002F`, `crates\u002Ffacegen\u002Fsrc\u002F`, `crates\u002Fsfmaterial\u002Fsrc\u002F{reader,index}.rs`, `crates\u002Fbgsm\u002Fsrc\u002Freader.rs`, `crates\u002Fmenuxml\u002Fsrc\u002F{parse,tex,font}.rs`, `crates\u002Fgame-detect\u002Fsrc\u002Fvdf.rs`.\nFirst step: `grep -n 'with_capacity\\|vec!\\[0\\|read_to_end\\|as usize' \u003CPaths>`; trace each hit to the bound before it.\nGuards (confirm live, not `#[ignore]`d): `entry_count_rejects_attacker_u32_max`, `over_ratio_payload_is_rejected_at_the_ceiling` (`crates\u002Fbsa\u002Fsrc\u002Fsafety.rs`); `malicious_bsa_folder_count_u32_max_rejected` (`crates\u002Fbsa\u002Fsrc\u002Farchive\u002Ftests.rs`); `malicious_file_count_u32_max_rejected_before_allocation`, `lz4_flex_is_pinned_to_the_safe_decoder` (workspace `lz4_flex` 0.14 keeps `safe-decode` — feature list unchanged by the 20ad630a6 bump; off = heap overflow no `catch_unwind` catches, HIGH), `lz4_decompress_is_panic_guarded` (`crates\u002Fbsa\u002Fsrc\u002Fba2.rs`); `oversized_read_len_is_rejected_before_it_is_reserved` (`crates\u002Fbsa\u002Fsrc\u002Fcsg.rs`); `decode_spline_animation_rejects_a_sample_count_bomb`, `decode_spline_animation_rejects_a_block_claiming_its_whole_clip` (`crates\u002Fhkx\u002Fsrc\u002Fanimation.rs`); `rejects_morph_count_over_cap` (`crates\u002Ffacegen\u002Fsrc\u002Fegm.rs`); `parse_with_limits_rejects_object_tree_before_materialising_it` (sfmaterial).\nBeyond the guards:\n- One implementation of the ceilings — `MAX_ENTRY_COUNT` 10 M, `MAX_CHUNK_BYTES` 1 GiB, `MAX_RECORD_TOTAL_BYTES` 2 GiB, `inflate_bounded` \u002F `inflate_bounded_zlib`. A reader passing a file-controlled length to `Vec::with_capacity`, or `read_to_end` on a decoder with no `declared + 1` cap, is the regression; so is a second copy of the constants.\n- Record reservations are also file-relative (#4661): `capacity_hint(count, remaining_bytes, min_record_bytes)` bounds `read_general_records`, `read_dx10_records` and the BSA folder\u002Ffile tables by the bytes left. A new table reader reserving on the absolute cap alone is the regression.\n- `declared_size` (BSA, BA2, `asset_provider\u002Farchive.rs`) hands file-controlled sizes to the streaming budget: BSA runs them through `checked_chunk_size`; BA2 sums raw `unpacked_size`s with no ceiling — check no consumer allocates from it.\n- Sizes computed from data *outside* the archive (NIF-carried counts into `CsgArchive::read_psg`, #3758) need their own overflow-checked bound.\n- File-value arithmetic (`count * stride`, `offset + len`) is `checked_*` or bounded by a preceding cap (hkx `bone_count > 4096` precedes its multiplies).\n- Nesting: menuxml (tiles `depth > 48`, expressions `> 64`, `MAX_READ_DEPTH` 32, include-cycle set; `include_cycles_terminate`; `MenuFileSource` providers must stay archive-backed); bgsm templates (`DEPTH_LIMIT` 16 + visited set, `resolve_breaks_self_reference_cycle`; a self-reference reached through a path alias is canonicalised and cached, multi-node cycles reparse — de034a53d, #5907). sfmaterial value nesting `MAX_NESTING_DEPTH` 64 → `Error::NestingTooDeep` (#4657, `self_referential_struct_is_rejected_instead_of_overflowing_the_stack`); `vdf::parse_entries` `MAX_VDF_DEPTH` 32 (#4673, `pathological_nesting_hits_the_depth_cap`); menuxml includes re-enter at `depth + 1` and are capped by `MAX_INCLUDE_SPLICES` 256 (#4650) *plus* document-wide byte\u002Ftile budgets, since splice count alone let a 64 KiB fragment x 256 splices materialise 2.4 M tiles (#5314, 23fd95055: `MAX_INCLUDE_BYTES` 256 KiB, `MAX_INCLUDE_FRAGMENT_BYTES` 64 KiB checked before the per-document cache, `MAX_DOCUMENT_TILES` 16,384); a fragment that cannot fit the splice budget is refused *before* it is cached (caches `None`), and every fetch, spliced or refused, is charged against `MAX_INCLUDE_FETCHED_BYTES` (2x the splice budget) before any probe (fixed in 6a132a967, #5399: `refused_includes_are_bounded_by_the_fetch_budget`). Decoded menu textures are a second unbounded-retention site: `MenuRenderer` keys decodes by resolved archive path (one fetch + decode + charge for every spelling of a file) and charges each against `MAX_DECODED_TEXTURE_BYTES` (1 GiB, from the DDS header via `Rgba8::decoded_len` before allocating); over budget the filename resolves to nothing, cached, with one warning (5dce486b6, #5658 — vanilla totals 980 MiB Oblivion all-resolution-sets, so it never trips on shipped content); refusals are remembered per *resolved* path and consulted before the fetch, and a refused candidate falls through to a smaller resolution-set sibling (4c979297b, #5693); a lower-priority set's copy substituting for an undecodable preferred texture warns once per spelling (64bca63af, #5670). hkx strings are borrowed, `read_cstr` capped at 256 B, and the absolute sample cap `MAX_TRANSFORM_SAMPLES` is 1 M (spline and lossless paths) — 8x Skyrim's census max of 124,821 but only 1.7x FO4's (voice lip-sync clips reach 584,155, pinned since a3574d65e #5896) (#5316, d8295d77c; the old 16 M admitted a 597 MiB decode) — and the spline frame count is bounded relative to the file's blocks (#4648\u002F#4655) with `max_frames_per_block` capped at `MAX_FRAMES_PER_BLOCK` 256 (fixed in a65595815, #5006 — the one-block bypass; `skyrim_se_spline_dimensions_census_stays_under_the_gate_ceilings` is the real-data guard) — a new per-entry owned copy of a shared string is the regression. Spline scalar bounds whose finite span overflows f32 flag the track like NaN bounds instead of pushing ±inf through the sampler (c3b578ae5, #5691, `overflowing_finite_span_drops_only_that_track`); the FO4 reference-frame twin — neighbouring finite samples whose difference overflows, non-finite \u002F non-positive duration — is refused by `check_reference_samples`, and a carried-but-unusable frame is reported in `HkxAnimation::reference_frame_dropped` (warned with the clip path) rather than reading as no frame (74733c655, #5894); its field offsets derive from the packfile pointer width (3674a045a, #5901). Lossless-clip rotations (`decode_lossless_in`) go through the same `normalize_quaternion` policy as the spline decoder: a zero-length \u002F overflowing quaternion drops the track into `dropped_bones` (like a non-finite value, #5662) and a non-unit one is normalised in the decoder (18ff2c3a0, #6242; `a_lossless_rotation_the_spline_policy_refuses_drops_its_track_and_others_are_normalised`) — a second quaternion policy per decoder is the regression. A lossless clip with no dynamic series (every channel absent or static) ties its declared frame count to nothing in the file, so it is capped at `MAX_STATIC_CLIP_SAMPLES` 65,536 (33x the FO4 census maximum of 1,974 samples across 11 such clips) and a zero duration over more than one frame is refused, as the spline path does (e4f72f3e5, #6306; `a_lossless_clip_with_no_dynamic_data_cannot_claim_a_sample_budget_of_frames`, `a_lossless_clip_cannot_stretch_many_frames_over_zero_duration`). The behavior-graph decoder (`behavior.rs`) bounds every file-driven table: `MAX_EVENTS` 4096, `MAX_STATES` 1024, `MAX_TRANSITIONS` 4096, `MAX_TRIGGERS` 1024, `MAX_VARIABLES` 4096, `MAX_BONES` 1024, generator recursion `MAX_GENERATOR_DEPTH` 32, modifier nesting `MAX_MODIFIER_DEPTH` 16, reference chasing `MAX_REFERENCE_HOPS` 8 \u002F `MAX_INLINED_REFERENCES` 64, inlined machine height `MAX_MACHINE_NESTING` 33 (event\u002Fvariable name merge is hashed, 3ce34ed28 #5893). `inline_references` spends one `InlineBudget` shared by the whole recursion: the 64 resolves *plus* a `MAX_INLINED_WEIGHT` (= `MAX_GRAPH_STEPS`) weight budget (`machine_weight`: states, transitions, clips, sequences, modifiers, **weighed by what each holds** — a clip's triggers and every modifier's leaf tables (assign bindings, event ranges, expressions) counted recursively through lists \u002F modify-once \u002F event-driven wrappers, the same entries the decode charges a step each; a list of 1,024 expression modifiers once weighed 3 while owning ~a million strings, so 64 references inlined 64 deep copies, +1.2 GiB, ec8f8af6f #6479, `a_modifier_list_of_expression_tables_is_weighed_by_what_it_holds`) charged as soon as a sub-behavior resolves and before anything recurses into it, and a reference already on the inlining stack (self or a cycle) is cut before it is resolved — a self-reference loads once, where it used to stack up to 64 live deep clones (1,990 MiB peak in the probe); the nesting height is checked before the recursion as well as after, and a sub-behavior whose modifier ids would wrap `u32` is left out (5696d0947, #6212; `mutually_referencing_behaviors_are_each_loaded_once`, `inlined_volume_is_bounded_by_one_shared_weight_budget`; the shared weight is `graph_weight`, which counts every entry a resolve deep-copies — bone-binding tables and bare reference trees included, each walked field named with no `..` so a new table does not compile until weighed — fixed in 9a6dd51dd, #6593, pinned by `a_blender_childs_bone_bindings_are_weighed_by_what_they_hold`, `a_tree_of_references_to_itself_is_weighed_by_what_it_holds`, `a_graphs_weight_counts_every_table_a_resolve_copies`), and the FO4 reference-frame sample table 65,536 — a new table reader without a cap is the regression. Depth caps alone do not bound work: a `Walk` guard carries an on-stack ancestor set (a pointer back to an ancestor is refused) and a per-decode `MAX_GRAPH_STEPS` 2,000,000 budget charged per object entered, per child-array entry walked (e730f9a77, #5891) and per leaf-table entry read inside an object — clip triggers, variable bindings (once per member looked up), bone weights, transitions, event ranges, expressions, assign bindings (#6211; `a_fan_out_onto_a_clip_with_full_tables_is_refused_by_the_step_budget`). FO4's shipped max is 245,831 steps (Skyrim SE 3,329), asserted ≥ 8x by the corpus census; a new table reader that does not charge `walk` is the regression. File-driven integer arithmetic uses `checked_*` — `bone_of_binding`'s `offsetInArrayPlusOne - 1` overflowed on `0x80000000` (49895b2df, #5895). `expression.rs` is a recursive-descent parser over graph-authored condition text (`!`\u002F`-`\u002Fparenthesis recursion has no depth counter of its own; it is bounded only by the 256 B `cstr` cap that feeds it — a longer string source would make it a stack-overflow surface). MenuXml include budget + cycle checks run *before* the archive fetch and probes are memoised per resolved path (fixed in 2465740cc, #5007: `self_including_fragment_costs_one_fetch`, `budget_exhausted_includes_never_fetch`; fixed in ba252c482, #5329 — the cycle check now tests only the *resolved* key, after the cached fetch, so a lower-priority spelling on the stack no longer rejects a distinct higher-priority include; a real cycle still resolves from the cache and never re-fetches). Every fetch is charged `max(len, INCLUDE_FETCH_FLOOR_BYTES)` (1 KiB) so the per-document extract count is bounded at 512, and the exhausted-budget warnings (`budget` \u002F `bytes` \u002F `fetched` \u002F `tiles`) fire once per *document* rather than once per path (547d3c385, #5668). `MaterialIndex::build` parses the schema with `ParseLimits::unlimited()` and streams the `DBFileIndex` collections element-by-element — its bound is the per-instance decode, not `parse_with_limits`. A degraded join on a non-vanilla CDB now errors instead of yielding an empty\u002Fwrong index (#5320, 6489ae225: `MissingDbFileIndex`, `RowInstanceMismatch`, `DuplicateDbFileIndex`, `UnsupportedDbFileIndexChunk`; plus `NoKeyedObjects`, a0da96c0f #5899); the skip path walks `Class::read_order` like the read path (#5323, 258ba2b90); `cdb_material_index` warns before memoizing a failed archive fetch (#5319, 8be8c3d3a).\n- MenuXml's own DDS decoder (`tex.rs`) rejects dimensions > 8192 and sizes the uncompressed payload against `width × height × bpp` *before* allocating the RGBA canvas; every BC path (BC1–5, incl. via the DX10 header) checks the full block payload with checked arithmetic in `bc_output` before allocating (0b7501e70, #5667); a zero channel mask is an absent channel and sub-8-bit masks replicate by shift-OR (the old `2*bits - 8` underflowed — dev panic, release mis-decode), L8 keys off `DDPF_LUMINANCE` (fixed in 015e10549, #5377: `truncated_payload_is_rejected_before_allocation`, `l8_grey_decodes_as_luminance_not_red`). 16-bpp A4R4G4B4 \u002F R5G6B5 decode through the same masks and the loader names \"present but undecodable\" separately from \"not found\" (fixed in 5c53ad9c7, #5400; the opt-in `menu_texture_header_census_decodes` pins zero undecodable vanilla menu headers).\n- `&str` byte-range slicing of disk-derived names panics mid-char (#3391); use `as_bytes()`.\n\n### Dimension 2: Truncation, EOF and error semantics\nPaths: Dimension 1's, plus `byroredux\u002Fsrc\u002Fasset_provider\u002F{archive,animation,material\u002Fprovider}.rs`, `byroredux\u002Fsrc\u002Fnpc_spawn\u002Fresumable\u002F` (consumer `Err` arms).\nFirst step: list non-test `unwrap()` \u002F `expect(` \u002F `[idx]` on file-derived indices in Paths; read each consumer's `Err` arm.\n- Short decode is a deliberate `Ok` + `log::warn!` (shipped padding deltas #622\u002F#812); over-run is `Err(InvalidData)`. Guards: `short_decode_stays_ok_for_the_shipped_padding_deltas`, `decompress_chunk_zlib_short_stream_returns_actual_length`, `decompress_chunk_lz4_under_run_returns_actual_length_not_declared`. A short **non-final** DX10 chunk is an `Err` naming the entry (#4662, `extract_dx10_rejects_a_short_non_final_chunk_naming_the_entry`); only the final chunk stays lenient. A DX10 `chunk_hdr_len != 24` is a hard `InvalidData` in every build, and no `debug_assert!` sits on a file-controlled field (fixed in cf2a36920, #5008: `dx10_open_rejects_a_non_24_chunk_hdr_len_as_invalid_data`, `dx10_open_tolerates_non_monotonic_start_mip_without_panicking`). An empty compressed BSA record (vanilla zero-byte `.txt` placeholders: 35 Oblivion, 1 FO3, 1 FNV) is decided in `BsaArchive::extract` *before* any decoder — size header 0 with no stream → `Ok(empty)` on the zlib and LZ4-frame arms, a non-zero declared size with no stream → `InvalidData` naming the entry — because flate2 1.1.10 reports an empty zlib stream as `UnexpectedEof` where 1.1.9 returned `Ok(0)` (fixed in fa2af0e89, #5073\u002F#5660; `extract_returns_empty_for_a_zero_size_compressed_placeholder`, `extract_rejects_a_nonzero_size_with_no_compressed_stream`; `safety.rs` pins the decoder semantics the readers rely on). \"Short decode stays `Ok`\" holds for complete streams only; a stream cut mid-block is an error.\n- A checksum-only zlib failure retries as raw DEFLATE; a corrupt body must still error (`corrupt_adler32_trailer_recovers_via_raw_deflate`, `corrupt_deflate_body_still_errors`).\n- Errors carry context (bgsm `Error::UnexpectedEof { offset }`, hkx labels, `checked_*` label). Consumer fall-through loops go through `extract_first` \u002F `Archive::extract_or_warn` (`asset_provider\u002Farchive.rs`, #4658): a non-`NotFound` error warns once per (archive, entry) instead of reading as \"missing\" and silently falling back to the vanilla copy — a raw `.extract(..).ok()` loop is the regression. The debug-load and `--mesh`\u002F`--tree` NIF paths (`byroredux\u002Fsrc\u002Fdebug_load.rs`, `scene\u002Fnif_loader.rs`) now resolve through `TextureProvider::extract_mesh` (BA2 dispatch, last-listed-wins, `open_shared`), not a raw first-wins `BsaArchive::open` (fixed in 44219e881, #5009; pinned by the hygiene test `no_production_code_opens_archives_outside_the_provider_layer` — bin-crate production code may not open `BsaArchive`\u002F`Ba2Archive` directly). `Archive::open_shared` (`asset_provider\u002Farchive.rs`, 5402b2530 #5288) is the process-wide opened-handle registry (validated by an `ArchiveStamp` — length + mtime everywhere, plus `(dev, ino, ctime)` on Unix so a same-length in-place rewrite that restores the mtime is caught, 04036a6fb #5663 — capped at 1024) that the texture \u002F material pools and the CDB index build now open through — a replaced archive must be re-read, not served stale (`open_shared_reuses_an_unchanged_archive_and_rereads_a_replaced_one`, `open_shared_rereads_a_same_length_rewrite_that_restores_the_mtime`); the path-keyed Starfield CDB caches are documented process-lifetime and never re-stamped.\n- A bad asset costs the asset, not the cell, and a partial result must be signalled: hkx skips one bad annotation and keeps the clip (`read_annotations_skips_an_out_of_range_time_and_keeps_the_rest`); menuxml truncates an over-deep subtree with a warning. Flag any silent partial result.\n- File-byte-reachable panics: NIF parse runs under `catch_unwind` in `byroredux\u002Fsrc\u002Fstreaming\u002Fpre_parse.rs`; archive extract, hkx, bgsm, facegen and menuxml callers may run unguarded on the main thread. `crates\u002Ffacegen\u002Fsrc\u002Fegm.rs` indexes `bytes[offset]` after its exact-size check — verify that precondition is unconditional.\n\n### Dimension 3: Version and generation gating\nPaths: `crates\u002Fbsa\u002Fsrc\u002Farchive\u002Fopen.rs`, `crates\u002Fbsa\u002Fsrc\u002Fba2.rs`, `crates\u002Fbgsm\u002Fsrc\u002F{base,bgsm,bgem}.rs`, `crates\u002Fsfmaterial\u002Fsrc\u002Freader.rs`, `crates\u002Fhkx\u002Fsrc\u002Fpackfile.rs`, `crates\u002Ffacegen\u002Fsrc\u002F`.\nFirst step: `grep -n 'version' \u003CPaths>`; every gate keys off the file's *own* version field, never a `GameKind` sniff.\n- BSA: allowlist {103,104,105}, unknown = `Err`; folder record 16 vs 24 B and `u64` offsets on v105; embed-name flag 0x100 only for >= 104; codec zlib v103\u002Fv104, **LZ4 frame** v105 — never `lz4_flex::block` (`synthetic_v105_block_codec_payload_is_rejected_by_frame_reader`); size-word bit 31 has no sourced meaning and is not acted on (#3367).\n- BA2: exhaustive `match` over {1,2,3,7,8} (`unknown_version_rejected`); v2 header +8 B, v3 +12 B carrying `compression_method` (0 zlib, 3 LZ4 **block**, else `Err`: `v3_unknown_compression_method_rejected`); v1\u002Fv2\u002Fv7\u002Fv8 always zlib; per-chunk `packed_size == 0` = stored raw regardless of codec; DX10 DDS header 148 B (`build_dds_header_is_148_bytes`) with cubemap, mip clamp and per-DXGI pitch\u002Flinear-size flag. `Ba2Archive::dx10_layout` (record-table read, no extract) and `extract_dx10_mips(path, first_mip)` (inflates only chunks reaching the range; `first_mip` 0 equals `extract`; GNRL refused; M39 TXS-SRC-001, 36b55c728 \u002F f70d041de) are file-controlled-range readers: `dx10_layout_reports_header_and_chunk_mip_ranges`, `dx10_extract_mip_range_reads_only_tail_chunks`.\n- BGSM\u002FBGEM: per-field `version >=` \u002F `\u003C` gates; a version above `NEWEST_KNOWN_VERSION` (22) still decodes with the newest layout but surfaces `version_over_ceiling` + `unconsumed_bytes` (#4664); strings decode lossily (#4672). `material\u002Fprovider.rs` logs those diagnostics on the BGEM path, and the BGSM template resolver (`crates\u002Fbgsm\u002Fsrc\u002Ftemplate.rs`) now parses every leaf and template through `parse_bgsm_diag`, reporting each via the `TemplateResolver::parsed` hook that the provider routes to the same `warn_parse_diagnostics` (746e0858e, #5010, pinned by `resolve_reports_per_file_parse_diagnostics`; cache hits are not re-reported). A new parse site that calls the non-diagnostic `parse_bgsm` \u002F `parse_bgem` is the regression.\n- CDB: header errors (`BadMagic`, `UnsupportedVersion`, `BigEndianUnsupported`); vocabularies pinned by `chunk_type_recognized_set_is_pinned`, `builtin_type_recognized_set_is_pinned`, `class_flags_known_mask_is_pinned`; `probe_header` tolerates unknown chunk FourCCs (#4273), the object-tree parse must reject them. User-class inline bytes are read in field-*offset* order (`Class::read_order`; XMCOLOR declares r,g,b,a at offsets 2,1,0,3 — fixed in 224a19372, `read_order_reorders_xmcolor_shape`); a declaration-order read is the regression.\n- HKX: the spline-clip dimension gate ties `mask_size` to the file's own track counts as `pad(4*transform_tracks + float_tracks, 4)` (Havok pads the mask table to 4 B; the unpadded equality rejected 229 SE \u002F 167 LE vanilla clips, #5659, `decode_spline_animation_rejects_a_mask_size_off_the_padded_value`). `Packfile::parse` gates magic, pointer width 4|8, little-endian, `fileVersion` 8 (Havok 2010, `hk_2010*`) or 11 (Havok 2014, `hk_2014*`; the contents string must match the version), reuse-padding 0 (#4332, `rejects_packfiles_that_are_not_havok_2010_msvc_layout`); a v11 file's section table starts after the `m_predicateArraySizePlusPadding` area and the spline `animation_type` is 3 (v8: 5), both keyed off the file's own version; offsets derive from pointer size (`layout_walk_reproduces_the_skyrim_se_offsets`); a track binding a bone the skeleton lacks is dropped with a warning in `convert_hkx_clip` (#3013), never zip-truncated.\n- FaceGen: magics `FREGM002` \u002F `FREGT003` \u002F `FRTRI003`; EGM demands *exact* file size (`rejects_size_count_mismatch`), so trailing bytes on a modded head are refused — intended? EGM deltas are `scale × i16`, not half-floats (#4653); EGT is planar signed chars (#4668); EGT header offset 8 is R = rows = `height`, 12 is C = `width` (fixed in 8f7acf3a7, #5014: `non_square_header_maps_rows_to_height_and_columns_to_width`).\n\n### Dimension 4: Corpus and baseline gates\nPaths: `crates\u002F*\u002Ftests\u002F*real*.rs`, `crates\u002Fbgsm\u002Ftests\u002Fparse_all.rs`, `crates\u002Fsfmaterial\u002Ftests\u002F`, `crates\u002Fmenuxml\u002Ftests\u002F`, `crates\u002Fhkx\u002Fsrc\u002Fanimation.rs` (test module), `crates\u002Fhkx\u002Fsrc\u002Fcorpus_tests.rs`, `.github\u002Fworkflows\u002Freal-data-gates.yml`.\nFirst step: `grep -rn '#\\[ignore' crates\u002F{bsa,bgsm,sfmaterial,hkx,facegen,menuxml}` vs `grep -n 'cargo test' .github\u002Fworkflows\u002F*.yml`; diff the two.\n- Strict lane (#3850): `BYROREDUX_REQUIRE_GAME_DATA=1` makes an absent corpus panic and a set `BYROREDUX_\u003CGAME>_DATA` override binding. Implemented in `crates\u002Fbsa\u002Ftests\u002F{bsa_real,ba2_real,csg_real}.rs`, `crates\u002Fbgsm\u002Ftests\u002Fparse_all.rs`, `crates\u002Fsfmaterial\u002Ftests\u002Freal_cdb.rs`, `crates\u002Ffacegen\u002Ftests\u002Fparse_real_facegen.rs`.\n- Vacuous-green sites (#4660 closed the known ones; verify no regression): the menuxml corpora (`crates\u002Fmenuxml\u002Ftests\u002F{vanilla,fnv,fo3}_corpus.rs`) are `#[ignore]`d and resolve through `tests\u002Fcommon` (binding override, strict on a missing install\u002Farchive, panic on an open failure); hkx's `animations_archive`, bgsm's `open_archive_at`, facegen's `Game::extract`, the bsa `require_archive` helpers and the in-crate `archive_missing` gate all fail under `REQUIRE` instead of skipping, and treat a present-but-unopenable archive as a failure. All six parser crates' `--ignored` suites run in `real-data-gates.yml`'s `parsers` job (#4659), which targets a `byroredux-game-data` self-hosted runner that is not registered — the workflow is now **manual-only** (*workflow_dispatch*; the nightly schedule was removed in fde8feec2, #5905 closed), so no automated parser real-data result exists and the closed-HIGH pin list (`.github\u002Freal-data-pins.txt`, 9ee74b6e7 #6183) carries no parser-crate lane; run the suites locally (the job runs `--no-fail-fast`, fa2af0e89, #5660, so one red binary no longer hides the rest of the crate); the Skyrim LE hkx parity test is `--skip`ped there with a `::warning::` unless `BYROREDUX_SKYRIMLE_DATA` is set.\n- Assertion strength: `real_cdb.rs` pins exact counts (97 classes \u002F 1,438,780 values); bgsm holds `MIN_SUCCESS_RATE` 1.0 over FO4 and FO76 `Materials.ba2` (FO76's two JSON exports are a named, counted bucket). #4665 added FO76 BA2\u002FDX10, FO3 BSA, FNV MenuXml and Oblivion EGM sweeps. Tabulate (format x game) sweeps and list what is still missing. The #5326 hardening landed (d4352d477): the HKX census pins the SE clip count (6,126), bounds skips, and runs every clip through `decode_spline_animation` against a reject baseline (the first run measured 288 vanilla rejects — a real decoder bug, then fixed: #5659 `mask_size` is Havok's 4-byte-padded size, eca0238e7, and #5662 a non-finite prop-bone track is dropped into `HkxAnimation::dropped_bones` rather than failing the clip, 9707cb367; the census now rejects 0 of 6,126), and `material_index_dlc_cdb_resolves_the_base_corpus` asserts the five Nightmare texture slots under the strict lane. #5671 (390241376) made the census exact — 6,126 clips \u002F 1,573 skipped \u002F 59 dropped-track clips \u002F 1,471 max frames \u002F 15 max blocks, rejects must be the empty set. The #5011 hardening landed (e8676b097): the Oblivion EGM test goes through `require_game_data` \u002F `require_archive` and pins its exact byte, vertex and morph counts, and the FO4 BGSM\u002FBGEM and FO76 sweeps parse through `parse_*_diag` and fail on unconsumed bytes or an over-ceiling version (the other (format x game) gaps the issue listed are not addressed). Havok 2014 is gated too (a3574d65e, #5896): `corpus_tests.rs` pins `fallout_4_animations_archive_census_is_exact` (14,370 clips — 13,514 spline \u002F 856 lossless, 848 additive), `fallout_4_behavior_graph_census_is_exact` and `skyrim_se_behavior_graph_census_is_exact` (three named non-decoders), and the default lane now builds version-11 packfiles (`PackfileBuilder` v11 mode: header, spline type gate, THREECOMP48, reference frame, modifier cycle\u002Ffan-out guards). Archive and FaceGen sweeps pin counts and extract every vanilla entry across FO3\u002FFNV\u002FSkyrim SE\u002FFO4\u002FFO76, Skyrim LE optional behind `BYROREDUX_SKYRIMLE_DATA` (ce008af1c, #5903: `fo3_fnv_and_skyrimse_bsa_corpus_census`, `fo4_ba2_vanilla_corpus_full_extract_zero_errors`, `vanilla_facegen_sidecar_census_is_exact`); the bgsm sweep pins vanilla corpus sizes (c5a0b98c6, #5904) — except FO76, a live-service corpus: its BGSM material count and BA2 archive\u002Fentry census are **floors** (29,991 materials, 40 archives, 679,240 entries) asserted *after* the clean-parse \u002F drift \u002F extract-everything checks, with a re-measure line on change (regression guard: fixed in a2fb1bb12, #6482, closed; last measured 2026-10-24: 30,078 materials, 43 archives, 688,096 entries). The menu DDS census is exact per resolution set (`textures\\menus*` 1,975 \u002F 1,712 \u002F 1,789) and covers the FO3\u002FFNV `textures\\interface\\` art (1,157 \u002F 1,767; 065c9d31d, #5669). The HKX dimension-gate fixtures isolate their own clauses (regression guard: fixed in c78e9241e, #5446 — every fixture carries the exact padded `mask_size` and a sample product that only its named clause can refuse, so deleting any one of the three clauses reddens exactly its test; probe by mutation, not by reading).\n- ROADMAP compat-matrix rates are NIF-level; any BGSM\u002FBA2\u002FCDB rate claim in `ROADMAP.md` or `docs\u002Ffeature-matrix.md` needs a dated measurement + sweep command. CI has no game data, so each format branch needs an in-memory fixture (`build_v105_archive`, `PackfileBuilder`, `synth_egm`, `bgsm::tests::minimal_v2_bytes`).\n\n### Dimension 5: Decode \u003C-> consumer wiring\nPaths: `crates\u002Fbgsm\u002Fsrc\u002F`, `byroredux\u002Fsrc\u002Fasset_provider\u002Fmaterial\u002F{merge,cdb}.rs`, `crates\u002Ffacegen\u002Fsrc\u002Flib.rs`, `byroredux\u002Fsrc\u002Fnpc_spawn\u002Fresumable\u002Fruntime.rs`, `crates\u002Fhkx\u002Fsrc\u002Fanimation.rs`, `byroredux\u002Fsrc\u002Fasset_provider\u002Fanimation.rs`, `crates\u002Fbsa\u002Fsrc\u002Fuvd.rs`.\nFirst step: list `pub` fields of `BgsmFile` \u002F `BgemFile` \u002F `HkxAnimation` \u002F `EgmFile`; grep each in its consumer.\n- BGSM\u002FBGEM fields vs `merge_external_material` (`byroredux\u002Fsrc\u002Fasset_provider\u002Ftests\u002Fbgsm_merge.rs`): classify unconsumed fields renderer-relevant (finding) vs editor-only. The merge takes `&mut ImportedMaterial`; a widened signature is a NIFAL violation (`\u002Faudit-nifal`).\n- CDB (#3398 still OPEN for the rest; spike `docs\u002Faudits\u002FSF_CDB_PHASE2_SPIKE_2026-08-29.md`): `MaterialIndex` (224a19372) joins path → material → layer → texture set → `MRTextureFile` slots (`SLOT_COLOR`..`SLOT_TRANSMISSIVE`) plus per-slot `TextureReplacement`, alpha test, `IsGlass`, `UseSSS`, and (9bb6b7dce, #5277\u002F#5283) `EffectSettingsComponent.BlendingMode` and `EmissiveSettingsComponent` (Enabled + the inline nested `EmittanceSettings` tint \u002F luminous emittance). `apply_cdb_material` (`material\u002Fmerge.rs`, 978d25c19) forwards colour \u002F normal \u002F emissive slots, the `SLOT_COLOR` replacement only when no colour texture landed (never a tint), alpha test only when `HasOpacity = true` is authored on the `Parent` chain (an absent field is the measured class default `false`; regression guard: fixed in 0d698d16c, #5661, closed), `IsGlass` → `bgem_glass`, `\"AlphaBlend\"` → `has_alpha`, (enabled emissive settings are captured but parked, 4667b884b #5503 — no `emissive_color` \u002F `emissive_mult`); it stamps `PbrMaterial::NO_SIGNAL_NEUTRAL` scalars on a hit (#5197). Parked by design: opacity \u002F metal \u002F rough \u002F AO \u002F transmissive slots, slot 6 `_height` (a layer height-blend input, not parallax — 46dfdcc6c, #5381), `MaterialParamFloat`, non-colour replacements, `UseSSS` \u002F `TransmissiveScale` (captured off `TranslucencySettingsComponent.Settings` since 595110e3c, #5673 — the old arm keyed on the nested struct's class name, which never occurs top-level). The `Objects` decode counts rows whose `DBID` \u002F `Parent` \u002F `PersistentID` shape drifted and warns once per build (fa6666053, #5672; `PersistentID` misses counted and an all-miss table is the typed `NoKeyedObjects` error, a0da96c0f, #5899); parent-graph defects (cycles, objects inheriting through one, chains past `MAX_PARENT_DEPTH` 64) come from one O(n) census at build and warn once — the per-merge `Parent` walk (`ancestry`, bounded + cycle-safe) is silent (a0da96c0f, #5900); `MaterialIndex` reads the `PersistentID` columns by name, not map order (30bdbcc3b, #5465). `ObjectInfo.Parent` inheritance is resolved for the settings components (5c14ebdf8; the decode is pinned in the default lane since 7ceda787d, #5578) and, since 1d7e2c6bd, for texture-set slots and `TextureReplacement` flat colours too (nearest-set-wins down the chain; an authored `Enabled = false` on a nearer set stops inheritance; real-data no-texture count 2,049 → 217 of 10,152, pinned by `material_index_resolves_inherited_texture_slots`). #5502 is closed (5c14ebdf8 + 1d7e2c6bd; the glass-coverage decision is recorded in `nifal.md`: `IsGlass` routes to `bgem_glass` directly, the `AlphaBlend` forward only passes the classifier's coverage early-return); `Additive` \u002F `Multiply` blend modes are forwarded (8e30e66e1, `cdb_blend_mode_tests`); the soft variants and `TakeSmaller` stay parked by name in `nifal.md` (dbfae5a34; #5509 open). `MergeOutcome::PresenceOnly` now means an index *miss* (`apply_cdb_pbr_fallback`, `mat_path_lookup_miss_keeps_presence_only_and_no_textures`). A loose `.mat` JSON file that *decodes* wins over the CDB (`material\u002Floose_mat.rs`, #4277, c2f28e06c); the decoder reads the installed layout — `Objects[].Components[]` with properties under `Data`, string-typed bools and scalars, `Data\\` stripped from `FileName`, `Index` via `u8::try_from`, all four colour channels required (fixed in faf09c6e6, #5396; vanilla ships none, four Creation archives carry 20). An undecodable file falls through to the CDB lookup instead of pre-empting it (#5436); both arms record `ImportedTextureSource::Mat`. `parse_loose_mat` is a second `CdbMaterial` producer, but the Alpha \u002F Effect (`BlendingMode`) \u002F Emissive settings rule lives once in `capture_settings` (`crates\u002Fsfmaterial\u002Fsrc\u002Fsettings.rs`, over a `FieldSource` trait both decoders implement; loose folds first-writer-wins) — fixed in 442901646, #5525, pinned by `the_compiled_and_loose_representations_capture_identically`; the loose decoder still walks no `Parent` chain, and `TranslucencySettingsComponent` stays a per-decoder arm (no installed loose file authors it). Do not file the parked fields as new; a parked field leaking into a live slot is the finding.\n- FaceGen `.egt` \u002F `.tri` have no consumer — documented in `crates\u002Ffacegen\u002Fsrc\u002Flib.rs` (#3544, closed by doc correction); only EGM reaches `apply_morphs`. Re-file only if that claim changes.\n- HKX partial results reach `convert_hkx_clip` as data, not silence: `HkxAnimation::dropped_bones` (#5662), `reference_frame_dropped` (#5894) and `additive` (Havok 2014 `blendHint`, #5897) — the engine has no additive layering for Havok clips, so an additive clip (848 of FO4's 14,370 vanilla clips per the census) is converted as an absolute pose with a warning; it is a known gap, not a new finding, unless the warning goes missing.\n- UVD: envelope only, no consumer yet (`parse_uvd_header` has no production caller; `cell_loader\u002Fprecombined.rs` owns the future previs consumer; #3810, the envelope spike, is closed); payload undecoded by design.\n- Consumed-but-unparsed: a consumer `unwrap_or(default)` standing in for an unexposed field. ESM\u002FNIF cases (TXST DecalData #3638, SkinAttach) go to `\u002Faudit-esm` \u002F `\u002Faudit-nif`.\n\n### Dimension 6: I\u002FO and path robustness\nPaths: `crates\u002Fbsa\u002Fsrc\u002F{naming.rs,read_at.rs,archive\u002Fmod.rs,ba2.rs,csg.rs}`, `byroredux\u002Fsrc\u002Fasset_provider\u002F{archive,texture,material\u002Fprovider}.rs`, `byroredux\u002Fsrc\u002Fasset_provider\u002Ftests\u002F`, `crates\u002Fgame-detect\u002Fsrc\u002F{vdf,steam}.rs`, `crates\u002Fmenuxml\u002Fsrc\u002Fparse.rs`.\nFirst step: `cargo test -p byroredux --bin byroredux asset_provider`; read `archive_precedence.rs` and `archive_siblings.rs`.\n- Key: both readers and the loose tier share one exported `byroredux_bsa::normalize_path` (Unicode `to_lowercase().replace('\u002F', \"\\\\\")`; the BSA module's duplicate is gone, f26eb657f #6308); consumer normalisers `normalize_mesh_path`, `normalize_texture_path`, `normalize_material_path`, `strip_build_prefix`, `canonical_texture_key` — check open-time and lookup keys agree and non-ASCII names do not diverge from ASCII-only folds elsewhere.\n- Siblings: `numeric_sibling_paths` (pure; shared with the launcher validator): `Foo.bsa` -> 2..9, `Foo0.bsa` -> 1..9, `Foo01.ba2` -> 02..09, mid-series none; guards `siblings_*` in `archive_siblings.rs`; `open_with_numeric_siblings` de-dups by case-fold.\n- Provider order is **last-listed wins** across a chain (#3637); a loose `Data\\` tier (`LooseDir`, indexed once per directory per process from the `--esm` plugin's directory, `loose_data_dir`) is appended after every archive in the texture, mesh and material pools, so loose mod\u002Fuser files win (48e422a71, #6200; `asset_provider\u002Ftests\u002Floose_override.rs`); loose keys fold through `normalize_path` like the archives (`loose_keys_fold_like_the_archives`), and two loose files folding onto one key resolve to the lexicographically last path with the collision count warned once, not in `read_dir` order (f26eb657f, #6308; `loose_case_variant_duplicates_resolve_to_the_last_path`); `every_content_provider_resolves_collisions_last_wins` scans providers for a first-wins loop; `count_shadowed_entries` logs shadowing. Within one archive a duplicate normalised name is counted at open and warned, last record wins (#4671, BSA and BA2).\n- File model: no mmap, no `Mutex\u003CFile>` — positional reads via `read_at.rs::ReadAt` (`pread` \u002F `seek_read`), so extracts from one archive run concurrently on Unix (Windows serialises in the kernel, #4999); CSG's mutex guards only its chunk cache. A shared-cursor read reintroduced anywhere is the regression (`concurrent_extracts_read_their_own_entries`, `synthetic_v105_concurrent_extracts_read_their_own_bytes`, `concurrent_reads_get_their_own_chunk_bytes`, #5000). Bounds checks must fail on a replaced archive, not return garbage (`extract_rejects_compressed_payload_too_short`).\n- VDF\u002FACF: malformed input rejected, not half-read (`malformed_documents_are_rejected_rather_than_half_read`); `libraryfolders.vdf` from `steamapps\u002F` + `config\u002F` is unioned; ACF `installdir` is contained under `steamapps\u002Fcommon\u002F` in `installs_in_library` via the host-independent string predicate `is_bare_install_dir` (#4673; widened in aad5dfd75, #5016 — `\u002F`, `\\`, `:`, `.`, `..` rejected so Windows-rooted `\\X` \u002F `D:X` cannot escape; `only_a_bare_directory_name_is_a_valid_installdir`); unreadable manifests are logged.\n\n## Phase 3: Merge and Cleanup\n\nWrite `docs\u002Faudits\u002FAUDIT_PARSERS_\u003CTODAY>.md`: Executive Summary (findings by severity; crates swept; real-data suites run vs skipped), deduplicated Findings, and a Gate Matrix (crate x {size caps, error policy, version gate, corpus sweep, CI lane}). Cross-audit dedup: NIF\u002FESM sizes `\u002Faudit-nif` Dim 1 \u002F `\u002Faudit-esm` Dim 1; `unsafe` `\u002Faudit-safety`; MenuXml rendering `\u002Faudit-ui`. Then `rm -rf \u002Ftmp\u002Faudit\u002Fparsers` and suggest `\u002Faudit-publish docs\u002Faudits\u002FAUDIT_PARSERS_\u003CTODAY>.md` (labels: `import-pipeline` BSA\u002FBA2\u002FCSG\u002FFaceGen, `nifal` BGSM\u002FCDB, `animation` HKX, `ui` MenuXml, `test-gap` gates, `tech-debt` game-detect; `game:*` when title-specific).\n",{"description":7,"argument-hint":39},"--focus \u003Cdimensions> [--crate bsa|bgsm|sfmaterial|hkx|facegen|menuxml|game-detect]","main","6afdb791976ed2c24a6c7e61fcbe0cd1f8776061",".claude\u002Fcommands\u002Faudit-parsers",7,"https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli","matiaszanolli.com","15+ years turning mate into code, and just getting started! All things Python and data lover overall. Music addict, always looking to unite music and tech.","2026-11-11 06:18:12",{"source":8,"skillName":5,"commands":50,"prompt":60,"zipAvailable":61},[51,54,57],{"tool":52,"command":53},"npx","npx skills add matiaszanolli\u002FByroRedux --skill audit-parsers",{"tool":55,"command":56},"bunx","bunx skills add matiaszanolli\u002FByroRedux --skill audit-parsers",{"tool":58,"command":59},"pnpm","pnpm dlx skills add matiaszanolli\u002FByroRedux --skill audit-parsers","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.claude\u002Fcommands\u002Faudit-parsers\u002FSKILL.md",true,[],[64,73,82,91,100,108],{"skillNo":65,"name":66,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":67,"categories":68,"keywords":69,"publishedAt":34},"01M6K028GPYDYZ5441BW6YDFNK","trace-flow","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.zcode\u002Fcommands\u002Ftrace-flow\u002FSKILL.md",[],[70,71,72],"trace flow","trace","flow",{"skillNo":74,"name":75,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":78,"publishedAt":34},"01M6K026GTTXBBXXCH09SDRNX6","session-close","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.zcode\u002Fcommands\u002Fsession-close\u002FSKILL.md",[],[79,80,81],"close","session close","session",{"skillNo":83,"name":84,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":87,"publishedAt":34},"01M6K024KA1JFTV7BGYFSZV2VV","legacy-lookup","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.zcode\u002Fcommands\u002Flegacy-lookup\u002FSKILL.md",[],[88,89,90],"lookup","legacy lookup","Legacy",{"skillNo":92,"name":93,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":34},"01M6K022H36BJ35KB6MT2NHVH6","fix-issue","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.zcode\u002Fcommands\u002Ffix-issue\u002FSKILL.md",[],[97,98,99],"issue","fix","fix issue",{"skillNo":101,"name":102,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":103,"categories":104,"keywords":105,"publishedAt":34},"01M6K0208JHG4WDJX143YKFF0W","audit-ui","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.zcode\u002Fcommands\u002Faudit-ui\u002FSKILL.md",[],[106,107,19],"audit ui","ui",{"skillNo":109,"name":110,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":111,"categories":112,"keywords":113,"publishedAt":34},"01M6K01XPHAPHF9BPS4CG5DDWX","audit-tooling","https:\u002F\u002Fgithub.com\u002Fmatiaszanolli\u002FByroRedux\u002Fblob\u002Fmain\u002F.zcode\u002Fcommands\u002Faudit-tooling\u002FSKILL.md",[],[19,114,115],"audit tooling","tooling"]