2025-11-17 22:19:46 -08:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import {
|
|
|
|
|
simulateBracket,
|
|
|
|
|
simulateBracketSync,
|
|
|
|
|
getExpectedCounts,
|
|
|
|
|
type TeamForSimulation,
|
|
|
|
|
type ProbabilityDistribution,
|
|
|
|
|
} from '../bracket-simulator';
|
|
|
|
|
|
|
|
|
|
describe('bracket-simulator', () => {
|
|
|
|
|
describe('simulateBracketSync', () => {
|
|
|
|
|
it('simulates 8-team bracket with equal ratings', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = [
|
|
|
|
|
{ participantId: '1', elo: 1500 },
|
|
|
|
|
{ participantId: '2', elo: 1500 },
|
|
|
|
|
{ participantId: '3', elo: 1500 },
|
|
|
|
|
{ participantId: '4', elo: 1500 },
|
|
|
|
|
{ participantId: '5', elo: 1500 },
|
|
|
|
|
{ participantId: '6', elo: 1500 },
|
|
|
|
|
{ participantId: '7', elo: 1500 },
|
|
|
|
|
{ participantId: '8', elo: 1500 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
|
|
|
|
|
|
|
|
|
expect(results.size).toBe(8);
|
|
|
|
|
|
|
|
|
|
// With equal ratings, each team should have roughly equal probability
|
2026-03-21 09:44:05 -07:00
|
|
|
results.forEach((distribution, _participantId) => {
|
2025-11-17 22:19:46 -08:00
|
|
|
// Sum of probabilities should be 1.0
|
|
|
|
|
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
|
|
|
|
expect(sum).toBeCloseTo(1.0, 1);
|
|
|
|
|
|
|
|
|
|
// Each team should win championship ~12.5% of the time (1/8)
|
|
|
|
|
expect(distribution[0]).toBeGreaterThan(0.08);
|
|
|
|
|
expect(distribution[0]).toBeLessThan(0.17);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('gives higher win probability to stronger team', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = [
|
|
|
|
|
{ participantId: 'strong', elo: 1700 }, // Much stronger
|
|
|
|
|
{ participantId: '2', elo: 1500 },
|
|
|
|
|
{ participantId: '3', elo: 1500 },
|
|
|
|
|
{ participantId: '4', elo: 1500 },
|
|
|
|
|
{ participantId: '5', elo: 1500 },
|
|
|
|
|
{ participantId: '6', elo: 1500 },
|
|
|
|
|
{ participantId: '7', elo: 1500 },
|
|
|
|
|
{ participantId: 'weak', elo: 1300 }, // Much weaker
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
|
|
|
|
|
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 strongDist = results.get('strong');
|
|
|
|
|
const weakDist = results.get('weak');
|
|
|
|
|
if (!strongDist || !weakDist) throw new Error('strong or weak result not found');
|
2025-11-17 22:19:46 -08:00
|
|
|
|
|
|
|
|
// Strong team should have higher championship probability
|
|
|
|
|
expect(strongDist[0]).toBeGreaterThan(weakDist[0]);
|
|
|
|
|
expect(strongDist[0]).toBeGreaterThan(0.2); // Should win >20% of the time
|
|
|
|
|
|
|
|
|
|
// Weak team should have lower championship probability
|
|
|
|
|
expect(weakDist[0]).toBeLessThan(0.1); // Should win <10% of the time
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles extreme Elo differences', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = [
|
|
|
|
|
{ participantId: 'champion', elo: 1900 }, // Dominant
|
|
|
|
|
{ participantId: '2', elo: 1400 },
|
|
|
|
|
{ participantId: '3', elo: 1400 },
|
|
|
|
|
{ participantId: '4', elo: 1400 },
|
|
|
|
|
{ participantId: '5', elo: 1400 },
|
|
|
|
|
{ participantId: '6', elo: 1400 },
|
|
|
|
|
{ participantId: '7', elo: 1400 },
|
|
|
|
|
{ participantId: '8', elo: 1400 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const results = simulateBracketSync(teams, 'nhl-8', 10000);
|
|
|
|
|
|
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 championDist = results.get('champion');
|
|
|
|
|
if (!championDist) throw new Error('champion result not found');
|
2025-11-17 22:19:46 -08:00
|
|
|
|
|
|
|
|
// Dominant team should win very often
|
|
|
|
|
expect(championDist[0]).toBeGreaterThan(0.6); // >60% championship probability
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures probabilities sum to 1.0 for each team', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = [
|
|
|
|
|
{ participantId: '1', elo: 1650 },
|
|
|
|
|
{ participantId: '2', elo: 1600 },
|
|
|
|
|
{ participantId: '3', elo: 1550 },
|
|
|
|
|
{ participantId: '4', elo: 1500 },
|
|
|
|
|
{ participantId: '5', elo: 1450 },
|
|
|
|
|
{ participantId: '6', elo: 1400 },
|
|
|
|
|
{ participantId: '7', elo: 1350 },
|
|
|
|
|
{ participantId: '8', elo: 1300 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const results = simulateBracketSync(teams, 'nhl-8', 5000);
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
results.forEach((distribution, _participantId) => {
|
2025-11-17 22:19:46 -08:00
|
|
|
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
|
|
|
|
expect(sum).toBeCloseTo(1.0, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws error for wrong number of teams', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = [
|
|
|
|
|
{ participantId: '1', elo: 1500 },
|
|
|
|
|
{ participantId: '2', elo: 1500 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
|
simulateBracketSync(teams, 'nhl-8', 1000);
|
|
|
|
|
}).toThrow('requires exactly 8 teams');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws error for invalid simulation count', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = Array.from({ length: 8 }, (_, i) => ({
|
|
|
|
|
participantId: String(i + 1),
|
|
|
|
|
elo: 1500,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
|
simulateBracketSync(teams, 'nhl-8', 0);
|
|
|
|
|
}).toThrow('must be positive');
|
|
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
|
simulateBracketSync(teams, 'nhl-8', -100);
|
|
|
|
|
}).toThrow('must be positive');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('produces consistent results with same random seed', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = Array.from({ length: 8 }, (_, i) => ({
|
|
|
|
|
participantId: String(i + 1),
|
|
|
|
|
elo: 1500 + i * 50,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Run with small sample size for speed
|
|
|
|
|
const results1 = simulateBracketSync(teams, 'nhl-8', 1000);
|
|
|
|
|
const results2 = simulateBracketSync(teams, 'nhl-8', 1000);
|
|
|
|
|
|
|
|
|
|
// Results won't be identical due to randomness, but should be in same ballpark
|
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 dist1 = results1.get('1');
|
|
|
|
|
const dist2 = results2.get('1');
|
|
|
|
|
if (!dist1 || !dist2) throw new Error('Result for participant 1 not found');
|
2025-11-17 22:19:46 -08:00
|
|
|
|
|
|
|
|
// Championship probabilities should be within 10% of each other
|
|
|
|
|
expect(Math.abs(dist1[0] - dist2[0])).toBeLessThan(0.1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns valid probability distribution structure', () => {
|
|
|
|
|
const teams: TeamForSimulation[] = Array.from({ length: 8 }, (_, i) => ({
|
|
|
|
|
participantId: String(i + 1),
|
|
|
|
|
elo: 1500,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const results = simulateBracketSync(teams, 'nhl-8', 1000);
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
results.forEach((distribution, _participantId) => {
|
2025-11-17 22:19:46 -08:00
|
|
|
// Should be array of 8 numbers
|
|
|
|
|
expect(distribution).toHaveLength(8);
|
|
|
|
|
|
|
|
|
|
// All probabilities should be between 0 and 1
|
|
|
|
|
distribution.forEach(prob => {
|
|
|
|
|
expect(prob).toBeGreaterThanOrEqual(0);
|
|
|
|
|
expect(prob).toBeLessThanOrEqual(1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('simulateBracket (async)', () => {
|
|
|
|
|
it('simulates bracket asynchronously', async () => {
|
|
|
|
|
const teams: TeamForSimulation[] = Array.from({ length: 8 }, (_, i) => ({
|
|
|
|
|
participantId: String(i + 1),
|
|
|
|
|
elo: 1500,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const results = await simulateBracket(teams, 'nhl-8', 5000);
|
|
|
|
|
|
|
|
|
|
expect(results.size).toBe(8);
|
|
|
|
|
|
|
|
|
|
results.forEach((distribution) => {
|
|
|
|
|
const sum = distribution.reduce((acc, p) => acc + p, 0);
|
|
|
|
|
expect(sum).toBeCloseTo(1.0, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls progress callback', async () => {
|
|
|
|
|
const teams: TeamForSimulation[] = Array.from({ length: 8 }, (_, i) => ({
|
|
|
|
|
participantId: String(i + 1),
|
|
|
|
|
elo: 1500,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const progressCalls: Array<{ current: number; total: number }> = [];
|
|
|
|
|
|
|
|
|
|
await simulateBracket(teams, 'nhl-8', 25000, (current, total) => {
|
|
|
|
|
progressCalls.push({ current, total });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should have progress updates at 10k, 20k, and 25k
|
|
|
|
|
expect(progressCalls.length).toBeGreaterThan(0);
|
|
|
|
|
expect(progressCalls[progressCalls.length - 1].current).toBe(25000);
|
|
|
|
|
expect(progressCalls[progressCalls.length - 1].total).toBe(25000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getExpectedCounts', () => {
|
|
|
|
|
it('converts probabilities to expected counts', () => {
|
|
|
|
|
const distribution: ProbabilityDistribution = [
|
|
|
|
|
0.25, // 25% 1st
|
|
|
|
|
0.20, // 20% 2nd
|
|
|
|
|
0.15, // 15% 3rd
|
|
|
|
|
0.10, // 10% 4th
|
|
|
|
|
0.10, // 10% 5th
|
|
|
|
|
0.10, // 10% 6th
|
|
|
|
|
0.05, // 5% 7th
|
|
|
|
|
0.05, // 5% 8th
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const counts = getExpectedCounts(distribution, 10000);
|
|
|
|
|
|
|
|
|
|
expect(counts[1]).toBe(2500);
|
|
|
|
|
expect(counts[2]).toBe(2000);
|
|
|
|
|
expect(counts[3]).toBe(1500);
|
|
|
|
|
expect(counts[4]).toBe(1000);
|
|
|
|
|
expect(counts[5]).toBe(1000);
|
|
|
|
|
expect(counts[6]).toBe(1000);
|
|
|
|
|
expect(counts[7]).toBe(500);
|
|
|
|
|
expect(counts[8]).toBe(500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rounds to nearest integer', () => {
|
|
|
|
|
const distribution: ProbabilityDistribution = [
|
|
|
|
|
0.123, 0.123, 0.123, 0.123, 0.123, 0.123, 0.131, 0.131,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const counts = getExpectedCounts(distribution, 1000);
|
|
|
|
|
|
|
|
|
|
// Should round 123 and 131
|
|
|
|
|
expect(counts[1]).toBe(123);
|
|
|
|
|
expect(counts[7]).toBe(131);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('integration: realistic NHL scenario', () => {
|
|
|
|
|
it('simulates NHL playoff bracket with realistic Elo ratings', async () => {
|
|
|
|
|
// Based on plan: Colorado (1648), NY Islanders (1324), etc.
|
|
|
|
|
const teams: TeamForSimulation[] = [
|
|
|
|
|
{ participantId: 'COL', elo: 1648 }, // Colorado (strongest)
|
|
|
|
|
{ participantId: 'FLA', elo: 1620 }, // Florida
|
|
|
|
|
{ participantId: 'VGK', elo: 1615 }, // Vegas
|
|
|
|
|
{ participantId: 'TBL', elo: 1590 }, // Tampa Bay
|
|
|
|
|
{ participantId: 'NJD', elo: 1565 }, // New Jersey
|
|
|
|
|
{ participantId: 'TOR', elo: 1510 }, // Toronto
|
|
|
|
|
{ participantId: 'NYR', elo: 1470 }, // NY Rangers
|
|
|
|
|
{ participantId: 'NYI', elo: 1324 }, // NY Islanders (weakest)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const results = await simulateBracket(teams, 'nhl-8', 10000);
|
|
|
|
|
|
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 colDist = results.get('COL');
|
|
|
|
|
const nyiDist = results.get('NYI');
|
|
|
|
|
if (!colDist || !nyiDist) throw new Error('COL or NYI result not found');
|
2025-11-17 22:19:46 -08:00
|
|
|
|
|
|
|
|
// Colorado should have highest championship probability
|
|
|
|
|
expect(colDist[0]).toBeGreaterThan(0.15); // >15%
|
|
|
|
|
|
|
|
|
|
// NY Islanders should have lowest championship probability
|
|
|
|
|
expect(nyiDist[0]).toBeLessThan(0.08); // <8%
|
|
|
|
|
|
|
|
|
|
// Colorado should be more likely to win than NYI
|
|
|
|
|
expect(colDist[0]).toBeGreaterThan(nyiDist[0]);
|
|
|
|
|
|
|
|
|
|
// Probabilities should sum to 1
|
|
|
|
|
const colSum = colDist.reduce((acc, p) => acc + p, 0);
|
|
|
|
|
const nyiSum = nyiDist.reduce((acc, p) => acc + p, 0);
|
|
|
|
|
|
|
|
|
|
expect(colSum).toBeCloseTo(1.0, 1);
|
|
|
|
|
expect(nyiSum).toBeCloseTo(1.0, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|