A review of this branch found four problems, all downstream of one decision:
calling runSportsSeasonSimulation from inside the result path. That function
does three jobs — recompute probabilities, recalculate standings, write the
daily EV snapshot — and the result path wants only the first.
1. The Discord standings post was silently suppressed on every scored match,
for all 13 bracket-aware sports.
recalculateAffectedLeagues detects change by snapshotting teamStandings,
recalculating, then diffing; changedTeamIds gates the notification. But
processMatchResult runs updateProbabilitiesAfterResult first, which now
reached the runner's own recalculateStandings. The new totals were therefore
already written when the "before" snapshot was taken, the diff came back
empty, and the post never fired. previousRank went the same way:
recalculateStandings rolls it forward on every call, so the extra one erased
rank movement.
runSportsSeasonSimulation now takes skipStandingsRecalc / skipSnapshots and
the probability updater passes both. The snapshot is skipped because it is a
per-day series keyed by snapshotDate — writing it per match result just
overwrites the day's row with intra-day values.
2. finalizeQualifyingPoints marks the season completed immediately before
calling the updater, and the runner rejects a completed season outright. With
the ICM fallback gone that failed every time, stranding anyone still in the
unfinished set on permanently stale probabilities — reachable for
cs2_major_qualifying_points, the one bracket-aware qualifying-points sport.
A completed season is not this branch's case rather than a failure: every
placement is final and the floor it protects can no longer be contradicted.
shouldRerunSimulator now excludes it and it falls through to ICM as before.
The genuine failure modes still leave probabilities alone rather than falling
back to the path being replaced.
3. match-sync calls processMatchResult in a per-match loop with no
skipSideEffects, so each synced match ran a full Monte Carlo plus EV rewrite,
snapshot and standings recalc. processMatchResult gains skipProbabilities —
mirroring the option processPlayoffEvent already takes, and narrower than
skipSideEffects — which match-sync passes in the loop before refreshing once
at the end. Per-match standings and Discord posts are unchanged.
4. A partially-seeded afl_10 bracket now throws from inside the result path.
The throw is correct and stays; the concern was that it was silent, which the
error surfacing in 2 covers.
Two claims from the review did not hold up and were left alone: the batch
bracket route already passes skipSideEffects per match and refreshes once after
the loop, and autoCompleteRoundIfDone already passes skipProbabilities, so there
is no double run per round completion.
Tests: the runner honors both skip flags and still writes EVs; the updater asks
for probabilities only; a completed season takes the ICM path without erroring;
processMatchResult skips the refresh but still announces. The two behavioral
ones were confirmed to fail against the previous behavior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPxDnSEKVoKx9HFcQ6gmcS
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>