/** * Referee impact signal. * * Game-day ref assignments live in `game_ref_assignments` (migration 017). * Per-referee tendencies live in `ref_profiles`, populated by the * Sports-Reference scraper (scripts/scrape-sports-reference.js). * * Crew impact is computed by averaging the three refs' profiles. If any * profile is missing we still return a partial impact (averaging only the * available refs) — the feature cache decides whether to surface the * feature or omit it based on coverage. */ const { getSupabaseServiceClient } = require('../../utils/supabase'); const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); async function getRefAssignment(gameId) { const supabase = getSupabaseServiceClient(); const { data, error } = await supabase .from('game_ref_assignments') .select('ref1_name, ref2_name, ref3_name, ref_crew_avg_fouls, ref_crew_pace_impact') .eq('game_id', gameId) .maybeSingle(); if (error) { console.warn('[refSignals] assignment lookup failed:', error.message); return null; } return data || null; } async function getRefProfiles(refNames) { const named = refNames.filter(Boolean); if (named.length === 0) return []; const supabase = getSupabaseServiceClient(); const { data, error } = await supabase .from('ref_profiles') .select('ref_name, avg_fouls_per_game, avg_free_throws_per_game, pace_impact, home_whistle_bias') .in('ref_name', named); if (error) { console.warn('[refSignals] profile lookup failed:', error.message); return []; } return data || []; } function average(values) { const clean = values.filter((v) => Number.isFinite(v)); if (clean.length === 0) return null; return clean.reduce((a, b) => a + b, 0) / clean.length; } async function getRefImpact(gameId) { const assignment = await getRefAssignment(gameId); if (!assignment) return null; const crew = [assignment.ref1_name, assignment.ref2_name, assignment.ref3_name].filter(Boolean); if (crew.length === 0) return null; // If precomputed crew values exist on the assignment row (scraper wrote // them), prefer those — they were derived from the same profiles but // baked at assignment time. Note: Number(null) === 0 is finite, so guard // explicitly against null/undefined before going through Number(). const hasFouls = assignment.ref_crew_avg_fouls != null && Number.isFinite(Number(assignment.ref_crew_avg_fouls)); const hasPace = assignment.ref_crew_pace_impact != null && Number.isFinite(Number(assignment.ref_crew_pace_impact)); if (hasFouls || hasPace) { return { crew, avg_fouls: assignment.ref_crew_avg_fouls, pace_impact: assignment.ref_crew_pace_impact, foul_adjustment: assignment.ref_crew_avg_fouls, home_bias: null, profilesUsed: crew.length, }; } const profiles = await getRefProfiles(crew); return { crew, avg_fouls: average(profiles.map((p) => p.avg_fouls_per_game)), pace_impact: average(profiles.map((p) => p.pace_impact)), foul_adjustment: average(profiles.map((p) => p.avg_free_throws_per_game)), home_bias: average(profiles.map((p) => p.home_whistle_bias)), profilesUsed: profiles.length, }; } // Manual entry endpoint helper — the route module (not built here) calls // this when ops POSTs an assignment. async function setRefAssignment(gameId, sport, gameDate, refs) { const supabase = getSupabaseServiceClient(); // Pull profiles synchronously to precompute crew impact at insert time so // downstream reads don't need a join. const profiles = await getRefProfiles(refs); const avgFouls = average(profiles.map((p) => p.avg_fouls_per_game)); const paceImpact = average(profiles.map((p) => p.pace_impact)); const { error } = await supabase .from('game_ref_assignments') .upsert({ game_id: gameId, sport, game_date: gameDate, ref1_name: refs[0] || null, ref2_name: refs[1] || null, ref3_name: refs[2] || null, ref_crew_avg_fouls: avgFouls, ref_crew_pace_impact: paceImpact, }, { onConflict: 'game_id' }); if (error) { console.warn('[refSignals] assignment upsert failed:', error.message); return { ok: false, error: error.message }; } return { ok: true, avg_fouls: avgFouls, pace_impact: paceImpact }; } module.exports = { getRefImpact, getRefAssignment, getRefProfiles, setRefAssignment, LOOPBACK_IPS };