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
+33 -1
View File
@@ -80,13 +80,45 @@ describe('zeroSettleAlarm', () => {
expect(zeroSettleAlarm([{ sport: 'mlb', settled: 3, pending: 9 }]).alarm).toBe(false);
});
test('ignores sports without a settled-result feed (WNBA pendings are honest)', () => {
test('WNBA pendings without a finals signal never page (off-day / stale rows)', () => {
// Wave 1 — wnba IS settleable now, but with no finals-probe signal its
// pendings are excluded so an offseason/off-day cannot false-page.
const z = zeroSettleAlarm([
{ sport: 'mlb', settled: 0, pending: 0 },
{ sport: 'wnba', settled: 0, pending: 40 },
]);
expect(z.alarm).toBe(false);
});
test('Wave 1 — WNBA zero-settle WITH finals present DOES page', () => {
const z = zeroSettleAlarm(
[{ sport: 'wnba', settled: 0, pending: 40 }],
{ finalsBySport: { wnba: true } },
);
expect(z.alarm).toBe(true);
expect(z.pending).toBe(40);
});
test('Wave 1 — NBA finals present but SOMETHING settled → no alarm', () => {
const z = zeroSettleAlarm(
[{ sport: 'nba', settled: 5, pending: 30 }],
{ finalsBySport: { nba: true } },
);
expect(z.alarm).toBe(false);
});
test('Wave 1 — NBA finals present but 0 rows to settle → no false alarm', () => {
const z = zeroSettleAlarm(
[{ sport: 'nba', settled: 0, pending: 0 }],
{ finalsBySport: { nba: true } },
);
expect(z.alarm).toBe(false);
});
test('legacy array 2nd arg (settleable list) is still honored', () => {
const z = zeroSettleAlarm([{ sport: 'mlb', settled: 0, pending: 7 }], ['mlb']);
expect(z).toEqual({ alarm: true, settled: 0, pending: 7 });
});
});
describe('morningHourUtc', () => {