The before/after QP diff (with its null = "no prior award" and null → value
"first-time score" semantics) was duplicated in processQualifyingEvent and
rescoreTennisBracketAndDetectChanges. The two entry points must stay separate
(tennis snapshots before an authoritative delete-all; CS2 preserves rows), but
the comparison itself should live in one place.
Add diffChangedQualifyingPoints(before, after) in qualifying-points.ts and call
it from both paths. Unit-tested directly; the two integration tests still cover
the end-to-end behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hq3fG4Zn3unntbSQAGSqCq
The tennis Grand Slam draw sync (`syncTennisDraw`) scores its primary
window by calling `processQualifyingBracketEvent` directly, which writes
QP but never emits the qualifying-points Discord notification (only
`processQualifyingEvent` and the CS2 stage scorer did). As a result,
running the Wimbledon sync produced no QP notifications for the season
that leagues actually draft from.
Extract the rescore into `rescoreTennisBracketAndDetectChanges`, which
snapshots each participant's awarded QP before rebuilding results and
reports whose QP changed. `syncTennisDraw` then calls
`notifyQualifyingPointsUpdate` for the primary window, scoped to the
changed participants so a re-run that changes nothing announces nothing.
The webhook call runs outside the rescore transaction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hq3fG4Zn3unntbSQAGSqCq
Add a per-event "Sync Draw" that pulls a tennis major's full 128-player
draw from its Wikipedia article, auto-creates/links participants (and
propagates them to every linked sibling season), builds the bracket, and
runs the qualifying-points scorer. Re-running advances the bracket and
scoring as matches complete.
Core
- match-sync: WikipediaTennisAdapter + wikitext bracket parser, DrawSync
DTOs, syncTennisDraw orchestrator, pure draw->rows mapping
- playoff-match: populateBracketFromDraw (idempotent upsert on externalMatchId)
- scoring_events.externalSourceKey column (Wikipedia article; migration 0123)
- admin bracket "Sync Draw" card (accepts URL or title) + cron pass
Scoring fix
- deriveBracketQualifyingStates only floors players who have reached the
scoring stage; for deep brackets (tennis_128) early-round losers earn 0 QP
instead of a phantom 9th-place floor. CS2/simple_8 behavior preserved
(gated on rounds[0].isScoring). Re-sync reconciles stale QP rows in a
transaction.
Matching & parsing
- accent-folding in normalizeTeamName; strip Wikipedia "(tennis)"
disambiguators; treat Bye/TBD/Qualifier as TBD; extract wikilink before
template-stripping so {{nowrap}}-wrapped players parse
Admin UX
- dry-run preview (matched / will-create / possible duplicates / unfilled
slots) with inline "rename existing" / "create as new" resolution via
fetcher (no full reload)
Tests: parser vs real 2025 Wimbledon fixture, draw mapping, tennis_128
scoring, accent/disambiguator/nowrap parsing, URL parsing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>