Wave 1: NBA/WNBA settlement — grades settle vs free ESPN game logs

Unblocks the self-learning loop for basketball. Once an NBA/WNBA grade
exists (Wave 0), it now settles against the FREE ESPN per-game log
(espnStatsAdapter.getPlayerGameLog) — the same {found, last10:[{date,stat}]}
contract MLB settlement already consumes. accuracy:{sport} + by_tier
calibration + the Wave-3 TierRecord light up automatically.

- outcomeService/ledgerService: defaultGetPlayerStats routes nba/wnba to
  espnStatsAdapter.getPlayerGameLog; MLB stays on mlbStatsAdapter.
- outcomeService: sport-aware statValue + a SEPARATE NBA_BOX_KEY/NBA_COMBO
  map (S11 three-map-split kept — never merged with MLB_LOG_FIELD). Combos
  (pts_reb_ast, reb_ast, stl_blk, …) sum components; a missing component
  never fabricates a total.
- logRowOnDate: ESPN gamelog rows carry a FULL ISO timestamp (a late tip
  rolls past UTC midnight), so basketball date-matches on UTC OR ET date;
  MLB keeps exact YYYY-MM-DD compare. Outcome `date` is normalized to the
  ET calendar day so the accuracy window filter + idempotency key behave
  identically across sports.
- Final-honesty guard: never settle a basketball row whose ET date is
  today (an in-progress partial box). MLB is final-only + settles same-day,
  so the guard is scoped to basketball. The ledger path is already guarded
  (.lt('game_date', today)) for all sports.
- opsWatch: nba/wnba added to SETTLEABLE_SPORTS; zeroSettleAlarm gates them
  behind a real-finals probe (finalsBySport) so an offseason/off-day's
  stale pendings never false-page "settled 0". snapshotScheduler counts
  yesterday's ESPN state==='post' events and feeds the map; MLB unchanged.
- snapshotScheduler: boot announce per settleable sport
  ([settle:mlb] [settle:nba] [settle:wnba]). Thrown-error paging already
  covers the new sports (settleAll* loop every sport).

Tests: tests/unit/nbaSettlement.test.js (16) — WNBA hit/miss/push, combo
pra, idempotent re-run, unplayed/today game does NOT settle, accuracy:wnba
+ byGrade + by_tier populate, ledger WNBA settle. opsWatch (+5) — finals
off-day no page, finals present DOES page. MLB suites unregressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-13 22:30:50 -04:00
parent cce99709c9
commit 873a92931c
6 changed files with 405 additions and 22 deletions
+18 -2
View File
@@ -69,6 +69,9 @@ function startSnapshotScheduler(opts = {}) {
const healthIssues = opts.healthIssues || require('./services/systemHealth').healthIssues;
const getQuotaStatus = opts.getQuotaStatus || require('./services/quotaTracker').getQuotaStatus;
const countLedgerRows = opts.countLedgerRows || require('./services/ledgerService').countRowsForDate;
// Wave 1 — real-finals probe for the zero-settle alarm (NBA/WNBA only page
// when yesterday actually had games). Reuses the free, cached ESPN scoreboard.
const getSchedule = opts.getSchedule || require('./services/scheduleService').getSchedule;
const failureTracker = opts.failureTracker || opsWatch.createFailureTracker();
let lastFiredSlot = null;
let lastOverdueSlot = null;
@@ -178,7 +181,17 @@ function startSnapshotScheduler(opts = {}) {
// once per ET date; a genuinely empty yesterday (0 rows fetched) never fires.
try {
if (h === opsWatch.morningHourUtc(HOURS_UTC) && ledgerResults) {
const z = opsWatch.zeroSettleAlarm(ledgerResults);
// Wave 1 — probe yesterday's ESPN scoreboard so an NBA/WNBA off-day
// (0 finals) never false-pages "settled 0" on stale pending rows.
const yEt = opsWatch.dateET(new Date(d.getTime() - 24 * 3600 * 1000));
const finalsBySport = {};
for (const fsp of opsWatch.FINALS_GATED_SPORTS) {
try {
const games = await getSchedule(fsp, yEt);
finalsBySport[fsp] = Array.isArray(games) && games.some((g) => g && g.status === 'post');
} catch { finalsBySport[fsp] = false; }
}
const z = opsWatch.zeroSettleAlarm(ledgerResults, { finalsBySport });
if (z.alarm) {
const dk = `ops:settle_zero:${opsWatch.dateET(d)}`;
if (!(await cacheGet(dk))) {
@@ -254,7 +267,10 @@ function startSnapshotScheduler(opts = {}) {
// settleAllLedgers run FIRST at every snapshot slot, before grading). It
// was invisible at boot, which made "is settlement scheduled?" unanswerable
// from logs. This line makes it verifiable forever.
console.log(`[settlement] armed — outcomes + ledger settle pass runs FIRST at each snapshot slot (${HOURS_UTC.join(',')} UTC), idempotent re-runs`);
// Wave 1 — announce settlement PER settleable sport so "does NBA settle?" is
// answerable from boot logs (mlb=statsapi, nba/wnba=ESPN game logs).
const settleTags = require('./services/opsWatch').SETTLEABLE_SPORTS.map((s) => `[settle:${s}]`).join(' ');
console.log(`[settlement] armed — ${settleTags} outcomes + ledger settle pass runs FIRST at each snapshot slot (${HOURS_UTC.join(',')} UTC), idempotent re-runs`);
// Session 8 — same verifiability rule: every watchdog states itself at boot.
console.log(`[opsWatch] armed — settle alarms (throw + morning zero-settle), failure pager (${failureTracker.threshold} consecutive), quota daily check, pulse ${PULSE_HOUR_UTC}:00 UTC`);
return { interval, tick, refreshTick, pulseTick };