brackt/scripts/backfill-qp-resplit.ts

137 lines
4.9 KiB
TypeScript
Raw Permalink Normal View History

/**
* Backfill: re-score qualifying majors so mis-split QP is corrected.
*
* Sibling/mirror windows used to compute a placement's tie span from the players
* present on THAT window's roster (a subset of the field), so a tied group split
* fewer ways and over-awarded tennis Round-of-16 losers landed at 2 QP instead of
* the correct 1.5 (positions 916: (2+2+2+2+1+1+1+1)/8). processQualifyingEvent now
* derives the tie span from the canonical full field, so re-running the fan-out
* rewrites the stored event_results QP, participant totals, and league standings.
*
* This is idempotent and SILENT: notifications are suppressed so re-scoring history
* (2 1.5 for many participants) does not re-ping every league's Discord. It does
* NOT re-link events or designate primaries run backfill-major-linking.ts first if
* the data predates the shared-major model. majorsCompleted needs no fix (it is now
* derived on read from completed qualifying events).
*
* Safe to re-run. Validate on a DB snapshot first. Reads DATABASE_URL.
*
* npx tsx scripts/backfill-qp-resplit.ts # apply
* npx tsx scripts/backfill-qp-resplit.ts --dry # report only
*/
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import { eq } from "drizzle-orm";
import * as schema from "../database/schema.js";
import { DatabaseContext, database } from "../database/context.js";
import { isBracketMajor } from "../app/lib/event-utils.js";
import {
syncTournamentResults,
syncMajorFromPrimaryEvent,
} from "../app/services/sync-tournament-results.js";
const DRY = process.argv.includes("--dry");
const log = (...a: unknown[]) => console.log(...a);
async function run() {
const db = database();
// Every tournament-linked qualifying event, grouped by canonical tournament.
const events = await db.query.scoringEvents.findMany({
where: eq(schema.scoringEvents.isQualifyingEvent, true),
with: { sportsSeason: { with: { sport: true } } },
});
const byTournament = new Map<string, typeof events>();
for (const ev of events) {
if (!ev.tournamentId) continue; // standalone/manual events: nothing to fan out
const arr = byTournament.get(ev.tournamentId) ?? [];
arr.push(ev);
byTournament.set(ev.tournamentId, arr);
}
log(`Tournaments with linked qualifying events: ${byTournament.size}`);
let ok = 0;
let failed = 0;
for (const [tournamentId, evs] of byTournament) {
const simulatorType = evs[0]?.sportsSeason?.sport?.simulatorType ?? null;
const bracketMajor = isBracketMajor(simulatorType);
const name = evs[0]?.name ?? tournamentId;
// Only re-score fully-scored majors so we don't mark in-progress ones complete.
const anyComplete = evs.some((e) => e.isComplete);
const markComplete = evs.every((e) => e.isComplete);
if (DRY) {
log(
` (dry) ${name}: ${evs.length} window(s), bracket=${bracketMajor}, ` +
`complete=${markComplete} — would re-score silently`
);
continue;
}
try {
if (bracketMajor) {
// Re-derive canonical from the primary, then fan out to siblings (which now
// split ties by the full field). Primary is scored in place and skipped.
const primary = evs.find((e) => e.isPrimary);
if (!primary) {
log(` ! ${name}: no primary window — skipping (run backfill-major-linking.ts)`);
failed += 1;
continue;
}
const report = await syncMajorFromPrimaryEvent(primary.id, {
markComplete,
skipNotifications: true,
});
ok += report.windowsSynced;
failed += report.windowsFailed;
log(
` ${name}: re-scored via primary — ${report.windowsSynced} ok, ${report.windowsFailed} failed`
);
} else {
// Golf/placement: canonical tournament_results already exist; just fan out.
if (!anyComplete) {
log(` ${name}: no completed window — skipping`);
continue;
}
const report = await syncTournamentResults(tournamentId, {
markComplete,
skipNotifications: true,
});
ok += report.windowsSynced;
failed += report.windowsFailed;
log(
` ${name}: re-scored — ${report.windowsSynced} ok, ${report.windowsFailed} failed`
);
}
} catch (e) {
failed += 1;
log(` ! ${name}: ${(e as Error).message}`);
}
}
log(`\nDone${DRY ? " (dry run — no writes)" : ""}. windows ok=${ok}, failed=${failed}.`);
}
async function main() {
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
console.error("ERROR: DATABASE_URL is required");
process.exit(1);
}
const client = postgres(dbUrl, { max: 1 });
const db = drizzle(client, { schema });
try {
await DatabaseContext.run(db, run);
} finally {
await client.end();
}
}
main().catch((e) => {
console.error(e);
process.exit(1);
});