Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user