Vigil
← All news

v1.16.0 — Fault scoring refinement sweep + heal_failure attribution

v1.14.6 left the scoring stack at four multipliers (phase × within × prog × repeat) — defensible per factor but compounding wildly (a single freak wipe could land 23× a baseline).…

Why

v1.14.6 left the scoring stack at four multipliers (phase × within × prog × repeat) — defensible per factor but compounding wildly (a single freak wipe could land 23× a baseline). A design pass identified eight friction points; this ship lands seven of them as mechanical cleanups plus a new heal_failure classification the user asked for: when a raidwide kills a player but mits successfully fired, the failure was on the healers (the raid wasn't topped), not the dying player.

Changed — fight_score_multiplier capped at 8× combined

analysis/fault_attribution.py. Phase × within × prog now clamps at COMBINED_MULTIPLIER_CAP = 8.0 before the repeat amplifier composes on top. P5 wipe at 5% HP at the wall used to compute 4.55×; cap doesn't bite. P7 wipe at 1% HP would have been 7.5×; still fine. P9 hypothetical → cap bites. Repeat amplifier (cap 5×) means a worst-case combined is now 8 × 5 = 40× per wipe, vs the prior uncapped composition that could exceed 100× in pathological corners.

Changed — continuous prog_distance for prog relevance

_prog_relevance(phase, best_phase, fp) now uses _prog_distance(phase, fp) = phase + (1 - fp/100) and exp decay max(0.3, exp(-0.3 * delta)). Previously phase-only delta with linear decay (max(0.3, 1.0 - 0.15 * delta)) had a sharp cliff at each phase boundary: P4 fp=1% (almost cleared the phase) and P5 fp=99% (just entered) both delta=1 → 0.85×. Now P4 fp=1% with wall=P5 → prog 4.99 vs 5.0 → 0.99×. Smooth. Side benefit: multi-boss phases (DSR P4 / P6) where fp is the lagging boss's HP no longer cliff-jump.

Changed — repeat_offender_multiplier cold-start floor + mit_failure coverage

  • Denominator now max(wipes_attended, REPEAT_RATE_MIN_DENOMINATOR=20). Old: 1 offense in 2 wipes = 50% rate → capped 5× multiplier. New: max(2,20)=20 → 1/20 = 5% rate → exp(0.2) ≈ 1.22× nudge. Once attendance exceeds 20 the floor stops applying.
  • Past-wall counter now sums roots + mit_failures together. A mit lead who keeps dropping Reprisal post-wall triggers the amplifier same as a serial root offender. Field past_wall_roots renamed to past_wall_offenses in the aggregate response (frontend updated).

Changed — avoidable damage per-hit floor

_avoidable_damage_by_player skips tankbuster events under AVOIDABLE_DAMAGE_MIN_HIT = 50_000. Tankbusters splash to neighboring rows for trivial amounts (a few thousand HP); those tiny ticks used to accumulate against whichever non-tank caught the AoE wash. 50k is roughly a third of a base raidwide hit at current iLvl — clearly intentional impact, not splash.

Added — heal_failure death classification

analysis/fault_attribution.py.

  • _death_kind returns heal_failure when ability_label == "raidwide" AND a mit plan exists AND it fully fired with no missed mits. Reasoning: if mits fired, the raidwide should be heal-survivable from full HP; if it killed, HP wasn't topped → healers missed the recover.
  • compute_fault_scores_for_fight then identifies the healers (jobs in HEALER_JOBS) and their death timestamps, computes who's alive at each heal_failure death's ts, splits HEAL_FAILURE_TOTAL_WEIGHT = 1.0 evenly across alive healers as heal_failure_caused_score. Dying player gets HEAL_FAILURE_VICTIM_SCORE = 0.0 for this death (they're counted in heal_failure count but contribute nothing to score). Per-incident attribution is persisted in reasons.heal_failure_incidents for future drill-down.
  • Edge: if 0 healers alive at death_ts (both dead), falls back to cascade — chain too broken to blame healers fairly. If only 1 healer alive, that healer takes the full 1.0.

Added — per-wipe score decomposition

Aggregate response now carries a worst_wipes array per player (top 5 by weighted contribution) with fight_id, last_phase, fight_percentage, best_phase_at_time, raw, phase_severity, within_phase, prog_relevance, repeat_multiplier, weighted. Home FaultSection row expansion renders this as a table at the top: "Top 5 worst-weighted wipes (raw × multipliers → weighted)" with each multiplier shown as Nx. The user can finally see WHY a row scored where it did — particularly useful for "why is Bob above Alice when she's eating more avoidables".

Changed — FaultSection table columns

web/src/Home.jsx. Cascade column dropped from the main table (cascade is 0.1 weight — it looked misleading at-a-glance next to roots in headline view). Replaced with Heal fail (heal_failure_caused). Both cascades and the dying-side of heal failures surface in the row expansion under "Also tracked but not score-relevant". Intro copy rewritten to explain the v1.16.0 mechanics + nudge users at the mit audit section for the mit-side view.

Tests

  • 4 new in tests/test_heal_failure_v1_16.py: 2-healer 50/50 split; 1-healer-dead → other healer takes full 1.0; both-healers-dead → falls back to cascade; amplifier counts mit_failure.
  • 1 new in tests/test_fault_repeat_offender_v1_14_6.py for the cold-start floor behavior.
  • 6 updated for renamed field (past_wall_rootspast_wall_offenses), new formula values (prog relevance exp decay vs linear), and renamed test (raidwide_mits_all_fired_is_cascade_is_heal_failure).
  • 499 tests passing (495 → 499, +4 net visible after renames).

Side cleanup — non-destructive test_roster_api

tests/test_roster_api.py had an autouse fixture _clean_roster that ran delete(Member) globally after each test. On a dev DB with real user-curated roster data this blanket-wiped legitimate members. Scoped the cleanup to a TEST_MEMBER_NAMES allow-list and ran it both before and after each test so test_empty_list doesn't flake on residue, and renamed members[0] indexed accesses to name-based lookups for the same reason.

Operator note

Existing fault_scores rows persisted before v1.16.0 still read correctly (the v1.16.0 reasons schema is additive — new fields default to 0/empty when absent). To get heal_failure attribution and per-wipe decomposition on existing wipes, click "Analyse all wipes" on Home (bulk recompute). New wipes ingested going forward get v1.16.0 semantics automatically.