brackt/app/models/simulator.ts

724 lines
26 KiB
TypeScript
Raw Normal View History

import { and, eq, inArray, isNotNull, sql } from "drizzle-orm";
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import {
getManifestSimulatorProfile,
simulatorInputLabel,
type SimulatorInputKey,
type SimulatorManifestProfile,
} from "~/services/simulations/manifest";
import {
getSimulatorInputPolicy,
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
ratingRequirementLabel,
resolveRatings,
resolveSourceElos,
sourceEloRequirementLabel,
} from "~/services/simulations/input-policy";
import { SIMULATOR_TYPES, type SimulatorType } from "~/services/simulations/registry";
export interface SimulatorProfile extends SimulatorManifestProfile {
isActive: boolean;
}
export interface SportsSeasonSimulatorConfig {
sportsSeasonId: string;
simulatorType: SimulatorType;
config: Record<string, unknown>;
profile: SimulatorProfile;
}
export interface ParticipantSimulatorInput {
participantId: string;
sportsSeasonId: string;
sourceOdds: number | null;
sourceElo: number | null;
worldRanking: number | null;
rating: number | null;
projectedWins: number | null;
projectedTablePoints: number | null;
seed: number | null;
region: string | null;
metadata: Record<string, unknown> | null;
}
export interface UpsertParticipantSimulatorInput {
participantId: string;
sportsSeasonId: string;
sourceOdds?: number | null;
sourceElo?: number | null;
worldRanking?: number | null;
rating?: number | null;
projectedWins?: number | null;
projectedTablePoints?: number | null;
seed?: number | null;
region?: string | null;
metadata?: Record<string, unknown> | null;
}
export interface SimulatorReadiness {
status: "ready" | "not_ready";
canRun: boolean;
participantCount: number;
participantInputCount: number;
derivedInputCount: number;
fallbackInputCount: number;
missingInputs: string[];
warnings: string[];
}
export interface SportsSeasonSimulatorSummary {
sportsSeasonId: string;
seasonName: string;
year: number;
seasonStatus: string;
simulationStatus: string;
fantasySeasonId: string | null;
fantasySeasonName: string | null;
leagueName: string | null;
sportName: string;
sportSlug: string;
simulatorType: SimulatorType;
simulatorName: string;
participantCount: number;
participantInputCount: number;
lastSimulatedDate: string | null;
readiness: SimulatorReadiness;
/** Whether this simulator exposes a Futures Odds setup section. */
supportsFuturesOdds: boolean;
/** Number of participants that currently have stored futures odds. */
oddsParticipantCount: number;
/** Weight (01) the season's policy gives futures odds when blending into Elo. */
oddsWeight: number;
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
}
function parseDecimal(value: string | null | undefined): number | null {
if (value === null || value === undefined) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function isSimulatorType(value: string | null | undefined): value is SimulatorType {
return typeof value === "string" && (SIMULATOR_TYPES as readonly string[]).includes(value);
}
function mergeProfile(
simulatorType: SimulatorType,
dbProfile?: typeof schema.simulatorProfiles.$inferSelect | null
): SimulatorProfile {
const manifest = getManifestSimulatorProfile(simulatorType);
if (!dbProfile) return { ...manifest, isActive: true };
return {
...manifest,
displayName: dbProfile.displayName,
description: dbProfile.description ?? manifest.description,
defaultConfig: {
...manifest.defaultConfig,
...dbProfile.defaultConfig,
},
isActive: dbProfile.isActive,
};
}
function hasInputValue(input: ParticipantSimulatorInput, key: SimulatorInputKey): boolean {
const value = input[key];
if (value === null || value === undefined) return false;
if (typeof value === "string") return value.trim().length > 0;
if (typeof value === "object") return Object.keys(value).length > 0;
return true;
}
function isFallbackMethod(method: string): boolean {
return method === "fallbackElo" || method === "fallbackRating" || method === "averageKnown" || method === "worstKnownMinus";
}
const GENERATED_RATING_METHODS = ["sourceOdds", "blend", "fallbackRating", "averageKnown", "worstKnownMinus"] as const;
const GENERATED_SOURCE_ELO_METHODS = ["projectedWins", "projectedTablePoints", "sourceOdds", "blend", "fallbackElo", "averageKnown", "worstKnownMinus"] as const;
function isGeneratedRatingMethod(method: unknown): boolean {
return typeof method === "string" && GENERATED_RATING_METHODS.includes(method as (typeof GENERATED_RATING_METHODS)[number]);
}
2026-05-13 15:02:23 -07:00
function isGeneratedSourceEloMethod(method: unknown): boolean {
return typeof method === "string" && GENERATED_SOURCE_ELO_METHODS.includes(method as (typeof GENERATED_SOURCE_ELO_METHODS)[number]);
}
function isGeneratedRatingInput(input: typeof schema.seasonParticipantSimulatorInputs.$inferSelect | undefined): boolean {
return isGeneratedRatingMethod(input?.metadata?.ratingMethod);
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
}
2026-05-13 15:02:23 -07:00
function isGeneratedSourceEloInput(input: typeof schema.seasonParticipantSimulatorInputs.$inferSelect | undefined): boolean {
return isGeneratedSourceEloMethod(input?.metadata?.sourceEloMethod);
}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
function inputRequirementLabel(
key: SimulatorInputKey,
profile: SimulatorManifestProfile
): string {
if (key === "sourceElo" && profile.derivableInputs?.sourceElo?.length) {
return sourceEloRequirementLabel(profile);
}
if (key === "rating" && profile.derivableInputs?.rating?.length) {
return ratingRequirementLabel(profile);
}
return simulatorInputLabel(key);
}
export async function getSimulatorProfile(simulatorType: SimulatorType): Promise<SimulatorProfile> {
const db = database();
const dbProfile = await db.query.simulatorProfiles.findFirst({
where: eq(schema.simulatorProfiles.simulatorType, simulatorType),
});
return mergeProfile(simulatorType, dbProfile);
}
export async function upsertSimulatorProfile(
profile: Pick<SimulatorProfile, "simulatorType" | "displayName" | "description" | "defaultConfig"> & {
inputSchema?: Record<string, unknown>;
isActive?: boolean;
}
): Promise<void> {
const db = database();
const now = new Date();
await db
.insert(schema.simulatorProfiles)
.values({
simulatorType: profile.simulatorType,
displayName: profile.displayName,
description: profile.description,
defaultConfig: profile.defaultConfig,
inputSchema: profile.inputSchema ?? {},
isActive: profile.isActive ?? true,
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: schema.simulatorProfiles.simulatorType,
set: {
displayName: sql`excluded.display_name`,
description: sql`excluded.description`,
defaultConfig: sql`excluded.default_config`,
inputSchema: sql`excluded.input_schema`,
isActive: sql`excluded.is_active`,
updatedAt: now,
},
});
}
export async function getSportsSeasonSimulatorConfig(
sportsSeasonId: string
): Promise<SportsSeasonSimulatorConfig | null> {
const db = database();
const season = await db.query.sportsSeasons.findFirst({
where: eq(schema.sportsSeasons.id, sportsSeasonId),
with: {
sport: true,
simulatorConfig: true,
},
});
const simulatorType = season?.simulatorConfig?.simulatorType ?? season?.sport?.simulatorType;
if (!season || !isSimulatorType(simulatorType)) return null;
const profile = await getSimulatorProfile(simulatorType);
const config = {
...profile.defaultConfig,
...season.simulatorConfig?.config,
};
return {
sportsSeasonId,
simulatorType,
config,
profile,
};
}
export async function upsertSportsSeasonSimulatorConfig(input: {
sportsSeasonId: string;
simulatorType: SimulatorType;
config?: Record<string, unknown>;
}): Promise<void> {
const db = database();
const now = new Date();
await db
.insert(schema.sportsSeasonSimulatorConfigs)
.values({
sportsSeasonId: input.sportsSeasonId,
simulatorType: input.simulatorType,
config: input.config ?? {},
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: schema.sportsSeasonSimulatorConfigs.sportsSeasonId,
set: {
simulatorType: sql`excluded.simulator_type`,
config: sql`excluded.config`,
updatedAt: now,
},
});
}
export async function getParticipantSimulatorInputs(
sportsSeasonId: string
): Promise<ParticipantSimulatorInput[]> {
const db = database();
const [seasonParticipants, simulatorInputs, legacyEvs] = await Promise.all([
db.query.seasonParticipants.findMany({
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
columns: { id: true },
}),
db.query.seasonParticipantSimulatorInputs.findMany({
where: eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId),
}),
db.query.seasonParticipantExpectedValues.findMany({
where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId),
columns: {
participantId: true,
sourceOdds: true,
sourceElo: true,
worldRanking: true,
},
}),
]);
const simulatorByParticipant = new Map(simulatorInputs.map((row) => [row.participantId, row]));
const legacyByParticipant = new Map(legacyEvs.map((row) => [row.participantId, row]));
return seasonParticipants.map((participant) => {
const input = simulatorByParticipant.get(participant.id);
const legacy = legacyByParticipant.get(participant.id);
return {
participantId: participant.id,
sportsSeasonId,
sourceOdds: input?.sourceOdds ?? legacy?.sourceOdds ?? null,
2026-05-13 15:02:23 -07:00
sourceElo: input
? isGeneratedSourceEloInput(input) ? null : input.sourceElo
: legacy?.sourceElo ?? null,
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
worldRanking: input?.worldRanking ?? legacy?.worldRanking ?? null,
rating: isGeneratedRatingInput(input) ? null : parseDecimal(input?.rating),
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
projectedWins: parseDecimal(input?.projectedWins),
projectedTablePoints: parseDecimal(input?.projectedTablePoints),
seed: input?.seed ?? null,
region: input?.region ?? null,
metadata: input?.metadata ?? null,
};
});
}
function generatedRatingMethodSql() {
return sql`${schema.seasonParticipantSimulatorInputs.metadata}->>'ratingMethod' in ('sourceOdds', 'fallbackRating', 'averageKnown', 'worstKnownMinus')`;
}
export async function batchSaveParticipantSimulatorSourceOdds(
inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
const seasonIds = [...new Set(inputs.map((input) => input.sportsSeasonId))];
await db.transaction(async (tx) => {
await tx
.update(schema.seasonParticipantSimulatorInputs)
.set({
rating: null,
metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod'`,
updatedAt: now,
})
.where(
and(
inArray(schema.seasonParticipantSimulatorInputs.sportsSeasonId, seasonIds),
generatedRatingMethodSql()
)
);
await tx
.insert(schema.seasonParticipantSimulatorInputs)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
sourceOdds: input.sourceOdds,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantSimulatorInputs.participantId,
schema.seasonParticipantSimulatorInputs.sportsSeasonId,
],
set: {
sourceOdds: sql`excluded.source_odds`,
rating: sql`case when ${generatedRatingMethodSql()} then null else ${schema.seasonParticipantSimulatorInputs.rating} end`,
metadata: sql`case when ${generatedRatingMethodSql()} then coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod' else ${schema.seasonParticipantSimulatorInputs.metadata} end`,
updatedAt: now,
},
});
});
}
Fix NCAAW futures odds simulation and admin import UX (#420) - Revert ncaaw-simulator to Barthag win probability formula; set realistic rating bounds (ratingMin: 0.70, ratingMax: 0.97) so derived ratings stay in the range where the formula behaves well - Add batchSaveFuturesOddsForSimulator which clears all ratings (manual and generated) before upserting sourceOdds, so futures odds always drive the simulation rather than being silently overridden by existing Barthag ratings from Simulator Setup - Add clearSourceOddsForParticipants to zero out both tables for participants excluded from a bulk import - Add "Clear existing odds" checkbox to the bulk import card; applies client-side on match and server-side on submit - Fix missing sportsSeasonId filter in batchSaveFuturesOddsForSimulator pre-clear UPDATE (could have wiped ratings across other seasons) - Fix race condition: run batchSaveSourceOdds then batchSaveFuturesOddsForSimulator sequentially so the simulator inputs table always ends in the correct cleared state - Fix Math.round in convertFuturesToElo collapsing Barthag-scale ratings to 0 or 1; Elo callers already round after clamping - Handle all-identical-odds edge case in convertFuturesToElo (assign midpoint instead of throwing) - Add missingRatingStrategy: worstKnownMinus to ncaaw_bracket manifest so fallbackRatingDelta is live config, not dead - Log warning in resolveRatings when only 1 participant has odds - Reset clearExisting checkbox after applyMatches Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:06:16 -07:00
export async function batchSaveFuturesOddsForSimulator(
inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
// Persist the odds non-destructively. How much these odds move a stored
// Elo/rating is now governed by the season's blend weight (see resolveSourceElos
// / SimulatorInputPolicy.oddsWeight), so we no longer null out manually entered
// Elo here — that policy decides at run time.
await db
.insert(schema.seasonParticipantSimulatorInputs)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
sourceOdds: input.sourceOdds,
createdAt: now,
Fix NCAAW futures odds simulation and admin import UX (#420) - Revert ncaaw-simulator to Barthag win probability formula; set realistic rating bounds (ratingMin: 0.70, ratingMax: 0.97) so derived ratings stay in the range where the formula behaves well - Add batchSaveFuturesOddsForSimulator which clears all ratings (manual and generated) before upserting sourceOdds, so futures odds always drive the simulation rather than being silently overridden by existing Barthag ratings from Simulator Setup - Add clearSourceOddsForParticipants to zero out both tables for participants excluded from a bulk import - Add "Clear existing odds" checkbox to the bulk import card; applies client-side on match and server-side on submit - Fix missing sportsSeasonId filter in batchSaveFuturesOddsForSimulator pre-clear UPDATE (could have wiped ratings across other seasons) - Fix race condition: run batchSaveSourceOdds then batchSaveFuturesOddsForSimulator sequentially so the simulator inputs table always ends in the correct cleared state - Fix Math.round in convertFuturesToElo collapsing Barthag-scale ratings to 0 or 1; Elo callers already round after clamping - Handle all-identical-odds edge case in convertFuturesToElo (assign midpoint instead of throwing) - Add missingRatingStrategy: worstKnownMinus to ncaaw_bracket manifest so fallbackRatingDelta is live config, not dead - Log warning in resolveRatings when only 1 participant has odds - Reset clearExisting checkbox after applyMatches Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:06:16 -07:00
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantSimulatorInputs.participantId,
schema.seasonParticipantSimulatorInputs.sportsSeasonId,
],
set: {
sourceOdds: sql`excluded.source_odds`,
updatedAt: now,
},
});
Fix NCAAW futures odds simulation and admin import UX (#420) - Revert ncaaw-simulator to Barthag win probability formula; set realistic rating bounds (ratingMin: 0.70, ratingMax: 0.97) so derived ratings stay in the range where the formula behaves well - Add batchSaveFuturesOddsForSimulator which clears all ratings (manual and generated) before upserting sourceOdds, so futures odds always drive the simulation rather than being silently overridden by existing Barthag ratings from Simulator Setup - Add clearSourceOddsForParticipants to zero out both tables for participants excluded from a bulk import - Add "Clear existing odds" checkbox to the bulk import card; applies client-side on match and server-side on submit - Fix missing sportsSeasonId filter in batchSaveFuturesOddsForSimulator pre-clear UPDATE (could have wiped ratings across other seasons) - Fix race condition: run batchSaveSourceOdds then batchSaveFuturesOddsForSimulator sequentially so the simulator inputs table always ends in the correct cleared state - Fix Math.round in convertFuturesToElo collapsing Barthag-scale ratings to 0 or 1; Elo callers already round after clamping - Handle all-identical-odds edge case in convertFuturesToElo (assign midpoint instead of throwing) - Add missingRatingStrategy: worstKnownMinus to ncaaw_bracket manifest so fallbackRatingDelta is live config, not dead - Log warning in resolveRatings when only 1 participant has odds - Reset clearExisting checkbox after applyMatches Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:06:16 -07:00
}
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
export async function batchUpsertParticipantSimulatorInputs(
inputs: UpsertParticipantSimulatorInput[]
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
await db.transaction(async (tx) => {
await tx
.insert(schema.seasonParticipantSimulatorInputs)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
sourceOdds: input.sourceOdds ?? null,
sourceElo: input.sourceElo ?? null,
worldRanking: input.worldRanking ?? null,
rating: input.rating?.toString() ?? null,
projectedWins: input.projectedWins?.toString() ?? null,
projectedTablePoints: input.projectedTablePoints?.toString() ?? null,
seed: input.seed ?? null,
region: input.region ?? null,
metadata: input.metadata ?? null,
createdAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantSimulatorInputs.participantId,
schema.seasonParticipantSimulatorInputs.sportsSeasonId,
],
set: {
sourceOdds: sql`excluded.source_odds`,
sourceElo: sql`excluded.source_elo`,
worldRanking: sql`excluded.world_ranking`,
rating: sql`excluded.rating`,
projectedWins: sql`excluded.projected_wins`,
projectedTablePoints: sql`excluded.projected_table_points`,
seed: sql`excluded.seed`,
region: sql`excluded.region`,
metadata: sql`excluded.metadata`,
updatedAt: sql`excluded.updated_at`,
},
});
// Compatibility bridge: keep legacy simulator metadata columns populated until
// every simulator has been migrated to season_participant_simulator_inputs.
await tx
.insert(schema.seasonParticipantExpectedValues)
.values(
inputs.map((input) => ({
participantId: input.participantId,
sportsSeasonId: input.sportsSeasonId,
probFirst: "0",
probSecond: "0",
probThird: "0",
probFourth: "0",
probFifth: "0",
probSixth: "0",
probSeventh: "0",
probEighth: "0",
expectedValue: "0",
source: input.sourceOdds !== null && input.sourceOdds !== undefined ? "futures_odds" as const : "elo_simulation" as const,
sourceOdds: input.sourceOdds ?? null,
sourceElo: input.sourceElo ?? null,
worldRanking: input.worldRanking ?? null,
calculatedAt: now,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [
schema.seasonParticipantExpectedValues.participantId,
schema.seasonParticipantExpectedValues.sportsSeasonId,
],
set: {
sourceOdds: sql`COALESCE(excluded.source_odds, ${schema.seasonParticipantExpectedValues.sourceOdds})`,
sourceElo: sql`COALESCE(excluded.source_elo, ${schema.seasonParticipantExpectedValues.sourceElo})`,
worldRanking: sql`COALESCE(excluded.world_ranking, ${schema.seasonParticipantExpectedValues.worldRanking})`,
updatedAt: now,
},
});
});
}
export async function validateSimulatorReadiness(
sportsSeasonId: string
): Promise<SimulatorReadiness> {
const config = await getSportsSeasonSimulatorConfig(sportsSeasonId);
const inputs = await getParticipantSimulatorInputs(sportsSeasonId);
const missingInputs: string[] = [];
const warnings: string[] = [];
let derivedInputCount = 0;
let fallbackInputCount = 0;
if (!config) {
return {
status: "not_ready",
canRun: false,
participantCount: inputs.length,
participantInputCount: 0,
derivedInputCount,
fallbackInputCount,
missingInputs: ["simulator type"],
warnings,
};
}
const participantCount = inputs.length;
if (participantCount === 0) {
missingInputs.push("participants");
}
const requiredInputs = config.profile.requiredInputs;
const resolvedSourceElos = requiredInputs.includes("sourceElo")
? resolveSourceElos(inputs, config.profile, config.config)
: new Map();
const resolvedRatings = requiredInputs.includes("rating")
? resolveRatings(inputs, config.profile, config.config)
: new Map();
derivedInputCount =
[...resolvedSourceElos.values()].filter((input) => input.method !== "direct").length +
[...resolvedRatings.values()].filter((input) => input.method !== "direct").length;
fallbackInputCount =
[...resolvedSourceElos.values()].filter((input) => isFallbackMethod(input.method)).length +
[...resolvedRatings.values()].filter((input) => isFallbackMethod(input.method)).length;
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
function hasRequiredInput(input: ParticipantSimulatorInput, key: SimulatorInputKey): boolean {
if (hasInputValue(input, key)) return true;
if (key === "sourceElo") return resolvedSourceElos.has(input.participantId);
if (key === "rating") return resolvedRatings.has(input.participantId);
return false;
}
const participantInputCount = requiredInputs.length === 0
? participantCount
: inputs.filter((input) => requiredInputs.every((key) => hasRequiredInput(input, key))).length;
if (requiredInputs.length > 0 && participantInputCount < participantCount) {
for (const key of requiredInputs) {
const missingCount = inputs.filter((input) => !hasRequiredInput(input, key)).length;
if (missingCount > 0) {
missingInputs.push(`${inputRequirementLabel(key, config.profile)} for ${missingCount} participant(s)`);
}
}
}
if (derivedInputCount > 0) {
warnings.push(`Derived simulator inputs will be used for ${derivedInputCount} participant(s).`);
}
if (fallbackInputCount > 0) {
warnings.push(`Configured fallback simulator inputs will be used for ${fallbackInputCount} participant(s).`);
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
}
if (config.simulatorType === "cs2_major_qualifying_points") {
const missingRankCount = inputs.filter((input) => !hasInputValue(input, "worldRanking")).length;
if (missingRankCount > 0) {
warnings.push(
`CS2 will use rank 9999 for ${missingRankCount} participant(s) without world ranking; this is acceptable for deep-tail teams but less precise.`
);
}
}
if (config.profile.setupSections.includes("regularStandings")) {
warnings.push("Regular-season standings may be needed for in-season accuracy.");
}
if (config.profile.setupSections.includes("bracket")) {
warnings.push("Bracket-aware simulators use completed bracket results when available.");
}
const status = missingInputs.length === 0 ? "ready" : "not_ready";
return {
status,
canRun: status === "ready",
participantCount,
participantInputCount,
derivedInputCount,
fallbackInputCount,
missingInputs,
warnings,
};
}
export async function prepareSimulatorInputsForRun(sportsSeasonId: string): Promise<void> {
const config = await getSportsSeasonSimulatorConfig(sportsSeasonId);
if (!config) return;
const inputs = await getParticipantSimulatorInputs(sportsSeasonId);
const resolvedSourceElos = config.profile.requiredInputs.includes("sourceElo")
? resolveSourceElos(inputs, config.profile, config.config)
: new Map();
const resolvedRatings = config.profile.requiredInputs.includes("rating")
? resolveRatings(inputs, config.profile, config.config)
: new Map();
const rowsToUpsert: UpsertParticipantSimulatorInput[] = [];
for (const input of inputs) {
const resolvedElo = resolvedSourceElos.get(input.participantId);
const resolvedRating = resolvedRatings.get(input.participantId);
if (!resolvedElo && !resolvedRating) continue;
rowsToUpsert.push({
participantId: input.participantId,
sportsSeasonId,
sourceOdds: input.sourceOdds,
sourceElo: resolvedElo?.sourceElo ?? input.sourceElo,
worldRanking: input.worldRanking,
rating: resolvedRating?.rating ?? input.rating,
projectedWins: input.projectedWins,
projectedTablePoints: input.projectedTablePoints,
seed: input.seed,
region: input.region,
metadata: {
...input.metadata,
...(resolvedElo ? { sourceEloMethod: resolvedElo.method } : {}),
...(resolvedRating ? { ratingMethod: resolvedRating.method } : {}),
},
});
}
await batchUpsertParticipantSimulatorInputs(rowsToUpsert);
}
export async function listSportsSeasonSimulatorSummaries(): Promise<SportsSeasonSimulatorSummary[]> {
const db = database();
const seasons = await db.query.sportsSeasons.findMany({
with: {
sport: true,
fantasySeason: { with: { league: true } },
participants: { columns: { id: true } },
simulatorConfig: true,
},
});
const simulatorSeasons = seasons.filter((season) => {
const simulatorType = season.simulatorConfig?.simulatorType ?? season.sport.simulatorType;
if (!isSimulatorType(simulatorType)) return false;
// Brackt's public sports season is a draftable template. The runnable
// simulations are the per-league private Brackt seasons.
if (season.sport.slug === "brackt") return season.fantasySeasonId !== null;
return season.fantasySeasonId === null;
});
const seasonIds = simulatorSeasons.map((season) => season.id);
const snapshotRows = seasonIds.length > 0
? await db
.select({
sportsSeasonId: schema.participantEvSnapshots.sportsSeasonId,
lastSimulatedDate: sql<string>`max(${schema.participantEvSnapshots.snapshotDate})`,
})
.from(schema.participantEvSnapshots)
.where(inArray(schema.participantEvSnapshots.sportsSeasonId, seasonIds))
.groupBy(schema.participantEvSnapshots.sportsSeasonId)
: [];
const lastSnapshotBySeason = new Map(snapshotRows.map((row) => [row.sportsSeasonId, row.lastSimulatedDate]));
const oddsCountRows = seasonIds.length > 0
? await db
.select({
sportsSeasonId: schema.seasonParticipantSimulatorInputs.sportsSeasonId,
oddsCount: sql<number>`count(*)::int`,
})
.from(schema.seasonParticipantSimulatorInputs)
.where(
and(
inArray(schema.seasonParticipantSimulatorInputs.sportsSeasonId, seasonIds),
isNotNull(schema.seasonParticipantSimulatorInputs.sourceOdds)
)
)
.groupBy(schema.seasonParticipantSimulatorInputs.sportsSeasonId)
: [];
const oddsCountBySeason = new Map(oddsCountRows.map((row) => [row.sportsSeasonId, Number(row.oddsCount)]));
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
// Each season calls getSimulatorProfile + validateSimulatorReadiness (~4 queries each).
// Acceptable for an admin-only page; revisit if season counts grow past ~50.
const summaries = await Promise.all(
simulatorSeasons.map(async (season) => {
const simulatorType = (season.simulatorConfig?.simulatorType ?? season.sport.simulatorType) as SimulatorType;
const profile = await getSimulatorProfile(simulatorType);
const readiness = await validateSimulatorReadiness(season.id);
const mergedConfig = { ...profile.defaultConfig, ...season.simulatorConfig?.config };
const policy = getSimulatorInputPolicy(mergedConfig);
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
return {
sportsSeasonId: season.id,
seasonName: season.name,
year: season.year,
seasonStatus: season.status,
simulationStatus: season.simulationStatus,
fantasySeasonId: season.fantasySeasonId,
fantasySeasonName: season.fantasySeason ? `${season.fantasySeason.year} Season` : null,
leagueName: season.fantasySeason?.league?.name ?? null,
sportName: season.sport.name,
sportSlug: season.sport.slug,
simulatorType,
simulatorName: profile.displayName,
participantCount: season.participants.length,
participantInputCount: readiness.participantInputCount,
lastSimulatedDate: lastSnapshotBySeason.get(season.id) ?? null,
readiness,
supportsFuturesOdds: profile.setupSections.includes("futuresOdds"),
oddsParticipantCount: oddsCountBySeason.get(season.id) ?? 0,
oddsWeight: policy.oddsWeight,
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409) Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
};
})
);
return summaries.toSorted((a, b) =>
a.sportName.localeCompare(b.sportName) ||
(a.leagueName ?? "").localeCompare(b.leagueName ?? "") ||
b.year - a.year ||
a.seasonName.localeCompare(b.seasonName)
);
}
export async function assertRegistrySchemaDriftFree(): Promise<void> {
const schemaValues = schema.simulatorTypeEnum.enumValues;
const missingFromSchema = SIMULATOR_TYPES.filter((type) => !schemaValues.includes(type));
const missingFromRegistry = schemaValues.filter((type) => !(SIMULATOR_TYPES as readonly string[]).includes(type));
if (missingFromSchema.length > 0 || missingFromRegistry.length > 0) {
throw new Error(
`Simulator registry/schema drift detected. ` +
`Missing from schema: ${missingFromSchema.join(", ") || "none"}. ` +
`Missing from registry: ${missingFromRegistry.join(", ") || "none"}.`
);
}
}