2026-06-26 05:16:54 +00:00
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 {
2026-06-26 05:16:54 +00:00
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" ;
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
import { countSeasonRaces } from "~/models/season-races" ;
/** Simulator types driven by a race calendar plus championship standings. */
const RACE_CALENDAR_SIMULATORS : SimulatorType [ ] = [ "f1_standings" , "indycar_standings" ] ;
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 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 ;
2026-06-26 05:16:54 +00:00
/** Whether this simulator exposes a Futures Odds setup section. */
supportsFuturesOdds : boolean ;
/** Number of participants that currently have stored futures odds. */
oddsParticipantCount : number ;
/** Weight (0– 1) 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 {
2026-05-12 22:26:25 -07:00
return method === "fallbackElo" || method === "fallbackRating" || method === "averageKnown" || method === "worstKnownMinus" ;
}
2026-06-26 05:16:54 +00:00
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 ;
2026-05-12 22:26:25 -07:00
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 ] ) ;
}
2026-05-12 22:26:25 -07:00
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 ,
2026-05-12 22:26:25 -07:00
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 ,
} ;
} ) ;
}
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 ,
] ,
2026-06-30 17:05:52 +00:00
// Non-destructive: only overwrite a column when the incoming value is
// non-null. This lets a partial import (e.g. odds-only) update just the
// columns it provides without clobbering previously stored inputs like a
// manually entered Elo or rating. Mirrors the COALESCE bridge used for the
// legacy EV table below.
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
set : {
2026-06-30 17:05:52 +00:00
sourceOdds : sql ` COALESCE(excluded.source_odds, ${ schema . seasonParticipantSimulatorInputs . sourceOdds } ) ` ,
sourceElo : sql ` COALESCE(excluded.source_elo, ${ schema . seasonParticipantSimulatorInputs . sourceElo } ) ` ,
worldRanking : sql ` COALESCE(excluded.world_ranking, ${ schema . seasonParticipantSimulatorInputs . worldRanking } ) ` ,
rating : sql ` COALESCE(excluded.rating, ${ schema . seasonParticipantSimulatorInputs . rating } ) ` ,
projectedWins : sql ` COALESCE(excluded.projected_wins, ${ schema . seasonParticipantSimulatorInputs . projectedWins } ) ` ,
projectedTablePoints : sql ` COALESCE(excluded.projected_table_points, ${ schema . seasonParticipantSimulatorInputs . projectedTablePoints } ) ` ,
seed : sql ` COALESCE(excluded.seed, ${ schema . seasonParticipantSimulatorInputs . seed } ) ` ,
region : sql ` COALESCE(excluded.region, ${ schema . seasonParticipantSimulatorInputs . region } ) ` ,
// Metadata carries the method flags (sourceEloMethod/ratingMethod) that
// tell readers whether the stored Elo/rating is generated vs. a trusted
// direct value. When a caller supplies explicit metadata, use it as-is
// (prepareSimulatorInputsForRun and the projection importer set the
// correct flags). Otherwise preserve existing metadata, but drop the
// method flag for any column receiving a fresh direct value — otherwise a
// stale "generated" flag would cause that newly-entered Elo/rating to be
// filtered out as derived (see getParticipantSimulatorInputs).
metadata : sql ` CASE
WHEN excluded . metadata IS NOT NULL THEN excluded . metadata
ELSE COALESCE ( $ { schema . seasonParticipantSimulatorInputs . metadata } , '{}' : : jsonb )
- ( CASE WHEN excluded . source_elo IS NOT NULL THEN 'sourceEloMethod' ELSE '' END )
- ( CASE WHEN excluded . rating IS NOT NULL THEN 'ratingMethod' ELSE '' END )
END ` ,
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
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 ;
2026-05-12 22:26:25 -07:00
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 ) {
2026-05-12 22:26:25 -07:00
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. `
) ;
}
}
Fix auto racing sims ignoring championship standings
The IndyCar simulator gave a near-clinched championship leader ~60% to win
the title. It was reporting the futures odds and nothing else.
`event_type` has no race value, so a season_standings calendar (F1, IndyCar)
is stored as `schedule_event` rows — the admin default for that scoring
pattern. The simulator skipped exactly those rows when counting races, so it
saw zero remaining races, took the branch labelled "pre-season", and never
looked at `participant_season_results`. Since `sourceOdds` is an admin input
that is never auto-refreshed, the output was whatever the market said months
ago. With a realistic late-season field (leader on 601 pts vs 480, 2 races
left, stale futures at -300) the old path returns ~55%; counting the races
returns 100.0%.
- Add `countSeasonRaces` in a new leaf model. A race is every event except
`final_standings`, and completion is inferred from the event date, since
nobody marks rows labelled "Non-Scoring" complete. Its own module because
`scoring-event.ts` reaches back into `simulator.ts` through
`scoring-calculator`.
- Split "season over" from "pre-season". Both had `remainingRaces === 0`, so
a finished season reverted to an odds draw instead of reporting the final
standings.
- Warn when a season has championship points but no calendar, in the
simulator and as a non-blocking readiness warning on the setup page.
- Replace proportional vig removal with a power devig. Dividing every runner
by the same book sum guts the favourite in a 27-driver market: a 75%
implied favourite came out at 55%, a -20000 near-lock at 95%. Power devig
gives 69.5% and 99.3%. Unpriced drivers now floor at the bottom of the
market instead of being handed 1/N.
- Move the race points tables to their own module so tests can read them
without tripping the manifest/registry import cycle.
The existing tests missed all of this because their event fixtures used
`eventType: "race"`, which is not a value the enum has. Rebuilt on real enum
values, plus a regression test for the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TUcV7KenckF893zQ46EDXt
2026-08-17 00:36:20 +00:00
if ( RACE_CALENDAR_SIMULATORS . includes ( config . simulatorType ) ) {
// Without a calendar the simulator cannot tell how many races are left, so
// it falls back to futures odds and ignores the championship standings
// entirely. A warning, not a blocker — a season drafted before the schedule
// is published still needs to run.
const races = await countSeasonRaces ( sportsSeasonId ) ;
if ( races . total === 0 ) {
warnings . push (
"No race calendar found for this season. Add the schedule on the events page — until then the simulation uses futures odds only and ignores championship standings."
) ;
}
}
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 . 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 ] ) ) ;
2026-06-26 05:16:54 +00:00
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 ) ;
2026-06-26 07:26:55 +00:00
const mergedConfig = { . . . profile . defaultConfig , . . . season . simulatorConfig ? . config } ;
2026-06-26 05:16:54 +00:00
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 ,
2026-06-26 05:16:54 +00:00
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" } . `
) ;
}
}