/** * Coach system + pace signal. * * Two signals exposed: * coach_pace_delta: coach's career pace MINUS current team's pace, * scaled by tenure (longer tenure = stronger * adjustment). * coach_player_interaction: magnitude of system shift when the primary * player is OUT vs IN. Drives suppression for * role players when the star sits. * * Profiles live in `coach_profiles` (migration 017). On first read for a * team we check the table; if empty, fall back to the seed file at * src/config/coaches.json so launch isn't blocked on a fully populated * table. */ const path = require('path'); const fs = require('fs'); const { getSupabaseServiceClient } = require('../../utils/supabase'); let seedCache = null; function loadSeed() { if (seedCache !== null) return seedCache; try { const raw = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'config', 'coaches.json'), 'utf8')); seedCache = { coaches: raw.coaches || [] }; } catch { seedCache = { coaches: [] }; } return seedCache; } function tenureAdjustment(games) { // Linear ramp to 1.0 over ~40 games — a coach inheriting a roster needs // time before the system actually drifts toward their preference. const g = Number(games) || 0; return Math.min(1.0, Math.max(0, g / 40)); } async function getCoachProfile(sport, teamAbbr) { const supabase = getSupabaseServiceClient(); const { data, error } = await supabase .from('coach_profiles') .select('coach_name, team, sport, career_avg_pace, current_team_pace, tenure_games, primary_player, system_style, without_primary_style, without_primary_pace_delta') .eq('team', teamAbbr) .eq('sport', sport) .maybeSingle(); if (error) { console.warn('[coachSignals] profile lookup failed:', error.message); } if (data) return data; // Fall back to the seed file — same shape, different home. const seed = loadSeed(); return seed.coaches.find((c) => c.team === teamAbbr && c.sport === sport) || null; } async function getCoachImpact(sport, teamAbbr, gameContext = {}) { const profile = await getCoachProfile(sport, teamAbbr); if (!profile) return null; const career = Number(profile.career_avg_pace); const team = Number(profile.current_team_pace); const paceDelta = Number.isFinite(career) && Number.isFinite(team) ? career - team : null; const tenureAdj = tenureAdjustment(profile.tenure_games); const adjustedPaceDelta = paceDelta != null ? paceDelta * tenureAdj : null; // Primary-player status comes from the caller — usually injuryParser told // them whether the star is OUT/DOUBTFUL. const primaryStatus = gameContext.primary_player_status ?? 'unknown'; const systemOverride = primaryStatus === 'out' || primaryStatus === 'doubtful' ? profile.without_primary_style : null; const withoutPrimaryShift = primaryStatus === 'out' ? Number(profile.without_primary_pace_delta) || 0 : 0; return { coach_name: profile.coach_name, system_style: profile.system_style ?? null, primary_player: profile.primary_player ?? null, pace_delta: paceDelta, tenure_adjustment: tenureAdj, adjusted_pace_delta: adjustedPaceDelta, primary_player_status: primaryStatus, system_override: systemOverride, without_primary_pace_shift: withoutPrimaryShift, }; } module.exports = { getCoachImpact, getCoachProfile, tenureAdjustment, loadSeed };