'use strict'; /** * opportunity_drift — the axis, its input, and the guards that keep it honest * (2026-08-01). * * The three properties that matter more than the arithmetic: * 1. UNDEFINED drift is a NO-OP. `Number(null) === 0` would read as "zero * at-bats" — the strongest possible fade — invented from missing data. * 2. It is a RATIO, not a level. The level is collinear with l20_avg (same * denominator), so using it would double-count opportunity the projection * already contains. * 3. It is labelled a PROXY. The real input is the confirmed batting order, * which no wired source exposes. */ const { __internals } = require('../../src/services/intelligence/featureCache'); const { mlbGameLogFeatures, MLB_LOG_FIELD } = __internals; const challenger = require('../../src/services/challengerProjection'); const { adjust, opportunityNudge, OPPORTUNITY_DEADBAND, MAX_OPPORTUNITY_NUDGE } = challenger; const g = (atBats, hits) => ({ stat: { atBats, hits } }); const season = (hits, atBats, games) => ({ hits, atBats, gamesPlayed: games }); describe('per-game at-bats is mapped for the OPPORTUNITY input only', () => { it('MLB_LOG_FIELD maps at_bats to the real boxscore field', () => { expect(MLB_LOG_FIELD.at_bats).toBe('atBats'); }); it('is NOT added to the settlement map — nothing grades at-bats', () => { // The three MLB maps are split on purpose (CLAUDE.md). Adding at_bats to // the settle map would imply a settlement path for a market we don't carry. const outcome = require('../../src/services/outcomeService'); const settleMap = (outcome.__internals && outcome.__internals.MLB_LOG_FIELD) || {}; expect(settleMap.at_bats).toBeUndefined(); }); }); describe('mlbGameLogFeatures — drift computation', () => { const logs = [g(4, 1), g(4, 2), g(4, 1), g(2, 0), g(2, 1), g(2, 0), g(2, 1), g(2, 0)]; it('computes recent-5 AB/G over the season baseline', () => { const out = mlbGameLogFeatures({ found: true, last10: logs, season: season(60, 300, 75) }, 'hits'); expect(out.ab_per_game).toBe(4); // 300/75 expect(out.recent_ab_per_game).toBe(2); // last five games are 2 AB each expect(out.opportunity_drift).toBeCloseTo(0.5, 3); }); it('is UNDEFINED when the logs carry no at-bats — never 1.0, never 0', () => { const noAb = [{ stat: { hits: 1 } }, { stat: { hits: 2 } }, { stat: { hits: 0 } }]; const out = mlbGameLogFeatures({ found: true, last10: noAb, season: season(60, undefined, 75) }, 'hits'); expect(out.opportunity_drift).toBeUndefined(); expect(out.recent_ab_per_game).toBeUndefined(); }); it('is UNDEFINED without a season baseline to drift FROM', () => { const out = mlbGameLogFeatures({ found: true, last10: logs, season: season(60, undefined, 75) }, 'hits'); expect(out.recent_ab_per_game).toBe(2); // recent is knowable expect(out.opportunity_drift).toBeUndefined(); // the ratio is not }); it('needs at least 3 at-bat rows — two games is noise, not a role change', () => { const out = mlbGameLogFeatures({ found: true, last10: [g(4, 1), g(4, 2)], season: season(60, 300, 75) }, 'hits'); expect(out.opportunity_drift).toBeUndefined(); }); }); describe('opportunityNudge — the guards', () => { it('an ABSENT drift is a no-op, not a maximal fade', () => { for (const v of [null, undefined, '', NaN, 0, -1]) expect(opportunityNudge(v, 1)).toBe(0); }); it('a drift inside the deadband is treated as noise', () => { expect(opportunityNudge(1, 1)).toBe(0); expect(opportunityNudge(1 + (OPPORTUNITY_DEADBAND / 2), 1)).toBe(0); expect(opportunityNudge(1 - (OPPORTUNITY_DEADBAND / 2), 1)).toBe(0); }); it('is capped so a noisy 5-game proxy cannot outvote the measured axes', () => { expect(opportunityNudge(0.01, 1)).toBe(-MAX_OPPORTUNITY_NUDGE); expect(opportunityNudge(100, 1)).toBe(MAX_OPPORTUNITY_NUDGE); }); it('flips sign with direction — less opportunity helps the UNDER', () => { expect(opportunityNudge(0.5, 1)).toBeLessThan(0); expect(opportunityNudge(0.5, -1)).toBeGreaterThan(0); }); }); describe('adjust — the opportunity axis on the challenger', () => { const base = { pWin: 0.55, direction: 'over', statType: 'hits' }; it('stands ALONE when there is no archetype, park or platoon', () => { const r = adjust({ ...base, opportunity: { drift: 0.5, recent_ab_per_game: 2, season_ab_per_game: 4 } }); expect(r.adjustments.map((a) => a.axis)).toEqual(['opportunity']); expect(r.p_win_challenger).toBeLessThan(0.55); }); it('labels itself a PROXY, so nothing downstream can mistake it for a lineup', () => { const r = adjust({ ...base, opportunity: { drift: 1.6 } }); expect(r.adjustments[0]).toMatchObject({ is_proxy: true, proxy_for: 'confirmed_batting_order', label: 'ROLE UP' }); }); it('leaves the challenger IDENTICAL to the champion when drift is absent', () => { const r = adjust({ ...base, opportunity: { drift: null } }); expect(r.p_win_challenger).toBe(0.55); expect(r.delta).toBe(0); expect(r.adjustments).toEqual([]); }); }); describe('attachChallenger — reads opportunity off the grade (zero extra I/O)', () => { it('uses the drift the grader already attached', async () => { const out = await challenger.attachChallenger([ { player: 'A', stat_type: 'hits', direction: 'over', p_win: 0.6, opportunity_drift: 0.62, recent_ab_per_game: 2.5, ab_per_game: 4 }, { player: 'B', stat_type: 'hits', direction: 'over', p_win: 0.6 }, ], () => null, null); expect(out[0].challenger_delta).toBeLessThan(0); expect(out[0].challenger_adjustments[0].axis).toBe('opportunity'); // No drift on the grade => challenger is the champion, exactly. expect(out[1].p_win_challenger).toBe(0.6); expect(out[1].challenger_delta).toBe(0); }); }); describe('environment context — joins on the GAME, not the roster team', () => { // Measured 2026-08-01: `team` was null on 416/416 stored grades, so an // environment resolver keyed only off the roster team could never find a // venue — the axis fired on ZERO rows while all 14 weather forecasts sat // resolved and unused. The park belongs to the GAME, so the game's own teams // are both the more reliable key and the more correct one. const envCtx = require('../../src/services/environmentContext'); it('abbrOf resolves both a full team name and an abbreviation', () => { expect(envCtx.abbrOf('Colorado Rockies')).toBe('COL'); expect(envCtx.abbrOf('COL')).toBe('COL'); }); it('a grade with no team AND no game resolves to nothing — never a default park', () => { // A fabricated park factor would be a silent multiplier on every prop. expect(envCtx.abbrOf(null)).toBeFalsy(); expect(envCtx.abbrOf(undefined)).toBeFalsy(); expect(envCtx.abbrOf('')).toBeFalsy(); }); }); describe('gradeSlateService — the graded row carries its game', () => { it('binds home/away from the prop so the challenger can find the venue', async () => { const { gradeBestSide } = require('../../src/services/gradeSlateService').__internals; const grade = async (b) => ({ player: b.player, stat_type: b.stat_type, line: b.line, direction: b.direction, grade: 'B', confidence: 60, }); const out = await gradeBestSide(grade, { player: 'A', stat_type: 'hits', line: 1.5, book: 'draftkings', home_team: 'Colorado Rockies', away_team: 'Chicago Cubs', }, 'mlb', {}); expect(out.home_team).toBe('Colorado Rockies'); expect(out.away_team).toBe('Chicago Cubs'); }); it('does not invent a game when the prop has none', async () => { const { gradeBestSide } = require('../../src/services/gradeSlateService').__internals; const grade = async (b) => ({ ...b, grade: 'B', confidence: 60 }); const out = await gradeBestSide(grade, { player: 'A', stat_type: 'hits', line: 1.5 }, 'mlb', {}); expect(out.home_team ?? null).toBeNull(); }); });