diff --git a/specs/STATE.md b/specs/STATE.md index f4ff8ac..2f2bc74 100644 --- a/specs/STATE.md +++ b/specs/STATE.md @@ -133,6 +133,66 @@ exist locally; harmless, the data restores completely. --- +## 🟢 GRADING DATE BINDING FIXED — wrong-opponent bug closed (2026-07-20 07:50 UTC) +`computeFeatures.lookupTodayGame` → **`lookupGameOnDate`**: it now sends +`?dates=YYYYMMDD` from the prop's BOUND game instead of taking ESPN's dateless +"today". Grading, settlement, retention and the ledger finally reference the +SAME game. **This was a MODEL-OUTPUT bug**: `opponentAbbr` feeds `opp_rank_stat` +(a ±1.0 factor) and `isHome` feeds `home_away` (+0.5). + +**PROVEN against live ESPN, same instant:** +``` +dateless "today" CLE→PIT NYY→LAD LAD→NYY (Jul 19 card) +bound to 2026-07-20 CLE→MIN NYY→PIT LAD→PHI (the real games) +bound to 2026-07-19 CLE→PIT NYY→LAD LAD→NYY (reproduces OLD exactly) +``` +**Every opponent was wrong.** Measured at 07:5x UTC = **03:5x ET** — ESPN's +dateless scoreboard was STILL serving the previous day's card, so **the affected +window is wider than the 01:00/03:00 slots**. + +- **Honest degradation:** no bound date → NO fallback lookup. Records + `no_bound_game_date`, leaves `opponentAbbr`/`isHome`/`gameId` null, and engine1 + omits those factors rather than scoring a wrong matchup. Tests lock both ways. +- **Same class, also fixed:** Tank01 augmentation keyed its cache on TODAY's UTC + date; now uses the bound game date. +- **Audited:** no other dateless/"today" lookups remain in the feature path + (weather is current-conditions by venue; park/pace are static). +- Live cycles at 07:50/07:51 UTC wrote retention rows with `game_date 2026-07-20` + and `home_away` on 38/50 — games binding. MLB `opp_rank_stat` stays 0 for the + separate, known reason (ESPN's MLB team endpoint carries no defensive metric). + +### 🔴 BLAST RADIUS (report-only — gates the heal) +**`ledger_entries` — grades by UTC hour (at-risk = late slots):** + +| UTC hour | grades | settled results | risk | +|---|---|---|---| +| 00 | 32 | 29 | 🔴 at risk | +| 01 | 68 | 67 | 🔴 at risk | +| 03 | 152 | 103 | 🔴 at risk | +| 06 | 51 | 0 | 🔴 at risk (ESPN still on prior card at 02 ET) | +| 13/14/19/22 | 390 | 379 | ✅ same-ET-day, correct | + +**At-risk: 303 grades (199 already settled with real results).** ~44 % of the +public record was graded in the affected window. + +**`model_snapshots`:** 350 rows total — **50 carry `opp_rank_stat`** (the 03:02 +UTC WNBA cycle) and are the poisoned moat rows; 190 carry `home_away`. Rows +captured 07:50+ are clean (post-fix). + +**Recoverable vs quarantine (PLAN ONLY, nothing written):** +- **RECOVERABLE:** the correct opponent for a past date is still fetchable from + the date-pinned ESPN scoreboard, so a grade can be RE-DERIVED — but only where + the feature vector is reproducible. `model_snapshots` rows store the full + feature vector, so those are re-computable; **ledger rows do not** and would + need a full re-grade against historical inputs. +- **QUARANTINE:** any at-risk ledger grade that cannot be re-derived should be + marked computed-against-wrong-opponent and EXCLUDED from harness/validation — + never silently trusted. It stays in the public record as a real settled bet + (the result happened) but must not train or validate anything. +- **Scope note for the heal:** at-risk is defined by grading HOUR, not by proven + wrongness. Proving per-row wrongness requires re-deriving each row's opponent — + that IS the heal work. + ## 🟢 game_date ROOT FIXED + 64 WRONG VOIDS REVERTED (2026-07-20 07:30 UTC) - **ROOT (not DNP, as first diagnosed):** PropLine emits **no `commence_time`** (grep-verified), so `ledgerService`'s `dateET(prop.game_time) ||