brackt/app/services/simulations/shared-major.ts
Chris Parsons 9480501932
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
Unify majors: score once, fan out across windows + tennis bracket EV
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>
2026-06-22 20:32:22 -07:00

88 lines
3.6 KiB
TypeScript

/**
* Shared helpers for "shared major" simulators (CS2, tennis).
*
* A major (a real tournament) can be linked to several sports_season windows.
* The bracket/stage structure is built and scored once on the PRIMARY window;
* sibling windows hold no structure of their own. When simulating a sibling, the
* simulator must read the structure from the primary and translate the primary
* window's season_participant ids into the local window's ids via the shared
* canonical participant. These helpers centralise that resolution + translation.
*/
import { getPrimaryEventForTournament } from "~/models/scoring-event";
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
/** Maps a participant id from one window to another (or passes it through). */
export type IdTranslator = (id: string | null) => string | null;
/** Identity translation — used when structure lives in the event's own window. */
export const IDENTITY_TR: IdTranslator = (id) => id;
/**
* Build a translator that maps a PRIMARY window's season_participant id to the
* equivalent id in a LOCAL (sibling) window, bridging via the shared canonical
* participant. Ids that can't be bridged (no canonical link, or no local
* counterpart) pass through unchanged so structure is never silently dropped.
*/
export function buildSpTranslator(
primaryParticipants: Array<{ id: string; participantId: string | null }>,
localParticipants: Array<{ id: string; participantId: string | null }>
): IdTranslator {
const primaryToCanonical = new Map(
primaryParticipants
.filter((p) => p.participantId)
.map((p) => [p.id, p.participantId as string])
);
const canonicalToLocal = new Map(
localParticipants
.filter((p) => p.participantId)
.map((p) => [p.participantId as string, p.id])
);
return (id: string | null): string | null => {
if (!id) return id;
const canonical = primaryToCanonical.get(id);
if (!canonical) return id;
return canonicalToLocal.get(canonical) ?? id;
};
}
export interface StructureSource {
/** Event id whose bracket/stage rows hold this event's structure. */
sourceId: string;
/** Translate the source window's participant ids into the local window's. */
tr: IdTranslator;
}
/**
* Resolve where an event's bracket/stage structure lives and how to map its
* participant ids into the local window. For a primary/standalone/non-linked
* event this is the event itself with identity translation. For a sibling
* (tournament-linked, non-primary) event, the structure is the primary event and
* ids are translated primary→local.
*
* `translatorCache` is keyed by primary sports_season id so a window's majors
* (whose primaries may live in different windows) each build their translator
* once.
*/
export async function resolveStructureSource(
event: { id: string; tournamentId: string | null; isPrimary: boolean },
localParticipants: Array<{ id: string; participantId: string | null }>,
translatorCache: Map<string, IdTranslator>
): Promise<StructureSource> {
if (!event.tournamentId || event.isPrimary) {
return { sourceId: event.id, tr: IDENTITY_TR };
}
const primary = await getPrimaryEventForTournament(event.tournamentId);
if (!primary || primary.id === event.id) {
return { sourceId: event.id, tr: IDENTITY_TR };
}
let tr = translatorCache.get(primary.sportsSeasonId);
if (!tr) {
const primaryParticipants = await findParticipantsBySportsSeasonId(
primary.sportsSeasonId
);
tr = buildSpTranslator(primaryParticipants, localParticipants);
translatorCache.set(primary.sportsSeasonId, tr);
}
return { sourceId: primary.id, tr };
}