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
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>
88 lines
3.6 KiB
TypeScript
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 };
|
|
}
|