#!/usr/bin/env node /** * Session 63 — GRADE-RANGE VERIFICATION (on merit, not by rescaling). * * Replays REAL props from the live board through the REAL engine path that * Session 63 repaired, and reports the resulting grade distribution. * * What is real here: * - the props (player / stat / line / side) come from the live API board * - the game logs come from statsapi.mlb.com + ESPN (free, no quota) * - the consistency factor + L5/L20 features are computed from those logs * - the grade comes from engine1.gradeProp, unmodified * * What is NOT covered (documented, not hidden): * - `opp_rank_stat` needs `team_stats:{sport}:{abbr}` in Redis, which only * the production snapshot populates. Locally it stays null, so this run * UNDERSTATES the restored range — it omits a ±1.0 factor. Any A/D seen * here is therefore a floor, not a ceiling. * * Usage: node scripts/verify-grade-range.js [sport] [limit] */ const engine1 = require('../src/services/intelligence/engine1'); const featureCache = require('../src/services/intelligence/featureCache'); const consistencyScore = require('../src/services/intelligence/consistencyScore'); const { estimateProbability } = require('../src/services/intelligence/probabilityEstimator'); const { fourLetterGrade } = require('../src/utils/gradeAdapter').__internals; const API = process.env.VERIFY_API || 'https://api.vyndr.app'; const SPORT = process.argv[2] || 'mlb'; const LIMIT = Number(process.argv[3] || 40); async function board(sport) { const res = await fetch(`${API}/api/snapshot/${sport}`); const json = await res.json(); const grades = Array.isArray(json.grades) ? json.grades : []; return grades.map((g) => ({ player: g.player, stat: g.stat_type, line: Number(g.line), direction: String(g.direction || 'over').toLowerCase(), oldGrade: g.grade, oldConfidence: g.confidence, })).filter((p) => p.player && p.stat && Number.isFinite(p.line)); } async function gradeOne(p, sport) { const rows = await featureCache.getStatRows(p.player, sport, p.stat); const features = await featureCache.__internals.gameLogFeatures(p.player, sport, p.stat); const consistency = await consistencyScore.getConsistency({ playerName: p.player, sport, statType: p.stat, gameLogs: rows, }); const prop = { line: p.line, direction: p.direction }; const res = engine1.gradeProp({ features, trap: {}, consistency, prop }); const est = estimateProbability({ gameLogs: rows, line: p.line, statType: p.stat, features }); const pWin = Number.isFinite(est.p_over) ? (p.direction === 'under' ? 1 - est.p_over : est.p_over) : null; return { ...p, rows: rows.length, consistency: consistency.consistency, newGrade11: res.grade, newGrade: fourLetterGrade(res.grade), p_win: pWin == null ? null : Math.round(pWin * 1000) / 1000, }; } (async () => { const props = (await board(SPORT)).slice(0, LIMIT); if (!props.length) { console.log(`no live props for ${SPORT}`); return; } console.log(`Replaying ${props.length} REAL ${SPORT.toUpperCase()} props through the repaired engine\n`); const out = []; for (const p of props) { try { out.push(await gradeOne(p, SPORT)); } catch (e) { console.warn(` ! ${p.player} ${p.stat}: ${e.message}`); } } const tally = (arr, key) => arr.reduce((m, r) => { const k = r[key] ?? 'null'; m[k] = (m[k] || 0) + 1; return m; }, {}); const pct = (n) => `${Math.round((n / out.length) * 1000) / 10}%`; console.log('--- 4-LETTER DISTRIBUTION ---'); console.log('BEFORE (live board):', tally(out, 'oldGrade')); const after = tally(out, 'newGrade'); console.log('AFTER (repaired) :', after); for (const g of ['A', 'B', 'C', 'D', 'F']) if (after[g]) console.log(` ${g}: ${after[g]} (${pct(after[g])})`); console.log('\n--- 11-STEP DISTRIBUTION (pre-collapse) ---'); console.log(tally(out, 'newGrade11')); console.log('\n--- REVIVED SIGNALS ---'); const withRows = out.filter((r) => r.rows > 0).length; const withP = out.filter((r) => r.p_win != null).length; const withCons = out.filter((r) => r.consistency && r.consistency !== 'unknown').length; console.log(`game-log rows present : ${withRows}/${out.length}`); console.log(`p_win computed : ${withP}/${out.length} (was 0 in prod)`); console.log(`consistency known : ${withCons}/${out.length} (was 0 for MLB)`); console.log('\n--- MOVERS (grade changed) ---'); for (const r of out.filter((r) => r.oldGrade !== r.newGrade).slice(0, 15)) { console.log(` ${r.oldGrade} → ${r.newGrade.padEnd(2)} (${r.newGrade11.padEnd(2)}) ${r.player} ${r.stat} ${r.direction} ${r.line} n=${r.rows} cons=${r.consistency} p=${r.p_win}`); } // Redis runs in degraded mode locally and keeps a reconnect timer alive, so // the process would never exit on its own — flush and leave deliberately. await new Promise((r) => process.stdout.write('', r)); process.exit(0); })().catch((e) => { console.error('verify failed:', e.message); process.exit(1); });