From c1df2d8edc8bdfa417c6f9ba5c7125be1f404e84 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Fri, 1 May 2026 05:54:27 +0000 Subject: [PATCH] Add baseline capture script for canonical layer migration Captures QP totals, completed event results, surface Elo, and Monte Carlo simulator output for every in-flight qualifying-points sports_season. Used across Phase 1a-4 to verify no scoring drift. Note: simulators don't currently accept a seed, so simulator output fixtures are non-deterministic (noted in the script header). Co-Authored-By: Claude Opus 4.7 --- scripts/capture-baseline.ts | 152 ++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 scripts/capture-baseline.ts diff --git a/scripts/capture-baseline.ts b/scripts/capture-baseline.ts new file mode 100644 index 0000000..838f83f --- /dev/null +++ b/scripts/capture-baseline.ts @@ -0,0 +1,152 @@ +/** + * Baseline Capture Script - Phase 1a Task 0 + * + * Captures the current state of qualifying-points data to JSON fixtures for + * verification across all 5 migration phases. This ensures no scoring drift + * occurs during the canonical tournament layer migration. + * + * Captured data: + * 1. participant_qualifying_totals rows for all qualifying_points sports_seasons + * 2. event_results rows with non-null qualifying_points_awarded + * 3. participant_surface_elos rows for tennis sports_seasons + * 4. Monte Carlo simulator output for each in-flight qualifying-points season + * + * WARNING: Simulator output is NON-DETERMINISTIC. The current simulator + * implementations use Math.random() directly without seed support. This means + * simulator fixture files will differ on each run and cannot be used for exact + * byte-for-byte comparisons. They serve as reference baselines for manual + * review and approximate validation only. + */ + +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { eq, sql } from "drizzle-orm"; +import { writeFileSync, mkdirSync } from "fs"; +import { join } from "path"; +import * as schema from "../database/schema.js"; +import { getSimulator } from "../app/services/simulations/registry.js"; + +const OUT_DIR = join(process.cwd(), "test-fixtures", "baselines"); + +async function main() { + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl) { + console.error("ERROR: DATABASE_URL is required"); + process.exit(1); + } + + console.log("Connecting to database..."); + const client = postgres(dbUrl, { max: 1 }); + const db = drizzle(client, { schema }); + + mkdirSync(OUT_DIR, { recursive: true }); + console.log(`Output directory: ${OUT_DIR}\n`); + + // ─── 1. Capture participant_qualifying_totals ───────────────────────────────── + + console.log("Capturing participant_qualifying_totals..."); + const qpTotals = await db.execute(sql` + SELECT pqt.*, ss.id as sports_season_id, ss.name as sports_season_name, s.name as sport_name + FROM participant_qualifying_totals pqt + JOIN sports_seasons ss ON ss.id = pqt.sports_season_id + JOIN sports s ON s.id = ss.sport_id + WHERE ss.scoring_pattern = 'qualifying_points' + ORDER BY ss.id, pqt.participant_id + `); + + writeFileSync( + join(OUT_DIR, "qp-totals-pre-phase1.json"), + JSON.stringify(qpTotals.rows, null, 2) + ); + console.log(` Wrote ${qpTotals.rows.length} rows → qp-totals-pre-phase1.json`); + + // ─── 2. Capture event_results with qualifying_points_awarded ───────────────── + + console.log("Capturing event_results with qualifying_points_awarded..."); + const eventResults = await db.execute(sql` + SELECT er.*, se.sports_season_id, se.name as event_name + FROM event_results er + JOIN scoring_events se ON se.id = er.scoring_event_id + JOIN sports_seasons ss ON ss.id = se.sports_season_id + WHERE ss.scoring_pattern = 'qualifying_points' + AND er.qualifying_points_awarded IS NOT NULL + ORDER BY se.sports_season_id, er.scoring_event_id, er.participant_id + `); + + writeFileSync( + join(OUT_DIR, "event-results-pre-phase1.json"), + JSON.stringify(eventResults.rows, null, 2) + ); + console.log(` Wrote ${eventResults.rows.length} rows → event-results-pre-phase1.json`); + + // ─── 3. Capture participant_surface_elos ────────────────────────────────────── + + console.log("Capturing participant_surface_elos..."); + const surfaceElos = await db.execute(sql` + SELECT pse.*, ss.sport_id, ss.name as sports_season_name, s.name as sport_name + FROM participant_surface_elos pse + JOIN sports_seasons ss ON ss.id = pse.sports_season_id + JOIN sports s ON s.id = ss.sport_id + WHERE ss.scoring_pattern = 'qualifying_points' + ORDER BY ss.id, pse.participant_id + `); + + writeFileSync( + join(OUT_DIR, "surface-elos-pre-phase1.json"), + JSON.stringify(surfaceElos.rows, null, 2) + ); + console.log(` Wrote ${surfaceElos.rows.length} rows → surface-elos-pre-phase1.json`); + + // ─── 4. Capture simulator output for in-flight qualifying-points seasons ───── + + console.log("\nCapturing Monte Carlo simulator output..."); + const inFlightSeasons = await db + .select() + .from(schema.sportsSeasons) + .where(eq(schema.sportsSeasons.scoringPattern, "qualifying_points")); + + console.log(` Found ${inFlightSeasons.length} qualifying-points sports_seasons`); + + let simCount = 0; + for (const ss of inFlightSeasons) { + if (!ss.simulatorType) { + console.log(` Skipping ${ss.name} (id: ${ss.id}) - no simulatorType set`); + continue; + } + + console.log(` Running simulator for ${ss.name} (type: ${ss.simulatorType})...`); + try { + const sim = getSimulator(ss.simulatorType); + + // NOTE: Simulators do not accept seed parameter - output is non-deterministic + const output = await sim.simulate(ss.id); + + const filename = `sim-output-${ss.simulatorType}-${ss.id}.json`; + writeFileSync( + join(OUT_DIR, filename), + JSON.stringify(output, null, 2) + ); + console.log(` Wrote ${output.length} results → ${filename}`); + simCount++; + } catch (err) { + console.error(` ERROR simulating ${ss.name}:`, err); + // Continue with other seasons even if one fails + } + } + + console.log(`\n✓ Baseline capture complete!`); + console.log(` ${qpTotals.rows.length} QP totals`); + console.log(` ${eventResults.rows.length} event results`); + console.log(` ${surfaceElos.rows.length} surface elo rows`); + console.log(` ${simCount} simulator outputs (non-deterministic)`); + console.log(`\nOutput: ${OUT_DIR}`); + + await client.end(); +} + +main() + .then(() => process.exit(0)) + .catch((e) => { + console.error("FATAL:", e); + process.exit(1); + });