The two-round adversarial review (5 parallel reviewers per round over both repos; see the 07-19 Framework-Review row) fixed every HIGH-severity finding the same day. This file tracks what was found, verified real, and deliberately deferred — so the list survives outside the session that produced it. Ranked by when they’ll bite.
convertRig flag?_loc0 and convert_rig) — a
legacy model with location keys + shape keys no longer aborts, and legacy means no rig manipulation. The
location-STRIP stays on BOTH paths deliberately: every verified legacy bake (drone, howitzer) went through it, and
un-stripping risked re-introducing the drone’s unscaled-translation wobble. Rationale: legacy rigs have a sane rest
by definition, and for them the fold was a near-no-op (frame-0 pose ≈ rest) — so gating it off converges on the
same output. Bake-level verification DONE (same day): smoke test 14/14 with the howitzer fresh-baked
animated-legacy through the gated pipeline. In-game verification DONE (2026-08-02) — the howitzer checked out
correctly after a real re-bake.ENTRY-STATE COHERENCE (user verdict 2026-07-26, tread-saga fallout: “this seems like a serious configuration bug”) — an entry’s config lives in FOUR places (Factory window memory, Animation Lab memory, the DEPLOYED pack.json the editor reads as its registry, the project dual-write copy) and the reconciliation rules ambushed the user repeatedly in one afternoon: (a) a stale Factory Model-file field silently baked the WRONG MODEL (the translation-test cube overwrote a good Jagdpanzer bake); (b) animated→static downgrade is IMPOSSIBLE without Remove — the bake-time ownership rebase resurrects the saved animation config even after Reset, and the animated pipeline then hard-fails on an unrigged file; (c) “Reduce to ~tris (0 = off)” silently substituted 12,000 on the animated path for years (FIXED same day); (d) external registry edits are detected by the Lab (yellow banner) but not by the Factory. Proposed fixes, in impact order: (1) Factory gets the Lab’s outside-change banner + a bake-time confirm when its Model file differs from the registry’s; (2) a real animated→static path: when the file has no armature, offer “strip animation config and bake static” instead of failing; (3) document (or collapse) the two-pack.json design — the DEPLOYED copy being the editor’s source of truth surprises every external tool; (4) audit remaining “label lies” like the tris slider (grep defaults substituted behind explicit user values).
deploy_convert.py recoil blocksrc_w when
its name isn’t barrel/cannon (was a guaranteed KeyError on non-M114 naming); (b) the RecoilArm holds now key an
IDENTITY BASIS (true pass-through at any parent pose) and the arc targets build on a parent-aware pass-through
baseline, so a parent chain that moves during the deploy no longer displaces the tube; (c) empty tube match now
fails loudly listing the animated part names; (d) dead key_bone removed. NOTE: the shipped m114_deploy.glb was
generated by the OLD code and stays as-is (verified in-game); the fixes matter for the next artillery-style model.ConfigForModelFactoryWindow.ConfigFor like the smoke test, so convertRig/rotation/keep-flags all
carry and the soldier is exercised on the conversion pipeline it actually ships on.SweepAllOutputs (the full
OutputSuffixes union, now incl. _ClipsPoseData.bytes) runs at the start of BOTH paths, so an animated↔static flip
leaves no orphans in shipped Resources; the E5 rollback and the Feature-Test cleanup cover the pose bytes too, and
the animated path gained the static path’s up-front resource-name validation.RearmModelRegistration now nulls distFxManager
and every entry’s plbc/privateLeaf/leaves/collected; DistrictApplyEntries re-derives them as the new
session loads. Verify alongside the model-axis second-session test.PoseNames/BoneRotationNames (was "Pose"+i strings per pawn per frame); the pose hook’s
descId fallback is a plain loop (was a ctx-capturing lambda per pawn add); ProcessFireQueues prunes with a reverse
for-loop (was a dur-capturing RemoveAll closure per entry per frame); ProcessEngineAudio throttles FIRST and
caches its filtered subset keyed on the entries reference (was Where().ToList() 60×/s); TickOne hoists the
field-name array and skips the 7 texture re-sets when _MainTex is already ours (re-set kept as the recovery path
when the game recreates the material); the [Grey] no _MainTex retry warns once; the audio-trace postfix gained
the try/catch every other patch body has. NOT done (deliberately): GetMember boxing elimination — it needs typed
delegates over reflected structs, high risk for marginal gain; revisit only if profiling shows it matters.
VERIFIED in-game same day: full animation sweep clean including the drone attack (fire-once path — exercises
the queue prune, the descId-fallback loop, and the pose-name arrays in one action). Residual: informally watch a
BIG late-game battle for stutter (the improvement claim, as opposed to the no-regression claim).deployProgress/deployLastPos/customSources/loopHoldUntil/engineLastPos/engineMoving, plus static
deployMoveState and respawnBase/respawnCount) clear on session re-arm, and deployLastPos joined the
in-session deploy prune. Remaining in-session growth of the engine-audio maps folds into the perf pass above.Retex_
entry for a pawn that already has a model entry (two entries, same pawn, undefined winner)._pending isn’t serialized — a domain reload drops staged, unsaved edits.ApplyTint no longer wipes the clipboard. Still
open: invalid impact-donor GUID silently ignored; muzzle swapped beyond the tooltip’s documented scope.FindType is cached (the per-repaint full-AppDomain scan is gone) and null
Amplitude GUIDs now fail the bake with the rebuild-then-re-bake guidance instead of writing zero-GUIDs.ExitGUIException is rethrown before the generic catch.LoadOrderedAlbedos drops no-map_Kd materials (index shift → wrong rects) and
can’t load .tga (red placeholder) — the static path handles both.models is empty; count
truncation via min(pd,skel,atlas); early-entry key omission misaligns later entries; resourceName default differs.animUnitFix announcement is discarded by the ownership merge for existing entries; case-sensitive
Upsert/Remove matching (case-only rename → twin entries); atlasGuid never validated; _ClipsPoseData.bytes
missing from the E5 rollback + Feature-Test cleanup listsReadToEnd pipe-deadlock pattern; texture leaks on bake failure paths; corrupt-registry error-spam from
per-OnGUI Load() in Retexture/Sound windows; ParseWav negative chunk-size guardblend_export.py repoints packed images it shouldn’t; prep_model strip matches object names only (not mesh-data
names, unlike deploy_convert); AtlasDebug likely double-converts in a Linear-color-space project; RefreshList comment
contradicts the settled Factory-lists-all design; 3-strike registry give-up latches per-process (“this session” log
text is wrong); Hk_AudioTrace postfix unguarded + per-event string scans; 4u fire-radius / 3u deploy-match adjacency.HumankindAssetFramework.dll (csproj FILE name kept — local clones, build docs and
the CLI compile-check unchanged), BepInEx GUID → community.humankind.haf (old cfg copied to the new name on this
machine, old DLL removed from plugins in the same deploy — BepInEx would load both and double-patch), editor menu
root → Tools ▸ HAF (all windows + Tech Tree + Database Browser consolidated under it; Tests submenu intact),
instructional docs swept (Framework-Review’s dated history rows keep their period-correct Tools ▸ ENC paths).
Deliberately NOT migrated (framework/pack split, decided 07-14 and reaffirmed 07-19): haf_models.json /
haf_sounds / haf_skins are ENC-the-PACK’s files — packs are branded, only the framework is neutral, and a
third-party pack never touches an haf_* path. Verified in-game same day (first session clean: new identity
loads, settings carried, units/districts/audio normal). Still open for the package release: hardcoded paths,
package scaffolding. (The ENCAccessProof C# namespace + project filename were renamed to HumankindAssetFramework on 2026-08-01; the local repo FOLDER is the last leftover — a manual git mv/reclone when convenient.)modId, missing dependsOn, cycles, conflicts →
clear warnings + haf_load_report.txt), and bad input fails soft (never crashes). But there’s no entry-level
content validation: a wrong bone name, an unresolvable GUID, or a missing texture path degrades silently rather
than producing a “pack X, entry Y: bone Z not found” message. For a distributable framework this is a real
barrier to entry for external authors. Build a pre-flight linter (editor button + a boot-time pass) that checks
each entry’s referenced assets/bones and reports mismatches in plain language before render. Fits the “guided, not
guessy” design goal; scoped for the package phase. (Raised by an external review 2026-08-02; the structure half was
already done in the 07-14/07-19 multi-mod work.) Designed — see
Pack-Validator-Design.md (what to validate, editor vs boot-time surfaces, message format,
phasing); build remains.TREADIZE v2 — hybrid link/shuttle rig (user’s design, 2026-07-26). On a straight run every link moves identically → ONE translating shuttle bone can carry the whole run (pattern maps at restart); per-link bones only on the WRAPS + RAMPS where links genuinely rotate. Bone math: Bradley ~23/track at full per-link wrap detail vs 75 today — quarter-link wrap smoothness inside half the budget. Skirted vehicles: the hidden top run can be fully STATIC (zero bones). The one risk is the two run↔wrap seams (static skin weights can’t switch carriers) — mitigated by everything v1 learned: seams AT the tangent points, where a wrap link’s velocity equals the run direction, speed-matched on the exact belt path. Prereq: none — build whenever tread bone budgets start pinching again (or for the twitch-ceiling escape).
Textures/ set).
Today the bake produces a SINGLE albedo atlas and the runtime injection NEUTRALIZES the donor’s PBR (flat albedo). The
albedo half of a source set is already consumable — bake the BaseOp down onto the game-mesh UVs — but the
_Normal maps are not processable at all; that is the missing pipeline. To render surface detail the Factory would
bake a matching normal atlas repacked to the combined-atlas UVs, and the injector would wire it into the pawn
material’s normal slot (_BumpMap) instead of clearing it.
What “fully process Ehrhardt_E_V/Textures/” actually takes (read off the shipped files, not hand-waved):
T_..._C_V*_Normal.1001–1005) and the gun a single tile
(T_..._G_V1_Normal); assemble the UDIM set into one image before repacking. (Same assembly the albedo/UDIM note
below needs — build it once, feed both maps.)TextureImporterType.NormalMap, normal-safe
compression + mips; a naively-imported normal atlas is read as colour and lights wrong.BaseOp + Normal, so this composes with the
runtime-retexture-variant axis (one skeleton/atlas, swap the pair per descriptor).
Priority moderate: at map zoom (~80px units) the payoff is subtle — but this is the concrete build if/when we want it,
and the Ehrhardt set is the ready test bed. Escape hatch today: bake the normal into the albedo’s lighting in Blender
(static, no runtime normal response) — cosmetic only. (If a source set also ships ORM/roughness/metallic, the same four
steps extend to a packed ORM atlas + the material’s metallic/smoothness slots.)
Related same-bucket gap — UDIM / multi-tile ALBEDO: the bake assumes ONE texture per material in a single 0–1 UV
tile, so the armored car’s cinematics mesh + its 5-tile .1001–.1005 UDIM camo can’t be consumed directly. Escape
hatch is the same manual Blender texture-transfer bake onto the single-tile game UVs. NOTE: a mesh authored with
single-tile UVs (the armored car’s game mesh) needs none of this — it bakes fine on the current flat-albedo path.TurretizeAimLayer runtime handler: turretBone (substring) + turretAxis (Lab dropdown) retarget the
streamed heading slot onto our turret bone. Axis is per-model (Ehrhardt: 2 = yaw; 1/0 = pitch — the pitch axis is
the future artillery-barrel elevation knob). Original design notes retained below for the static-model corollary.[Aim] log in ClearAimLayer shows the
stream). The feature is therefore an ADDRESS REWRITE, not an aiming system: an aimBone registry knob (name
substring on our skeleton, the handPropBone pattern) + a remap mode where ClearAimLayer currently drops the
entries — rewrite their bone index to ours, with an axis/offset knob (donor axis conventions won’t match every
model; stamp explicitly, the props import-angles lesson). Open: does elevation stream separately from traverse
(second bone)?; does the sim only stream for donors it considers aim-capable (a donor-matching criterion)?
Intended first test candidate (2026-07-24): an Ehrhardt‑style armored car (“Ehrhdrdt E V” by Red Blue Pixel
Studio, Fab, Standard License, FBX + PBR) — it ships already rigged with a turret bone, so it’s the EASY case
(point aimBone at the existing turret bone; no auto-rig step). Bake static first, then remap the aim stream onto
the turret bone once the feature lands.
The static-model corollary (“turretize”): this gives STATIC models a tracking turret with zero animation
authoring — split turret from hull (part-name detection exists), auto-create a 2-bone rig at the turret pivot
and bind each part full-weight (the mech bone-parent→skin conversion’s exact mechanics, just with created bones),
bake through the animated path with a 2-frame identity clip (the held-stance pattern), then remap the aim stream
onto the turret bone — the ENGINE animates the aiming, same as vanilla armor. Reactive motion (aim/facing) never
needed clips even in vanilla; only cyclic motion (walks, bobs) does. Open extra: pivot placement quality
(auto part-centroid vs a manual nudge knob).silenceDonorGroundFx, spotted 2026-07-24). Ground effects ride the DONOR like
audio does: the Light Assault Mech (legged) stamps WHEELED TRACK decals from its APC donor. Fix = the donor-audio
pattern, not a re-donor (animal donors are melee-presentation pawns — swapping would break the mech’s ranged fight
infrastructure): find the track/decal emitter chokepoint (likely MecanimEvent- or movement-state-driven FX on the
sub-pawn — the same neighborhood the audio investigation mapped) and gate it per opted-in unit. Later composable
with a “replace with footprints” mode. Adds GROUND FX to the donor-matching criteria list (rotor/wheels, audio,
ranged capability, aim streaming, now decals).1751b74 “muzzle endgame lands — flash, smoke and tracers on the tracking turret”; was its own scoped session).
Implemented as the muzzleBone field + Hk_MuzzleRelocate prefix on PresentationSubPawn.GetBoneTRS(string) — see the
cracked mechanism + fix below; ArmouredCar set to muzzleBone: "Turret". The flash now anchors on the turret/gun on
fire. (If a turret pivot ever reads too low/centre on another model, pick a barrel-tip bone instead.)
On the Ehrhardt armored car the MG muzzle flash fires off-side (“mirrored”). ROOT CAUSE (verified): the donor is
Unit_Era6_Common_AntiAirGuns_01 (an anti-air gun — bones Azimuth, bras-*, Canon_down_*), and the flash is
the projectile’s Muzzle FxEvolverMaterial (“launch flash”, ProjectileAsset.muzzle) — a TRANSIENT VFX (NOT a
fragment; every donor lists only its body mesh) spawned at the AA gun’s Canon weapon socket, which doesn’t exist
on our renamed b###_ rig → it lands off-side. The spawn is NOT in PawnRangedFightSequence (stores the shooter
only) nor PresentationPawn (3525 lines, no muzzle/socket) — it’s buried in the HgFx projectile/particle
system. CHAIN TRACED (2026-07-24): the projectile+muzzle fire from a FireProjectile mecanim event on the
attack clip – PresentationSubPawn scans the clip for MecanimEvent.AlterationType.FireProjectile and stores it
as SimpleAttackMecanimEvent (~L1255-1267), processed by MecanimEventInterpreter (Amplitude.Mercury.Animation)
as the clip plays. The bone->world resolver is PresentationSubPawn.GetBoneTRS(boneName) (~L378:
GetBoneIndex(boneName) -> AnimationManager.GetBoneTRS). The AIM layer resolves the SAME way (SubPawn ~L639/657:
GetBoneIndex(reference.BoneName) – the donor’s Azimuth/Canon names), so the muzzle socket almost certainly
resolves by the donor’s weapon-bone NAME -> invalid on our b###_ rig -> off-side. Fire info via
IAlterationFireProjectileInfoProvider (SubPawn L179/813 = the pawn). NEXT: decompile MecanimEventInterpreter’s
FireProjectile handling (NESTED-type friction with ilspycmd 8.2 -> use dnSpy or a newer ilspycmd) to pin the
socket-NAME source + the muzzle-FX spawn call. QUICK-ALT CAVEAT: the ProjectileAsset is SHARED across all AA guns,
so nulling its Muzzle in place breaks the real anti-air units -> needs a per-unit projectile OVERRIDE.
✅ MECHANISM FULLY CRACKED (2026-07-24, decompiled Assembly-CSharp whole). AlterationFireProjectile.StartEvent
(the FireProjectile alteration handler): TRS boneTRS = controller.SubPawn.GetBoneTRS(mecanimEvent.ParentNameToLaunchVFXPosition);
Vector3 startPosition = boneTRS.Transform(mecanimEvent.PositionToLaunchVFX); then
PresentationProjectileManager.Instance.LaunchMuzzle(projectileAsset, startPosition, startDirection, up) (or
LaunchProjectile for the flying shot). So the muzzle position = SubPawn.GetBoneTRS(<donor socket name>).Transform(offset)
— and ParentNameToLaunchVFXPosition is the DONOR clip’s socket name (the AA gun’s Canon socket), absent on our
renamed rig. THE FIX (low risk): Harmony postfix on PresentationSubPawn.GetBoneTRS(string boneName) — for
our unit (match SubPawn→entry by SkeletonId, GetEntryBySkeletonId exists) with a muzzleBone set, when
Skeleton.GetBoneIndex(boneName) < 0 (donor socket not on our rig), replace __result with GetBoneTRS(ourMuzzleBone)
(our bone IS found → no re-redirect → recursion terminates). Config = muzzleBone (substring, e.g. Turret or a
central bone), runtime-only. Broadness note: this redirects ALL unfound-socket VFX on our unit to muzzleBone,
which for a donor-mismatched rig is the DESIRED behavior (all its VFX land on our gun instead of off-side). QUICK ALT (no relocate): null the
projectile’s Muzzle → no launch flash (Projectiles.md already documents clearing it). Note: this donor is one of
the few that fire MULTIPLE times (AA burst) so the flash repeats. General lesson recorded: a donor’s effect = its
skeleton + weapon sockets (already half-logged: donor.Skeleton / BoneInfos / donor fragment[N]). The new
Disable override flag (ModelDef.disabled, runtime) A/B’s our model vs the raw donor for exactly this kind of probe.socketBones) — ✅ VERIFIED IN-GAME 2026-07-24 night (ArmouredCar): flash, smoke AND tracers
all on the tracking turret. The winning recipe: socketBones: "Canon_Up_left=MW_T;Move_bloc=MW_T" (socket
ROLES decoded from the pin log: Move_bloc = fire POSITION anchor, Canon_Up_left = rotation/direction — not
what the names suggest) + runtime donor-offset compensation on native socket hits + the muzzleOffset world
dial ("0,2.6,0" — the rig’s gun-bone head sits at the model base, and the socket’s correct BIND height
provably does not reach the runtime pose; open engine question, the dial closes it empirically, no re-bake per
step). War-story hazards now guarded: prefix reentrancy (stack-overflow crash), the external-registry-edit slim
cache trap, per-shot log throttled to once-per-entry after calibration. Wired
end-to-end: rig_anim argv[11] (exact-named zero-weight leaf bones after the rename, before the fold; A###_
prefix on socketed models; loud failures for unmatched parents and sort-order violations), BakeConfig/ConfigFor/
slim-cache diff, Lab “Donor sockets (bake)” field, ModelDef.socketBones (bake-time; guard PASS). The ArmouredCar
entry is pre-configured (Canon_Up_left=MW_T; Move_bloc=Root) — next session: Unity recompile → re-Bake →
rebuild → fire: flash, smoke AND tracer origin should all sit on the (tracking) turret gun natively. Original
design rationale below. The interception chain
(GetBoneTRS redirect → StartVFXEvent pin → offset compensation) moved/killed the FLASH but smoke + tracer origin
still read the donor socket, and the compensated TRS raised a space question (flash vanished off-screen). The
correct architecture: bake EXACT-NAMED donor socket bones onto our rig (socketBones: "Canon_Up_left=MW_T;...",
zero-weight leaves, optional tip offset) so the game’s own lookups resolve NATIVELY — flash, smoke, and bullet
origin all correct-by-construction and turret-following. Wrinkle: Amplitude sorts bones alphabetically requiring
parents-first — socketed models switch the rename prefix b###_→A###_ so every real bone precedes any donor
name (gated; existing bakes byte-identical). Obsoletes muzzleBone for rebaked models; the runtime knobs stay for
quick fixes. Donor socket names discovered via the [Muzzle] GetBoneTRS diagnostic (armoured car donor asks for
Canon_Up_left + Move_bloc).RotationTranslation clips
(decompiled: vanilla tank treads/shuttle bones; GetPoseTRS zeroes translation only for Rotation-encoded
curves) — Laws 1/5 were OUR bake’s strip. Built: per-model keepTranslations (registry + Lab toggle), kept
curves scoped to the attack clip, delta-rebased, ×100 sandwich-compensated on the legacy path; multi-segment
recoil windows with /N speed steps. Verified end-to-end twice: a sliding test bone, then the M114’s real
kickback (recipe 442..530,305..441/2, Return 0, Slam 0). Root-caused en route: the slam-0 R=1e9 sentinel
put the RecoilArm pivot at a billion units → float32 chain collapse → every historical NaN import warning.
OPENS: treadize (tank tread shuttle bones — design ready, Jagdpanzer waiting), real deploy translations,
whole-carriage recoil, soldier run-bob restoration.donorOff= 0.80/0.85/1.20) from the single
Move_bloc anchor; the compensation currently flattens all onto one point. (1) Barrel variation — subtract
the MEAN donor offset instead of each event’s own: flashes scatter slightly around the muzzle like the donor’s
real barrels, essentially free. (2) Multi-mount fire — rotate successive fire events across several of the
model’s own gun bones (the Ehrhardt has four rigged MG mounts, MW_B/F/L/T) — needs per-event socket selection
state; bigger. Both are polish on a verified base, not fixes.SKM_ rips carry their own armature — two skeletons
in one GLB), @file part lists (the ~32 k Windows command-line limit), Blender 5.x Action.fcurves removal
(curves live in layers→strips→channelbags), spin-sign rule (+360 = forward for a +X nose), review UI
(6 roles incl. Edgecase, keyboard marking, classification filter, 4 hide sliders), JSON recipes, and a
clustering-accurate Verify report. Generated-rig calibration: turret axis Y, sockets/muzzle bone →
Turret, offset re-dialed from the dome center. SKM fast path — BUILT same day, preview-verified:
probe detects skeleton + ≥90% weights → bone-marking mode → rigfast spins the SOURCE bones (local axle axis,
signed for mirrored rigs), artist skeleton shipped unchanged (pivots + MW_* socket bones free). Field
finding: it inherits the artist’s weighting — the Ehrhardt’s front steering knuckles are weighted to the wheel
bones and rotate with them, so the shard path stays the quality reference (the shipped unit uses it); the fast
path is the four-checkbox route for clean-weighted rips. Original spec below. The Ehrhardt’s
_Spin.glb was hand-made in Blender (now documented step-by-step in Animated-Models.md); the tool version is the
missing sibling of turretize and the biggest lever on the “huge pool of static vehicle models” thesis: a headless
Blender script that (1) detects wheel parts — name pattern wheel|tyre|tire first, geometric fallback (cylindrical,
near-ground, mirrored pairs — the organ-gun classifier’s approach), (2) creates Root + a bone per wheel at each
part’s centroid (+ a Turret bone for a turret-named part), rigid full-weight skinning, (3) generates the LINEAR
Spin action (frame 0 = rest), (4) exports <name>_Spin.glb. Factory affordance: a “Prepare static vehicle…”
button that runs it and repoints the Model file. Output feeds the EXISTING verified path (Spin[0..0] idle +
Spin slice movement + convertRig + auto-ground + turretize/sockets). Risks: wheel detection on messy meshes
(single-mesh models need loose-part separation), axle-axis inference (mirrored left/right wheels spin opposite
if the axis flips — normalize to model-space).clipDeath) — play the model’s own death animation on PresentationPawn.TriggerDeath (the
hook already fires for the death SOUND; arming a one-shot clip window from the same seam is the pattern the
attack clip proved). Proving model: the gray wolf’s idle injured to dead reaction lft/rgt (private test rig).idlePatrolRadius/
idlePatrolSpeed): offset ObjectSpace.Translation along a slow closed loop (the position-offset path already
writes Translation per frame), play the MOVE clip, face the path tangent (needs an ObjectSpace.Rotation write —
read-only today). Risks: stride matching (path speed vs walk-clip foot speed, or it ice-skates), yielding to
every real state, battle second-PresentationUnit interactions. Composes with idle-alt: stroll → pause →
howl/eat → stroll.GUID nibble-swap encoding + keep-GUID re-bake; registry corrupt-guard/atomic-write/backup lifecycle; two-window ownership merge (both directions, post-fix); Harmony patch exception discipline; cross-thread sample locking + ConcurrentQueue handoff; deploy ramp math; join/decimate + albedo-extraction blocks; frame clamping; noise-filter re-entrancy; district bake+registry editor flow; Plugin.cs config wiring.
Ten open findings from the pass that followed the Abomination spike-geometry incident (root cause: a safety net that could never arm itself). Recorded separately with file:line, in-game symptom, trigger and suggested fix: Audit-2026-07-31.md.
Top item (now FIXED in c6154a6, pending in-game verification) — the wrong-skeleton rescue was gated on Hooked (animated-or-freeze), so eight shipped STATIC models have
no rescue path at all: the same failure 0c0b12f fixed, still live for them.