diff --git a/specs/MASTER-PLAN.md b/specs/MASTER-PLAN.md index babb643..a70f936 100644 --- a/specs/MASTER-PLAN.md +++ b/specs/MASTER-PLAN.md @@ -29,25 +29,34 @@ stay provisional until re-run** ยท documented โ‰  verified. ## โ–ถ NEXT EXECUTABLE ORDER -**AWAIT the tb-v1 verdict, then `hits`.** `specs/tb-v1-compound.md`. +**๐Ÿ”ด (a) TAKEABLE ENFORCEMENT โ€” urgent.** `specs/takeable-enforcement-audit.md`. -**tb-v1 is LIVE as a challenger** โ€” total_bases modelled as the compound outcome -it is (per-component Poisson rates convolved exactly) instead of one negative -binomial that treats a home run as four events. Firing on **10/10 TB props**; -means agree with the ladder, **only the shape differs** โ€” the intended -intervention. +**The ledger โ€” the table every accruing holdout resolves against โ€” is stamping +`book`, `locked_odds` and the `takeable` flag itself from books you cannot bet** +(DFS `dabble` 707 rows, offshore `bovada` 214, `onexbet` 42, exchange `kalshi` 7). +**0% before 2026-08-01, 47.9% on 08-01, 42.5% on 08-02** โ€” it began with the +display widening. `recordPipelineGrades` indexes over the FULL widened props list +and prefers that prop's book/odds over the graded one. -**The diagnosis is on trial, and the branch is pre-registered:** -- **tb-v1 improves TB resolution** โ†’ family-mismatch **HOLDS**, similarity stays - **off** the critical path, **`hits` is next** (AB-bounded, mostly 0.5 lines, so - it rides on P(0)). -- **tb-v1 does not improve** โ†’ hypothesis **WRONG**, the **mean-weakness / - MLB-similarity branch REOPENS**. +**47 contaminated rows have already settled; ~700 are pending and will settle into +the holdouts.** The damage is mostly ahead of us. -**Verdict n-blocked:** 0 settled TB rows carry tb-v1 yet; first settle is -tomorrow. `scripts/tb-compound-holdout.sql` is TB-rows-only and direction-aligned. +Also latent: **`MODEL_BOOKS` โŠ„ `TAKEABLE_BOOKS`** (`pinnacle` is model-eligible but +not takeable) โ€” harmless while pinnacle returns nothing, live again if it recovers. -**Then:** `archetype_x_archetype` โ†’ retire/rewrite `bayesianEngine`. +**Keep the split:** the *prediction target* must be takeable; `fair_prob` / +consensus / `edge` **stay reference** and must not be over-enforced. + +**Then (b) a structural `Number(null) === 0` guard** โ€” six occurrences, most +recently in my own new module. **`hits` will re-trigger it**: its 0.5 lines make +P(0) the whole game, where a null rate read as zero is maximally wrong. + +**Then (c) the `hits` fix**, built on both invariants โ€” not before them. + +**Carry-forward:** tb-v1 verdict (accruing) ยท the third pre-registered branch +(improves-ranking-but-low โ†’ promote **and** recalibrate) ยท the **100s Cloudflare +origin timeout** vs a ~115s snapshot (work completes; the trigger returns 524 โ€” +flag it before it reads as a failure). ### FIVE challengers accruing in parallel โ€” do NOT re-run early Verified firing on a real prod snapshot (293 grades), not inferred: diff --git a/specs/takeable-enforcement-audit.md b/specs/takeable-enforcement-audit.md new file mode 100644 index 0000000..b873f36 --- /dev/null +++ b/specs/takeable-enforcement-audit.md @@ -0,0 +1,147 @@ +# TAKEABLE-ENFORCEMENT AUDIT + +**Date:** 2026-08-02 ยท **READ-ONLY** โ€” nothing enforced, built or fixed. The five +accruing challengers were not touched. + +--- + +## VERDICT: ๐Ÿ”ด GAPS EXIST, AND ONE IS LIVE CONTAMINATION OF THE LEDGER + +> **The ledger โ€” the exact table every accruing holdout resolves against โ€” is +> stamping `book`, `locked_odds` and the `takeable` flag itself from books you +> cannot bet: DFS pick'em, offshore, and exchanges.** +> +> **0% before 2026-08-01. 47.9% on 08-01. 42.5% on 08-02.** +> +> **I caused this.** It started the day I widened the books for display. + +**Enforcement is the next order, ahead of `hits`.** + +--- + +## PER-PATH ANCHOR REPORT + +| path | anchors to | takeable? | +|---|---|---| +| **PROJECTION** | `g.line` via `dedupeProps` โ†’ `MODEL_BOOKS` | โš ๏ธ **model-gated, not takeable-gated** | +| **GRADE** | same | โš ๏ธ same โ€” clean in practice today | +| **CHALLENGERS** (arch-v1, tb-v1, proj-v1.1) | read off the grade | โœ… inherit the grade's anchor | +| **LEDGER** | `prop.book` / `oddsForSide(prop)` over the **full widened props list** | ๐Ÿ”ด **CONTAMINATED** | +| **DISPLAY / RANKING** | `takeablePWin` โ†’ `isTakeable(price)` | โš ๏ธ gate is correct, **but applied to a contaminated price** | + +### The grade path is clean *today*, but not by construction + +Served snapshot, 428 grades: **draftkings 422, fanduel 6 โ€” 100% MODEL books.** +`dedupeProps`' `MODEL_BOOKS` filter works. + +**But `MODEL_BOOKS` โŠ„ `TAKEABLE_BOOKS`.** `MODEL_BOOKS` contains **`pinnacle`**, +which `isTakeableBook` correctly excludes โ€” it is a reference book, not a US +retail one. So the projection *may* anchor to a reference line by design. + +**Currently harmless** (pinnacle has returned 0 props since 07-31) and therefore +**exactly the kind of latent gap that bites when the feed recovers.** + +--- + +## THE CONTAMINATION, LOCATED + +`ledgerService.recordPipelineGrades`: + +```js +const byKey = indexProps(oddsProps); // โ† the FULL, display-widened list +const prop = byKey[`${nameKey(player)}|${stat}`]; // โ† first match wins +... +locked_odds: locked.odds != null ? String(locked.odds) : oddsForSide(prop, side), +takeable: takeableFor(locked.odds != null ? locked.odds : oddsForSide(prop, side)), +book: (prop && prop.book) || g.book || null, +``` + +The grade is computed on a MODEL book, then the ledger row is **re-stamped** from +whatever book happened to index first in the widened list. + +### Books actually stamped on ledger rows since 2026-08-01 + +| book | rows | role | settled | +|---|---:|---|---:| +| draftkings | 518 | โœ… takeable | 49 | +| **dabble** | **707** | ๐Ÿ”ด **DFS pick'em** | **44** | +| betmgm | 348 | โœ… takeable | 126 | +| hardrockbet | 298 | โœ… takeable | 0 | +| **bovada** | **214** | ๐Ÿ”ด reference/offshore | 3 | +| **onexbet** | **42** | ๐Ÿ”ด excluded | 0 | +| **pinnacle** | **36** | ๐Ÿ”ด reference | 0 | +| **kalshi** | **7** | ๐Ÿ”ด exchange โ€” mean \|odds\| **1120** | 0 | + +**`dabble` alone is 24% of all rows.** `kalshi`'s mean absolute price of **1120** +is a self-evident tell: no US bettor is taking that as the anchor of a graded prop. + +### What is confirmed contaminated, and what is not + +**Confirmed:** `book`, `locked_odds`, and **`takeable`** โ€” the flag is computed +*from* the contaminated price, so it is not merely mislabelled, it is **wrong on +its own terms**. + +**Not confirmed:** `line`. It comes from `locked.line`/`g.line`, which trace to the +graded (model-book) prop. **I did not verify it end-to-end and am not claiming it +is clean.** The enforcement order should check it. + +### Blast radius on the accruing verdicts โ€” bounded, but growing + +| | settled rows since 08-01 | +|---|---:| +| clean-book | 178 | +| **contaminated-book** | **47 (21%)** | + +**Most contaminated rows have not settled yet** โ€” 707 `dabble` rows are pending and +**will settle into the holdouts tonight and tomorrow.** The damage is mostly +*ahead* of us, which is what makes this urgent rather than merely historical. + +--- + +## PREDICTION-TARGET vs REFERENCE-RULER โ€” the distinction holds + +**Do NOT over-enforce takeable onto the reference ruler.** They answer different +questions and collapsing them would break market measurement: + +| quantity | must be | current | correct? | +|---|---|---|---| +| the line the model **projects against** | **TAKEABLE** | model-gated | โš ๏ธ gap | +| the price the row is **scored on** (`locked_odds`) | **TAKEABLE** | contaminated | ๐Ÿ”ด gap | +| `fair_prob` / consensus / `edge` diagnostics | **REFERENCE** (incl. exchanges, pinnacle) | reference | โœ… **correct โ€” leave alone** | + +`bookRoles`' three-way split is right and must not be collapsed. **The bug is not +the split โ€” it is that one write path ignores it.** + +--- + +## SEQUENCED STACK + +**(a) ๐Ÿ”ด TAKEABLE ENFORCEMENT โ€” next order.** Index the ledger's prop lookup over +**takeable books only**, verify `line` end-to-end, decide whether `pinnacle` may +anchor a projection (it may price the *ruler*, but should it price the *target*?), +and decide what to do with the ~1,031 already-contaminated rows: **re-stamp, or +tag and exclude from holdouts.** Do not silently pool them. + +**(b) Structural `Number(null) === 0` guard.** Six occurrences, the most recent in +my own new module yesterday. A shared unknown-vs-zero helper. **`hits` will +re-trigger it** โ€” its 0.5 lines make P(0) the whole game, and a null rate read as +zero is maximally wrong exactly there. + +**(c) `hits` fix** โ€” built on both invariants above, not before them. + +**Carry-forward:** the tb-v1 verdict (accruing, slower clock) ยท the third +pre-registered branch (*improves ranking but stays low โ†’ promote **and** +recalibrate*) ยท the **100s Cloudflare origin timeout** on a ~115s snapshot (the +work completes server-side, but the trigger returns 524 โ€” worth a flag before it +is mistaken for a failure). + +## TAGS + +**VERIFIED:** ledger `book`/`locked_odds`/`takeable` contaminated on 42โ€“48% of rows +since 2026-08-01, 0% before ยท leak located at `recordPipelineGrades`' index over +the widened props list ยท served grade path clean today (100% MODEL books) ยท +`MODEL_BOOKS` โŠ„ `TAKEABLE_BOOKS` via `pinnacle` ยท 47 contaminated rows already +settled, ~700 pending. + +**NOT VERIFIED:** whether the stored `line` is also contaminated. Check it in the +enforcement order rather than assuming either way.