v1.4.0 — cactbot Stage 2 (per-pull expected-vs-actual timeline diff)
- **`analysis/timeline_diff.py::timeline_diff_for_fight(session, fight_id)`** computes per-pull expected-vs-actual drift. For each cactbot-annotated canonical mechanic in this enc…
Added — per-pull timeline diff
analysis/timeline_diff.py::timeline_diff_for_fight(session, fight_id)computes per-pull expected-vs-actual drift. For each cactbot-annotated canonical mechanic in this encounter'sfight_model, finds the closest matching boss-cast event in this pull's events (byability_game_idwithin the phase window), reportsexpected_t_ms,actual_t_ms,drift_ms, and afired: boolflag. Cosmetic abilities filtered (not actionable). Per-phasemedian_drift_mssummary surfaces the "everything shifted +Ns after a slow P1" cascade signal in one number.- API:
GET /api/fights/{fight_id}/timeline-diff— returns the diff payload. - React
TimelineDiffcomponent added below MitAudit in PullDetail. Collapsible (▸/▾). Per-phase header shows{fired}/{total} fired+ median drift colored by magnitude (green ≤500ms, yellow ≤2s, red >2s or missing). Each row: mechanic name, type, expected time, actual time, drift. Missing mechanics highlighted with a pink row + "did not fire" cell. - 8 new unit tests (
tests/test_timeline_diff.py): unknown fight, empty fight, fired-with-drift, missing mechanic, cosmetic filtered, multi-cast closest-match, no fight_model, per-phase median drift. 334 tests passing (326 → 334).
Live AC — FRU kill fight 1500
P1: median drift +1.2s (clean, normal cast-fire offset)
P2: median drift +10.9s ← cascade: P1 ran long, all P2 mechanics shifted +10.9s
P3: median drift -14.0s (P3 mechanics earlier than expected)
P4: median drift +7.8s
P5: median drift +11.0s
The "Phase 1 ran +Ns long → all of P2 shifted late" diagnostic surfaces directly as a single per-phase number. Working as designed.
Known limitations (Stage 2.1 candidates)
- Multi-cast assignment. When an ability fires N times in a pull and has M fight_model rows, each row picks the closest cast independently — competing rows can both pick the same cast or pick distant casts. Visible on FRU P1 "Sinsmoke" showing +42.8s drift (algorithm picked the 2nd cast for what should match the 1st). Hungarian-style optimal assignment would fix it.
- Adds-phase abilities in FRU phase 2 don't get cactbot labels — phase index alignment between T-103 and cactbot timelines isn't always perfect for short transition phases. Not blocking the headline cascade-drift signal.