Files
vyndr/specs/takeable-enforcement.md
builtbykev e29ab6fd6a Takeable enforcement: verified on real rows, 1,006 tagged, re-stamp call ready
PART 1 verified by inducing the REAL rowsFromSnapshot over REAL lock_lines
rows from prod. Three cases, 0 non-takeable anchors:
  Narvaez  (dabble/kalshi/prizepicks/smarkets, NO takeable book)
           -> book=null, price=null, takeable=null  [honest absent]
  Schwarber(bovada/dabble/novig/PINNACLE before draftkings)
           -> draftkings +102  [pinnacle SKIPPED, proving TAKEABLE not MODEL]
  Ohtani   (dabble/onexbet before draftkings) -> draftkings -266
Narvaez is the case that matters: pre-fix he was stamped dabble +104
takeable=true; he is now honestly absent.

A HARNESS BUG RECORDED: my first verification pulled live /api/odds/mlb,
which returned {"error":"Odds data temporarily unavailable"}. The script
read that as 0 props and printed "all from takeable books? true" -- a
VACUOUSLY TRUE pass. I caught it only because I also printed the book list
and it was empty. Same family as the silent-false traps: a probe that finds
nothing looks identical to a probe that finds nothing wrong.

PART 2: 1,006 rows tagged via the purpose-built quarantine_reason at ROW
level with three sub-cases (recoverable_same_line 936, no_takeable_quote
49, takeable_line_differs 21). getModelAggregate ALREADY excluded
quarantined rows, so the public record and the n>=20 gate were clean
automatically; all five committed holdout scripts now carry the exclusion
explicitly.

PART 3 -- the re-stamp call is now fact-based. The takeable LOCK-TIME price
is recoverable for 936/1,006 (93.0%) from lock_lines, the correct
instrument. Only 431 appear in closing_captures, which is the wrong timing
for a lock price anyway.

LINE CONTAMINATION ANSWERED (previously unverified): the stored line
MATCHES a takeable book's line on 936 (93.0%), DIFFERS on 21 (2.1%), and is
unverifiable on 49 (4.9%) where no takeable book quoted the prop at all.

That makes it cleanly row-level: re-stamp the 936 as an honest JOIN and
recover 886 pending rows for the holdouts, or leave all 1,006 excluded.
Either way the 21 + 49 stay out -- re-stamping those would invent a lock
price, or a line, we never captured. Nothing re-stamped; Kev's call.

Gates: 4,111 tests / 330 suites green; next build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-08-02 14:24:28 -04:00

5.8 KiB
Raw Permalink Blame History

TAKEABLE ENFORCEMENT — FIXED, TAGGED, AND THE RE-STAMP CALL

Date: 2026-08-02 · shipped before tonight's settle · served path, champion, ranking and the reference ruler untouched.

Gates: 4,111 tests / 330 suites green · next build exit 0 · write path verified on real prod rows · 1,006 rows tagged and excluded.


PART 1 — THE WRITE PATH: TWO LEAKS, NOT ONE

The audit found ledgerService.indexProps. Tracing the lock price found snapshotService.indexOdds has the same defect — it also indexed the full props list, so gradedAt.odds, the price a grade is locked at, could itself be a DFS or exchange price. Fixing only the ledger would have left contamination flowing in through the lock.

Both now gate on TAKEABLE_BOOKS — deliberately not MODEL_BOOKS. pinnacle is model-eligible and correctly not takeable; a MODEL gate would re-break this the moment pinnacle's feed recovers.

Two indexes, two roles, because the row needs different things with different correctness rules:

  • PRICE / BOOK / TAKEABLE → takeable books only.
  • GAME FACTS (game_time, game_date, team/opponent) → book-independent. First pitch is first pitch whichever book listed it; gating these too would drop valid rows for no gain.

Collapsing those roles into one index was the bug.

Verified on REAL prod feed rows

Real lock_lines rows, run through the real rowsFromSnapshot:

player books available (in feed order) result
Carlos Narváez dabble, kalshi, prizepicks, smarkets — no takeable book book=null, price=null, takeable=null — honest absent
Kyle Schwarber (TB) bovada, dabble, novig, pinnacle, draftkings draftkings +102, takeable=true
Shohei Ohtani dabble, onexbet, draftkings draftkings 266, takeable=false

0 non-takeable anchored rows. Narváez is the case that matters most: before the fix he would have been stamped dabble +104, takeable=true. He is now honestly absent — no price beats a price you cannot bet. And pinnacle was skipped, which proves the gate is TAKEABLE and not MODEL.

A harness bug worth recording: my first verification pulled live /api/odds/mlb, which returned {"error": "Odds data temporarily unavailable"}. My script read that as 0 props and printed "all from takeable books? true" — a vacuously true pass. I caught it only because I also printed the book list and it was empty. Sixth-ish instance of this family: a probe that finds nothing looks identical to a probe that finds nothing wrong.


PART 2 — TAGGED AND EXCLUDED (never pooled)

1,006 rows tagged via the purpose-built quarantine_reason, at row level with three distinct sub-cases so the re-stamp decision can be made per row:

tag rows settled pending
nontakeable_book:recoverable_same_line 936 47 886
nontakeable_book:no_takeable_quote 49 0 48
nontakeable_book:takeable_line_differs 21 0 21

getModelAggregate already excludes quarantined rows (.is('quarantine_reason', null)) — a pre-existing guard doing exactly the right thing, so the public model record and the n≥20 gate are clean automatically. All five committed holdout scripts now carry the same exclusion explicitly.


PART 3 — THE RE-STAMP CALL, ON FACT

The takeable book's LOCK-TIME price is recoverable for 936 of 1,006 (93.0%) from lock_lines — the correct instrument (lock time, not close time). For reference, only 431 appear in closing_captures, which is the wrong timing for a lock price anyway.

So re-stamping is a legitimate JOIN for 93%, not a fabrication. It would pull the captured takeable price onto the row — a value we really recorded, at the right moment.

And the line question — answered

rows verdict
stored line matches a takeable book's line 936 (93.0%) line is CLEAN
a takeable book existed but posted a different line 21 (2.1%) line IS contaminated
no takeable book quoted the prop at all 49 (4.9%) unverifiable — and arguably should never have been graded

This makes the decision cleanly row-level:

OPTION A — re-stamp the 936. The takeable lock price and the same line both exist in the archive. Honest join, sample preserved, 886 pending rows recovered for the holdouts.

OPTION B — leave all 1,006 tagged and excluded. Simplest, loses 886 pending rows from the accruing verdicts, costs nothing in correctness.

Either way, the 21 + 49 stay excluded. Re-stamping those would invent a lock price, or a line, we never captured.

I have not re-stamped anything. This is your call, and it is now a fact-based one.


STACK BEHIND A CLEAN LEDGER

(b) Structural Number(null) === 0 guard — a shared unknown-vs-zero helper. hits will re-trigger it: its 0.5 lines make P(0) the whole game, exactly where a null read as zero is maximally wrong.

(c) hits fix — 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 — the work completes server-side, so a 524 is not a failure; worth a flag before someone reads it as one.

TAGS

VERIFIED: both leaks fixed and gated on TAKEABLE (pinnacle skipped, proving it) · 0 non-takeable anchors on real prod rows · no-takeable-book → honest absent · 1,006 rows tagged with three sub-cases · getModelAggregate already excluded them · 93.0% recoverable from lock_lines · line clean 93.0% / contaminated 2.1% / unverifiable 4.9%.

AWAITING KEV: re-stamp the 936, or leave all 1,006 excluded.