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 <noreply@anthropic.com>
This commit is contained in:
parent
81af8907cc
commit
c1df2d8edc
1 changed files with 152 additions and 0 deletions
152
scripts/capture-baseline.ts
Normal file
152
scripts/capture-baseline.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue