Four related bugs in the Qualifying Points subsystem surfaced on Wimbledon:
1. Some leagues stored 2 QP instead of 1.5 for Round-of-16 losers. Sibling/
mirror windows scored via the placement-group path in processQualifyingEvent,
which took the tie span from the players present on that window's roster
(a draftable subset) instead of the full field. A window holding fewer than
the 8 tied R16 losers split the 9-16 points too few ways and over-awarded.
The tie span is now derived from the canonical tournament_results (the whole
field) for tournament-linked events, falling back to the live count only for
standalone events. Every league now scores identically.
2. Discord notifications rounded QP with Math.round, turning 1.5 into "2".
Both the "Points Awarded" and "QP Standings" values now use a 2-decimal
formatter mirroring the web UI's formatQP, so Discord and the site agree.
3. The Discord "QP Standings" block ranked players only among that event's
scorers, showing two R16 losers as T1 instead of T9. Rank is now computed
over the full season field and passed through to the notifier.
4. "N of M majors completed" reached "11 of 4": majorsCompleted was a stored
counter incremented on every fan-out sync with a guard that misfired. It is
now derived on read (getMajorsCompleted = count of completed qualifying
events), which is self-correcting; the increment/decrement writes are removed
along with the now-unused hasProcessedQualifyingPlacement helper.
Adds scripts/backfill-qp-resplit.ts to silently re-score existing majors so
already-corrupted seasons are corrected (2 -> 1.5), and threads a
skipNotifications option through processQualifyingEvent / syncTournamentResults
so the backfill does not re-ping every league.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AUcHy9m7aF6axsY6Grpe4
- Auto-heal now only promotes a new primary when the deleted event was
itself the primary window. Golf-style shared majors intentionally have
no primary (scored on the canonical tournament page), so deleting one
of their windows no longer flips a sibling into a primary and changes
its scoring/guard behavior. Adds a regression test for that case.
- Replace the relation-heavy getSportsSeasonsByTournament + double
Array.find in the events loader with a new countWindowsByTournament
helper (count(distinct sports_season_id)) and a single cached lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2
Deleting an event from a season only affects that season's scoring
event; the shared canonical tournament and the other linked seasons
survive. The old flow never communicated this and offered no way to
manage the relationship, so it felt like deleting an event might wipe
the whole shared tournament.
- deleteScoringEvent is now shared-tournament-aware: it auto-heals the
primary window (promotes the earliest remaining window when the
primary is removed) and optionally deletes the canonical tournament
when the last linked window is removed. Returns a result describing
what happened.
- Add deleteTournament model helper.
- Events page: delete confirmation now explains exactly what a delete
does (removed from this season only vs. last window), with an opt-in
checkbox to also remove the orphaned shared tournament.
- Tournament page: add a per-window "Remove" control on the Linked
Sports Seasons card to unlink a season, reusing the auto-heal path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2