v1.17.0 — Canonical encounter unification (cloned-encounter merge)
FFLogs occasionally cuts the same logical encounter into a new `encounter_id` after a re-cut. DSR today is split across **1065 (legacy)** and **1076 (current)**. Before this ship,…
Why
FFLogs occasionally cuts the same logical encounter into a new encounter_id after a re-cut. DSR today is split across 1065 (legacy) and 1076 (current). Before this ship, every per-encounter analytics function filtered Fight.encounter_id == X, so a fight_model built for 1076 only learned from 1076 reports — missing ~27 field reports / 15 kills_with_events that live under 1065. Same problem will hit any future re-cut. Project scope per memory: all ultimates + Dawntrail savages — DSR is in-scope, so the gap was material.
Added — analysis/_encounter.py
Two helpers + a tiny lookup table:
canonical_encounter_id(eid)— returns the canonical ID for any member of a cloned group (1065 → 1076), oreidunchanged.encounter_id_group(eid)— returns the sorted tuple of every ID in the group (e.g.(1065, 1076)), or(eid,)for non-cloned._CLONED_GROUPS: tuple[tuple[int, ...], ...] = ((1076, 1065),)— append to this when FFLogs re-cuts another encounter. First element is canonical, remaining are legacy aliases.- 7 unit tests in tests/test_encounter_canonical.py.
Changed — analysis layer unions across the cloned group
Every encounter-scoped analysis function now uses Fight.encounter_id.in_(encounter_id_group(X)) instead of == X, and uses canonical_encounter_id(X) for FightModel / StratConfig reads and writes (those tables live at the canonical ID only). Updated modules:
- analysis/consensus.py —
consensus_timeline_for_encounter,write_consensus_to_fight_model,read_fight_model. Consensus now learns from kills under EITHER half of the group; writes only to canonical. - analysis/cartography.py —
cartography_for_encounterunions fights + scoped watchlist queries. - analysis/mechanic_classifier.py —
_kill_fight_ids+classify_canonical_abilities. - analysis/dps_check.py —
dps_check_for_encounter,dps_comparison_for_encounter,compare_fight_to_target. - analysis/mit_audit.py —
_raidwide_casts(fight_model lookup), strat_config lookups,mit_audit_aggregate_for_encounter. - analysis/fault_attribution.py —
classify_wipe_type,_aoe_party_casts,compute_fault_scores_for_fight,fault_aggregate_for_encounter(5 sites total). - analysis/fault_breakdown.py —
fault_breakdown_for_encounterunions FaultScores across the group. - analysis/fault_disambiguation.py —
_raidwide_ability_idsreads at canonical. - analysis/consistency.py —
_our_fight_ids+consistency_for_encounterreads. - analysis/prog_trajectory.py — our-sessions + field-distribution queries union across group.
- analysis/optimization.py —
_our_kill_fightsunions across group. - analysis/death_inference.py —
build_inference_contextreads fight_model + cactbot timeline at canonical. - analysis/timeline_diff.py —
timeline_diff_for_fightlooks up fight_model + cactbot timeline at canonical. - analysis/strat_config.py —
list_for_encounter,get_one,upsert,delete_oneall key on canonical so a strat authored for DSR 1076 applies to fights from 1065 too. - analysis/gate_diagnostic.py — surfaces canonical encounter_id in response.
Every updated function also returns the canonical ID in its encounter_id response field. UI dedupe (below) reads this back consistently.
Changed — cactbot vendor map keyed on canonical
ingest/cactbot.py TIMELINE_FILES rekeyed: 1065 → 1076 for DSR (the current canonical). load_timeline_for_encounter canonicalizes its input before lookup so legacy-aliased callers still resolve. annotate_fight_model_for_encounter reads + writes fight_model at canonical.
Changed — API endpoints dedupe cloned IDs in listings
- api/main.py
GET /api/me/encounters— cloned encounter rows from the underlyingFightaggregate collapse into one canonical row. Counts merge (pulls / kills / wipes / latest_end_time). - api/main.py
GET /api/encounters— same dedupe, with distinct-report-code counts so reports under both halves of a group aren't double-counted. - jobs/backfill_field.py
field_stats— input encounter IDs canonicalize first; duplicate canonical entries (e.g. passing both 1065 and 1076) collapse into one row. DEFAULT_ENCOUNTERSkeeps BOTH 1076 and 1065 so backfill discovery hits both halves of the FFLogsfightRankings(rankings are served separately per encounter_id).
Changed — React UI removes "(alt)" rows
Cloned encounter labels collapse to canonical only. web/src/Encounters.jsx, web/src/FieldStats.jsx, and web/src/Home.jsx lose the 1065: 'DSR (alt)' entry. The API now only emits canonical IDs in encounter-listing endpoints, so 1065 should never surface in the picker.
Added — scripts/migrate_canonical_encounters.py
One-shot script to clean up orphan fight_model / strat_config rows that landed under legacy aliases before v1.17.0. Walks every cloned group, reports per-(table, alias) counts vs. canonical, prompts for confirmation, then deletes. Supports --dry-run and --yes. Live AC on dev DB: identified 162 fight_model orphans under DSR 1065 (the bulk-clone leftover the user noted in v1.16.5). Not auto-run — operator decision.
Tests
- 5 new end-to-end tests in tests/test_canonical_merge_v1_17.py: consensus unions both halves of the DSR group; DPS check pools kills across the group; cartography aggregates deaths across the group; fight_model writes at canonical only and readers under either alias return the same rows (uses a fake test-only clone group via monkeypatch to avoid clobbering real dev DSR data); helper identity for non-cloned encounters.
- 7 new unit tests in tests/test_encounter_canonical.py for the helpers.
- 528 tests passing (516 → 528, +12). No regressions across the full suite.
Operator notes
- No schema migration needed — pure query-side normalization.
- Existing fight_model rows under canonical IDs are unchanged. Orphan rows under legacy aliases (e.g. DSR 1065 fight_model) become invisible to the analytics layer but stay in the DB until you run
python -m scripts.migrate_canonical_encounters(dry-run first to inspect). - DSR strats authored before v1.17.0 against either ID still work:
strat_configreads now canonicalize. Strats stored at the alias (1065) will be invisible until migrated; check--dry-runoutput to see if the operator has any. - The 162 orphan fight_model rows on the dev DB are the v1.16.5-era bulk-clone from 1065 → 1076; canonical (1076) already has 163 rows. Running the migration drops the orphans cleanly.
Adding a new cloned group later
Append a new tuple to _CLONED_GROUPS in analysis/_encounter.py with the canonical ID first. Everything else (lookups, queries, UI dedupe) flows from that one change. Add a fresh entry to TIMELINE_FILES in ingest/cactbot.py if cactbot ships a timeline for the new canonical ID.