Commit Graph

2 Commits

Author SHA1 Message Date
builtbykev 270c4db47a STOP voiding on player-absence — it destroyed real results
Correctness fix to code I shipped minutes ago. The induced live settle
pass voided 64 rows as 'player_dnp' and a large share of them are WRONG:
the Jul 18 set is everyday starters (Freeman, Bellinger, Tucker, Chisholm,
Conforto). They played.

ROOT CAUSE — and my Phase 0 diagnosis was wrong. It is not DNP. The
ledger row's game_date is WRONG. ledgerService derives game_date from the
GRADE timestamp when the feed carries no game_time, and a 01:00/03:00 UTC
snapshot is 21:00/23:00 ET the PREVIOUS day, so rows get labelled with the
previous ET date. Verified against fresh season logs (cache disabled, so
not staleness; found:true, so not name resolution):
  Freddie Freeman  played Jul 17 and Jul 19 (x2, doubleheader) — NOT Jul 18
  Steven Kwan      played Jul 18 (x2) and Jul 19               — NOT Jul 17
Settlement was correct to find no game on the labelled date. My void logic
then converted a data-labelling bug into destroyed results.

FIX: never void on player-absence alone. Voiding now requires POSITIVE
evidence — the games themselves postponed/cancelled. Absence returns
'unknown' (reason player_absent_unconfirmed), so the row retries and ages
out to 'unrecoverable' at the cap. We cannot distinguish "did not play"
from "mislabelled date", so we must not claim DNP. Both terminal states
are excluded from the record denominator either way.

Window-decay remains genuinely fixed (full season log vs a rolling
window), and terminal states still prevent immortal rows.

NOT DONE HERE: the 64 wrong voids are still in the table, and the
game_date derivation is still wrong at the source. Both are reported for
the table — no healing in this order.

Suite green, build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
2026-07-20 03:10:34 -04:00
builtbykev d4a6170ffa Settlement fix forward: date-targeted resolution + terminal states
Order 1 of 2. Push scoring UNTOUCHED — it is correct. No healing here.

PHASE 1 — DATE-TARGETED FETCH replaces the rolling window for settlement.
settleSource.resolveOutcome() resolves the SPECIFIC DATE and, when the
player is absent, reads GAME STATE to learn what the absence MEANS:
  game final + player has a line  -> SETTLE (a partial game is a real
                                     result, never a void)
  game final + player absent      -> VOID (confirmed DNP)
  postponed / cancelled           -> VOID
  scheduled / in progress / SUSPENDED -> PENDING (a suspended game resumes;
                                     voiding it would destroy a real bet)
  player played, stat missing     -> unknown, NEVER void a real appearance
This is FREE for MLB: mlbStatsAdapter.getPlayerGameLog already returned the
full season log and getPlayerStats was discarding it with .slice(-10).
Settlement now reads fullLog — same request, same cache — which removes
window-decay entirely (the verified failure was a Jul 12 game outside a
last10 starting Jul 6). Projections keep using last10, unchanged.

PHASE 2 — TERMINAL STATES (migration 026 applied). outcome CHECK widened to
hit/miss/push/void/unrecoverable; added settle_attempts, settlement_source,
settlement_version, model_version. A row that cannot be resolved after
SETTLE_ATTEMPT_CAP (4) date-targeted attempts becomes 'unrecoverable'
rather than pending forever. CRITICAL: getModelAggregate now EXCLUDES void
and unrecoverable from the settled selection — it used
.not('outcome','is',null), so without this a void would have counted as a
settled row and silently moved the public record. Verified in the record
calc, not just the settle path.

PHASE 3 — SETTLEMENT-RATE ALARM. zeroSettleAlarm only caught a TOTAL zero
while ~30% of a slate failed quietly (Jul 17: 57/86). opsWatch
.settlementRateAlarm pages when resolved/attempted falls below
SETTLE_RATE_FLOOR (0.8). Voids count as RESOLVED — a void is a legitimate
terminal state — so healthy voiding never pages. Third silent-failure
surface of the night, now closed.

PHASE 4 — VERSION STAMPING. src/config/modelEras.js defines the cutoff
ONCE (2026-07-19T22:50:00Z); migration 026 backfilled pre-cutoff rows as
'pre-retention-unknown' (naming the uncertainty, not implying knowledge);
new rows carry model_version.

Regression caught pre-deploy: getScheduleFn was not injectable, so the
ledger suite hit the real network and HUNG. Now injectable via opts and a
no-op under NODE_ENV=test. The "no row -> pending" test was updated to the
new behaviour deliberately: a missing row on a FINAL game now voids.

Suite 281/3373 green, build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
2026-07-20 03:04:55 -04:00