Unify majors: score once, fan out across windows + tennis bracket EV #106

Merged
chrisp merged 1 commit from feat/unify-majors-fanout into main 2026-06-23 03:43:16 +00:00
Owner

What

Make a "major" (golf / tennis / CS2) scored once on its canonical tournament and fan out across every linked sports_season window and league — for results entry (golf), brackets (tennis), and group/Swiss stages + bracket (CS2). Also makes in-progress EV simulation condition on the real bracket for both CS2 and (new) tennis.

Highlights

Fan-out & completion (app/services/sync-tournament-results.ts)

  • syncTournamentResults marks each synced window complete (gated by markComplete), recalculates affected leagues, and counts recalc failures so a stale league can't hide behind a "completed" badge.
  • syncMajorFromPrimaryEvent promotes a primary window's derived results to canonical tournament_results (deleting rows for dropped placements) and fans out to siblings; fanOutMajorIfPrimary wires it into the bracket/CS2 scoring actions.
  • Placement removals now propagate (stale per-window rows reset to filler).

Primary-event model (scoring_events.is_primary, migration 0122)

  • getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent / setPrimaryEvent.
  • Event creation auto-seeds a primary for bracket majors; "Make primary" button on the tournament page.
  • Per-window event/bracket/cs2 pages are read-only for non-primary linked events (not-participating stays editable, since it's a per-window sim input).

Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG)

  • Bracket-scored qualifying major via the existing qualifying-bracket pipeline.
  • Simulator conditions in-progress EV on the real bracket (honors completed matches, walkover for withdrawals), derives QP from the season config, and reads round structure from the template (one source of truth). CS2 + tennis share resolveStructureSource / buildSpTranslator.

Backfill (scripts/backfill-major-linking.ts)

  • One-time idempotent reconcile of existing majors: link orphans → designate primary → promote canonical → sync. Run --dry first.

Testing

  • npm run typecheck clean; full unit suite 2681/2681 pass (the one failing suite is the pre-existing TeamAvatar Storybook a11y story).
  • New coverage: completion/status/skip/stale-removal in sync, syncMajorFromPrimaryEvent, ensurePrimaryEvent/setPrimaryEvent, tennis drawFromBracket + bracket-honoring + exclusions + tennis_128 scoring.
  • Manual: link a major to 2 windows; golf scored on the canonical page fans out + completes both; CS2/tennis built on the primary window fan out to siblings + reflect in-progress EV.

Notes

  • Includes a Drizzle migration (0122_soft_longshot.sql) adding scoring_events.is_primary — run npm run db:migrate.
  • The backfill is not run automatically; apply it once (npx tsx scripts/backfill-major-linking.ts) to reconcile existing data.

🤖 Generated with Claude Code

## What Make a "major" (golf / tennis / CS2) scored **once** on its canonical tournament and fan out across every linked `sports_season` window and league — for results entry (golf), brackets (tennis), and group/Swiss stages + bracket (CS2). Also makes in-progress EV simulation condition on the real bracket for both CS2 and (new) tennis. ## Highlights **Fan-out & completion** (`app/services/sync-tournament-results.ts`) - `syncTournamentResults` marks each synced window complete (gated by `markComplete`), recalculates affected leagues, and counts recalc failures so a stale league can't hide behind a "completed" badge. - `syncMajorFromPrimaryEvent` promotes a primary window's derived results to canonical `tournament_results` (deleting rows for dropped placements) and fans out to siblings; `fanOutMajorIfPrimary` wires it into the bracket/CS2 scoring actions. - Placement *removals* now propagate (stale per-window rows reset to filler). **Primary-event model** (`scoring_events.is_primary`, migration `0122`) - `getPrimaryEventForTournament` / `isReadOnlySibling` / `ensurePrimaryEvent` / `setPrimaryEvent`. - Event creation auto-seeds a primary for bracket majors; **"Make primary"** button on the tournament page. - Per-window event/bracket/cs2 pages are read-only for non-primary linked events (not-participating stays editable, since it's a per-window sim input). **Tennis Grand Slam bracket** (`tennis_128` template + `TEMPLATE_ROUND_CONFIG`) - Bracket-scored qualifying major via the existing qualifying-bracket pipeline. - Simulator conditions in-progress EV on the real bracket (honors completed matches, walkover for withdrawals), derives QP from the season config, and reads round structure from the template (one source of truth). CS2 + tennis share `resolveStructureSource` / `buildSpTranslator`. **Backfill** (`scripts/backfill-major-linking.ts`) - One-time idempotent reconcile of existing majors: link orphans → designate primary → promote canonical → sync. Run `--dry` first. ## Testing - `npm run typecheck` clean; full unit suite **2681/2681** pass (the one failing suite is the pre-existing `TeamAvatar` Storybook a11y story). - New coverage: completion/status/skip/stale-removal in sync, `syncMajorFromPrimaryEvent`, `ensurePrimaryEvent`/`setPrimaryEvent`, tennis `drawFromBracket` + bracket-honoring + exclusions + `tennis_128` scoring. - Manual: link a major to 2 windows; golf scored on the canonical page fans out + completes both; CS2/tennis built on the primary window fan out to siblings + reflect in-progress EV. ## Notes - Includes a Drizzle migration (`0122_soft_longshot.sql`) adding `scoring_events.is_primary` — run `npm run db:migrate`. - The backfill is **not** run automatically; apply it once (`npx tsx scripts/backfill-major-linking.ts`) to reconcile existing data. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chrisp added 1 commit 2026-06-23 03:34:35 +00:00
Unify majors: score once, fan out across windows + tennis bracket EV
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m12s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m24s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m27s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m25s
🚀 Deploy / 🐳 Build (push) Successful in 1m14s
🚀 Deploy / 🚀 Deploy (push) Successful in 14s
9480501932
Make a "major" (golf/tennis/CS2) scored once on its canonical tournament
and fan out to every linked sports_season window and league.

Fan-out & completion (app/services/sync-tournament-results.ts):
- syncTournamentResults now marks each synced window's event complete
  (gated by markComplete), recalculates affected leagues, and counts
  recalc failures so a stale league can't hide behind a "completed" badge
- syncMajorFromPrimaryEvent promotes a primary window's derived results to
  canonical tournament_results (deleting rows for dropped placements) and
  fans out to siblings; fanOutMajorIfPrimary guards on the primary
- placement removals now propagate (stale rows reset to filler)

Primary-event model (scoring_events.is_primary, migration 0122):
- getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent /
  setPrimaryEvent; event creation auto-seeds a primary for bracket majors;
  "Make primary" button on the tournament page
- per-window event/bracket/cs2 pages are read-only for non-primary linked
  events (not-participating stays editable)

Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG):
- bracket-scored qualifying major via the existing bracket pipeline
- simulator conditions in-progress EV on the real bracket (honoring
  completed matches, walkover for withdrawals), QP derived from config,
  round structure read from the template; CS2 + tennis share resolveStructureSource

Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile
of existing majors (link orphans, designate primary, promote canonical, sync).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chrisp merged commit 9480501932 into main 2026-06-23 03:43:16 +00:00
chrisp deleted branch feat/unify-majors-fanout 2026-06-23 03:43:16 +00:00
Sign in to join this conversation.
No description provided.