#!/usr/bin/env node 'use strict'; /** * wnba-feed-verify — prove the tap, the ingest and the as-of read on REAL data. * * Runs the production parsers over the live WNBA season and holds the rows in * memory, then drives `wnbaUsageService.profileAsOf` against them through a stub * client. It exercises the SAME functions the pipeline will call — not a * restatement of them — so a passing run is evidence about the code that ships. * * node scripts/wnba-feed-verify.js [--from=2026-05-01] [--to=2026-08-12] */ const adapter = require('../src/services/adapters/espnWnbaAdapter'); const usage = require('../src/services/wnbaUsageService'); const { nameKey } = require('../src/utils/playerName'); const arg = (k, d) => { const hit = process.argv.find((a) => a.startsWith(`--${k}=`)); return hit ? hit.split('=')[1] : d; }; /** A stub client that serves in-memory rows through the real read path. */ function memClient(rows) { return { from() { const state = { rows: rows.slice(), order: [], range: null }; const q = { select: () => q, eq: (col, val) => { state.rows = state.rows.filter((r) => r[col] === val); return q; }, lt: (col, val) => { state.rows = state.rows.filter((r) => String(r[col]) < String(val)); return q; }, order: (col, o) => { state.order.push([col, o && o.ascending !== false]); return q; }, range: async (from, to) => { const sorted = state.rows.slice().sort((a, b) => { for (const [c, asc] of state.order) { const cmp = String(a[c]).localeCompare(String(b[c])); if (cmp) return asc ? cmp : -cmp; } return 0; }); return { data: sorted.slice(from, to + 1), error: null }; }, }; return q; }, }; } async function main() { const to = arg('to', usage.dateET()); const from = arg('from', '2026-05-01'); console.log(`\nWNBA FEED — pulling completed games ${from} .. ${to}\n`); const rows = []; const games = new Set(); const dateStats = []; let errors = 0; for (const date of usage.eachDate(from, to)) { let ids = []; try { ids = await adapter.getFinalGameIds(date); } catch (e) { errors += 1; continue; } if (!ids.length) continue; let dayRows = 0; for (const id of ids) { try { const parsed = await adapter.getGameRows(id); if (parsed.skipped || !parsed.rows.length) continue; games.add(id); for (const r of parsed.rows) { if (!r.source_id || !r.player_name) continue; rows.push({ ...r, player_key: nameKey(r.player_name) }); dayRows += 1; } } catch { errors += 1; } } dateStats.push({ date, games: ids.length, rows: dayRows }); process.stdout.write(` ${date}: ${ids.length} games, ${dayRows} rows\r`); } const dates = [...new Set(rows.map((r) => r.game_date))].sort(); console.log('\n\n──────── COVERAGE ────────'); console.log(` rows ${rows.length}`); console.log(` games ${games.size}`); console.log(` players ${new Set(rows.map((r) => r.player_key)).size}`); console.log(` teams ${new Set(rows.map((r) => r.team)).size}`); console.log(` date range ${dates[0]} .. ${dates[dates.length - 1]} (${dates.length} game days)`); console.log(` fetch errors ${errors}`); const cov = (f) => `${rows.filter((r) => r[f] != null).length}/${rows.length}`; console.log('\n FIELD COVERAGE (the chainFn inputs)'); for (const f of ['minutes', 'usage_rate', 'team_possessions', 'team_pace', 'ts_pct', 'efg_pct', 'starter', 'final_margin']) { console.log(` ${f.padEnd(20)} ${cov(f)}`); } // A worked example, so the derivation is checkable by hand. const sample = rows.find((r) => r.minutes > 25 && r.usage_rate != null); if (sample) { console.log('\n WORKED EXAMPLE (derivation is exact arithmetic, not a proxy)'); console.log(` ${sample.player_name} (${sample.team}) ${sample.game_date}`); console.log(` MIN ${sample.minutes} FGA ${sample.fga} FTA ${sample.fta} TOV ${sample.tov} PTS ${sample.points}`); console.log(` team: MIN ${sample.team_minutes} FGA ${sample.team_fga} FTA ${sample.team_fta} TOV ${sample.team_tov} OREB ${sample.team_oreb}`); const poss = sample.team_fga - sample.team_oreb + sample.team_tov + 0.44 * sample.team_fta; const usg = 100 * ((sample.fga + 0.44 * sample.fta + sample.tov) * (sample.team_minutes / 5)) / (sample.minutes * (sample.team_fga + 0.44 * sample.team_fta + sample.team_tov)); console.log(` possessions stored ${sample.team_possessions} recomputed ${poss.toFixed(3)}`); console.log(` usage_rate stored ${sample.usage_rate} recomputed ${usg.toFixed(3)}`); console.log(` pace ${sample.team_pace} TS% ${sample.ts_pct} starter ${sample.starter} margin ${sample.final_margin}`); } // ── AS-OF VERIFICATION, through the REAL read path ────────────────────── console.log('\n──────── AS-OF READ (through wnbaUsageService.profileAsOf) ────────'); const sb = memClient(rows); const counts = new Map(); for (const r of rows) counts.set(r.player_key, (counts.get(r.player_key) || 0) + 1); const busiest = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]; const pk = busiest[0]; const theirs = rows.filter((r) => r.player_key === pk).sort((a, b) => a.game_date.localeCompare(b.game_date)); const name = theirs[0].player_name; console.log(` player: ${name} (${pk}) — ${theirs.length} games on record\n`); const mid = theirs[Math.floor(theirs.length / 2)].game_date; for (const asOf of [theirs[2].game_date, mid, theirs[theirs.length - 1].game_date, '2026-12-31']) { const p = await usage.profileAsOf(sb, { playerKey: pk, asOf }); const expected = theirs.filter((r) => r.game_date < asOf).length; const ok = p ? p.games === expected : expected < usage.MIN_GAMES; console.log(` asOf ${asOf} games=${p ? p.games : 'REFUSED'} (expected ${expected}) ${ok ? 'OK' : 'MISMATCH'}` + (p ? ` usage ${p.usage_rate} min/g ${p.minutes_per_game} pace ${p.team_pace} TS ${p.ts_pct}` : '')); } // THE LEAK TEST: a game ON the as-of date must never be counted. const onDate = theirs[theirs.length - 1].game_date; const p = await usage.profileAsOf(sb, { playerKey: pk, asOf: onDate }); const includesSameDay = p && p.last_game_date === onDate; console.log(`\n STRICT CUTOFF — a game ON the as-of date is ${includesSameDay ? 'INCLUDED (LEAK)' : 'EXCLUDED (correct)'}`); console.log(` last game counted: ${p ? p.last_game_date : 'n/a'} vs as-of ${onDate}`); // And the refusal. const thin = await usage.profileAsOf(sb, { playerKey: pk, asOf: theirs[0].game_date }); console.log(` THIN HISTORY — asOf before his first game returns ${thin === null ? 'null (refused, correct)' : 'A PROFILE (WRONG)'}`); process.exit(0); } if (require.main === module) main().catch((e) => { console.error(e); process.exit(1); });