7cf3892e761fb64763da872b1a0c62e74202f659
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7b25d97891 |
Wire the four dormant adjusters live — pure input-wiring
Verified state going in: parkBase, weatherMod and platoonSplits were called by nothing, and env_multiplier was non-null on zero rows across four orders. The adjusters were correct in isolation and starved of inputs. This gives them their inputs and changes none of their internal logic — the five adjuster files are byte-identical after this commit. PHASE 0 GATE — all three inputs are available at snapshot build, and the two join keys already existed. Venue: always, on every schedule game object. First-pitch: always, gameTime on the same object. Opposing-pitcher hand: present once the probable is declared, via the pitchers endpoint's pitcherId joined to statsapi handedness — 15 of 15 games declared this afternoon, though morning locks precede declaration and those props honest-absent on platoon, correctly. The batter-handedness join (statcast bats) and the MLBAM id were already on each grade from earlier sessions. environmentContext.js is the wiring, kept separate from the adjusters so they stay pure. It fetches once per snapshot: the schedule (team to venue, gameTime), probable pitchers (team to opposing pitcher id), one batched handedness call, one Open-Meteo forecast per home park, and batter splits per graded hitter. Park coordinates for 30 parks live here as public geometry, the same class as the dome list and centre-field bearings already in weatherMod, rather than inside an adjuster. Everything is best-effort: a missing venue drops park and weather, an undeclared pitcher drops platoon, and any fetch failure degrades that prop to archetype-only rather than breaking the pipeline the adjusters are measured inside. attachChallenger becomes async and takes a per-grade contextFor that returns the environment coefficient (park_base x weather_mod, composed) and the matchup (platoon). Point-in-time holds: the weather is a forecast for first pitch fetched now, and the split is the hitter's line entering the game — neither reads a settle-time value. Attribution is independent. env_multiplier, env_park_base, env_weather_mod and env_weather_state land in their own ledger columns, and challenger_adjustments keeps every axis — archetype, environment, matchup — as a separate entry, so when volume accrues each of the four can be measured for its own marginal contribution rather than as one blended delta. The combined move stays bounded, tested on the worst case: a Coors slugger with wind out and a favourable platoon, all at once, still moves under 12 percent, because every layer is capped and the total nudge is clamped. Stacking leans, it does not compound into a re-forecast. Non-MLB honest-absents entirely — park, weather and platoon are MLB-only today, so a WNBA prop gets no environment and no matchup. The champion is untouched throughout: p_win is read, never written, the served snapshot payload is still the enriched object, and a test confirms p_win passes through byte-for-byte while the challenger moves. Tests 3741 passed / 301 suites, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj |
||
|
|
3ac91c3d96 |
Layer 3 Step 4: derived park factors, composable for weather
PHASE 0 GATE — the answer is BOTH, and the important half was already here. A STATIC FanGraphs park-factor table has existed since Session 15 (src/data/parkFactors.js) and computeFeatures already consumes it, so park is not a new idea in this codebase. What was missing is OUR derivation. I nearly built a second source of truth before finding it; the new service lives at src/services/parkFactors.js and the two are deliberately distinct. That discovery changes the point of this order rather than just its scope. If the champion already sees a park factor, adding one to the challenger risks double-counting — which is exactly the redundancy the Session-72 harness exists to catch. So park ships as a NOMINATED CHALLENGER whose job is to be tested for marginal contribution, not as an assumed improvement. Checked and worth noting: the static table reaches computeFeatures but NOT probabilityEstimator, so it does not currently touch p_win at all. DERIVATION, not ingestion. statsapi gives every game with venue, linescore and scoringPlays in one call per date range — and since every home run scores at least the batter, HR totals are fully recoverable from scoring plays. Derived from 5,055 real games across 2022-2025: Coors tops the run environment at 1.099, Dodger Stadium tops home runs at 1.106, Oracle Park and PNC suppress them at 0.923 and 0.917. Eighteen parks cleared the floor, eighteen did not and are honestly absent. COMPOSABLE BY CONSTRUCTION — the architectural point. Park emits a multiplier around 1.0, never an additive nudge, because weather has to modulate it next order: effective = park_base x weather_mod. Additive terms do not compose correctly (a 5% park and an 8% wind are 1.05 x 1.08, not +13%), and the challenger converts the multiplier to log-odds so stacking stays correct. A test multiplies a placeholder weather term onto the park base to prove the shape composes with no rearchitecting. DIRECTIONAL BY PROP-OWNER: home_runs and home_runs_allowed both key off hr_base in the same direction, because the sign lives in the STAT, not the park. Coors inflates the hitter's home run prop and the pitcher's home-runs-allowed prop identically. THREE HONEST STATES, deliberately distinct. Absent (thin sample, adjust nothing), present (adjust), and weather_na for domes — where the park factor STILL APPLIES because a dome has a real run environment, and the flag exists so next order's weather modulation correctly does nothing there. N/A is not absent; conflating them would either drop a valid park factor or apply wind indoors. Structural breaks: a season deviating past the threshold starts a new regime only if the FOLLOWING season confirms it — one odd year is noise, two consecutive years on the same side is a rebuilt park. Only post-break seasons are used, so a humidor or moved wall cannot be diluted by the stadium that preceded it. Factors regress toward neutral by sample size, so a two-season park cannot assert a Coors-sized coefficient, and fine conditioning stays unavailable until its own larger floor. Tests 3669 passed / 297 suites, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj |
||
|
|
f2da9dd7e8 |
Layer 3 Step 2: archetype-aware CHALLENGER, measured not claimed
The champion (probabilityEstimator -> p_win) keeps serving and grading users, completely unchanged. The challenger is a second probability computed from the same inputs at the same instant, landing on the same ledger row so it joins to the same outcome and the same close. Identical conditions, one difference — the only clean A/B. NOTHING IS CLAIMED. Running a challenger is honest beta; asserting it is better before the settled ledger says so is not. Promotion stays a later decision gated on Brier and calibration over sufficient segmented volume. INTERPRETABLE, NOT A RE-ESTIMATION. The challenger is the champion's probability adjusted by the Layer-2 axes, applied in log-odds space so a nudge cannot push past 0 or 1 and means the same thing at p=0.5 as at p=0.9. Every deviation is attributable to a named axis and a signed nudge, stored as challenger_adjustments, and the total is capped at 0.45 log-odds — a lean on a real signal, never a re-forecast. Only mechanically obvious stat/axis relationships are mapped; a speculative mapping would be the same guessing this layer exists to replace. IDENTICAL WHERE THERE IS NO SIGNAL, by construction. An unremarkable player, a thin sample, an unmapped stat or a missing classification all return the champion's probability byte-for-byte with an empty adjustment list and a stated reason. The experiment therefore differs only where archetype-awareness could possibly help or hurt, with no dilution from rows the treatment never touched. Induced on real players. Judge home runs over: 0.42 -> 0.447, via BOMBER +0.22 and WHIFF RISK -0.11 — two real opposing signals netting positive. The same prop under mirrors it exactly to -0.027. Judge strikeouts: delta exactly 0, because WHIFF RISK and GRINDER cancel — an honest "no lean" with both signals still recorded. Skubal strikeouts over: 0.60 -> 0.702 via WHIFF, TRAPDOOR and CANNON all aligned; his hits-allowed goes the other way, 0.50 -> 0.392, because a strikeout arm makes hits less likely. Josh Bell and a 12-PA sample are untouched. Isolation is structural: adjust() is pure, the champion field is read and never written, the served snapshot payload is still the untouched champion object, and a challenger failure is caught so it can never break the pipeline it is measured inside. Statcast aggregates load once per snapshot run rather than per prop, so grade-time I/O stays at zero. Migration 034. Tests 3634 passed / 295 suites, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj |