Vigil
← All news

v1.12.0 — fault classifier overhaul (mit-aware primary, strict causality, continuous decay)

The death classifier had three structural weaknesses called out in the fault-improvement review: a binary 5s cliff for cascade detection (#9), over-eager cascade attribution that…

Why

The death classifier had three structural weaknesses called out in the fault-improvement review: a binary 5s cliff for cascade detection (#9), over-eager cascade attribution that lumped any preceding death together regardless of type (#3), and mit-awareness as a downstream T-304 patch rather than primary classification (#4). v1.12.0 fixes all three at the root in _death_kind. The disambiguation pass (T-304) becomes a backward-compat no-op for the normal flow; it stays around for fault_scores rows persisted before this ship.

Changed — _death_kind signature and semantics

Old: _death_kind(killing_ability_id, ability_label, preceding_death_in_window: bool) New: _death_kind(killing_ability_id, ability_label, cascade_pressure: float, mit_audit_info: dict | None = None)

Three new behaviors for raidwide deaths:

  1. Mit-aware primary (#4): if a strat plan exists for the killing raidwide's occurrence and mits were missed → mit_failure (full root weight, 1.0). If the plan fully fired → cascade (heal/mit overwhelm despite plan, weight 0.1). If no plan exists → fall through to the preceding-death heuristic. Means raidwide deaths get classified by the actually-load-bearing signal (mit state) rather than the proxy (was there a recent death).
  2. Strict causality (#3): preceding-death pressure only counts deaths whose killing ability is raidwide or aoe_party (raid-wounding). A tank dying to a single-target tankbuster no longer makes the next raidwide death "cascade" — those are independent faults. Closes the over-cascading hole called out in the review.
  3. Continuous decay (#9): preceding-death weight decays linearly from 1.0 at t-0 to 0.0 at PRECEDING_DEATH_WINDOW_MS (5s). Threshold flip at CASCADE_PRESSURE_THRESHOLD = 0.5. Replaces the binary cliff with smooth behavior — but a single death 100ms before still flips cleanly, so the practical "noisy timing" cases are unchanged.

Added — _cascade_pressure() helper

Pure function in analysis/fault_attribution.py. Takes now_ts and a list of (ts, label) preceding deaths, returns the summed decay weight. Reusable + testable in isolation.

Changed — compute_fault_scores_for_fight flow

Loads the T-303 mit audit upfront and builds an ability_id → sorted casts index. For each raidwide death, walks the index backwards from death_ts within RAIDWIDE_DEATH_LOOKBACK_MS = 15_000 to find the cast that most likely killed the player, then passes that occurrence's plan info to _death_kind. Same death→occurrence matching pattern T-304 used; lifted into the primary classifier.

Per-death reasons now carries cascade_pressure (rounded) and mit_audit ({no_plan, missed_count} snapshot) for full transparency on classification choices.

Changed — T-304 disambiguation is now effectively a no-op

Under the new flow, mit_failure is set upstream — T-304's "walk cascades, upgrade to mit_failure when audit shows misses" has nothing to do in the normal pipeline. The function stays for backward compat: fault_scores rows persisted before v1.12.0 still get correct mit_failure attribution if disambiguate_for_fight runs on them. PullDetail's "compute then disambiguate" button now does the work in step 1, with step 2 a confirming pass.

Changed — score formula carries mit_failure × 1.0

Same weight as root, since mit_failure IS the originating fault (the cooldown didn't go out). Constants added: MIT_FAILURE_SCORE = 1.0, RAID_WOUNDING_LABELS = ("raidwide", "aoe_party"), RAIDWIDE_DEATH_LOOKBACK_MS = 15_000.

Changed — fault_scores.reasons JSONB carries mit_failure count

Encounter aggregate (fault_aggregate_for_encounter) carries mit_failure per player. Confidence math counts it as classified (alongside root/cascade/enrage).

UI (web/src/Home.jsx)

"Who's contributing to wipes" table grew a Mit fail column between Roots and Cascades (red when > 0). Updated section description to explain mit_failure semantics.

Tests

  • 9 new in tests/test_fault_classifier_v1_12.py: cascade_pressure decay math, strict-causality filter (tankbuster doesn't contribute), pressure summation, threshold-flip behavior, missed-mit → mit_failure directly via T-302, tankbuster-then-raidwide produces two roots not one+cascade.
  • _death_kind pure tests updated in tests/test_fault_attribution.py to the new signature: False/True boolean preceding-death replaced with 0.0/0.9 cascade pressure; 4 new tests for the mit-aware path (missed/fired/no_plan).
  • Seeded-fixture tests updated: test_compute_classifies_root_vs_cascade now expects 2 roots + 1 cascade (was 1+2) because the leading tankbuster no longer cascades the follow-up raidwides — exactly the strict-causality outcome.
  • T-304 disambiguation tests rewritten to verify the final state (after full pipeline) rather than asserting T-304 did the upgrade itself. The fixture's missed-mit scenario still produces mit_failure; the path is just upstream now.
  • All 4 fault_disambiguation tests pass alongside the new T-302 mit-aware tests, proving the backward-compat no-op behavior works.
  • 439 tests passing (426 → 439, +13).

No schema migration. No new env vars. Old fault_scores rows remain readable; running compute-all on them regenerates with v1.12.0 semantics.