b742230d94
FOUNDATION-FIRST re-order, phase 1 (tooling + safety). BACKUP (highest-severity open item) — INSTALLED, not re-proven. src/backupScheduler.js runs scripts/backup-db.sh nightly from inside the API container, armed at boot in server.js. The container already has SUPABASE_DB_URL, pg_dump and the Supabase route, so deploy == installed: no host crontab, no Coolify click. Arming is deliberately opt-OUT (armed whenever SUPABASE_DB_URL exists; BACKUP_CRON=0 kills it) because the S62 design was opt-in and nobody ever opted in — the DB went unbacked every night for weeks. A failed run pages high-priority ntfy; silence is the danger with backups. Durability is the one part still needing a human: the container FS is ephemeral, so a dump dies on redeploy unless BACKUP_REMOTE (off-box rsync) or BACKUP_DIR (persistent volume) is set. The scheduler detects that and pages a WARNING at boot rather than letting an undurable backup read as "backed up". Runbook rewritten to lead with the code path. MANUAL REGRADE TRIGGER — scripts/run-snapshot.js, runnable via docker exec with no VYNDR_INTERNAL_KEY and no new HTTP surface. Runs the SAME snapshotService.runSnapshot the cron runs (including the team-stats refresh that powers opp_rank_stat), supports `all` and `--settle`, and prints the grade/confidence distribution plus p_win/ev_pct presence — which is the thing you actually want when verifying a grading change. ACCESS BLOCKER, logged honestly in specs/model-train.md: there is no VYNDR_INTERNAL_KEY in the local .env and SSH to the box times out from WSL2, so I can neither curl the internal endpoints (which already exist from S45) nor docker exec. The trigger is built and correct but only Kev can run it until a key or SSH access exists. This is the highest-leverage unblock for phases 2 and 3, which both need on-demand regrade+settle to verify anything. Also logged the standing cautions: CLV ledger stays private until backtest-proven; "self-improving model" is unsupported marketing until the loop closes; the engine is MLB/WNBA-calibrated and NFL/NBA/soccer need their own calibration before the hub grades them (scaling gate). Suite 277/3300 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
90 lines
3.9 KiB
JavaScript
90 lines
3.9 KiB
JavaScript
#!/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 <api-container> node scripts/run-snapshot.js mlb
|
|
* docker exec <api-container> node scripts/run-snapshot.js all
|
|
* docker exec <api-container> 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); });
|