#!/usr/bin/env node /** * MANUAL REGRADE TRIGGER (Session 64, Phase 1). * * Fires the snapshot pipeline on demand so a fix can be verified in minutes * instead of waiting for the 14/19/22/01/03 UTC cron. Runs INSIDE the API * container, so it needs no VYNDR_INTERNAL_KEY and no open HTTP surface: * * docker exec node scripts/run-snapshot.js mlb * docker exec node scripts/run-snapshot.js all * docker exec node scripts/run-snapshot.js mlb --settle * * It runs the SAME `snapshotService.runSnapshot` the cron runs — including the * team-stats refresh that populates `opp_rank_stat` — so what you verify is what * production does, not a parallel code path. * * `--settle` additionally runs the outcome + ledger settle pass first, matching * the scheduler's real order (settle yesterday, then grade today). * * Prints a grade-distribution summary at the end, which is the thing you * actually want when verifying a grading change. */ const args = process.argv.slice(2); const target = (args[0] || 'all').toLowerCase(); const doSettle = args.includes('--settle'); function tally(list, key) { return list.reduce((m, g) => { const k = g?.[key] ?? 'null'; m[k] = (m[k] || 0) + 1; return m; }, {}); } (async () => { const snapshotService = require('../src/services/snapshotService'); if (doSettle) { console.log('--- settle pass (outcomes + ledger) ---'); try { const outcomeService = require('../src/services/outcomeService'); for (const sp of ['mlb', 'wnba', 'nba']) { try { const r = await outcomeService.settleSnapshot(sp); console.log(` ${sp}: ${JSON.stringify(r)}`); } catch (e) { console.warn(` ${sp}: settle failed — ${e.message}`); } } const ledger = require('../src/services/ledgerService'); if (typeof ledger.settleLedger === 'function') { for (const sp of ['mlb', 'wnba']) { try { console.log(` ledger ${sp}: ${JSON.stringify(await ledger.settleLedger(sp))}`); } catch (e) { console.warn(` ledger ${sp}: ${e.message}`); } } } } catch (e) { console.warn('settle pass failed:', e.message); } } const sports = target === 'all' ? ['mlb', 'wnba', 'nba', 'soccer'] : [target]; console.log(`--- snapshot: ${sports.join(', ')} ---`); const results = []; for (const sp of sports) { const t = Date.now(); try { const r = await snapshotService.runSnapshot(sp); console.log(` ${sp}: status=${r.status} grades=${r.gradeCount}${r.reason ? ` reason=${r.reason}` : ''} (${Math.round((Date.now() - t) / 1000)}s)`); results.push({ sp, r }); } catch (e) { console.error(` ${sp}: THREW — ${e.message}`); } } // Distribution — the point of running this by hand. console.log('\n--- GRADE DISTRIBUTION (from the freshly written cache) ---'); const { cacheGet } = require('../src/utils/redis'); for (const { sp } of results) { try { const snap = await cacheGet(`snapshot:${sp}:latest`); const grades = (snap && Array.isArray(snap.grades)) ? snap.grades : []; if (!grades.length) { console.log(` ${sp}: (no grades)`); continue; } const withEv = grades.filter((g) => Number.isFinite(Number(g.ev_pct))).length; const withP = grades.filter((g) => Number.isFinite(Number(g.p_win))).length; const withOpp = grades.filter((g) => g.opp_rank_stat != null).length; console.log(` ${sp}: n=${grades.length} ${JSON.stringify(tally(grades, 'grade'))}`); console.log(` confidence: ${JSON.stringify(tally(grades, 'confidence'))}`); console.log(` p_win present: ${withP}/${grades.length} · ev_pct present: ${withEv}/${grades.length} · opp_rank on grade: ${withOpp}`); } catch (e) { console.warn(` ${sp}: could not read cache — ${e.message}`); } } await new Promise((r) => process.stdout.write('', r)); process.exit(0); })().catch((e) => { console.error('run-snapshot failed:', e); process.exit(1); });