ecf78b911c96618e3ffb10943a7bbc872fa1d6d8
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43f65d30cb |
Wire the three proven hits factors pre-grade: transmission proven, gain
inconclusive THE BUG THIS NEARLY SHIPPED AS A FINDING. The first audit reported 0 factors fired on all 1,140 rows. Not a result -- my paging helper ordered by `id`, and batter_spray, team_defense, platoon_splits and statcast_aggregates have composite primary keys with NO id column. The query errored, the loop broke on error, and four fully-populated tables read as empty. hitsFactorContext.js -- the PRODUCTION loader -- had the identical defect, so live wiring would have loaded nothing and served unadjusted while logging success. Third occurrence of this class in one session. Both loaders now order by a real column and THROW rather than degrade. The Phase 2 gate is what caught it: no resolution number was quoted until transmission was proved. PHASE 1 — pipeline is now base -> FACTORS -> CALIBRATE -> GRADE. Context built in snapshotService BEFORE gradeAndCacheSlate (was line 640+, grade at 454), threaded per prop, applied to p_over before p_win is set with p_win_prefactor and a full trace retained. Hits only. Coverage 859/1140 rows (75%): 474 with all three factors, 256 two, 129 one, 281 none. PHASE 2 — TRANSMISSION PROVEN, 12/12 sign-correct, 4/4 per factor, each applied IN ISOLATION. My first table compared each factor's expected sign against the COMPOSITE change and showed 3 false failures -- with three factors firing the net can oppose any single member; that was a flaw in the test, not the wiring. Two under-side rows confirm the flip is handled: a factor raising p(over) correctly lowers p_win. Switch hitters (Bailey, Bell, Rocchio) took no spray adjustment while their other factors fired normally -- the refusal is selective, not a blanket skip. PHASE 3/4 — both maps refit on the factor-adjusted forecast; the shadow-duel baseline is VOID and restarts, since it accumulated against a different forecast. Point-in-time, 765 held-out rows: reliability 0.00795 -> 0.00828 RESOLUTION 0.00229 -> 0.00345 (variance explained 0.93% -> 1.39%) Brier 0.25398 -> 0.25305 delta -0.00093 CI [-0.00225,+0.00002] Resolution rose 51% relative. The CI TOUCHES ZERO on 4 eval dates, so the composition does NOT earn a proven keep -- three isolated passes did not grant a composed pass. INCONCLUSIVE, reported as such. The gain is far below the sum of the isolated effects, which is expected: all three run through the same pitcher-batter confrontation and share signal. PHASE 5 — 1.39% of variance is still far below what band separation needs. The pivot was correct and incomplete: the plumbing defect was real and is fixed, three proven factors reach the served number for the first time, and transmission alone did not buy grade separation. Next arc is factor STRENGTH and BREADTH, not more plumbing. PHASE 6 — rbi anomaly logged, not chased: 14.51% variance explained vs hits 1.03%, on the stat we do not serve corrected and which has no proven factors. Either the biggest lever on the board or a mirage; it deserves its own order. The byte-identical invariant INVERTED for hits by design. All 13 frozen non-hits modules verified unchanged, probabilityEstimator included -- the factors ride outside it. No new Bonferroni slot; the composed OOS claim is reported with its CI and not claimed as a pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9 |
||
|
|
843c8c6d4b |
Build the pitcher engine, and find the cap was eating the whole board
Strikeouts are NOT proven -- n=57 against a bar of 500. But the finding that matters is not a correlation. THE CAP. Measured on the live slate via the refusal diagnostic: 1,244 unique gradeable props exist, the 500 cap graded about 334, and because dedupeProps takes first-row-wins in FEED ORDER, what survives is decided by feed position rather than value. Pitchers are 2.6% of a batter-dominated feed, so we were grading SIX strikeout props a slate against 32 available -- putting n>=500 three months away for every pitcher stat. Pitcher props were never being refused (graded 5, refused 0, suppressed 0); it was truncation. Raised 500 -> 1500 on measured cost: 721ms per prop at concurrency 5 is about 179 seconds for the full board, against a cron that runs five times a day and a fire-and-forget caller that never holds an HTTP response. statsapi is free and unlimited. Concurrency stays at 5 -- one variable at a time. This unblocks every n-blocked stat in the programme, not just pitchers. THE ENGINE. pitcherEngine.js is its own engine, not the batter engine pointed at pitchers: the batter model asks whether contact becomes a hit and reads contact quality, the pitcher model asks whether the plate appearance ends without contact at all and reads stuff. Archetypes are FLAME (whiff-led), SCALPEL (chase-led), SINKER (pitches to contact) and DEFAULT, and a test asserts the weight keys are not the batter engine's. The projection is K% by log5 against THIS lineup, times batters faced, through a binomial. An unclassifiable arm gets the balanced map, never a guessed archetype. THE MEASUREMENT, at n=57 and contaminated. Four solo features clear the 0.15 effect bar and fail only on sample: arm angle at -0.250 -- the largest correlation measured anywhere in this programme -- then whiff +0.213, k rate +0.206, chase +0.195. The batter cluster's best was 0.135. Head to head, pitch-v1 resolves 0.1285 against the counter's -0.0639, delta +0.192 with a CI spanning zero. That negative is the interesting number. The counter is ANTI-PREDICTIVE on strikeouts: counting a pitcher's recent Ks is worse than useless, because his recent totals track which lineups he drew and how long he was left in rather than his skill. It is the one stat where the incumbent has no defensible edge. A bug caught on the way. resolveTeam wants an abbreviation and the game log supplies full team names, so the roster join silently resolved nothing and the first run reported 0% lineup coverage -- the theorized stuff x lineup carrier was never being tested, not failing. Fixed; coverage is now 94.7%. The carrier still shows no incremental signal over whiff alone, and adding the lineup term lowered head-to-head resolution, which is recorded rather than dropped. Calibration was not reached: nothing passed the first bar. The batter model and the counter are byte-identical, verified by diff. 4,221 tests green (335 suites); web build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9 |
||
|
|
4435856f46 |
Audit finds env/matchup axes DEAD in prod; fix the environment join
STEP 0 AUDIT -- the "already partly live" premise was half true: the CODE is wired, the axes are NOT firing. Across 634 graded prod rows the environment and matchup axes fired on ZERO rows, while 13 archetype axes fired normally (power 80, swing_miss 69, contact 56, launch 51, line_drive 43, ...) plus opportunity 142. Ledger confirms it from the other side: env_multiplier, env_park_base, env_weather_mod, wx_forecast and env_weather_state are ALL null on 634/634. ROOT CAUSE, located rather than inferred. A drop-off audit against the live snapshot: with_team_field 0/120, with_bats 0/120, with_playerId 120/120, oppPitcherByTeam 0, handById 0. `team` is a KEY on every stored grade and NULL on 416/416 -- so an environment resolver keyed off the player's roster team could never find a venue, while buildContext sat there with all 30 teams mapped and 14 weather forecasts resolved and unused. Coors composes to 1.241 the moment it gets a key. FIX -- and it is the more correct join, not just a workaround. The park and the weather belong to the GAME, not to the player's roster team, and the game rides on the prop from the odds feed. gradeBestSide now carries home_team/away_team onto the graded row (the legacy grade shape dropped them), and contextFor joins on the game first, keeping the roster team as a fallback. This no longer depends on a stats-resolve that can legitimately fail. MATCHUP/PLATOON IS NOT FIXED HERE and is not claimed as fixed: it needs the opposing starter and both hands, and the audit shows oppPitcherByTeam=0, handById=0 and bats=0 on the slate -- three separate absences. Per "one axis at a time" that is its own order with its own diagnosis, not a second fix smuggled into this one. Champion p_win, ranking, calibration and opportunity_drift's accruing verdict are all untouched. Gates: 4,077 tests / 326 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 |
||
|
|
a7d6cf8e36 |
Raise the grade cap 25 -> 500 on measured cost; refusals are correct
PART 1 (read-only, measured on a live prod slate, n=80) OVERTURNS THE
PREMISE. The refusal rate is not a data problem -- it is 98% correct
behaviour. The cap is the entire problem, and it is worse than "25 of 546".
Composition: GRADED 44 (55.0%) | POLICY-SUPPRESSION 35 (43.8%) |
FETCHABLE-GAP 1 (1.3%) | FALSE-THRESHOLD 0 | ARCHETYPE-GAP 0 |
GENUINE-ABSENCE 0.
THE FIFTH BUCKET the order did not anticipate: all 35 "refusals" are
rare_event_over_below_line -- the 2026-07-19 betting-logic audit
deliberately refusing 0.5-line rare events, setting the SAME
insufficient_data flag as a real data gap, which is why they read as one.
They are entirely doubles (18) and stolen_bases (17), while hits (19/19),
rbi (19/19) and total_bases (5/5) grade at ~100%. Had we "fixed" this we
would have re-introduced exactly the bets a previous audit removed, and the
count would have looked like progress.
THE CAP: 585 unique gradeable props, cap 25 -> 560 discarded (95.7%).
Traced to Session 32 (
|
||
|
|
f0543b57a4 |
Product identity + widen books for DISPLAY, model input byte-identical
IDENTITY (CLAUDE.md top + MASTER-PLAN header). VYNDR is a PREDICTIVE MODEL: it projects what a player will DO and picks accurately. Market edge is a BYPRODUCT of a good prediction, never the success criterion. Success = the forecast is honest about its own confidence AND still ranks -- calibration and resolution, both. No edge/CLV term belongs in a pass/fail gate; they are diagnostics we report, not thresholds a model must clear. A model tuned to beat a closing line has been fitted to the market instead of to the game. Per-sport doctrine (Phillips 2022, classify by what players DO not by position): each sport is its own model -- own variables, archetypes, conditions, calibration, honest ceiling. Shared across sports: ONLY the Bayesian inference math. Truth Law: no fabricated data; honest-absent over invented; label limitations in-band; provisional stays provisional until re-run; documented is not verified. PHASE 2 -- AGGREGATOR WIDENING (live). normalizeProps now emits every DISPLAY book instead of 5 of 18. Before this we discarded 13 books of our own accord and 64.8% of the MLB slate was invisible to users. Every prop carries book_role (both/takeable/reference/dfs/offshore) so the display layer can say WHAT a price is -- a fixed-payout DFS number and a two-way sportsbook price are not interchangeable objects. Unknown books are still dropped. PHASE 3 -- MODEL GATE (the model does not move). bookRoles splits MODEL_BOOKS (the legacy allow-list, character for character) from DISPLAY_BOOKS. Both model paths re-filter before they pick a line: gradeSlateService.dedupeProps (before first-row-wins AND before the limit) and intradayRefreshService.indexOddsProps (which RE-GRADES at the current line -- without the gate, widening would have silently moved locked lines onto books the model has never been calibrated against). A test asserts the graded set is byte-identical through the widening. CURRENT_RULER_VERSION stays v1_first_book. The gate lifts only when the MLB calibration is re-run on the consensus ruler and v2 is promoted. HONEST FRAMING, recorded in the plan: this is an AGGREGATOR win and it does NOT fix the model. WNBA still abstains -- a model problem, not a coverage problem; it is better covered than MLB. MLB isotonic still provisional. The consensus is MARKET, not SHARP: pinnacle, matchbook and polymarket are 0% on both sports, so no sharp anchor exists in our feed. Two superseded tests updated to stronger properties rather than deleted: roleOf now names the KIND of book, and the normalizer test asserts the display set widens WHILE the model set does not. Gates: 4,027 tests / 322 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 |
||
|
|
6415751f2e |
Grading binds opponent features to the REAL game, not ESPN's "today"
Order 1.6 Phase 1. This is a MODEL-OUTPUT fix, not bookkeeping. computeFeatures.lookupTodayGame called the ESPN scoreboard with NO date param and took whatever ESPN calls "today". Renamed to lookupGameOnDate and now sends ?dates=YYYYMMDD from the prop's BOUND game — the same game the ledger, retention and settlement use, so all four finally agree. PROVEN against live ESPN (before/after, 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) Every opponent was wrong. opponentAbbr feeds opp_rank_stat (a +/-1.0 factor) and isHome feeds home_away (+0.5), so late-slot grades were scored against the wrong matchup. Note the window is WIDER than the 01:00/03:00 UTC slots: this ran at 07:5x UTC = 03:5x ET and ESPN's dateless scoreboard was STILL returning the previous day's card. HONEST DEGRADATION: with no bound game date the grader does NOT fall back to a dateless lookup — it records 'no_bound_game_date' and leaves opponentAbbr/isHome/gameId null, so engine1 simply omits the opponent and home/away factors rather than scoring a wrong matchup. Tests assert both directions. Same class of bug fixed alongside: the Tank01 augmentation used TODAY's UTC date for its cache key; it now uses the bound game date. gradeSlateService threads game_date/game_time/home_team/away_team into the grader so the binding reaches computeFeatures at all. Audited the rest of the feature path for dateless/"today" lookups — none remain (weather is current-conditions by venue, park/pace are static). Suite 282/3383 green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA |
||
|
|
d3ffa1b8c2 |
Retention: model_snapshots live + base64 SSH key support
RETENTION (Phase 2, priority zero). History starts compounding tonight. migration 025 model_snapshots — APPLIED to prod. Append-only, one row per graded prop PER SIDE PER CYCLE, with a unique index on (snapshot_id, player_key, stat, line, side) so a retried cycle cannot duplicate. RLS on, service-role writes only. What it captures that the ledger never did: - features jsonb — the model's INPUTS. Without these a backtest can only grade our own homework; with them any future model can be replayed against the exact conditions this one faced. - REFUSALS (refused + refusal_reason). The ledger drops them, so a gate refusing props that would have WON is invisible — unmeasurable lost edge. Captured via a new onGraded hook in gradeSlateService that fires with BOTH sides before any filtering. - grade_11, the pre-collapse grade. The 4-letter map throws away the entire live C-/C/C+/B- range. - model_version + code_sha on every row. ledger_entries mixes pre/post-fix grades with no marker and cannot be separated retroactively. - p_win / ev_pct / fair_odds / takeable / value — none of which any permanent store held. Wiring: analyzeViaEngine1 attaches _features/_grade_11 (underscore = internal); gradeSlateService fires onGraded then STRIPS them so they never reach a cache or API payload; snapshotService builds rows and persists best-effort. Retention reuses the LEDGER's dateET/gameIdFor helpers so rows share the ledger's natural key exactly — otherwise the settle pass could never join outcomes onto them. Rows are written BEFORE the empty- slate early return: a slate that refused everything is exactly the case worth recording. CONTRACT HELD: retention is injectable and every path is caught. persist() returns errors, never throws; a missing Supabase client is SKIPPED, not an error. A retention failure can never break a snapshot. BACKUP: backup-db.sh now accepts BACKUP_SSH_KEY as base64 (recommended — survives env-var newline mangling, which is how injected SSH keys usually break silently) OR raw PEM, detected by decoding and looking for the PEM header. Verified both forms detect correctly against a real generated key. Suite 279/3325 green, build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA |
||
|
|
d242b11b4b |
S1 (a1): feature-promise audit — no claim survives unverified
PROMISE-AUDIT.md: every /pricing claim → verified/built/reworded. BUILT (was vapor): alt line ladder + edge ranking (same-features regrade at shifted lines, Desk-gated at the API), quarter-Kelly (engine quantile P(win) x real captured odds — either missing → no sizing), free-tier kill-condition locked previews. FIXED (was false): analyst 15/day cap vs the Founder 'Unlimited reads' promise → analyst unlimited; every '40+ factors' claim (real count: 22 named features) reworded truthfully in 7 files. VERIFIED: cascade alerts (real, wired), phi correlation, leg history, cross-book comparison, WC soccer, real-time feed. Locked by tests/unit/promiseAudit.test.js. Jest now ignores .claude/worktrees (parallel agents' suites no longer leak into runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d296e40cb6 |
Session 58: Phase 1 — Truth Infrastructure (2327 tests)
ledger_entries is live (migration 019 applied to prod, RLS + NULLS NOT
DISTINCT dedupe verified against the real database). Every grade now
persists, settles against the real result, and carries closing-line value.
- ledgerService: pipeline pre-grade upserts (public model record, user_id
null, idempotent), closing capture on every snapshot (last write before
game start = the close), settlement with SIGNED CLV (over = locked -
closing; beat/faded/flat), 30d model aggregate with the hard n>=20 rule.
- Write paths: snapshotService -> ledger (priority path); Next /api/scan ->
ledger for authenticated users only (anon never touches the public
record). Refused reads write nothing and don't burn a scan.
- Honest refusal (work-order 1.5): no projection => insufficient_data,
grade null, "INSUFFICIENT DATA - no read" UI. The web gradeAdapter no
longer displays the line as the model projection (the audit's
model==line / +0% edge degenerate); the card renders absent states.
projectionFor is sport-aware (l5 -> l20 -> {stat}_per_90 -> xG).
- /ledger: MY READS | MODEL tabs; model header shows hit% + beat-close%
only at n>=20, else RECORD BUILDING + live pending count. ModelRecord
deferred-render strip on landing + player hero. CLV + outcome chips,
revised_from_grade strikethrough (Phase 2.5 ready).
- SYNC (Task 5): thresholds vs SNAPSHOT_EXPECTED_INTERVAL (normal <1.5x,
amber >=1.5x, STALE red >=3x) via /api/snapshot/summary.
- Phase 2.5 logged in specs/vyndr-roadmap.md (build after Phase 3).
- Data-semantics hardening: strict null-safe numeric parsing everywhere a
market value is handled (Number(null)===0 would have fabricated lines).
Backend 2309 -> 2327 tests (201 suites), web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f0c8b4f29b |
Session 32: Grades pipeline + NFL/NHL wiring + rate limiting + audit cleanup (1718 tests)
- gradeSlateService writes grades:{sport} cache (closes content pipeline →
dataLevel full); fire-and-forget from oddsService.recordDownstream, gated
by shouldGradeSlate (off in test, GRADE_SLATE_ON_FETCH override)
- NFL/NHL wired: oddsService SPORT_KEYS/SPORT_MARKETS (correct the-odds-api
keys americanfootball_nfl/icehockey_nhl), proplineAdapter MARKETS, NHL
MARKET_MAP keys to avoid silent-zero
- rate limiting mounted on 8 public cached routers (odds/parlay 30/min,
rest 60/min)
- jsonlLogger writes to temp under test (no more dirtied tracked artifact);
5MB pipeline test given 20s timeout
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|