/** * VYNDR Ship Build — Data Source Component Tests * Pure logic tests with inline data shapes and parsing logic. */ // ─── Inline Data Shapes ─────────────────────────────────────────────── const STARTING_TRUST = { beat_writer: 'reliable', national: 'authoritative', aggregator: 'unverified', insider: 'reliable', }; const REPORTER_DATABASE = { nba: { shams_charania: { name: 'Shams Charania', source_type: 'insider', trust: 'reliable' }, woj: { name: 'Adrian Wojnarowski', source_type: 'national', trust: 'authoritative' }, local_beat: { name: 'Local Beat Writer', source_type: 'beat_writer', trust: 'reliable' }, }, }; const TRUST_LEVELS = ['unverified', 'reliable', 'verified', 'authoritative']; function escalateTrust(reporter) { const { tracked, accuracy } = reporter; if (tracked >= 30 && accuracy >= 0.95) return { level: 'authoritative', badge: 'confirmed' }; if (tracked >= 20 && accuracy >= 0.90) return { level: 'verified', badge: 'trusted' }; return { level: reporter.trust, badge: null }; } // ─── Tweet Parsing ──────────────────────────────────────────────────── const STATUS_PATTERNS = [ { pattern: /will start/i, status: 'confirmed_playing', confidence: 0.85 }, { pattern: /scratched/i, status: 'scratched', confidence: 0.90 }, { pattern: /game[- ]time decision/i, status: 'questionable', confidence: 0.70 }, { pattern: /\bOUT\b/, status: 'out', confidence: 0.90 }, ]; const PAST_TENSE_FILTERS = [/was out/i, /yesterday/i, /last night/i]; function parseTweet(text) { if (!text) return null; for (const filter of PAST_TENSE_FILTERS) { if (filter.test(text)) return null; } for (const { pattern, status, confidence } of STATUS_PATTERNS) { if (pattern.test(text)) return { status, confidence }; } return null; } // ─── Odds API Parsing ───────────────────────────────────────────────── function parseOddsOutcome(market, bookmakerKey) { if (!market || !market.outcomes || market.outcomes.length === 0) { return null; } const outcome = market.outcomes[0]; return { player_name: outcome.name || null, line: outcome.point != null ? outcome.point : null, bookmaker: bookmakerKey, }; } // ─── Line Movement Detection ────────────────────────────────────────── function detectLineMovement(opening, current) { const diff = Math.abs(current - opening); if (diff < 0.5) return null; return { movement: diff, direction: current > opening ? 'up' : 'down', flagged: true, }; } // ─── Weather + Dome Detection ───────────────────────────────────────── const DOME_PARKS = ['tropicana_field', 'chase_field', 'minute_maid', 'rogers_centre', 'loanDepot_park', 'globe_life']; function getWeatherForPark(park, conditions) { if (DOME_PARKS.includes(park)) { return { temperature: 72, wind: 0, humidity: 50, ball_carry_factor: 1.0 }; } return conditions; } function ballCarryFactor(temperature, humidity) { let factor = 1.0; if (temperature > 72) factor += (temperature - 72) * 0.003; if (temperature < 72) factor -= (72 - temperature) * 0.003; if (humidity > 50) factor -= (humidity - 50) * 0.002; if (humidity < 50) factor += (50 - humidity) * 0.002; return parseFloat(factor.toFixed(4)); } // ─── Catcher Framing ────────────────────────────────────────────────── function clampFramingValue(raw) { return Math.max(-0.5, Math.min(0.5, raw)); } // ─── Umpire / Referee Minimums ──────────────────────────────────────── function getUmpireAdjustment(umpire) { if (umpire.games < 30) return 0.0; return umpire.k_rate_delta; } function getRefereeAdjustment(referee) { if (referee.games < 30) return 0.0; return referee.foul_rate_delta; } // ─── MLB Lineup Parsing ────────────────────────────────────────────── function parseLineupEntry(raw) { return { player: raw.player, batting_order: raw.batting_order, position: raw.position, status: raw.source === 'official_api' ? 'confirmed' : 'projected', }; } // ─── ABS Challenge System ───────────────────────────────────────────── function disciplineScore(chase_rate, bb_rate) { // chase_rate: 0-1 (lower = better), bb_rate: 0-1 (higher = better) const raw = (1 - chase_rate) * 0.5 + bb_rate * 0.5; return Math.max(0, Math.min(1, parseFloat(raw.toFixed(4)))); } function absKAdjustment(discipline) { if (discipline > 0.7) return -0.05; return 0; } function framingVsDisciplined(framingValue, discipline) { // Framing is 50% less effective against disciplined batters if (discipline > 0.7) return framingValue * 0.5; return framingValue; } // ═══════════════════════════════════════════════════════════════════════ // TESTS // ═══════════════════════════════════════════════════════════════════════ describe('Reporter seeding + trust', () => { test('beat_writer starts at reliable', () => { expect(STARTING_TRUST.beat_writer).toBe('reliable'); }); test('national starts at authoritative', () => { expect(STARTING_TRUST.national).toBe('authoritative'); }); test('aggregator starts at unverified', () => { expect(STARTING_TRUST.aggregator).toBe('unverified'); }); test('insider starts at reliable', () => { expect(STARTING_TRUST.insider).toBe('reliable'); }); test('reporter_database has nba key', () => { expect(REPORTER_DATABASE).toHaveProperty('nba'); expect(Object.keys(REPORTER_DATABASE.nba).length).toBeGreaterThan(0); }); test('reporter has source_type field', () => { const reporter = REPORTER_DATABASE.nba.shams_charania; expect(reporter).toHaveProperty('source_type'); expect(typeof reporter.source_type).toBe('string'); }); }); describe('Reporter trust escalation', () => { test('promote from reliable to verified at 20+ tracked and 90%+ accuracy', () => { const result = escalateTrust({ trust: 'reliable', tracked: 25, accuracy: 0.92 }); expect(result.level).toBe('verified'); }); test('stay at reliable if accuracy below 90%', () => { const result = escalateTrust({ trust: 'reliable', tracked: 25, accuracy: 0.85 }); expect(result.level).toBe('reliable'); }); test('authoritative requires 30+ tracked and 95%+ accuracy', () => { const result = escalateTrust({ trust: 'verified', tracked: 35, accuracy: 0.96 }); expect(result.level).toBe('authoritative'); }); test('badge for authoritative is confirmed', () => { const result = escalateTrust({ trust: 'verified', tracked: 35, accuracy: 0.96 }); expect(result.badge).toBe('confirmed'); }); }); describe('Tweet parsing', () => { test('"will start" detected as confirmed_playing', () => { const result = parseTweet('LeBron James will start tonight'); expect(result.status).toBe('confirmed_playing'); }); test('"scratched" detected as scratched', () => { const result = parseTweet('Giannis has been scratched from the lineup'); expect(result.status).toBe('scratched'); }); test('"game-time decision" detected as questionable', () => { const result = parseTweet('Jaylen Brown is a game-time decision'); expect(result.status).toBe('questionable'); }); test('past tense "was out" filtered', () => { const result = parseTweet('Curry was out for the game last week'); expect(result).toBeNull(); }); test('"yesterday" filtered', () => { const result = parseTweet('Player was scratched yesterday'); expect(result).toBeNull(); }); test('"last night" filtered', () => { const result = parseTweet('He sat out last night'); expect(result).toBeNull(); }); test('returns null for irrelevant text', () => { const result = parseTweet('Great weather in Boston today'); expect(result).toBeNull(); }); test('player OUT returns confidence 0.90', () => { const result = parseTweet('Player is OUT tonight'); expect(result.confidence).toBe(0.90); }); }); describe('Odds API response parsing', () => { test('extracts player_name from outcome', () => { const market = { outcomes: [{ name: 'LeBron James', point: 25.5 }] }; const result = parseOddsOutcome(market, 'draftkings'); expect(result.player_name).toBe('LeBron James'); }); test('extracts line from point', () => { const market = { outcomes: [{ name: 'LeBron James', point: 25.5 }] }; const result = parseOddsOutcome(market, 'draftkings'); expect(result.line).toBe(25.5); }); test('extracts bookmaker key', () => { const market = { outcomes: [{ name: 'LeBron James', point: 25.5 }] }; const result = parseOddsOutcome(market, 'fanduel'); expect(result.bookmaker).toBe('fanduel'); }); test('handles missing outcomes gracefully', () => { expect(parseOddsOutcome({}, 'draftkings')).toBeNull(); expect(parseOddsOutcome({ outcomes: [] }, 'draftkings')).toBeNull(); expect(parseOddsOutcome(null, 'draftkings')).toBeNull(); }); }); describe('Line movement detection', () => { test('flags movement >= 0.5', () => { const result = detectLineMovement(24.5, 25.5); expect(result.flagged).toBe(true); expect(result.movement).toBe(1.0); }); test('ignores movement < 0.5', () => { const result = detectLineMovement(24.5, 24.8); expect(result).toBeNull(); }); test('direction is up when current > opening', () => { const result = detectLineMovement(24.5, 25.5); expect(result.direction).toBe('up'); }); }); describe('Weather dome detection', () => { test('dome parks return temperature=72', () => { const weather = getWeatherForPark('tropicana_field', {}); expect(weather.temperature).toBe(72); }); test('dome parks return wind=0', () => { const weather = getWeatherForPark('chase_field', {}); expect(weather.wind).toBe(0); }); test('ball_carry_factor for dome is 1.0', () => { const weather = getWeatherForPark('minute_maid', {}); expect(weather.ball_carry_factor).toBe(1.0); }); }); describe('Weather ball carry', () => { test('hot weather increases carry (>72F)', () => { const factor = ballCarryFactor(90, 50); expect(factor).toBeGreaterThan(1.0); }); test('humid weather decreases carry (>50%)', () => { const factor = ballCarryFactor(72, 80); expect(factor).toBeLessThan(1.0); }); test('neutral at 72F/50%', () => { const factor = ballCarryFactor(72, 50); expect(factor).toBe(1.0); }); }); describe('Catcher framing', () => { test('framing value clamped at upper bound 0.5', () => { expect(clampFramingValue(0.8)).toBe(0.5); }); test('framing value clamped at lower bound -0.5', () => { expect(clampFramingValue(-0.9)).toBe(-0.5); }); }); describe('Umpire / referee minimums', () => { test('umpire returns 0.0 below 30 games', () => { expect(getUmpireAdjustment({ games: 15, k_rate_delta: 0.12 })).toBe(0.0); }); test('referee returns 0.0 below 30 games', () => { expect(getRefereeAdjustment({ games: 20, foul_rate_delta: 0.08 })).toBe(0.0); }); test('umpire returns adjustment at 30+ games', () => { const adj = getUmpireAdjustment({ games: 45, k_rate_delta: 0.12 }); expect(adj).toBe(0.12); }); test('referee returns adjustment at 30+ games', () => { const adj = getRefereeAdjustment({ games: 30, foul_rate_delta: 0.08 }); expect(adj).toBe(0.08); }); }); describe('MLB lineup parsing', () => { test('lineup has batting_order field', () => { const entry = parseLineupEntry({ player: 'Mookie Betts', batting_order: 1, position: 'RF', source: 'official_api' }); expect(entry).toHaveProperty('batting_order'); expect(entry.batting_order).toBe(1); }); test('lineup has position field', () => { const entry = parseLineupEntry({ player: 'Freddie Freeman', batting_order: 3, position: '1B', source: 'official_api' }); expect(entry).toHaveProperty('position'); expect(entry.position).toBe('1B'); }); test('status is confirmed from official API', () => { const entry = parseLineupEntry({ player: 'Shohei Ohtani', batting_order: 2, position: 'DH', source: 'official_api' }); expect(entry.status).toBe('confirmed'); }); }); describe('ABS challenge system', () => { test('discipline_score from chase_rate + bb_rate is 0-1', () => { const score = disciplineScore(0.3, 0.12); expect(score).toBeGreaterThanOrEqual(0); expect(score).toBeLessThanOrEqual(1); }); test('elite discipline (>0.7) gets -5% K adjustment', () => { const adj = absKAdjustment(0.8); expect(adj).toBe(-0.05); }); test('low discipline gets no benefit', () => { const adj = absKAdjustment(0.4); expect(adj).toBe(0); }); test('framing 50% effective vs disciplined batter', () => { const full = framingVsDisciplined(0.3, 0.5); const reduced = framingVsDisciplined(0.3, 0.8); expect(full).toBe(0.3); expect(reduced).toBe(0.15); }); });