* docs: plan NLL preseason simulator
* Add NLL Season + Playoffs Monte Carlo simulator
14-team regular season (18 games) projects top-8 via Elo + decaying
projectedWins prior (adjusts for games already played). Playoff bracket:
QF single-game (1v8, 2v7, 3v6, 4v5), SF and Finals best-of-3. Three
runtime modes: bracket-aware → known-seed → regular-season projection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix MLB streak doubling bug, hide empty standings columns, improve mobile layout
- mlb.ts: use streakCode alone (the API already returns the full string like
"W3"); appending streakNumber was doubling the digit, causing "L33" display
- Update mlb.test.ts mocks to match real API format (streakCode "W3" not "W")
- RegularSeasonStandings: conditionally hide GB/L10/STK columns when no rows
have data, so pre-season or stats-free sports don't show blank columns
- Mobile two-row layout: GP/PCT/GB/L10 move to a secondary sub-row (sm:hidden)
so all data stays visible without horizontal scroll on small screens; STK and
W/L remain on the primary row; reduce min-w from 740px to 360px
https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF
* Address code review: cleanup mlb streak type, fix soccer GP on mobile, hoist showSubRow
- mlb.ts: drop redundant `?? undefined` after optional chain; document that
streakCode is the full string (e.g. "W3") and streakNumber is unused
- RegularSeasonStandings: hoist hasSecondaryStats → showSubRow to component
level (it only depends on a prop, not on individual row data)
- Remove non-functional `truncate`/`min-w-0` from team name cell — truncation
requires table-layout:fixed which we don't use; team names size naturally
- Soccer GP was hidden on mobile with no sub-row to surface it; GP now shows
inline for soccer on all viewports, hidden only for non-soccer (which has
the secondary sub-row)
- Add comment explaining totalCols counts hidden-on-mobile columns for colSpan
- Add missing test for GB column hiding when no rows have gamesBack data
https://claude.ai/code/session_01RADi3LhYMPbRDm5no1ZpdF
---------
Co-authored-by: Claude <noreply@anthropic.com>
NCAAM and NCAAW simulators imported getParticipantSimulatorInputs from
models/simulator, which imports manifest.ts, which imports registry.ts —
creating a cycle back through registry.ts → simulator files. This caused
a TDZ ReferenceError in the client bundle.
Fix: inline a direct seasonParticipantSimulatorInputs query in both
simulators (they already run direct DB queries) instead of going through
the model layer, breaking the cycle.
Also moves getSimulatorInputPolicy from the component body to the loader
on the simulator setup page. The component calling it pulled input-policy.ts
→ manifest.ts → registry.ts → all simulator implementations into the client
bundle, which contain Node.js-only code and caused "Error loading route
module" in production.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Commissioners can add Brackt to any league. Each team drafts one manager
from their own league; at season end, that manager's final overall fantasy
ranking earns placement points. Resolution fires automatically when all
other sports finalize.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a sports_season_tournaments junction table and bidirectional
link/unlink UI on both the sports season and tournament admin pages.
- New junction table with unique index on (sports_season_id, tournament_id)
and a separate index on tournament_id for reverse lookups
- New model with link/unlink/query functions and cross-sport validation
- Migration includes backfill from existing scoring_events tournament links
- Admin sports season page: Tournaments card with add/remove UI
- Admin tournament page: Linked Sports Seasons card with add/remove UI
- Inline success/error feedback, empty states, aria-labels on remove buttons
- cloneSportsSeason now copies tournament links
- Fixes darts simulator test timeout (15s for 128-player pre-bracket sim)
cutover + cleanup (2/2) (#367)
* refactor(schema): rename per-window tables to season_* prefix
Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: rename participant.ts model file to season-participant.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(models): update model layer to use renamed schema exports
Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant
Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts
Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route layer to use renamed schema exports
- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file
Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts
Error count reduced from 499 to 453 (46 errors fixed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route files for schema rename
Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(services): update simulators and services for renamed schema
Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId
Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts
Typecheck errors reduced from 365 to 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: rename per-window tables to season_* prefix
* fix(tests): update mock query keys after participants table rename
Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.
Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(tests): update remaining mock paths and keys after schema rename
* fix(tests): final two mock stragglers after schema rename
- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: add post-phase1a baseline capture (temp, for diff verification)
* chore: capture pre-migration baselines
* chore: remove post-phase1a capture helper after verification
* schema: add canonical tournament & participant tables
Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(models): add canonical tournament, participant, result, surface-elo models
Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).
Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
createCanonicalParticipant, etc.) to avoid collision with existing
season-participant.ts exports
- All four models include comprehensive unit tests following the
audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints
Part of Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: create canonical tables, add nullable FKs
* scripts: add extractTournamentIdentity helper for backfill
Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.
* scripts: add backfill orchestrator for canonical layer
Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.
Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
(recorded in report.errors) rather than overwriting
* scripts: add backfill CLI with dry-run default
Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).
* fix(backfill-cli): wrap runBackfill in DatabaseContext.run
The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(backfill-cli): exit 0 on success so pg pool doesn't block
The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* schema: add check constraint requiring tournament_id for qualifying events
Team sports (NHL, NBA, etc.) have scoring_events with no canonical
tournament, so we can't require tournament_id globally. The constraint
only applies when is_qualifying_event=true.
Phase 3 Task 1 of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(BatchResultEntry): make form intent configurable
Canonical tournament admin page will submit with a different intent
(batch-upsert-results). Existing per-window callers continue to get
"batch-add-results" as the default, so no behavior changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(services): syncTournamentResults fan-out service
* schema: add unique (scoring_event_id, season_participant_id) on event_results
syncTournamentResults uses check-then-write on event_results. The
unique index defends against race conditions (unlikely today — admin
is single-writer — but safe to enforce).
WARNING: if existing prod data has duplicate (scoring_event_id,
season_participant_id) pairs, the migration will fail. Verify with:
SELECT scoring_event_id, season_participant_id, count(*)
FROM event_results
GROUP BY 1, 2
HAVING count(*) > 1;
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(surface-elo): getSurfaceEloMap reads from canonical
Switch the tennis simulator's Elo source from the per-window table
to the canonical participant_surface_elos via season_participants.participant_id.
Map key stays season_participant.id so the simulator's call pattern is unchanged.
Other functions in this file still read/write the per-window table
(seasonParticipantSurfaceElos). They back the old admin surface-elo page
and are removed in Phase 4 after the canonical admin flow replaces them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(admin): canonical tournament list + result entry routes
New routes /admin/tournaments and /admin/tournaments/:id. The detail
page lets admins enter tournament results once via paste-and-parse,
then calls syncTournamentResults to fan out to every linked window.
Phase 3 Task 4 of canonical tournament layer migration.
* feat: per-window batch-add-results routes through canonical when linked
If the scoring_event has a tournament_id, translate season_participant ids
to canonical participant ids, upsert tournament_results, and fan out via
syncTournamentResults. Team sports (no tournament link) keep the legacy
direct-write path.
Phase 3 Task 8 of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(surface-elo): mirror batchUpsert writes to canonical table
The simulator now reads surface Elo from the canonical table
(participant_surface_elos). The existing per-window admin page still
posts to batchUpsertSurfaceElos; without this change, those edits
would silently fail to affect simulator output. Mirror every write
to both tables until Phase 4 removes the per-window path entirely.
Phase 3 Task 5 (partial — canonical write path; dedicated canonical
admin UI deferred; existing page continues to work and now
edits both tables).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat: auto-provision canonical tournaments and participants on create
Creating new qualifying scoring_events or season_participants now
auto-upserts the matching canonical rows. Needed so the check constraint
doesn't reject qualifying events, and so new season participants
flow through syncTournamentResults.
- Extract tournament identity shared between backfill and event-creation
into app/lib/tournament-identity.ts; scripts/backfill re-exports it.
- createScoringEvent + bulkCreateScoringEvents now accept tournamentId.
- Event creation routes auto-compute (name, year) and upsert tournament.
- createParticipant + createManyParticipants auto-link to canonical
participants by (sportId, name).
Phase 3 Task 7 of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(tests): update surface-elo tests for canonical mirror
getSurfaceEloMap now joins through season_participants and returns
rows keyed by seasonParticipantId (was participantId).
batchUpsertSurfaceElos now performs a second db.select to resolve
canonical links before mirroring writes; tests mock the lookup
explicitly and exercise both the "no canonical link" short-circuit
path and the "mirror to canonical" path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: add check constraint + unique index for canonical
* refactor(surface-elo): canonical-only; drop per-window path
batchUpsertSurfaceElos now writes only to the canonical
participant_surface_elos table (the mirror step is now the only step).
getSurfaceElosForSeason joins through season_participants → canonical
participants so the admin UI keeps its existing (id, participantId,
sportsSeasonId, ...) row shape.
season_participants without a canonical link get silently skipped. In
practice every qualifying-points roster entry is canonical-linked after
Phase 2 backfill + Phase 3 auto-linking on createParticipant.
Phase 4 Tasks 2 + 3 of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: remove legacy direct-write fallback in batch-add-results
Every qualifying scoring_event now has tournament_id (check constraint
enforces it), so the non-canonical fallback is dead. Replace with an
explicit error surface so any configuration regression becomes visible
instead of silently bypassing canonical.
Phase 4 Task 4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* schema: drop season_participant_surface_elos table
Table had no remaining readers after Phase 3 and no remaining writers
after Phase 4 Task 2. Also removes the now-unrunnable Phase 2 backfill
scripts and the backfill:canonical npm script — the backfill ran once
and is preserved in git history (commits 85bca8b, 8186dbb, 6f3438b,
bc0f530, 327c7b9).
Phase 4 Task 5.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(agents): describe canonical/per-window split
Update database.md with the canonical/per-window model explanation,
the syncTournamentResults fan-out contract, the check constraint,
and the tennis-Men/Women quirk.
Update domain-models.md to reflect the renamed per-window entities
and the new canonical layer.
Also update the drizzle-kit gotchas section with lessons from the
migration: do not use --custom for renames; FK constraint naming
can mismatch Drizzle convention.
Phase 4 Task 6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: drop season_participant_surface_elos
* feat(admin): add Tournaments link to admin sidebar
Missed in Phase 3 Task 4 — the /admin/tournaments route exists but
was not reachable from the admin nav without typing the URL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(lint): address 4 oxlint errors on canonical-layer-pr2
- Remove unused filterNewResults import (events batch-add-results legacy
path was removed in Phase 4 Task 4).
- Replace import() type annotation in sync-tournament-results test with
a top-level type import (consistent-type-imports rule).
- Hoist tableName helper out of makeFakeDb closure so it's not recreated
per call (consistent-function-scoping rule).
- eqeqeq: replace `!= null` with explicit null/undefined checks in
tournament-identity.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(schema): rename per-window tables to season_* prefix
Renames participants, participant_expected_values, participant_qualifying_totals,
participant_results, participant_surface_elos to season_* prefixed names.
Renames event_results.participant_id to season_participant_id.
Phase 1a of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor: rename participant.ts model file to season-participant.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(models): update model layer to use renamed schema exports
Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant
Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts
Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route layer to use renamed schema exports
- Update model import from ~/models/participant to ~/models/season-participant
- Rename schema.participants to schema.seasonParticipants
- Rename schema.participantResults to schema.seasonParticipantResults
- Rename db.query.participants to db.query.seasonParticipants
- Update 9 route files and 1 test file
Affected files:
- admin.sports-seasons.$id.events.$eventId.bracket.server.ts
- admin.sports-seasons.$id.participants.tsx
- api/draft.force-manual-pick.ts
- api/draft.make-pick.ts
- api/draft.replace-pick.ts
- api/seasons.$seasonId.draft.ts
- leagues/$leagueId.draft-board.$seasonId.tsx
- leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts
- admin/__tests__/sports-seasons-participants.test.ts
Error count reduced from 499 to 453 (46 errors fixed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(routes): update route files for schema rename
Update route imports from ~/models/participant to ~/models/season-participant
and fix references to .participant/.participantId on event results to use
.seasonParticipant/.seasonParticipantId after schema rename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(services): update simulators and services for renamed schema
Update all simulators, services, and server files to use renamed schema tables:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantResults → seasonParticipantResults
- eventResults.participantId → eventResults.seasonParticipantId
Files updated:
- 20 sport simulators (NBA, NHL, NFL, MLB, etc.)
- probability-updater.ts
- standings-sync/index.ts
- sports-data-sync.server.ts
- server/socket.ts
Typecheck errors reduced from 365 to 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: rename per-window tables to season_* prefix
* fix(tests): update mock query keys after participants table rename
Change mock db.query.participants to db.query.seasonParticipants in test
files to match the schema rename from commit 66145a9. This fixes
"Cannot read properties of undefined (reading 'findFirst'/'findMany')"
errors that occurred when production code queries db.query.seasonParticipants
but test mocks only defined the old participants key.
Files updated:
- app/services/simulations/__tests__/world-cup-simulator.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.test.ts
- app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts
- app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts
- server/__tests__/timer-autodraft.test.ts
- app/models/__tests__/team-score-events.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(tests): update remaining mock paths and keys after schema rename
* fix(tests): final two mock stragglers after schema rename
- draft-pick.test.ts: assertion on db.query.participantQualifyingTotals
- process-match-result.test.ts: mock key participants → seasonParticipants
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: add post-phase1a baseline capture (temp, for diff verification)
* chore: capture pre-migration baselines
* chore: remove post-phase1a capture helper after verification
* schema: add canonical tournament & participant tables
Adds tournaments, participants (canonical), tournament_results, and
participant_surface_elos (canonical). Adds nullable tournament_id to
scoring_events and nullable participant_id to season_participants.
Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(models): add canonical tournament, participant, result, surface-elo models
Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).
Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
createCanonicalParticipant, etc.) to avoid collision with existing
season-participant.ts exports
- All four models include comprehensive unit tests following the
audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints
Part of Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* migration: create canonical tables, add nullable FKs
* scripts: add extractTournamentIdentity helper for backfill
Pure function that derives canonical (name, year) identity from a
scoring_events row, stripping trailing 4-digit years from the name or
falling back to eventDate. Used by the Phase 2 backfill to group
per-window events into canonical tournaments.
* scripts: add backfill orchestrator for canonical layer
Populates canonical tournaments, participants, tournament_results, and
participant_surface_elos from per-window data for qualifying-points
sports. Skips already-linked rows, is idempotent, and supports dry-run
mode.
Critical invariants enforced by the implementation:
- qualifying_points_awarded is never copied to tournament_results
- season_participant_qualifying_totals is never touched
- conflicting surface-Elo values between windows raise a loud error
(recorded in report.errors) rather than overwriting
* scripts: add backfill CLI with dry-run default
Wires backfill-canonical-layer.ts to a CLI entry point exposed as
`npm run backfill:canonical`. Defaults to --dry-run; requires --apply
to actually write. Supports --sport=<uuid> to limit to a single sport.
Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts).
* fix(backfill-cli): wrap runBackfill in DatabaseContext.run
The orchestrator uses database() from ~/database/context, which requires
AsyncLocalStorage to be populated. Wrap the CLI invocation with
DatabaseContext.run(db, ...) using server/db's cached connection pool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(backfill-cli): exit 0 on success so pg pool doesn't block
The cached postgres connection pool keeps the Node event loop open after
main() returns. Explicit process.exit(0) on success mirrors the pattern
in scripts/capture-baseline.ts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Chris Parsons <chrisp@extrahop.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Fix NHL bracket-aware simulation and MLB sourceElo support
NHL: NHLSimulator now checks for a populated playoff_game bracket before
falling back to season-projection mode. When a bracket exists (e.g. via
simple_16 template), it simulates the actual bracket structure, respecting
completed matches and using ELO+odds blending for the rest — matching the
pattern used by Snooker, NBA, and UCL simulators.
MLB: MLBSimulator now reads sourceElo from participantExpectedValues and
converts it to an effective RDif (via eloToRDif), overriding the hardcoded
TEAMS_DATA.rdif when present. This wires up the expected-wins admin form
(which saves sourceElo) to the simulation engine.
https://claude.ai/code/session_01139GJVg2gqgDjcUzqhnBNn
* Wire sourceElo from projected wins form into NBA, NHL, and WNBA simulators
All three simulators had entries in simulator-config.ts (enabling the projected
wins form in the admin UI) but silently ignored the sourceElo that was saved.
NBA: Both bracket-aware and season-projection modes now load sourceElo and
prefer it over hardcoded TEAMS_DATA elo. Added resolvedElo field to TeamEntry
so eloOfEntry() uses the correct value throughout.
NHL: Season-projection mode now fetches sourceElo + sourceOdds in a single
parallel query (removing the duplicate evRows query). Bracket-aware mode also
loads sourceElo alongside sourceOdds. Both modes use sourceElo > TEAMS_DATA > 1400.
WNBA: resolveElo() now accepts sourceElo as an optional highest-priority
argument. The evRows query fetches sourceElo alongside sourceOdds and passes
it through when building team entries.
https://claude.ai/code/session_01139GJVg2gqgDjcUzqhnBNn
* Add review fixes: NHL R1 validation, WNBA sourceElo tests, NHL bracket unit tests
- NHLSimulator.simulateBracket: remove unused nameById variable
- NHLSimulator.simulateBracket: validate all R1 participants before entering
Monte Carlo loop; throws with a clear message if any slot is missing
- wnba-simulator.test.ts: add 5 tests covering the sourceElo override parameter
on resolveElo() (overrides SRS, overrides futures, overrides fallback,
null/undefined fall through to normal priority chain)
- nhl-simulator.test.ts: add full bracket-aware integration suite (10 tests)
covering fallback to season-projection, structure validation errors, probability
distributions, fully-decided champion, and source tag
https://claude.ai/code/session_01139GJVg2gqgDjcUzqhnBNn
* Fix TS error: add resolvedElo to makeEntry in nhl-simulator.test.ts
TeamEntry requires resolvedElo after the simulator refactor.
https://claude.ai/code/session_01139GJVg2gqgDjcUzqhnBNn
* Fix lint: replace non-null assertions with null-safe alternatives in NHL test
https://claude.ai/code/session_01139GJVg2gqgDjcUzqhnBNn
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add projected-wins input mode to admin Elo Ratings page
Admins can now enter projected season win totals instead of raw Elo
numbers on the Elo Ratings page. Wins are auto-converted to Elo using
the inverse formula and stored as sourceElo, keeping the rest of the
simulation pipeline unchanged.
- Add `projectedWinsToElo` / `eloToProjectedWins` to probability-engine
- Add `simulator-config.ts` centralising per-sport season length, parity
factor, and average opponent Elo (AFL, NFL, NBA, NHL, MLB, WNBA)
- Admin Elo Ratings page: toggle between Elo and Projected Wins input
modes; bulk import parses wins format; existing sourceElo back-fills
the wins field on load
- AFL simulator now reads sourceElo from participantExpectedValues first,
falling back to hardcoded TEAMS_DATA then 1400
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix lint errors in simulator-config tests and probability-engine
- Replace non-null assertions (`!`) with optional chaining (`?.`) in simulator-config tests
- Remove redundant type annotations on default parameters in probability-engine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: NBA play-in advancement writes E7/W7 to wrong participant slot
advanceNBAPlayInWinner was writing the PIR1 7v8 winner to participant1Id
of First Round M3/M7, but those slots already hold E2/W2 (seeded at
generation time). The "already filled" guard fired, silently swallowing
both the winner advancement and the loser's placement into Play-In
Round 2 — so the 7/8 loser never appeared in the second play-in game.
Fix: write the 7v8 winner to participant2Id (the correct null slot) and
update the guard, doc comment, and load-bearing comment accordingly.
Also swap the p2Override arguments in the simulator and test fixture to
match the corrected slot layout (FR M3: E2=p1, E7=p2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: hasGroupStage check for templates without groupStage property
undefined !== null is true, so templates that omit groupStage (like
nba_20) were incorrectly treated as group-stage templates, triggering
the generate-groups action and the "Invalid template or template has
no group stage" error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the NBA_20 bracket template (20-team structure with play-in
tournament) and rewrites the NBA simulator to be bracket-aware like UCL.
- Add NBA_20 bracket template: PIR1 (4) → PIR2 (2) → First Round (8)
→ Conference Semifinals → Conference Finals → NBA Finals
- Add participantLabels (East/West 1–10) for admin bracket UI slot labels
- Add generateNBA20Bracket and advanceNBAPlayInWinner in playoff-match.ts
with custom play-in advancement logic (PIR1 winners go to two different
destinations)
- Rewrite NBASimulator with two auto-detected modes:
- Bracket-aware (preferred): reads actual bracket state, simulates only
remaining rounds forward using 50k Monte Carlo iterations
- Season-projection (fallback): existing logic for pre-playoff use
- Guard resolveGame/resolveSeries against null participants (throws with
match ID and slot info instead of silently using empty string)
- Document load-bearing First Round match ordering in generateNBA20Bracket
- Add 25 unit tests covering Elo math, team data, template structure,
and all simulator integration paths
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Participants are created with externalId=null by default, causing the
LLWS simulator to crash at runtime. Fix in two parts:
1. Name-prefix inference: if externalId is null, participants whose
name starts with "US " or equals "US" are assigned to the US side;
all others are assigned to Intl. Pools are always randomized when
inferred (no pool suffix).
2. Admin UI: add an inline-editable "Group" column to the Manage
Participants page so admins can explicitly set externalId for any
simulator that uses it (LLWS, and future cases like NHL conferences).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: lower darts ELO_DIVISOR to 400 and fallback Elo to 1400
ELO_DIVISOR 500 → 400: elite darts is more skill-dominated than the
previous setting implied. A 400-point gap now gives ~78% per-set win
probability (vs ~73% before).
fallbackElo 1600 → 1400: unseeded PDC World Championship entrants
(regional qualifiers, Q-School players) are materially weaker than
seeded tour professionals. 1400 better reflects that gap.
Combined effect: a dominant player (e.g. 2000 Elo vs 1400-1600 field)
now produces EV ≈ 65 instead of ≈ 30, which better matches expectations
for a top-ranked player in a 128-player bracket.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: lower darts ELO_DIVISOR to 200 for realistic EV distribution
With the actual PDC field (top players clustered 1800–1970 Elo, Littler at
~2080), ELO_DIVISOR=400 made the gap between world #1 and the top 8 too
soft — EV for Littler came out ~37 instead of the expected 65–70.
ELO_DIVISOR=200 calibrates correctly for this field: a 100-pt gap now
gives ~62% per-set win probability (vs ~56% at 400), sharply rewarding
the best players while still allowing upsets. Littler at 2084 produces
EV ≈ 65–70 against the real PDC tour roster.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: seed darts bracket by Elo instead of world ranking
World ranking (PDC Order of Merit) is prize-money-based and can diverge
from current skill — e.g. Kevin Doets (1818 Elo) was ranked 35th while
Joe Cullen (1667 Elo) held the 32nd seed. Seeding the weaker player and
leaving the stronger one unseeded contradicts the Elo-based match model.
Seeding by Elo descending ensures the 32 strongest players (by the same
metric used to compute match probabilities) get protected bracket positions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* revert: restore world-ranking-based seeding for darts bracket
Seeding should follow the PDC Order of Merit (world ranking), not Elo.
Reverts the previous Elo-seeding commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: interleave seeded and unseeded R1 pairs in darts bracket
Previously, all 32 seeded-vs-unseeded matches were added to r1Pairs
first (indices 0–31) and all 32 unseeded-vs-unseeded matches last
(indices 32–63). Since R2 pairs adjacent R1 winners, this created two
completely separate sub-brackets that only converged at the Final:
- Seeded sub-bracket: top players eliminating each other early
- Unseeded sub-bracket: high-Elo unseeded players (e.g. Doets 1818,
Zonneveld 1806) steamrolling weak opponents and making the Final
~20% of the time
Fix: interleave each seeded match with its adjacent unseeded-vs-unseeded
match. Seeded pair i is at r1Pairs[2i], unseeded pair i is at r1Pairs[2i+1].
From R2 onwards, winners from seeded and unseeded regions now merge
correctly as in a real single-elimination bracket.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address VORP code review issues
- Switch admin EV form to batchUpsertParticipantEVs to avoid N×N
concurrent UPDATE storm (was calling upsertParticipantEV per
participant, each triggering a full syncVorpForSeason)
- deleteParticipantEV now resets vorpValue to "0" and calls
syncVorpForSeason so remaining participants' ranks stay correct
- syncVorpForSeason issues a single bulk CASE UPDATE instead of
N individual UPDATE statements
- Add doc warning on recalculateEV that callers must sync VORP manually
- Extract REPLACEMENT_LEVEL_START/END_IDX constants; clarify comment
that 12-14 is a fixed product decision, not derived from league size
- Include vorpValue in draft room participant select projection
- Update drizzle-orm mock to support sql.join; update test assertions
to reflect single bulk-update call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve lint errors (toSorted, unused var)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix NHL simulator dynamic seeding and code quality issues
- Replace hardcoded seeding probabilities with standings-based Monte Carlo
projection: simulates remaining regular season games per team using Elo
win probability vs. a league-average opponent, so mathematically eliminated
teams naturally fall out of playoff contention
- Fix double-simulation bug in wildcard pool: all 16 conference teams are now
projected exactly once via sortProjected([...divA, ...divB].map(projectTeam)),
then filtered — wildcard pool no longer re-runs simulateProjectedPoints with
fresh randomness independent of the division seeding
- Move ProjectedTeam interface, projectTeam(), and sortProjected() to module
scope (defined once, not recreated on every buildConferenceBracket call)
- Replace conference-level participant count check (east < 8 || west < 8) with
per-division checks (each division < 3) for a more actionable error message
- Add logger.warn when standings.length === 0 so admins know the sim is running
without synced data
- Remove unused easternTeams/westernTeams variables
- Simplify test: replace Object.fromEntries pattern with a plain for..of loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix oxlint errors in NHL simulator and tests
- Replace non-null assertions (data!) with optional chaining (data?.x ?? "")
- Move makeEntry helper to module scope to avoid recreating on every call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add LLWS bracket Monte Carlo simulator
Simulates the 20-team Little League World Series: pool play round-robin
(5 teams/pool, top 2 advance) → 4-team double-elimination per side
(US and International) → consolation game → World Series final.
Uses championship futures odds as the sole win-probability signal.
Pool assignments are auto-detected from externalId: bare "US"/"Intl"
randomizes pools each simulation (pre-draw mode); "US:A"/"Intl:B" etc.
uses fixed assignments (post-draw mode). Sides can differ.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix oxlint violations in LLWS simulator
- Replace non-null assertions with null-safe access (! → ?? / optional chaining)
- Replace .sort() with .toSorted() per linting rules
- Promote bump() to simulate() scope and pass it into simulateSideBracket,
removing the need to pass counts into that function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#124
* Add NCAA Football CFP simulator (12-team bracket)
Implements a Monte Carlo simulator for the College Football Playoff using
the 2024-present 12-team format. Elo/FPI ratings are entered manually via
the existing admin Elo Ratings page; championship futures odds can
optionally be blended in (60% Elo / 40% odds).
- Add CFP_12 bracket template (First Round not scoring, QFs onward score)
- Add generateCFP12Bracket() with correct seeding: 5v12, 6v11, 7v10, 8v9
in First Round; seeds 1–4 receive QF byes
- Add NCAAFootballSimulator: 50k Monte Carlo sims, seeds teams by blended
Elo+odds strength, tracks champion/finalist/SF/QF placement tiers
- Register ncaa_football_bracket simulator type in registry and schema enum
- Add migration 0071: ALTER TYPE simulator_type ADD VALUE 'ncaa_football_bracket'
- Add tests: 30 tests covering bracket template structure and simulator
probability distributions, seeding, edge cases, futures blending
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix lint errors in NCAA Football CFP simulator
Replace non-null assertions with optional chaining, change let to const,
use toSorted() instead of sort(), and add a bump() helper to avoid
repeated map lookups with non-null assertions in simulateBracket.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TS2345 in NCAA football simulator test
Add ?? 0 fallback so optional-chained probFirst is number, not number | undefined.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix CS Major simulator bugs and accuracy issues (fixes#275)
- Fix crash when pool teams are missing Elo ratings by including all
participants with FALLBACK_ELO, ensuring Champions Stage always gets
exactly 8 teams
- Add AdvancedTeam type tracking losses-at-advancement; seed Champions
Stage by stage performance (fewer losses = higher seed) instead of
world rank alone
- Promote decisive Stage 2 matches (either team at ≥2W or ≥2L) to Bo3,
matching the real Challengers Stage format
- Tie-split QP for QF losers (slots 5–8) and SF losers (slots 3–4)
instead of assigning arbitrary individual placements
- Change stage-complete threshold from === 8 to >= 8 for robustness
- Validate even team count in simulateSwiss to prevent silent infinite loops
- Short-circuit Monte Carlo loop when all events are complete, returning
deterministic 0/1 probabilities
- Parallelize stage results DB fetches with Promise.all
- Export calcStage3ExitQP and simulateOneMajor; add test coverage for
both, plus new simulateSwiss and simulateChampionsStage edge cases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix oxlint violations in CS Major simulator
- Replace non-null assertion (!) with null-safe guard in simulateOneMajor
- Replace non-null assertions in test expectations with nullish coalescing
- Move makeStage3QPConfig out of describe block (no captured variables)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a new `nfl_bracket` simulator that projects the NFL regular
season and full playoff bracket using nfelo Elo ratings.
- Simulates remaining regular season games (17 total) per team using
Elo win probability vs an average opponent, then seeds both conferences
(division winners = seeds 1–4, wildcards = 5–7) per simulation
- Simulates Wild Card, Divisional, Conference Championship, and Super
Bowl rounds with correct NFL bracket structure (seed 1 bye)
- Applies +48 Elo home-field advantage (~57% win rate) for all rounds
except the neutral-site Super Bowl
- Pre-season mode (no standings in DB): simulates all 17 games from
scratch; falls back to futures odds → Elo conversion if no sourceElo
- Validates all 8 NFL divisions have Elo-rated teams before simulating,
with a clear error listing missing divisions
- Registers as `nfl_bracket` simulator type in the registry and schema
- Adds migration to extend the `simulator_type` enum
- Also updates drizzle.config.ts to prefer DIRECT_DATABASE_URL when set
Fixes#129
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
Fixes#123
* Add PDC World Darts Championship simulator (128-player bracket)
- New DartsSimulator: 128-player single-elimination, 7 rounds with
PDC-accurate best-of-sets formats (bo3/bo5/bo5/bo7/bo7/bo11/bo13).
ELO_DIVISOR=500 via set-level Bernoulli model. Two simulation paths:
Path A (bracket drawn) simulates from actual DB matches; Path B
(pre-bracket) seeds top 32 by world ranking in fixed positions and
randomly draws the remaining 96 per simulation run (50,000 iterations).
- darts_bracket added to simulatorTypeEnum and simulator registry.
- world_ranking nullable integer column added to participant_expected_values
(migration 0067); batchSaveSourceElos now accepts and persists it.
- Admin route /admin/sports-seasons/:id/darts-elo: bulk import with format
"Player Name, 2099, 1" (name, Elo, optional world ranking), fuzzy name
matching, auto-runs simulation and updates EV/snapshots on save.
- DARTS_128 bracket template added (scoring starts at Quarterfinals).
- 30 unit tests: math helpers, bracket seeding structure, Path A/B integration.
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
* Review fixes: pre-compute hot-loop invariants, import normalizeName
- Pre-compute seededSlots and getSeededMatchOrder(32) before the 50k
simulation loop — was being recomputed every iteration
- Pad unseeded pool to 96 once before the loop instead of inside it
- Inline bracket-building in the hot loop using pre-computed seededSlots;
removes the per-iteration call to buildR1Bracket/getSeededMatchOrder
- Remove dead SEEDED_R1_PAIRS constant (was never referenced)
- Import normalizeName from ~/lib/fuzzy-match instead of redefining it locally
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
* Fix lint failures: unused vars, eqeqeq, toSorted
- Remove unused seededSet/unseededSet variables in test
- Replace != null with !== null && !== undefined (eqeqeq rule)
- Replace .sort() with .toSorted() in simulator and route (unicorn/no-array-sort)
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
* Fix TS2552: restore seededSet declaration removed during lint fix
https://claude.ai/code/session_01WZ6FjMmC2eBeZ1564M9Ruz
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add FIFA World Cup 2026 support with group stage display and Monte Carlo simulator, fixes#127
- New `groupStageMatches` table for recording group play results (W/D/L, scores, matchday, schedule)
- `computeGroupStandings()` model function: pts → GD → GF → name tiebreaker ordering
- `GroupStageStandings` component showing all 12 groups with standings table and manager column
- Admin bracket UI: group match score entry, per-group standings, "Recalculate Floors" action
- `WorldCupSimulator`: 50k Monte Carlo covering group stage + best-8 3rd-place + knockout + 3rd place game
- Fuzzy name matching for national team Elo lookup (exact → substring → word-overlap), warns on miss
- Partial group completion: completed matches replayed with real scores, remaining matches simulated
- Elo priority: admin-entered sourceElo > futures odds converted to Elo > hardcoded national team ratings
- `fifa_48` bracket template: added Third Place Game round with `loserFeedsInto` on Semifinals
- Scoring rules: distinct 3rd/4th place for `fifa_48` (not averaged), QF losers share 5th–8th equally
- Floor scoring: SF participants guaranteed 4th (provisional), finalized after 3rd place game
- `recalculate-floors` admin action deletes and replays all results from scratch (fixes stale guard bug)
- Unique index on `(tournamentGroupId, participant1Id, participant2Id)` to prevent duplicate pairings
- Batch `findMatchesByGroupIds()` replacing N sequential queries in the sport season loader
- League home mini-standings now shows `actualPoints` (includes floor) instead of `totalPoints` only
- Elo ratings admin page supports World Cup (same bulk-import flow as snooker)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors: update GroupStandingData type to use findMatchesByGroupIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Increase Node heap to 4GB for unit tests in CI to prevent OOM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests
Tests now pass numSimulations=500 instead of the production default of 50,000.
Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub
Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Show tied ranks with T prefix across standings and Discord notifications, fixes#197
- Add buildTiedRankChecker() utility to standings-display.ts; replaces
four copies of inline rank-count logic spread across components,
the league home route, and the Discord service
- Prefix shared ranks with "T" (e.g. "T3") in StandingsTable (league
panel), StandingsTable (full standings), the league home preview, and
Discord webhook notifications
- Add useMemo wrapping in StandingsTable components so tie detection
does not recompute on every render
- Fix compareTeamsForRanking to round total points to hundredths before
comparing, preventing floating-point noise from producing spurious
non-ties in the standings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix discord test to expect escaped period in rank display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add WNBA playoff simulator with SRS-based Elo ratings, fixes#125
- New WNBASimulator: Monte Carlo (50k sims) projecting remaining regular
season games → seeding → R1 Bo3 / Semis Bo5 / Finals Bo7 bracket
- Hybrid Elo sourcing: pre-season uses futures odds (ICM); once avg
gamesPlayed ≥ 5, switches automatically to SRS-derived Elo
(elo = 1500 + srs × 20)
- New WnbaStandingsAdapter: fetches ESPN standings + teams endpoints in
parallel; includes zero-records for 2026 expansion teams (Portland
Fire, Toronto Tempo) not yet in standings
- Added srs column to regular_season_standings (migration 0063);
stored as net rating proxy (avgPointsFor − avgPointsAgainst)
- Added wnba_bracket to simulatorTypeEnum
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix missing afterEach import in wnba standings test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes#227
- Replace hardcoded Elo ratings with 2026 FanGraphs Depth Charts projected
run differential (RDif) for all 30 teams
- Replace Elo formula with Bill James log5 using RDif → win rate conversion
- Restore p_div/p_wc from FanGraphs playoff odds (divTitle/wcTitle) for
seeding draws — previously these were incorrectly derived from raw win
rates, which gave the Dodgers only ~24% division win probability instead
of the correct ~90%
- Add RDIF_DIVISOR constant to compress team strengths toward .500 for
playoff parity; set to 8000 (Dodgers +137 → ~51.7% per-game win rate)
- Fix minimum team check: < 5 → < 6 (need 3 div winners + 3 wild cards)
- Fix Colorado Rockies p_div 0.000 → 0.001 for consistency
- Fix stale comments throughout; update tests
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add MLB playoff simulator with AL/NL division standings, fixes#121
- New `mlb-simulator.ts`: 50k Monte Carlo sim drawing division winners
(weighted by p_div) and 3 WC teams per league, then simulating
WC best-of-3 → DS best-of-5 → LCS best-of-7 → World Series.
Elo parity factor 350; blends Vegas odds 30/70 when sourceOdds available.
- New `standings-sync/mlb.ts`: adapter for free statsapi.mlb.com API,
mapping AL/NL conferences, division ranks, streaks, home/away/L10 splits.
- New `mlb-divisions` display mode in RegularSeasonStandings: shows 1 division
winner per division section + Wild Card section with 3-spot playoff line,
headings read "AL/NL League" instead of "Conference".
- Refactored buildNhlSections/buildMlbSections into shared buildDivisionSections
(divisionSpots + wcSpots params); conferenceLabel now flows through group
objects rather than being derived from displayMode in the render.
- DB migration 0062 adds `mlb_bracket` to simulator_type enum.
- 37 new tests across simulator, adapter, and component.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix flaky tennis simulator test: increase trials and lower threshold
500 trials with a ~3–5% expected win rate had enough variance to
occasionally land below the 0.03 threshold (~2% failure rate).
Bumping to 2000 trials reduces the std dev by 2x; lowering the
threshold to 0.02 keeps the assertion meaningful (still well above
random 0.78%) while eliminating the flakiness.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add golf QP simulator with Plackett-Luce model, fixes#120
- New `participant_golf_skills` table (migration 0061) for SG: Total and
per-major American odds per player/season
- New `app/models/golf-skills.ts` with getGolfSkillsMap, getGolfSkillsForSeason,
batchUpsertGolfSkills
- Full `GolfSimulator` implementation replacing the TODO stub: Plackett-Luce
ranking model (PL_BETA=1.5, FIELD_SIZE=156), 10k Monte Carlo iterations,
awards QP by finishing position, ranks by total QP across all 4 majors
- New admin route `sports-seasons/:id/golf-skills` with bulk CSV import,
fuzzy name matching, per-player SG + per-major odds inputs; saves skills
and auto-runs simulation on submit
- Simulator dropdown on sport admin sorted alphabetically; renamed to
"Golf Qualifying Points Monte Carlo"
- Golf Skills button shown on sports season admin when simulator type is
golf_qualifying_points
- Extract normalizeName/diceCoefficient to shared `app/lib/fuzzy-match.ts`,
removing duplication from surface-elo and golf-skills routes
- Parallelize 4 DB queries in GolfSimulator.simulate() with Promise.all
- O(1) field array removal via swap-to-end + pop (was O(N) splice)
- Fix source tag: performance_model (not elo_simulation) for SG-based model
- 23 unit tests covering americanToImplied, getMajorOddsKey, resolveSkill,
simulateMajor, and Monte Carlo calibration properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix oxlint errors: no-non-null-assertion and eqeqeq
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a Monte Carlo simulator for men's/women's tennis seasons scored
on the qualifying_points pattern. Simulates all 4 Grand Slam majors
(Australian Open, French Open, Wimbledon, US Open) using surface-specific
Elo ratings and ATP/WTA world rankings for seeding.
New table: participant_surface_elos — one row per (participant, season)
storing worldRanking, eloHard, eloClay, eloGrass.
Key design decisions:
- Seeding uses ATP/WTA world ranking (not Elo), matching real draw procedure
- Top 32 seeded with standard slot placement (1→0, 2→64, 3-4→quarters, etc.)
- QP per round with tie-splitting pre-applied: W=20, F=14, SF=9, QF=4, R16=1.5
- Completed majors read actual qualifyingPointsAwarded from eventResults
- 10,000 Monte Carlo simulations; column sums naturally 1.0 (no normalization)
Admin UI at /admin/sports-seasons/:id/surface-elo:
- 5-column grid (Player | Rank | Hard | Clay | Grass)
- Bulk import: "Name, ranking, hardElo, clayElo, grassElo" one per line
- Fuzzy name matching (bigram Dice coefficient) with "Did you mean?" suggestions
- Inline participant creation for unmatched names via useFetcher
- Saves Elos and auto-runs simulation on submit
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- SnookerSimulator: Monte Carlo simulation of the 32-player World
Championship bracket using per-frame Bernoulli win probabilities
derived from Elo ratings (ELO_DIVISOR=700). Two paths: bracket-
populated (respects completed matches) and pre-bracket (simulates
qualifying, then seeds full draw).
- Admin route /sports-seasons/:id/elo-ratings: bulk-import and per-
player Elo entry with fuzzy name matching; saves ratings and auto-
runs the simulation in one action.
- schema: add snooker_bracket simulator type, source_elo column on
participant_expected_values, unique index on (participant_id,
sports_season_id).
- Fix batchSaveSourceElos to use INSERT ... ON CONFLICT DO UPDATE
instead of N+1 SELECT+UPDATE loop.
- Fix existingElos returned as plain Record (Map doesn't survive JSON
serialization through useLoaderData).
- Fix "How It Works" formula to show correct divisor (700, not 400).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Monte Carlo simulation of the 2026 AFL season and 10-team finals series.
Elo ratings are backsolved from Squiggle's projected season win totals using
the inverse formula: elo = 1500 - 450×log₁₀((1−wins/23)/(wins/23)).
- New `afl_bracket` simulator type (schema + migration)
- `afl-simulator.ts`: projects remaining regular season via Elo, seeds the
AFL_10 finals bracket (Wildcard → QF/EF → SF → PF → Grand Final), and
correctly tracks P5/P6 and P7/P8 as separate scoring tiers
- `afl.ts` standings sync adapter pulling from Squiggle API (no auth required)
- Finals line on standings display set to 10 for AFL seasons
- Substring name matching with longest-key-wins to prevent "Port Adelaide"
colliding with "Adelaide" when matching "Port Adelaide Power"
- 27 unit tests covering bracket logic, column-sum guarantees, and name matching
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 70/30 Elo + Vegas futures blending for per-game win probability,
mirroring the UCL simulator pattern; falls back to Elo-only when no
sourceOdds are stored in participantExpectedValues
- Adjust PARITY_FACTOR to better match Vegas championship implied
probabilities (COL was ~12% vs ~20% market expectation)
- Add 15 unit tests covering name normalization, team data lookup,
parity-factor math, seeding probability sanity, and eliminated-team checks
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static Basketball-Reference seed probability distributions
(p_1..p_10) with dynamic simulation of each team's remaining games
based on current standings from the DB.
- Load regularSeasonStandings in parallel with participants query
- Compute remainingGames = 82 - gamesPlayed per team
- Pre-compute per-game win probability (Elo vs average opponent 1500)
on TeamEntry at construction time — not inside the hot loop
- Sort conference standings by projected wins to assign seeds, replacing
the drawSeed() weighted-probability approach
- Conference resolved from standings table, falls back to TEAMS_DATA
- Strip all p_1..p_10 seed probability data from TEAMS_DATA
- Move simulateProjectedWins to module scope (no closure captures)
- Remove const N alias; use NUM_SIMULATIONS directly
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add app/lib/logger.ts: dev passes through to console; prod routes errors
to Sentry.captureException and warnings to Sentry.captureMessage, with
extra context preserved. Uses captureMessage (not captureException) for
string-only args to avoid fabricated stack traces.
- Add server/logger.ts: dev passes through; prod silences log/info but
keeps warn/error on stderr (Sentry not initialized in that process).
- Replace all console.* calls across 44 app files and 4 server files.
- Upgrade no-console from warn → error in oxlint; exempt logger files and
scripts/** via overrides.
- Add typescript/no-inferrable-types rule; fix violations in services and
simulators. Exempt test files (intentional string widening for switch/if
tests would break under literal type inference).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-shadow and consistent-function-scoping lint violations
Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint
warnings and promotes both rules to errors in .oxlintrc.json.
no-shadow: renamed Drizzle callback params (sports→s, matches→m,
seasons→s) to avoid shadowing outer imports; removed shadowed
destructures (eq, inArray) from where callbacks; renamed inner
template→bracketTemplate, prev→currentTimers, season→ss, name→teamName
(with name: teamName fix to preserve semantics).
consistent-function-scoping: moved formatDate, getRankBadge,
getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr,
elo (×2), weightedPick, sortByMatchNumber (×2) to module scope;
moved formatTime (×2), isValidLeagueName, getDraftTimes,
makeSeasonQueues to file scope in test files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-non-null-assertion lint violations and promote to error
Eliminates all 208 no-non-null-assertion warnings across 38 files.
Promotes typescript/no-non-null-assertion from warn to error in
.oxlintrc.json.
Fix patterns applied:
- Map.get(key)! after .has() check → extract with get() + null guard
- Map.get(key)! on pre-populated count maps → ?? 0 default
- .set(id, map.get(id)! + 1) increment → ?? 0 before adding
- participant1Id!/participant2Id! on DB matches → ?? "" fallback
- array.find()! in tests → guard + throw or expect().toBeDefined()
- bracketTemplateCache.get(id)! → null guard extract
- Various nullable field accesses → optional chain or ?? default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix prefer-add-event-listener, no-unassigned-import, require-module-specifiers
Resolves all 9 remaining non-console lint warnings and promotes all
three rules to errors in .oxlintrc.json.
- prefer-add-event-listener: converted onchange/onclick/onload
assignments to addEventListener in useDraftNotifications.ts and
admin.data-sync.tsx; stored changeHandler ref for proper cleanup
with removeEventListener
- no-unassigned-import: configured rule with allow list for legitimate
side-effect imports (*.css, @testing-library/jest-dom,
@testing-library/cypress/add-commands)
- require-module-specifiers: removed redundant `export {}` from
cypress/support/e2e.ts (file already has an import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TypeScript errors from no-non-null-assertion fixes
Two fixes introduced by the non-null assertion cleanup produced type
errors:
- scoring-event.ts: `?? ""` was wrong type for a participant object map;
restructured to explicit null guards so TypeScript can narrow correctly
- standings-sync/index.ts: `?? null` after name-match lookup lost the
truthy guarantee, causing TS18047 on the write-back block; added
`participant &&` guard before accessing its properties
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add npm run typecheck as Stop hook in Claude settings
Runs a full project typecheck at the end of each Claude turn so type
errors surface as feedback before the next message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add oxlint and fix all lint errors
- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix no-explicit-any warnings and upgrade tsconfig to ES2023
- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Promote no-explicit-any to error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds live standings sync and display for bracket-based sports (NBA/NHL),
so league members can see W/L tables and which teams their opponents drafted
during the regular season — not just after the playoff bracket is set.
- New `regular_season_standings` table with upsert-on-conflict sync
- Standings sync service with NHL (api-web.nhle.com) and NBA (ESPN) adapters,
externalId write-back for future syncs, and unmatched-team resolution UI
- `RegularSeasonStandings` component: flat (NBA) + division/wild-card (NHL) modes,
playoff line, TeamOwnerBadge, projected Brackt points (EV), mobile horizontal scroll
- Admin "Sync Standings" card + "Resolve Unmatched" UI on sports season page
- Admin manual standings edit hatch at /admin/sports-seasons/:id/regular-standings
- Show standings above bracket until matches exist; below once bracket is set
- `normalize-team-name` utility extracted to shared lib
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refine Discord webhook scoring notifications (fixes#180, #181)
- Filter scored matches to only show matchups where a manager scores
Brackt points (winner drafted) or has a team eliminated (loser drafted);
matches with no manager involvement are suppressed entirely.
- Escape Discord markdown characters (_*~`|\) in team names and usernames
to prevent formatting issues (e.g. double-underscore names causing
unintended underlines).
- Replace full standings with a "Standings Changes" section that shows
only teams whose points changed, plus any teams whose rank shifted as a
result, with ↑N / ↓N indicators for rank movement.
- Pass previousRanks map from scoring-calculator to the notification so
rank-displaced teams (who didn't score points themselves) are included.
- Update test webhook in league settings to demonstrate rank changes and
a sample scored match.
- Update all discord service tests to cover the new filtering, escaping,
and standings-change behaviour.
https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
* fix: only set winnerUsername when winning team's score actually changed
Previously, winnerUsername was set for any drafted winner regardless of
whether points were actually scored. This caused R64 wins (where the
scoring system may not award points until later rounds) to appear in
Discord's Scored Matches section even when no Brackt points were earned.
Now winnerUsername is only set when the winner's team's totalPoints
changed after recalculation. loserUsername (eliminations) is always set
when the loser is drafted, since being knocked out is always notable.
https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
* Refactor Discord notification logic in scoring calculator
- Compute changedTeamIds once up front; derive hasChanges from it
instead of duplicating the same iteration
- Merge usernameForParticipant and winnerUsernameForParticipant into a
single function with a requireScoreChange flag
- Replace hasDraftedParticipantMatches with hasScoredMatchesToShow,
which checks that at least one scoredMatch has a displayable username
— prevents sending a contentless Discord embed when a drafted winner
doesn't score any points and the loser isn't drafted
- Update inline comment to be sport-agnostic
https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
---------
Co-authored-by: Claude <noreply@anthropic.com>
Fixes the "slot is already filled" simulation error when First Four games
have already been played and their winners advanced to the Round of 64.
Changes:
- Fix FF→R64 validation to allow a filled R64 slot when the corresponding
First Four match is already complete (the original bug)
- Fix isComplete guard for E8/FF/Championship: derive loser from stored
participant IDs when loserId is null rather than re-simulating a real game
- Add round count validation for all rounds (R32 16, S16 8, E8 4, FF 2, Champ 1)
to catch malformed brackets before the hot sim loop instead of producing
silent garbage output
- Add participant1Id null check in the First Four path (mirrors the no-FF path)
- Add console.warn when participant IDs are missing from DB (fall-back to
average strength instead of crashing)
- Import NCAA_68.regions from bracket-templates.ts instead of duplicating the
hardcoded fallback in each simulator
- Fix duplicate step-9 comment number in ncaam-simulator.ts
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#115
- Add `NHLSimulator` using NHL's divisional bracket format (top 3 per
division + 2 wildcards per conference, best-of-7 series, parity
factor 800). Elo ratings and seeding probabilities sourced from
elo.harvitronix.com/nhl/2025-2026 as of March 18, 2026.
- Add `nhl_bracket` to `simulatorTypeEnum` with migration.
- Register `NHLSimulator` in the simulator registry.
- Fix simulator type dropdown in admin to auto-generate from the
registry instead of a hardcoded list, so new simulators appear
automatically without a separate UI change.
Code review fixes applied to the simulator:
- Narrow `weightedPick` key type to the five seeding probability fields
(was `keyof NhlTeamData`, allowing `"elo"` which would silently
corrupt results)
- Remove uniform-random fallback in `weightedPick` — zero-weight draws
now return `undefined` and are skipped, preventing eliminated teams
from becoming wildcards
- Exclude unrecognized participants entirely (with a console warning)
instead of silently routing them into the Eastern wildcard pool
- Replace the confusing for-of loop over both conferences (with unused
`champ` variable and `!`-suppressed unitialized vars) with two
explicit sequential East/West blocks
- Track `effectiveN` for skipped degenerate draws so probability
denominators stay accurate; throw if all iterations are skipped
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
**Discord webhook improvements**
- Show team owner username in parentheses in standings (e.g. "1. Alpha FC (christhrowsrocks) — 150 pts")
- Show username in parentheses in match results for drafted participants
(e.g. "Sporting (christhrowsrocks) def. Bodø/Glimt (apatel)"), omitting
the parenthesis for the undrafted side
- Username lookup only runs after the webhook URL guard to avoid wasted DB queries
- 4 new tests covering all username display combinations
**Fix calculateTeamScore partial-score counting bug**
- isPartialScore participants were incorrectly counted in participantsCompleted
and placementCounts, causing standings to show wrong remaining count and
phantom placement badges (e.g. "5th×1") for still-alive bracket participants
- Floor points still flow into totalPoints (guaranteed value for ranking)
- 3 new unit tests covering partial vs finalized behavior in calculateTeamScore
**Admin Force Re-score action**
- New "Fantasy Standings" card on every sports season admin page with a
Force Re-score button that triggers recalculateStandings on all linked
fantasy seasons — useful after data corrections without waiting for a
new scoring event
- Auth guard added to the action (was missing — layout loader only protects GET)
- Returns a meaningful message when no linked seasons exist
- Intent discriminant on action returns so success banners use actionData.intent
instead of fragile message.includes() checks
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- NCAAM R64/R32 winners no longer receive T5 floor points; only Sweet
Sixteen winners entering the first scoring round get a floor
- AFL T5-T6 and T7-T8 score as separate tiers (avg([5,6]) and avg([7,8]))
instead of one shared avg([5-8]) pool
- Eliminated teams now always show their correct final rank (e.g. T33 for
NCAAM R64 losers) even during partial round scoring, by deriving the
rank label from the total match count per round rather than the dynamic
still-alive count
- Discord notifications now fire when a drafted participant is eliminated
with 0 points (e.g. R64 losers whose team score doesn't change)
- Discord notifications no longer include all prior matches — scoped to
the current batch via matchIds
- Discord section headers have spacing; rank medals replaced with numbers
- Rounds auto-complete when all matches are marked done; manual Complete
Round dropdown removed
- Fixed double Discord notification when the last match in a round
triggers auto-complete: processPlayoffEvent now accepts skipRecalculate
so the caller controls when the notification fires
- Fixed falsy check on finalPosition=0 in calculateTeamScore
- Undrafted 0-point participants filtered from Eliminated Teams display
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Discord webhook notifications for standings updates
Adds a Discord webhook integration that posts standings to a configured
Discord channel whenever scores change after a scoring event. Commissioners
set the webhook URL in league settings and can send a test notification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix Discord notification edge cases from code review
- Fix point delta sign: negative diffs now correctly show -5 pts not +-5 pts
- Replace hardcoded soccer emoji with a neutral bullet (works for all sports)
- Truncate embed description at Discord's 4096-char limit
- Thread eventId through processMatchResult and bracket batch handler so
scored matches appear in single-match notifications too
- Pass eventName to finalizeQualifyingPoints ("Final Standings") and
processSeasonStandings ("Season Complete") so those notifications have context
- Test notification now fetches current season to show "League 2025" format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix process-match-result tests: add sportsSeasons to db mock
recalculateAffectedLeagues now queries sportsSeasons to get the sport
name for Discord notifications; the test mock db needed the stub added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
immediately (loser → final placement, winner → provisional floor).
Called from `set-winner` and `set-round-winners` so points are awarded
as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
and `updateProbabilitiesAfterResult` once after the loop instead of per-match
(`skipSideEffects: true` per match).
## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
`getRoundConfig()` helper, eliminating three parallel `if/else` chains in
`processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
`isScoring ?? true` default, and AFL Semi-Finals template requirement.
## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
wins), so AFL QF losers who advance to Semi-Finals via double-chance are
correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
SF loss, and normal advancement not protecting a later loser.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new `nba_bracket` simulator that projects both regular season
seedings and the full NBA playoff bracket, unlike other simulators that
operate on a pre-set bracket stored in the DB.
Algorithm:
- Per iteration: each team draws a conference seed (1–10) weighted by
BBRef seed probabilities, then seeds 7–10 play the Play-In tournament
(single elimination), then the resulting 8-team bracket is simulated
as best-of-7 series using Elo playoff ratings
- Elo source: playoff rating (last 110 games, no regression to mean,
postseason games weighted 3×) — appropriate for playoff matchup quality
- Seed probs source: Basketball-Reference (March 16, 2026), scaled from
conditional to unconditional by multiplying by each team's Playoffs%
Placement tiers map to SimulationProbabilities:
probFirst/Second → NBA champion / Finals loser
probThird/Fourth → Conference Finals losers (2 per sim)
probFifth–Eighth → Conference Semis losers (4 per sim)
Files:
- app/services/simulations/nba-simulator.ts (new)
- drizzle/0046_add_nba_bracket_simulator_type.sql (new)
- database/schema.ts: adds `nba_bracket` to simulatorTypeEnum
- app/services/simulations/registry.ts: registers NBASimulator
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>