Files
vyndr/src/services/intelligence/refSignals.js
T

116 lines
4.3 KiB
JavaScript

/**
* 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 };