3 Commits

Author SHA1 Message Date
builtbykev b06a84af80 Settlement has been dead since 2026-08-01: a 500-id filter overflowed the URL
The self-learning loop stopped two days ago and reported success the whole
time. 1,444 ledger rows from 2026-08-01 sit unsettled with settle_attempts=0
-- never even attempted -- and every accruing challenger has been starved of
settled sample as a result.

ROOT CAUSE. settleLedger fetched open ids, then REFETCHED the full rows with
.in('id', ids). PostgREST puts filters in the URL, so 500 UUIDs became an
18,499-character request that the fetch layer rejects with "TypeError: fetch
failed". The result was destructured as `const { data: rows } = ...` with NO
error binding, so rows came back null, the loop body never executed, and the
function returned {settled:0, voided:0, unrecoverable:0, pending:0} --
byte-identical to a clean "nothing to settle". Reproduced against prod before
changing anything.

WHY IT HID FOR TWO DAYS. It is volume-triggered. Daily volume ran 20-260 rows
and settled perfectly for weeks; 2026-08-01 was the first day past the 500-row
fetch limit. And the zero-settle ops alarm reads these very return values, so
pending:0 told the watchdog the backlog was empty -- the alarm built to catch
exactly this could not see it.

THE FIX. The refetch existed only to add game_date/settle_attempts/
dclv_computed_at. Selecting them in the first query removes the id list
entirely, so there is no URL to overflow at any volume. A failed fetch now
surfaces its error instead of being reported as an empty backlog.

captureClosing carried the same shape one level down -- .in('id', g.ids) on an
UPDATE, which fails identically once a single line|odds group gets large on a
big slate. Its id filters are now chunked at 100 (~3.7 KB).

Tests: the regression is locked by asserting settlement issues NO id-list
filter at 500 rows, and that a failed fetch is never reported as an empty
backlog -- the two properties that would have caught this. Two existing
suites asserted the old two-query shape and were updated to the real one.

4,159 tests green (332 suites); web build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
2026-08-02 21:49:30 -04:00
builtbykev 9fc4edf3a9 Blast radius: exclude projection<=0 grades from the public model record
The degraded grades (projection=0 → model_value=0) are already settled in the
append-only ledger and must NOT be deleted (Data Semantics law). But their
hit/miss is noise, not model skill — they never had a real projection. So
getModelAggregate now filters `.gt('model_value', 0)` on both the settled and
pending queries: the rows stay in ledger_entries, but leave the public hit_pct /
CLV / per-tier record. `.gt` also drops NULL model_value. Post-fix no such row
can be written (projection<=0 refuses), so this only sheds the historical set.

This is the functional form of the "marking" the work order asked for — the
degraded locks are effectively marked as non-counting without mutating history.

Test builder mocks gained `.gt`; a lock asserts the filter is applied to both
queries. Suite 269/3253 green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 03:15:50 -04:00
builtbykev 873a92931c 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>
2026-07-13 22:30:50 -04:00