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
+13 -5
View File
@@ -28,7 +28,7 @@
*/
const { nameKey, normalizeName } = require('../utils/playerName');
const { settleResult, statValue } = require('./outcomeService');
const { settleResult, statValue, logRowOnDate } = require('./outcomeService');
const CONFLICT = 'user_id,player_key,stat,line,side,game_id';
const UPSERT_CHUNK = 200;
@@ -330,9 +330,12 @@ async function settleLedger(sport, opts = {}) {
let pending = 0;
for (const row of rows || []) {
const log = logByPlayer[row.player_name] || [];
const gameRow = log.find((r) => r && r.date === row.game_date);
// Sport-aware date match: MLB rows are YYYY-MM-DD, NBA/WNBA ESPN rows are
// ISO timestamps normalized to their ET date. game_date is already < today
// (the .lt('game_date', cutoff) fetch), so an unplayed game never settles.
const gameRow = log.find((r) => logRowOnDate(r, row.game_date, sp));
if (!gameRow) { pending += 1; continue; }
const actual = statValue(gameRow.stat, row.stat);
const actual = statValue(gameRow.stat, row.stat, sp);
if (actual == null) { pending += 1; continue; }
const outcome = settleResult(row.side, actual, row.line);
if (!outcome) { pending += 1; continue; }
@@ -386,10 +389,15 @@ async function settleAllLedgers(opts = {}) {
}
async function defaultGetPlayerStats(name, sport) {
if (String(sport).toLowerCase() === 'mlb') {
const sp = String(sport || '').toLowerCase();
if (sp === 'mlb') {
return require('./adapters/mlbStatsAdapter').getPlayerStats(name);
}
return { found: false }; // no free settled-result feed yet → pending
// Wave 1 — NBA/WNBA settle against the FREE ESPN per-game log.
if (sp === 'nba' || sp === 'wnba') {
return require('./adapters/espnStatsAdapter').getPlayerGameLog(name, sp);
}
return { found: false }; // soccer — no free settled-result feed yet → pending
}
/**