The fix only changes how new results advance, so a bracket already advanced under the fixed crossover keeps its wrong Elimination Finals pairings — and the admin UI cannot re-trigger advancement, since a completed match renders as "Complete" with no way to re-submit the winner. Extract the reconcile step as reseedAflEliminationFinals, which resolves the pairings from the results recorded so far (the winner being advanced is passed in, since that row can predate the write), and add a script that runs it over afl_10 events. It moves only Wildcard teams between the two Elimination Final slots, leaves a correct bracket untouched, and reports rather than rewrites an Elimination Final that has already been played. npx tsx scripts/fix-afl-wildcard-reseed.ts --dry An event with no Wildcard/Elimination Finals matches now raises instead of reporting nothing to do — a mistyped event id should not read as success. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDbHrCce1UhahbkwKkc7hK
133 lines
5 KiB
TypeScript
133 lines
5 KiB
TypeScript
/**
|
|
* Repair: re-seed AFL Wildcard winners into the Elimination Finals they belong in.
|
|
*
|
|
* Brackets advanced before the re-seeding fix crossed each Wildcard winner into a fixed
|
|
* Elimination Final — the 7v10 winner always met 6th and the 8v9 winner always met 5th —
|
|
* instead of pairing them by ladder position (5th hosts the lower-ranked winner). The
|
|
* fix only changes how new results advance, so an already-advanced bracket keeps its
|
|
* wrong pairings until this runs. The admin UI cannot re-trigger it: a completed match
|
|
* renders as "Complete", with no way to re-submit the winner.
|
|
*
|
|
* This runs the same reseedAflEliminationFinals the advancement path now uses, so it
|
|
* makes exactly the correction a fresh bracket would have. It only ever moves Wildcard
|
|
* teams between the two Elimination Final slots — no results, scores or placements are
|
|
* touched, and nothing else in the bracket is written. A bracket that is already correct
|
|
* is left alone.
|
|
*
|
|
* If an Elimination Final has already been played, its qualifier cannot be moved without
|
|
* rewriting who contested a recorded result; the script reports that event and skips it.
|
|
* Clear and regenerate that bracket in Admin instead, then Reprocess Bracket.
|
|
*
|
|
* Safe to re-run. Validate on a DB snapshot first. Reads DATABASE_URL.
|
|
*
|
|
* npx tsx scripts/fix-afl-wildcard-reseed.ts # apply to every afl_10 event
|
|
* npx tsx scripts/fix-afl-wildcard-reseed.ts --dry # report only
|
|
* npx tsx scripts/fix-afl-wildcard-reseed.ts --event <id> # one event
|
|
*/
|
|
|
|
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 {
|
|
findPlayoffMatchesByEventIdAndRound,
|
|
reseedAflEliminationFinals,
|
|
} from "../app/models/playoff-match.js";
|
|
import { findParticipantsBySportsSeasonId } from "../app/models/season-participant.js";
|
|
|
|
const DRY = process.argv.includes("--dry");
|
|
const eventFlag = process.argv.indexOf("--event");
|
|
const ONLY_EVENT = eventFlag === -1 ? null : process.argv[eventFlag + 1];
|
|
const log = (...a: unknown[]) => console.log(...a);
|
|
|
|
async function run() {
|
|
const db = database();
|
|
|
|
const events = await db.query.scoringEvents.findMany({
|
|
where: eq(schema.scoringEvents.bracketTemplateId, "afl_10"),
|
|
});
|
|
const targets = ONLY_EVENT ? events.filter((e) => e.id === ONLY_EVENT) : events;
|
|
|
|
if (ONLY_EVENT && targets.length === 0) {
|
|
log(`No afl_10 event with id ${ONLY_EVENT}.`);
|
|
return;
|
|
}
|
|
log(`afl_10 events to check: ${targets.length}`);
|
|
|
|
let fixed = 0;
|
|
let alreadyRight = 0;
|
|
let skipped = 0;
|
|
|
|
for (const event of targets) {
|
|
const name = event.name ?? event.id;
|
|
|
|
const participants = await findParticipantsBySportsSeasonId(event.sportsSeasonId);
|
|
const nameOf = (id: string | null) =>
|
|
id === null ? "TBD" : participants.find((p) => p.id === id)?.name ?? id;
|
|
|
|
/** "M1: <host> vs <qualifier>" for both Elimination Finals. */
|
|
const pairings = async () => {
|
|
const efMatches = await findPlayoffMatchesByEventIdAndRound(event.id, "Elimination Finals");
|
|
return efMatches
|
|
.toSorted((a, b) => a.matchNumber - b.matchNumber)
|
|
.map((m) => `M${m.matchNumber}: ${nameOf(m.participant1Id)} vs ${nameOf(m.participant2Id)}`)
|
|
.join(", ");
|
|
};
|
|
|
|
try {
|
|
const before = await pairings();
|
|
|
|
// The dry run still resolves the pairings — it just reports them instead of writing.
|
|
if (DRY) {
|
|
const efMatches = await findPlayoffMatchesByEventIdAndRound(event.id, "Elimination Finals");
|
|
const wcMatches = await findPlayoffMatchesByEventIdAndRound(event.id, "Wildcard Round");
|
|
const decided = wcMatches.filter((m) => m.isComplete && m.winnerId).length;
|
|
log(` ${name}: ${before} (${decided}/${wcMatches.length} Wildcard results, ` +
|
|
`${efMatches.filter((m) => m.isComplete).length} Elimination Final(s) played)`);
|
|
continue;
|
|
}
|
|
|
|
const reseed = await reseedAflEliminationFinals(event.id);
|
|
if (reseed.vacated.length === 0 && reseed.filled.length === 0) {
|
|
alreadyRight += 1;
|
|
log(` = ${name}: already correct — ${before}`);
|
|
continue;
|
|
}
|
|
|
|
fixed += 1;
|
|
log(` ~ ${name}:`);
|
|
log(` was: ${before}`);
|
|
log(` now: ${await pairings()}`);
|
|
} catch (e) {
|
|
skipped += 1;
|
|
log(` ! ${name}: ${(e as Error).message}`);
|
|
}
|
|
}
|
|
|
|
if (DRY) {
|
|
log("\nDry run — no writes. Re-run without --dry to apply.");
|
|
return;
|
|
}
|
|
log(`\nDone. re-seeded=${fixed}, already correct=${alreadyRight}, skipped=${skipped}.`);
|
|
}
|
|
|
|
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);
|
|
});
|