b33612675d
Order 2 Phases 2 + 4. Pre-heal rollback point secured first: vyndr-20260720-093821.dump (856,890 bytes) VERIFIED ON THE BOX, not just exit 0. MIGRATION 027 — two DISTINCT exclusion scopes, deliberately separate: - quarantine_reason: the row's GRADE is untrustworthy (wrong_opponent_grade). The row REMAINS a real public settled result — the bet happened, the outcome is real — but it must never train or validate, so getModelAggregate now excludes it from the denominator alongside void/unrecoverable. - analysis_flags: the row is VALID for settlement and the record but unattributable for PER-GAME analysis (doubleheader dates). Explicitly NOT filtered from aggregates. Collapsing these would either wrongly drop 166 doubleheader rows from the record or wrongly keep 25 wrong-opponent grades inside model validation. Tests assert both directions, including that analysis_flags is NOT filtered. Also adds re_settled_at + settlement_source to model_snapshots. DNP VOIDING RE-ENABLED — reversing my own Order 1.5 disable, with scrutiny, because its premise was FALSE. Order 1.5 assumed a missing player row meant the row's DATE was wrong. The Phase 0 dry-run disproved it: across every bindable row the stored date matched a real game (MIS-DATED: 0), and the players I had cited as counter-evidence were genuine DNPs on their true dates (Freeman 07-18; Kwan/Hedges/Davis 07-17 — their teams played, they did not). The evidence is positive: games FINAL + no line in a full-season log = no bet existed. I got this wrong twice tonight in opposite directions; the dry-run is what caught it. Recording the reasoning in the code so the next reader sees why the flag flipped back. Suite 282/3386 green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
141 lines
5.4 KiB
JavaScript
141 lines
5.4 KiB
JavaScript
/**
|
|
* Session 64 Order 1 — date-targeted settlement + terminal states.
|
|
*
|
|
* The bug: a lookup miss did `pending += 1` with no terminal state, so a row
|
|
* that could NEVER settle (DNP, or the rolling window passed the game) was
|
|
* indistinguishable from one settling tomorrow. These lock the distinctions.
|
|
*/
|
|
const src = require('../../src/services/settleSource');
|
|
|
|
const matchesDate = (r, d) => r.date === d;
|
|
const statValue = (s, st) => (s && s[st] != null ? Number(s[st]) : null);
|
|
const base = {
|
|
sport: 'mlb', playerName: 'X', gameDate: '2026-07-17', statType: 'doubles',
|
|
matchesDate, statValue,
|
|
};
|
|
|
|
describe('classifyGameState', () => {
|
|
test('final states', () => {
|
|
expect(src.classifyGameState({ status: 'Final' })).toBe('final');
|
|
expect(src.classifyGameState({ state: 'post' })).toBe('final');
|
|
});
|
|
test('postponed/cancelled are void', () => {
|
|
expect(src.classifyGameState({ detailedState: 'Postponed' })).toBe('void');
|
|
expect(src.classifyGameState({ status: 'Cancelled' })).toBe('void');
|
|
});
|
|
test('SUSPENDED is NOT final — it resumes and must stay pending', () => {
|
|
expect(src.classifyGameState({ detailedState: 'Suspended' })).toBe('not_final');
|
|
});
|
|
test('scheduled / in progress are not final', () => {
|
|
expect(src.classifyGameState({ status: 'Scheduled' })).toBe('not_final');
|
|
expect(src.classifyGameState({ status: 'In Progress' })).toBe('not_final');
|
|
});
|
|
test('unknown when there is nothing to read', () => {
|
|
expect(src.classifyGameState({})).toBe('unknown');
|
|
});
|
|
});
|
|
|
|
describe('absenceMeaning — what a missing player row MEANS', () => {
|
|
test('any unplayed game → pending (never void a bet that may still settle)', () => {
|
|
expect(src.absenceMeaning([{ status: 'Final' }, { status: 'Suspended' }])).toBe('pending');
|
|
});
|
|
test('all games postponed → void', () => {
|
|
expect(src.absenceMeaning([{ status: 'Postponed' }])).toBe('void');
|
|
});
|
|
test('game final but player absent → void (confirmed DNP)', () => {
|
|
expect(src.absenceMeaning([{ status: 'Final' }])).toBe('void');
|
|
});
|
|
test('no schedule → unknown, not a guess', () => {
|
|
expect(src.absenceMeaning([])).toBe('unknown');
|
|
});
|
|
});
|
|
|
|
describe('resolveOutcome', () => {
|
|
test('player has a line on that date → SETTLED with the real value', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => [{ date: '2026-07-17', stat: { doubles: 1 } }],
|
|
getSchedule: async () => [{ status: 'Final' }],
|
|
});
|
|
expect(r.state).toBe('settled');
|
|
expect(r.value).toBe(1);
|
|
});
|
|
|
|
test('a ZERO value still settles (0 is a real result, not absence)', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => [{ date: '2026-07-17', stat: { doubles: 0 } }],
|
|
getSchedule: async () => [{ status: 'Final' }],
|
|
});
|
|
expect(r.state).toBe('settled');
|
|
expect(r.value).toBe(0);
|
|
});
|
|
|
|
// Session 64 — this MUST NOT void. Player-absence from a labelled date can
|
|
// equally mean the row's date is wrong (ledger derives game_date from the
|
|
// GRADE time when the feed has no game_time, so late-UTC snapshots label the
|
|
// previous ET day). Verified live: Freddie Freeman's 97-game log has no
|
|
// 2026-07-18 because he played the 17th and 19th. Voiding on absence
|
|
// destroyed real results.
|
|
test('player absent on a FINAL game → VOID (dates proven correct; DNP is real)', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => [{ date: '2026-07-16', stat: { doubles: 0 } }],
|
|
getSchedule: async () => [{ status: 'Final' }],
|
|
});
|
|
expect(r.state).toBe('void');
|
|
expect(r.reason).toBe('player_dnp');
|
|
});
|
|
|
|
test('postponed game → VOID with the postponed reason', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => [],
|
|
getSchedule: async () => [{ status: 'Postponed' }],
|
|
});
|
|
expect(r.state).toBe('void');
|
|
expect(r.reason).toBe('game_postponed_or_cancelled');
|
|
});
|
|
|
|
test('SUSPENDED game stays PENDING — never voided', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => [],
|
|
getSchedule: async () => [{ status: 'Suspended' }],
|
|
});
|
|
expect(r.state).toBe('pending');
|
|
});
|
|
|
|
test('a game OUTSIDE a rolling window still settles from the full log', async () => {
|
|
// The exact live failure: Jul 12 game, window now starts Jul 6.
|
|
const full = Array.from({ length: 40 }, (_, i) => ({
|
|
date: `2026-07-${String(i + 1).padStart(2, '0')}`, stat: { doubles: i % 3 },
|
|
}));
|
|
const r = await src.resolveOutcome({
|
|
...base, gameDate: '2026-07-12',
|
|
getFullLog: async () => full,
|
|
getSchedule: async () => [{ status: 'Final' }],
|
|
});
|
|
expect(r.state).toBe('settled');
|
|
});
|
|
|
|
test('player PLAYED but the stat is missing from the box row → unknown, NEVER void', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => [{ date: '2026-07-17', stat: { hits: 2 } }],
|
|
getSchedule: async () => [{ status: 'Final' }],
|
|
});
|
|
expect(r.state).toBe('unknown');
|
|
expect(r.reason).toBe('stat_not_in_box_row');
|
|
});
|
|
|
|
test('a throwing source degrades to unknown, never throws', async () => {
|
|
const r = await src.resolveOutcome({
|
|
...base,
|
|
getFullLog: async () => { throw new Error('net'); },
|
|
getSchedule: async () => { throw new Error('net'); },
|
|
});
|
|
expect(r.state).toBe('unknown');
|
|
});
|
|
});
|