Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Per-grade-tier accuracy tracking.
|
||||
*
|
||||
* Every resolution increments counters for (sport, grade, period).
|
||||
* The "all_time" period is the canonical record; "last_30d" and
|
||||
* "last_7d" are derived views recomputed by a periodic refresh job
|
||||
* (n8n). We update all three counters on every resolve so callers can
|
||||
* read instant values without rolling a window themselves.
|
||||
*
|
||||
* BASELINE LOCK:
|
||||
* After a (sport, grade, 'all_time') accumulates 100 decisive
|
||||
* resolutions (hits + misses, not push/void), the hit rate at that
|
||||
* moment is locked as `baseline_hit_rate` and `baseline_locked`
|
||||
* flips to true. Future accuracy compares to the baseline to detect
|
||||
* drift.
|
||||
*
|
||||
* EXPECTED HIT RATES (from spec):
|
||||
* A+ ≥ 65%, A ≥ 60%, A- ≥ 58%, B+ ≥ 55%, B ≥ 53%, B- ≥ 51%
|
||||
* C+ ≈ 50%, C ≈ 48%, C- ≈ 45%, D ≈ 40%, F ≈ 35%
|
||||
*/
|
||||
|
||||
const { getSupabaseServiceClient } = require('../../utils/supabase');
|
||||
|
||||
const BASELINE_LOCK_AT = 100;
|
||||
const PERIODS = ['all_time', 'last_30d', 'last_7d'];
|
||||
|
||||
const EXPECTED_HIT_RATES = Object.freeze({
|
||||
'A+': 0.65, 'A': 0.60, 'A-': 0.58,
|
||||
'B+': 0.55, 'B': 0.53, 'B-': 0.51,
|
||||
'C+': 0.50, 'C': 0.48, 'C-': 0.45,
|
||||
'D': 0.40, 'F': 0.35,
|
||||
});
|
||||
|
||||
function computeHitRate(hit, miss) {
|
||||
const denom = hit + miss;
|
||||
return denom > 0 ? hit / denom : null;
|
||||
}
|
||||
|
||||
async function fetchRow(supabase, sport, grade, period) {
|
||||
const { data, error } = await supabase
|
||||
.from('accuracy_tracking')
|
||||
.select('*')
|
||||
.eq('sport', sport)
|
||||
.eq('grade', grade)
|
||||
.eq('period', period)
|
||||
.maybeSingle();
|
||||
if (error) {
|
||||
console.warn('[accuracy] fetch failed:', error.message);
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function upsertRow(supabase, row) {
|
||||
const { error } = await supabase
|
||||
.from('accuracy_tracking')
|
||||
.upsert(row, { onConflict: 'sport,grade,period' });
|
||||
if (error) console.warn('[accuracy] upsert failed:', error.message);
|
||||
}
|
||||
|
||||
async function recordResolution(sport, grade, result) {
|
||||
if (!sport || !grade || !result) return;
|
||||
const supabase = getSupabaseServiceClient();
|
||||
for (const period of PERIODS) {
|
||||
const existing = await fetchRow(supabase, sport, grade, period) || {
|
||||
sport, grade, period,
|
||||
total_graded: 0, total_hit: 0, total_miss: 0, total_push: 0, total_void: 0,
|
||||
hit_rate: null, baseline_hit_rate: null, baseline_locked: false,
|
||||
};
|
||||
existing.total_graded += 1;
|
||||
if (result === 'hit') existing.total_hit += 1;
|
||||
else if (result === 'miss') existing.total_miss += 1;
|
||||
else if (result === 'push') existing.total_push += 1;
|
||||
else if (result === 'void') existing.total_void += 1;
|
||||
existing.hit_rate = computeHitRate(existing.total_hit, existing.total_miss);
|
||||
|
||||
if (
|
||||
period === 'all_time'
|
||||
&& !existing.baseline_locked
|
||||
&& (existing.total_hit + existing.total_miss) >= BASELINE_LOCK_AT
|
||||
) {
|
||||
existing.baseline_hit_rate = existing.hit_rate;
|
||||
existing.baseline_locked = true;
|
||||
}
|
||||
existing.last_updated = new Date().toISOString();
|
||||
await upsertRow(supabase, existing);
|
||||
}
|
||||
}
|
||||
|
||||
async function getAccuracy(sport, grade, period = 'all_time') {
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const row = await fetchRow(supabase, sport, grade, period);
|
||||
if (!row) {
|
||||
return {
|
||||
sport, grade, period,
|
||||
hit_rate: null,
|
||||
baseline: null,
|
||||
expected: EXPECTED_HIT_RATES[grade] ?? null,
|
||||
total: 0,
|
||||
delta: null,
|
||||
locked: false,
|
||||
};
|
||||
}
|
||||
const total = row.total_hit + row.total_miss;
|
||||
const delta = row.baseline_hit_rate != null && row.hit_rate != null
|
||||
? row.hit_rate - row.baseline_hit_rate
|
||||
: null;
|
||||
return {
|
||||
sport, grade, period,
|
||||
hit_rate: row.hit_rate,
|
||||
baseline: row.baseline_hit_rate,
|
||||
expected: EXPECTED_HIT_RATES[grade] ?? null,
|
||||
total,
|
||||
delta,
|
||||
locked: !!row.baseline_locked,
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllAccuracy(sport, period = 'all_time') {
|
||||
const grades = Object.keys(EXPECTED_HIT_RATES);
|
||||
const out = [];
|
||||
for (const grade of grades) out.push(await getAccuracy(sport, grade, period));
|
||||
return out;
|
||||
}
|
||||
|
||||
async function isBaselineLocked(sport, grade) {
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const row = await fetchRow(supabase, sport, grade, 'all_time');
|
||||
return !!row?.baseline_locked;
|
||||
}
|
||||
|
||||
async function getAccuracyDashboard() {
|
||||
const supabase = getSupabaseServiceClient();
|
||||
const { data, error } = await supabase
|
||||
.from('accuracy_tracking')
|
||||
.select('*')
|
||||
.eq('period', 'all_time');
|
||||
if (error) {
|
||||
console.warn('[accuracy] dashboard query failed:', error.message);
|
||||
return [];
|
||||
}
|
||||
return data || [];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
recordResolution,
|
||||
getAccuracy,
|
||||
getAllAccuracy,
|
||||
isBaselineLocked,
|
||||
getAccuracyDashboard,
|
||||
EXPECTED_HIT_RATES,
|
||||
BASELINE_LOCK_AT,
|
||||
__internals: { computeHitRate, PERIODS },
|
||||
};
|
||||
Reference in New Issue
Block a user