Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* NCAAM Tournament Bracket Simulator
|
|
|
|
|
|
*
|
|
|
|
|
|
* Monte Carlo simulation of the NCAA Men's Basketball Tournament (64-team bracket).
|
|
|
|
|
|
* Win probability is derived from KenPom Adjusted Efficiency Margin (AEM).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Algorithm:
|
|
|
|
|
|
* 1. Load the bracket scoring event and all playoff matches from DB
|
|
|
|
|
|
* (6 rounds: R64, R32, S16, E8, FF, Final — 63 total matches)
|
|
|
|
|
|
* 2. Load participant names from DB; look up KenPom net rating by name
|
|
|
|
|
|
* from the hardcoded KENPOM_NET_RATINGS map (updated each season)
|
|
|
|
|
|
* 3. Per-match win probability = 1 / (1 + exp(-(netrtgA - netrtgB) / 7.5))
|
|
|
|
|
|
* (neutral-court logistic formula, calibrated to KenPom AEM scale)
|
|
|
|
|
|
* 4. Simulate 50,000 tournaments, honoring completed match results
|
|
|
|
|
|
* 5. Track placements only for point-scoring rounds:
|
|
|
|
|
|
* - Champion (1st)
|
|
|
|
|
|
* - Finalist (2nd)
|
|
|
|
|
|
* - Final Four losers (3rd/4th) — 2 per sim
|
|
|
|
|
|
* - Elite Eight losers (5th–8th) — 4 per sim
|
|
|
|
|
|
* R64 / R32 / S16 exits score 0 points → not tracked
|
|
|
|
|
|
*
|
|
|
|
|
|
* Probability output (8 slots — same SimulationProbabilities type as UCL):
|
|
|
|
|
|
* probFirst = champion / N
|
|
|
|
|
|
* probSecond = finalist / N
|
|
|
|
|
|
* probThird/Fourth = ffLoser / (2 * N) — 2 FF losers per sim
|
|
|
|
|
|
* probFifth–Eighth = e8Loser / (4 * N) — 4 E8 losers per sim
|
|
|
|
|
|
* All pre-E8 exits → 0 (no points scored)
|
|
|
|
|
|
*
|
|
|
|
|
|
* Column sums are guaranteed to equal 1.0 by construction:
|
|
|
|
|
|
* probFirst/Second — 1 per sim, N total → sums to 1
|
|
|
|
|
|
* probThird/Fourth — 2 per sim, each column = total/2N → sum = 1
|
|
|
|
|
|
* probFifth–Eighth — 4 per sim, each column = total/4N → sum = 1
|
|
|
|
|
|
*
|
|
|
|
|
|
* KenPom net ratings are hardcoded in KENPOM_NET_RATINGS below (2025-26 season).
|
|
|
|
|
|
* Update this map each season. Participant names must match DB records
|
|
|
|
|
|
* (lookup is case-insensitive, whitespace-normalized).
|
|
|
|
|
|
* Unknown teams fall back to netrtg = 0.0 (near-average strength).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Bracket advancement path (same as UCL):
|
|
|
|
|
|
* nextMatchNumber = Math.ceil(matchNumber / 2)
|
|
|
|
|
|
* i.e. R64 matches 1+2 → R32 match 1, R64 matches 3+4 → R32 match 2, …
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
|
import { eq, and, inArray } from "drizzle-orm";
|
|
|
|
|
|
import * as schema from "~/database/schema";
|
|
|
|
|
|
import type { BracketRegion } from "~/lib/bracket-templates";
|
2026-03-18 01:37:55 -07:00
|
|
|
|
import { buildNCAA68SlotMap, matchIndexForSeedSlot, NCAA_68 } from "~/lib/bracket-templates";
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
import type { Simulator, SimulationResult } from "./types";
|
2026-03-21 13:41:39 -07:00
|
|
|
|
import { logger } from "~/lib/logger";
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
|
|
|
|
|
|
// ─── Simulation parameters ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const NUM_SIMULATIONS = 50_000;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* KenPom scale factor for the neutral-court logistic win probability formula.
|
|
|
|
|
|
* A difference of 7.5 AEM points crosses the logit 50% mark.
|
|
|
|
|
|
* Source: KenPom documentation; widely used in academic NCAAM models.
|
|
|
|
|
|
*/
|
|
|
|
|
|
const KENPOM_SCALE_FACTOR = 7.5;
|
|
|
|
|
|
|
|
|
|
|
|
// ─── KenPom net rating data (2025-26 season) ─────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Update this map at the start of each tournament season with current
|
|
|
|
|
|
// KenPom Adjusted Efficiency Margin values.
|
|
|
|
|
|
// Source: kenpom.com, data through March 15, 2026 (6,195 games).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Keys: lowercase, whitespace-normalized team names (normalizeTeamName output).
|
|
|
|
|
|
// Multiple aliases are included for common abbreviation variants.
|
|
|
|
|
|
// If a participant name is not found, it falls back to 0.0 (average strength).
|
|
|
|
|
|
|
|
|
|
|
|
const KENPOM_NET_RATINGS: Record<string, number> = {
|
|
|
|
|
|
// ── 1–10 ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
"duke": 38.90,
|
|
|
|
|
|
"arizona": 37.66,
|
|
|
|
|
|
"michigan": 37.59,
|
|
|
|
|
|
"florida": 33.79,
|
|
|
|
|
|
"houston": 33.43,
|
|
|
|
|
|
"iowa st.": 32.42, "iowa state": 32.42,
|
|
|
|
|
|
"illinois": 32.10,
|
|
|
|
|
|
"purdue": 31.20,
|
|
|
|
|
|
"michigan st.": 28.31, "michigan state": 28.31,
|
|
|
|
|
|
"gonzaga": 28.10,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 11–30 ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
"connecticut": 27.87, "uconn": 27.87,
|
|
|
|
|
|
"vanderbilt": 27.51,
|
|
|
|
|
|
"virginia": 26.71,
|
|
|
|
|
|
"nebraska": 26.16,
|
|
|
|
|
|
"arkansas": 26.05,
|
|
|
|
|
|
"tennessee": 26.02,
|
|
|
|
|
|
"st. john's": 25.91, "st johns": 25.91,
|
|
|
|
|
|
"alabama": 25.72,
|
|
|
|
|
|
"louisville": 25.42,
|
|
|
|
|
|
"texas tech": 25.22,
|
|
|
|
|
|
"kansas": 24.44,
|
|
|
|
|
|
"wisconsin": 23.39,
|
|
|
|
|
|
"byu": 23.25,
|
|
|
|
|
|
"saint mary's": 23.07,
|
|
|
|
|
|
"iowa": 22.44,
|
|
|
|
|
|
"ohio st.": 22.24, "ohio state": 22.24,
|
|
|
|
|
|
"ucla": 21.67,
|
|
|
|
|
|
"kentucky": 21.48,
|
|
|
|
|
|
"north carolina": 20.84,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 30–50 ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
"utah st.": 20.76, "utah state": 20.76,
|
|
|
|
|
|
"miami fl": 20.68, "miami (fl)": 20.68, "miami": 20.68,
|
|
|
|
|
|
"georgia": 20.48,
|
|
|
|
|
|
"villanova": 19.97,
|
|
|
|
|
|
"nc state": 19.60, "n.c. state": 19.60,
|
|
|
|
|
|
"santa clara": 19.40,
|
|
|
|
|
|
"clemson": 19.24,
|
|
|
|
|
|
"texas": 19.03,
|
|
|
|
|
|
"auburn": 19.02,
|
|
|
|
|
|
"texas a&m": 18.67,
|
|
|
|
|
|
"oklahoma": 18.37,
|
|
|
|
|
|
"saint louis": 18.32,
|
|
|
|
|
|
"smu": 18.09,
|
|
|
|
|
|
"tcu": 17.59,
|
|
|
|
|
|
"cincinnati": 17.49,
|
|
|
|
|
|
"vcu": 17.21,
|
|
|
|
|
|
"indiana": 17.18,
|
|
|
|
|
|
"south florida": 16.39, "usf": 16.39,
|
|
|
|
|
|
"san diego st.": 16.39, "san diego state": 16.39,
|
|
|
|
|
|
"baylor": 16.00,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 50–80 ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
"new mexico": 15.81,
|
|
|
|
|
|
"seton hall": 15.71,
|
|
|
|
|
|
"missouri": 15.39,
|
|
|
|
|
|
"washington": 15.12,
|
|
|
|
|
|
"ucf": 15.04,
|
|
|
|
|
|
"virginia tech": 13.69,
|
|
|
|
|
|
"florida st.": 13.48, "florida state": 13.48,
|
|
|
|
|
|
"northwestern": 13.41,
|
|
|
|
|
|
"stanford": 13.37,
|
|
|
|
|
|
"west virginia": 13.27,
|
|
|
|
|
|
"lsu": 13.23,
|
|
|
|
|
|
"grand canyon": 13.19,
|
|
|
|
|
|
"boise st.": 13.18, "boise state": 13.18,
|
|
|
|
|
|
"tulsa": 12.97,
|
|
|
|
|
|
"akron": 12.80,
|
|
|
|
|
|
"ole miss": 12.62, "mississippi": 12.62,
|
|
|
|
|
|
"oklahoma st.": 12.58, "oklahoma state": 12.58,
|
|
|
|
|
|
"arizona st.": 12.52, "arizona state": 12.52,
|
|
|
|
|
|
"mcneese": 12.48, "mcneese st.": 12.48, "mcneese state": 12.48,
|
|
|
|
|
|
"belmont": 12.26,
|
|
|
|
|
|
"colorado": 12.11,
|
|
|
|
|
|
"providence": 11.81,
|
|
|
|
|
|
"northern iowa": 11.81,
|
|
|
|
|
|
"california": 11.43, "cal": 11.43,
|
|
|
|
|
|
"wake forest": 11.39,
|
|
|
|
|
|
"nevada": 11.30,
|
|
|
|
|
|
"creighton": 11.01,
|
|
|
|
|
|
"minnesota": 10.91,
|
|
|
|
|
|
"dayton": 10.91,
|
|
|
|
|
|
"georgetown": 10.89,
|
|
|
|
|
|
"usc": 10.81,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 81–120 ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
"yale": 10.65,
|
|
|
|
|
|
"wichita st.": 9.78, "wichita state": 9.78,
|
|
|
|
|
|
"syracuse": 9.74,
|
|
|
|
|
|
"marquette": 9.66,
|
|
|
|
|
|
"george washington": 9.64, "gwu": 9.64,
|
|
|
|
|
|
"butler": 9.49,
|
|
|
|
|
|
"hofstra": 9.49,
|
|
|
|
|
|
"colorado st.": 9.49, "colorado state": 9.49,
|
|
|
|
|
|
"notre dame": 8.88,
|
|
|
|
|
|
"utah valley": 8.82,
|
|
|
|
|
|
"stephen f. austin": 8.43, "sf austin": 8.43,
|
|
|
|
|
|
"high point": 8.40,
|
|
|
|
|
|
"miami oh": 8.26, "miami (oh)": 8.26,
|
|
|
|
|
|
"pittsburgh": 7.60, "pitt": 7.60,
|
|
|
|
|
|
"south carolina": 7.54,
|
|
|
|
|
|
"george mason": 7.50,
|
|
|
|
|
|
"xavier": 7.47,
|
|
|
|
|
|
"wyoming": 7.40,
|
|
|
|
|
|
"oregon": 7.03,
|
|
|
|
|
|
"mississippi st.": 7.00, "mississippi state": 7.00,
|
|
|
|
|
|
"kansas st.": 6.98, "kansas state": 6.98,
|
|
|
|
|
|
"depaul": 6.83,
|
|
|
|
|
|
"illinois st.": 6.66, "illinois state": 6.66,
|
|
|
|
|
|
"uc irvine": 6.21,
|
|
|
|
|
|
"illinois chicago": 6.16, "uic": 6.16,
|
|
|
|
|
|
"cal baptist": 5.99,
|
|
|
|
|
|
"unlv": 5.97,
|
|
|
|
|
|
"hawaii": 5.97, "hawai'i": 5.97,
|
|
|
|
|
|
"st. thomas": 5.88,
|
|
|
|
|
|
"unc wilmington": 5.79, "uncw": 5.79, "nc wilmington": 5.79,
|
|
|
|
|
|
"sam houston": 5.56, "sam houston st.": 5.56, "sam houston state": 5.56,
|
|
|
|
|
|
"pacific": 5.42,
|
|
|
|
|
|
"north dakota st.": 5.13, "north dakota state": 5.13,
|
|
|
|
|
|
"davidson": 4.94,
|
|
|
|
|
|
"saint joseph's": 4.56,
|
|
|
|
|
|
"southern illinois": 4.55,
|
|
|
|
|
|
"uc san diego": 4.53, "ucsd": 4.53,
|
|
|
|
|
|
"seattle": 4.49,
|
|
|
|
|
|
"maryland": 4.25,
|
|
|
|
|
|
"san francisco": 4.20,
|
|
|
|
|
|
"murray st.": 4.12, "murray state": 4.12,
|
|
|
|
|
|
"bradley": 3.96,
|
|
|
|
|
|
"rutgers": 3.95,
|
|
|
|
|
|
"liberty": 3.90,
|
|
|
|
|
|
"utah": 3.65,
|
|
|
|
|
|
"uab": 3.46,
|
|
|
|
|
|
"duquesne": 3.45,
|
|
|
|
|
|
"florida atlantic": 3.31,
|
|
|
|
|
|
"uc santa barbara": 3.17, "ucsb": 3.17,
|
|
|
|
|
|
"toledo": 3.15,
|
|
|
|
|
|
"fresno st.": 3.09, "fresno state": 3.09,
|
|
|
|
|
|
"montana st.": 3.00, "montana state": 3.00,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 134–170 (tournament bubble / auto-bid range) ─────────────────────────
|
|
|
|
|
|
"memphis": 2.52,
|
|
|
|
|
|
"rhode island": 2.47,
|
|
|
|
|
|
"north texas": 2.46,
|
|
|
|
|
|
"washington st.": 2.32, "washington state": 2.32,
|
|
|
|
|
|
"penn st.": 2.18, "penn state": 2.18,
|
|
|
|
|
|
"st. bonaventure": 2.17,
|
|
|
|
|
|
"wright st.": 2.04, "wright state": 2.04,
|
|
|
|
|
|
"northern colorado": 1.87,
|
|
|
|
|
|
"navy": 1.84,
|
|
|
|
|
|
"troy": 1.72,
|
|
|
|
|
|
"robert morris": 1.69,
|
|
|
|
|
|
"idaho": 1.53,
|
|
|
|
|
|
"portland st.": 1.52, "portland state": 1.52,
|
|
|
|
|
|
"bowling green": 1.51,
|
|
|
|
|
|
"kent st.": 1.50, "kent state": 1.50,
|
|
|
|
|
|
"william & mary": 1.49,
|
|
|
|
|
|
"penn": 1.47,
|
|
|
|
|
|
"arkansas st.": 1.39, "arkansas state": 1.39,
|
|
|
|
|
|
"harvard": 1.26,
|
|
|
|
|
|
"central arkansas": 1.25, "c arkansas": 1.25,
|
|
|
|
|
|
"winthrop": 1.13,
|
|
|
|
|
|
"western kentucky": 0.02, "western ky.": 0.02,
|
|
|
|
|
|
"kennesaw st.": 0.57, "kennesaw state": 0.57,
|
|
|
|
|
|
"cornell": 0.51,
|
|
|
|
|
|
"east tennessee st.": 0.44, "etsu": 0.44,
|
|
|
|
|
|
"eastern washington": 0.00, "east washington": 0.00,
|
|
|
|
|
|
"charleston": -0.19,
|
|
|
|
|
|
"austin peay": -0.28,
|
|
|
|
|
|
"merrimack": -0.83,
|
|
|
|
|
|
|
|
|
|
|
|
// ── 170–220 (low seeds / play-in range) ─────────────────────────────────
|
|
|
|
|
|
"montana": -1.72,
|
|
|
|
|
|
"umbc": -1.67,
|
|
|
|
|
|
"tennessee st.": -1.83, "tennessee state": -1.83,
|
|
|
|
|
|
"furman": -1.98,
|
|
|
|
|
|
"siena": -2.10,
|
|
|
|
|
|
"appalachian state": -2.33, "app state": -2.33,
|
|
|
|
|
|
"south alabama": -3.14,
|
|
|
|
|
|
"howard": -3.19,
|
|
|
|
|
|
"marshall": -3.19,
|
|
|
|
|
|
"samford": -3.93,
|
|
|
|
|
|
"liu": -3.95,
|
|
|
|
|
|
"lipscomb": -2.84,
|
|
|
|
|
|
"queens": -1.44,
|
|
|
|
|
|
"quinnipiac": -4.25,
|
|
|
|
|
|
"south dakota st.": -4.31, "south dakota state": -4.31,
|
|
|
|
|
|
"se missouri st.": -5.84, "southeast missouri state": -5.84,
|
|
|
|
|
|
"tennessee martin": -4.81, "ut martin": -4.81,
|
|
|
|
|
|
"vermont": -6.45,
|
|
|
|
|
|
"bethune": -6.60, "bethune-cookman": -6.60,
|
|
|
|
|
|
"colgate": -7.02,
|
|
|
|
|
|
"saint peter's": -7.51, "st. peter's": -7.51, "st peters": -7.51,
|
|
|
|
|
|
"mercer": -1.97,
|
|
|
|
|
|
"morehead st.": -10.35, "morehead state": -10.35,
|
|
|
|
|
|
"lehigh": -10.37,
|
|
|
|
|
|
"prairie view a&m": -10.69, "prairie view": -10.69,
|
|
|
|
|
|
"grambling": -11.78, "grambling st.": -11.78,
|
|
|
|
|
|
"central connecticut": -12.71, "c connecticut": -12.71,
|
|
|
|
|
|
"wagner": -12.68,
|
|
|
|
|
|
"norfolk st.": -15.38, "norfolk state": -15.38,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Public helpers (exported for unit testing) ───────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** Normalize a team name for KenPom map lookup. */
|
|
|
|
|
|
export function normalizeTeamName(name: string): string {
|
|
|
|
|
|
return name.toLowerCase().trim().replace(/\s+/g, " ");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Look up KenPom AEM for a participant name. Returns 0.0 if not found. */
|
|
|
|
|
|
export function getNetRating(name: string): number {
|
|
|
|
|
|
return KENPOM_NET_RATINGS[normalizeTeamName(name)] ?? 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* NCAAM neutral-court win probability using KenPom Adjusted Efficiency Margin.
|
|
|
|
|
|
* P(A beats B) = 1 / (1 + exp(-(netrtgA - netrtgB) / KENPOM_SCALE_FACTOR))
|
|
|
|
|
|
* Exported for unit testing.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function kenpomWinProbability(netrtgA: number, netrtgB: number): number {
|
|
|
|
|
|
return 1 / (1 + Math.exp(-(netrtgA - netrtgB) / KENPOM_SCALE_FACTOR));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
function sortByMatchNumber<T extends { matchNumber: number }>(matches: T[]): T[] {
|
|
|
|
|
|
return [...matches].toSorted((a, b) => a.matchNumber - b.matchNumber);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
// ─── Simulator ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
export class NCAAMSimulator implements Simulator {
|
|
|
|
|
|
async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
|
|
|
|
|
|
const db = database();
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Find the bracket scoring event (playoff_game) for this sports season.
|
|
|
|
|
|
const bracketEvent = await db.query.scoringEvents.findFirst({
|
|
|
|
|
|
where: and(
|
|
|
|
|
|
eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
|
eq(schema.scoringEvents.eventType, "playoff_game")
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!bracketEvent) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`No bracket event found for sports season ${sportsSeasonId}. ` +
|
|
|
|
|
|
`Create a playoff_game scoring event and set up the 64-team bracket first.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Load all playoff matches for this bracket event.
|
|
|
|
|
|
const allMatches = await db.query.playoffMatches.findMany({
|
|
|
|
|
|
where: eq(schema.playoffMatches.scoringEventId, bracketEvent.id),
|
|
|
|
|
|
orderBy: (m, { asc }) => [asc(m.matchNumber)],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (allMatches.length === 0) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`No playoff matches found for the bracket event. ` +
|
|
|
|
|
|
`Generate the 64-team bracket from the admin panel first.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Group matches by round name.
|
|
|
|
|
|
// Rounds: "First Four" (optional, 4) | "Round of 64" (32) | "Round of 32" (16)
|
|
|
|
|
|
// | "Sweet Sixteen" (8) | "Elite Eight" (4) | "Final Four" (2) | "Championship" (1)
|
|
|
|
|
|
//
|
|
|
|
|
|
// We look up by name rather than sorting by count to avoid the ambiguity between
|
|
|
|
|
|
// "First Four" and "Elite Eight" (both 4 matches).
|
|
|
|
|
|
const byRound = new Map<string, typeof allMatches>();
|
|
|
|
|
|
for (const m of allMatches) {
|
|
|
|
|
|
if (!byRound.has(m.round)) byRound.set(m.round, []);
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
byRound.get(m.round)?.push(m);
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
const rawFirstFour = byRound.get("First Four");
|
|
|
|
|
|
const rawR64 = byRound.get("Round of 64");
|
|
|
|
|
|
const rawR32 = byRound.get("Round of 32");
|
|
|
|
|
|
const rawS16 = byRound.get("Sweet Sixteen");
|
|
|
|
|
|
const rawE8 = byRound.get("Elite Eight");
|
|
|
|
|
|
const rawFF = byRound.get("Final Four");
|
|
|
|
|
|
const rawChamp = byRound.get("Championship");
|
|
|
|
|
|
|
|
|
|
|
|
const firstFourMatches = rawFirstFour ? sortByMatchNumber(rawFirstFour) : [];
|
|
|
|
|
|
const r64Matches = rawR64 ? sortByMatchNumber(rawR64) : null;
|
|
|
|
|
|
const r32Matches = rawR32 ? sortByMatchNumber(rawR32) : null;
|
|
|
|
|
|
const s16Matches = rawS16 ? sortByMatchNumber(rawS16) : null;
|
|
|
|
|
|
const e8Matches = rawE8 ? sortByMatchNumber(rawE8) : null;
|
|
|
|
|
|
const ffMatches = rawFF ? sortByMatchNumber(rawFF) : null;
|
|
|
|
|
|
const champMatches = rawChamp ? sortByMatchNumber(rawChamp) : null;
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
|
|
|
|
|
|
if (!r64Matches || !r32Matches || !s16Matches || !e8Matches || !ffMatches || !champMatches) {
|
|
|
|
|
|
const found = [...byRound.keys()].join(", ");
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Missing expected rounds. Found: [${found}]. ` +
|
|
|
|
|
|
`Required: Round of 64, Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (r64Matches.length !== 32) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Expected 32 Round of 64 matches, found ${r64Matches.length}. ` +
|
|
|
|
|
|
`This simulator only supports the standard 64-team NCAAM format.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-18 01:37:55 -07:00
|
|
|
|
if (r32Matches.length !== 16) {
|
|
|
|
|
|
throw new Error(`Expected 16 Round of 32 matches, found ${r32Matches.length}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s16Matches.length !== 8) {
|
|
|
|
|
|
throw new Error(`Expected 8 Sweet Sixteen matches, found ${s16Matches.length}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e8Matches.length !== 4) {
|
|
|
|
|
|
throw new Error(`Expected 4 Elite Eight matches, found ${e8Matches.length}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ffMatches.length !== 2) {
|
|
|
|
|
|
throw new Error(`Expected 2 Final Four matches, found ${ffMatches.length}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (champMatches.length !== 1) {
|
|
|
|
|
|
throw new Error(`Expected 1 Championship match, found ${champMatches.length}.`);
|
|
|
|
|
|
}
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
|
|
|
|
|
|
// 4. Build the First Four → Round of 64 slot mapping (if First Four games exist).
|
|
|
|
|
|
//
|
|
|
|
|
|
// First Four match N feeds into R64 match:
|
|
|
|
|
|
// regionIndex * 8 + matchIndexForSeedSlot(seedSlot) + 1
|
|
|
|
|
|
// This mirrors the advanceFirstFourWinner() logic in playoff-match.ts.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The region config is stored on the scoring event (bracketRegionConfig) so
|
|
|
|
|
|
// per-event overrides are respected. Falls back to the ncaa_68 template default.
|
|
|
|
|
|
//
|
|
|
|
|
|
// ffToR64: Map<firstFourMatchNumber, r64MatchNumber>
|
|
|
|
|
|
// r64NullSlots: Set of R64 match numbers whose participant2Id is null (pending FF)
|
|
|
|
|
|
const ffToR64 = new Map<number, number>();
|
|
|
|
|
|
const r64NullSlots = new Set<number>(
|
|
|
|
|
|
r64Matches.filter((m) => !m.participant2Id).map((m) => m.matchNumber)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (firstFourMatches.length > 0) {
|
|
|
|
|
|
const regions =
|
|
|
|
|
|
(bracketEvent.bracketRegionConfig as BracketRegion[] | null) ??
|
2026-03-18 01:37:55 -07:00
|
|
|
|
// Fall back to the template's built-in regions — single source of truth
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
NCAA_68.regions ?? [];
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
|
|
|
|
|
|
const slotMap = buildNCAA68SlotMap(regions);
|
|
|
|
|
|
for (let i = 0; i < slotMap.playInOffsets.length; i++) {
|
|
|
|
|
|
const { regionIndex, seedSlot } = slotMap.playInOffsets[i];
|
|
|
|
|
|
const r64MatchNumber = regionIndex * 8 + matchIndexForSeedSlot(seedSlot) + 1;
|
|
|
|
|
|
ffToR64.set(i + 1, r64MatchNumber); // First Four matchNumbers are 1-based
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validate First Four matches have participants before simulation
|
|
|
|
|
|
for (const m of firstFourMatches) {
|
|
|
|
|
|
if (!m.participant1Id || !m.participant2Id) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`First Four match ${m.matchNumber} is missing participants. ` +
|
|
|
|
|
|
`Assign both teams before running simulation.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 01:37:55 -07:00
|
|
|
|
// Validate First Four → R64 mapping covers all null R64 slots.
|
|
|
|
|
|
// Note: FF-mapped slots may already be filled if the First Four game was completed
|
|
|
|
|
|
// and the winner was advanced — that's expected and handled in the sim loop.
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
const r64SlotsCoveredByFF = new Set(ffToR64.values());
|
|
|
|
|
|
for (const nullSlot of r64NullSlots) {
|
|
|
|
|
|
if (!r64SlotsCoveredByFF.has(nullSlot)) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Round of 64 match ${nullSlot} has no participant2 but is not covered ` +
|
|
|
|
|
|
`by any First Four mapping. Check the bracket configuration.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 01:37:55 -07:00
|
|
|
|
for (const [ffMatchNum, ffR64Slot] of ffToR64) {
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
if (!r64NullSlots.has(ffR64Slot)) {
|
2026-03-18 01:37:55 -07:00
|
|
|
|
// Slot is already filled — only valid if the FF game is complete
|
|
|
|
|
|
const ffMatch = firstFourMatches.find((m) => m.matchNumber === ffMatchNum);
|
|
|
|
|
|
if (!ffMatch?.isComplete) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`First Four maps to Round of 64 match ${ffR64Slot}, but that slot is ` +
|
|
|
|
|
|
`already filled. Check the bracket configuration.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// In the First Four path, participant1Id must always be set; participant2Id
|
|
|
|
|
|
// may be null only for FF-pending slots (already validated above).
|
|
|
|
|
|
for (const m of r64Matches) {
|
|
|
|
|
|
if (!m.participant1Id) {
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
throw new Error(
|
2026-03-18 01:37:55 -07:00
|
|
|
|
`Round of 64 match ${m.matchNumber} is missing participant1. ` +
|
|
|
|
|
|
`Check the bracket configuration.`
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No First Four — all R64 slots must be directly filled
|
|
|
|
|
|
for (const m of r64Matches) {
|
|
|
|
|
|
if (!m.participant1Id || !m.participant2Id) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Round of 64 match ${m.matchNumber} is missing participants. ` +
|
|
|
|
|
|
`Assign all 64 teams to the bracket before running simulation.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Collect all participant IDs (up to 68 when First Four is present).
|
|
|
|
|
|
// R64 direct slots (60 or 64) + First Four teams (8) — First Four losers get
|
|
|
|
|
|
// all-zero probabilities since they don't enter the scored rounds.
|
|
|
|
|
|
const participantIdSet = new Set<string>();
|
|
|
|
|
|
for (const m of r64Matches) {
|
|
|
|
|
|
if (m.participant1Id) participantIdSet.add(m.participant1Id);
|
|
|
|
|
|
if (m.participant2Id) participantIdSet.add(m.participant2Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const m of firstFourMatches) {
|
|
|
|
|
|
if (m.participant1Id) participantIdSet.add(m.participant1Id);
|
|
|
|
|
|
if (m.participant2Id) participantIdSet.add(m.participant2Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
const participantIds = [...participantIdSet];
|
|
|
|
|
|
|
|
|
|
|
|
// 6. Load participant names from DB; build net rating lookup by ID.
|
|
|
|
|
|
const participantRows = await db
|
|
|
|
|
|
.select({ id: schema.participants.id, name: schema.participants.name })
|
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
|
.where(inArray(schema.participants.id, participantIds));
|
|
|
|
|
|
|
2026-03-18 01:37:55 -07:00
|
|
|
|
if (participantRows.length < participantIds.length) {
|
|
|
|
|
|
const foundIds = new Set(participantRows.map((r) => r.id));
|
|
|
|
|
|
const missing = participantIds.filter((id) => !foundIds.has(id));
|
2026-03-21 13:41:39 -07:00
|
|
|
|
logger.warn(
|
2026-03-18 01:37:55 -07:00
|
|
|
|
`[NCAAMSimulator] ${missing.length} participant ID(s) not found in DB: ${missing.join(", ")}. ` +
|
|
|
|
|
|
`They will be treated as average strength (KenPom 0.0).`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
const netRatingById = new Map<string, number>();
|
|
|
|
|
|
for (const { id, name } of participantRows) {
|
|
|
|
|
|
netRatingById.set(id, getNetRating(name));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. Build per-round O(1) lookup maps keyed by matchNumber.
|
|
|
|
|
|
const firstFourByNum = new Map(firstFourMatches.map((m) => [m.matchNumber, m]));
|
|
|
|
|
|
const r64ByNum = new Map(r64Matches.map((m) => [m.matchNumber, m]));
|
|
|
|
|
|
const r32ByNum = new Map(r32Matches.map((m) => [m.matchNumber, m]));
|
|
|
|
|
|
const s16ByNum = new Map(s16Matches.map((m) => [m.matchNumber, m]));
|
|
|
|
|
|
const e8ByNum = new Map(e8Matches.map((m) => [m.matchNumber, m]));
|
|
|
|
|
|
const ffByNum = new Map(ffMatches.map((m) => [m.matchNumber, m]));
|
|
|
|
|
|
const champMatch = champMatches[0];
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const simMatch = (p1: string, p2: string): { winner: string; loser: string } => {
|
|
|
|
|
|
const r1 = netRatingById.get(p1) ?? 0;
|
|
|
|
|
|
const r2 = netRatingById.get(p2) ?? 0;
|
|
|
|
|
|
const w = Math.random() < kenpomWinProbability(r1, r2) ? p1 : p2;
|
|
|
|
|
|
return { winner: w, loser: w === p1 ? p2 : p1 };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 8. Integer placement count maps — initialized to 0 for all participants.
|
|
|
|
|
|
const championCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
|
|
|
|
|
|
const finalistCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
|
|
|
|
|
|
const ffLoserCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
|
|
|
|
|
|
const e8LoserCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
|
|
|
|
|
|
|
|
|
|
|
|
// 9. Monte Carlo simulation loop.
|
|
|
|
|
|
for (let s = 0; s < NUM_SIMULATIONS; s++) {
|
|
|
|
|
|
// ── First Four (4 play-in games → 4 winners placed into R64 slots) ───
|
|
|
|
|
|
// ffSimWinners: Map<r64MatchNumber, winnerId> for the null participant2Id slots
|
|
|
|
|
|
const ffSimWinners = new Map<number, string>();
|
|
|
|
|
|
for (const [ffMatchNum, r64MatchNum] of ffToR64) {
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
const m = firstFourByNum.get(ffMatchNum);
|
|
|
|
|
|
if (!m) continue;
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
const winner = m.isComplete && m.winnerId
|
|
|
|
|
|
? m.winnerId
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
: simMatch(m.participant1Id ?? "", m.participant2Id ?? "").winner;
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
ffSimWinners.set(r64MatchNum, winner);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Round of 64 (32 matches → 32 winners) ────────────────────────────
|
|
|
|
|
|
const r64Winners: string[] = [];
|
|
|
|
|
|
for (let i = 1; i <= 32; i++) {
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
const m = r64ByNum.get(i);
|
|
|
|
|
|
if (!m) continue;
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
// participant2Id may be null if this slot is filled by a First Four winner
|
|
|
|
|
|
const p2 = m.participant2Id ?? ffSimWinners.get(i) ?? null;
|
|
|
|
|
|
if (m.isComplete && m.winnerId) {
|
|
|
|
|
|
r64Winners.push(m.winnerId);
|
|
|
|
|
|
} else {
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
r64Winners.push(simMatch(m.participant1Id ?? "", p2 ?? "").winner);
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Round of 32 (16 matches → 16 winners) ────────────────────────────
|
|
|
|
|
|
// Match i uses r64Winners[(i-1)*2] and [(i-1)*2+1]
|
|
|
|
|
|
const r32Winners: string[] = [];
|
|
|
|
|
|
for (let i = 1; i <= 16; i++) {
|
|
|
|
|
|
const dbMatch = r32ByNum.get(i);
|
|
|
|
|
|
if (dbMatch?.isComplete && dbMatch.winnerId) {
|
|
|
|
|
|
r32Winners.push(dbMatch.winnerId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const p1 = r64Winners[(i - 1) * 2];
|
|
|
|
|
|
const p2 = r64Winners[(i - 1) * 2 + 1];
|
|
|
|
|
|
r32Winners.push(simMatch(p1, p2).winner);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Sweet 16 (8 matches → 8 winners) ─────────────────────────────────
|
|
|
|
|
|
const s16Winners: string[] = [];
|
|
|
|
|
|
for (let i = 1; i <= 8; i++) {
|
|
|
|
|
|
const dbMatch = s16ByNum.get(i);
|
|
|
|
|
|
if (dbMatch?.isComplete && dbMatch.winnerId) {
|
|
|
|
|
|
s16Winners.push(dbMatch.winnerId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const p1 = r32Winners[(i - 1) * 2];
|
|
|
|
|
|
const p2 = r32Winners[(i - 1) * 2 + 1];
|
|
|
|
|
|
s16Winners.push(simMatch(p1, p2).winner);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Elite Eight (4 matches → 4 winners + 4 tracked losers) ───────────
|
|
|
|
|
|
const e8Winners: string[] = [];
|
|
|
|
|
|
for (let i = 1; i <= 4; i++) {
|
|
|
|
|
|
const dbMatch = e8ByNum.get(i);
|
|
|
|
|
|
let winner: string;
|
|
|
|
|
|
let loser: string;
|
2026-03-18 01:37:55 -07:00
|
|
|
|
if (dbMatch?.isComplete && dbMatch.winnerId) {
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
winner = dbMatch.winnerId;
|
2026-03-18 01:37:55 -07:00
|
|
|
|
// Derive loser from stored participants if loserId is missing
|
|
|
|
|
|
loser = dbMatch.loserId ??
|
|
|
|
|
|
(dbMatch.participant1Id === dbMatch.winnerId
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
? dbMatch.participant2Id ?? ""
|
|
|
|
|
|
: dbMatch.participant1Id ?? "");
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
const p1 = s16Winners[(i - 1) * 2];
|
|
|
|
|
|
const p2 = s16Winners[(i - 1) * 2 + 1];
|
|
|
|
|
|
({ winner, loser } = simMatch(p1, p2));
|
|
|
|
|
|
}
|
|
|
|
|
|
e8Winners.push(winner);
|
|
|
|
|
|
e8LoserCounts.set(loser, (e8LoserCounts.get(loser) ?? 0) + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Final Four (2 matches → 2 winners + 2 tracked losers) ────────────
|
|
|
|
|
|
const ffWinners: string[] = [];
|
|
|
|
|
|
for (let i = 1; i <= 2; i++) {
|
|
|
|
|
|
const dbMatch = ffByNum.get(i);
|
|
|
|
|
|
let winner: string;
|
|
|
|
|
|
let loser: string;
|
2026-03-18 01:37:55 -07:00
|
|
|
|
if (dbMatch?.isComplete && dbMatch.winnerId) {
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
winner = dbMatch.winnerId;
|
2026-03-18 01:37:55 -07:00
|
|
|
|
loser = dbMatch.loserId ??
|
|
|
|
|
|
(dbMatch.participant1Id === dbMatch.winnerId
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
? dbMatch.participant2Id ?? ""
|
|
|
|
|
|
: dbMatch.participant1Id ?? "");
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
const p1 = e8Winners[(i - 1) * 2];
|
|
|
|
|
|
const p2 = e8Winners[(i - 1) * 2 + 1];
|
|
|
|
|
|
({ winner, loser } = simMatch(p1, p2));
|
|
|
|
|
|
}
|
|
|
|
|
|
ffWinners.push(winner);
|
|
|
|
|
|
ffLoserCounts.set(loser, (ffLoserCounts.get(loser) ?? 0) + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Championship ──────────────────────────────────────────────────────
|
|
|
|
|
|
let champion: string;
|
|
|
|
|
|
let finalist: string;
|
2026-03-18 01:37:55 -07:00
|
|
|
|
if (champMatch?.isComplete && champMatch.winnerId) {
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
champion = champMatch.winnerId;
|
2026-03-18 01:37:55 -07:00
|
|
|
|
finalist = champMatch.loserId ??
|
|
|
|
|
|
(champMatch.participant1Id === champMatch.winnerId
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
? champMatch.participant2Id ?? ""
|
|
|
|
|
|
: champMatch.participant1Id ?? "");
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
({ winner: champion, loser: finalist } = simMatch(ffWinners[0], ffWinners[1]));
|
|
|
|
|
|
}
|
|
|
|
|
|
championCounts.set(champion, (championCounts.get(champion) ?? 0) + 1);
|
|
|
|
|
|
finalistCounts.set(finalist, (finalistCounts.get(finalist) ?? 0) + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 01:37:55 -07:00
|
|
|
|
// 10. Convert integer counts to probability distributions.
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
// Exact denominators guarantee column sums of 1.0 by construction:
|
|
|
|
|
|
// probFirst/Second → N total per sim → sum = 1
|
|
|
|
|
|
// probThird/Fourth → ffLoserCounts / (2*N) → 2 losers * 1/(2N) = 1
|
|
|
|
|
|
// probFifth–Eighth → e8LoserCounts / (4*N) → 4 losers * 1/(4N) = 1
|
|
|
|
|
|
const N = NUM_SIMULATIONS;
|
|
|
|
|
|
const results: SimulationResult[] = participantIds.map((participantId) => {
|
Fix oxlint warnings: no-shadow, consistent-function-scoping, no-non-null-assertion, and others (#196)
* 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>
2026-03-21 10:59:51 -07:00
|
|
|
|
const c = championCounts.get(participantId) ?? 0;
|
|
|
|
|
|
const f = finalistCounts.get(participantId) ?? 0;
|
|
|
|
|
|
const ff = ffLoserCounts.get(participantId) ?? 0;
|
|
|
|
|
|
const e8 = e8LoserCounts.get(participantId) ?? 0;
|
Add NCAAM and NCAAW bracket Monte Carlo simulators (#150)
- NCAAM: KenPom AEM logistic formula (1/(1+exp(-diff/7.5))), data through 2025-26 March 15
- NCAAW: Barttorvik Barthag Log5 formula (A*(1-B)/(A*(1-B)+B*(1-A))), same bracket structure
- Both simulators: 50,000-iteration Monte Carlo, First Four simulation, honors completed matches
- Track E8+ placements only: champion, finalist, FF losers (3rd/4th), E8 losers (5th–8th)
- Add bracket configuration validation: null R64 slots must exactly match First Four mapping
- Fix DEFAULT_SCORING_RULES to 100/70/45/45/20/20/20/20 (3rd/4th=45, 5th–8th=20)
- Align scoring constants across simulate route, expected-values display, and server action
- Zero out EVs for non-bracket participants on every simulation run (prevents EV inflation)
- Add EV total invariant warning (expected ~340) on expected-values admin page
- 98 unit tests across NCAAM, NCAAW, and UCL simulators — all passing
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 23:31:47 -07:00
|
|
|
|
return {
|
|
|
|
|
|
participantId,
|
|
|
|
|
|
probabilities: {
|
|
|
|
|
|
probFirst: c / N,
|
|
|
|
|
|
probSecond: f / N,
|
|
|
|
|
|
probThird: ff / (2 * N),
|
|
|
|
|
|
probFourth: ff / (2 * N),
|
|
|
|
|
|
probFifth: e8 / (4 * N),
|
|
|
|
|
|
probSixth: e8 / (4 * N),
|
|
|
|
|
|
probSeventh: e8 / (4 * N),
|
|
|
|
|
|
probEighth: e8 / (4 * N),
|
|
|
|
|
|
},
|
|
|
|
|
|
source: "ncaam_bracket_kenpom",
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 10. Per-position normalization — belt-and-suspenders guard against
|
|
|
|
|
|
// floating-point residuals. Column sums are already near-exactly 1.0
|
|
|
|
|
|
// after step 9, but this guarantees the invariant before persisting.
|
|
|
|
|
|
const positionKeys: Array<keyof (typeof results)[0]["probabilities"]> = [
|
|
|
|
|
|
"probFirst", "probSecond", "probThird", "probFourth",
|
|
|
|
|
|
"probFifth", "probSixth", "probSeventh", "probEighth",
|
|
|
|
|
|
];
|
|
|
|
|
|
for (const key of positionKeys) {
|
|
|
|
|
|
const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
|
|
|
|
|
|
const residual = 1.0 - colSum;
|
|
|
|
|
|
if (residual !== 0) {
|
|
|
|
|
|
const maxResult = results.reduce((best, r) =>
|
|
|
|
|
|
r.probabilities[key] > best.probabilities[key] ? r : best
|
|
|
|
|
|
);
|
|
|
|
|
|
maxResult.probabilities[key] += residual;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|