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
This commit is contained in:
@@ -176,7 +176,17 @@ async function buildContext(sport, deps = {}) {
|
||||
* playerId. Anything missing → that half is null.
|
||||
*/
|
||||
const contextFor = async (grade) => {
|
||||
const teamAbbr = abbrOf(grade && grade.team);
|
||||
// JOIN ON THE GAME FIRST (2026-08-01). This used to key ONLY off the
|
||||
// player's roster team, which is null on the graded slate — measured at
|
||||
// 0/416 — so the environment axis could never resolve a venue.
|
||||
//
|
||||
// The park and the weather belong to the GAME, so the game's own teams are
|
||||
// both the more reliable key and the more correct one: they ride on the
|
||||
// prop from the odds feed and do not depend on a stats-resolve that can
|
||||
// legitimately fail. The roster team stays as a fallback.
|
||||
const teamAbbr = abbrOf(grade && grade.team)
|
||||
|| abbrOf(grade && grade.home_team)
|
||||
|| abbrOf(grade && grade.away_team);
|
||||
const game = teamAbbr ? gameByTeam.get(teamAbbr) : null;
|
||||
const stat = grade && (grade.stat_type || grade.stat);
|
||||
|
||||
|
||||
@@ -126,6 +126,17 @@ async function gradeBestSide(grade, prop, sport, opts = {}) {
|
||||
// Strip the internal retention fields so they never reach a cache or payload.
|
||||
delete winner._features;
|
||||
delete winner._grade_11;
|
||||
// CARRY THE GAME (2026-08-01). The legacy grade shape drops home/away, so by
|
||||
// the time the challenger runs, nothing on the grade says WHICH GAME it is —
|
||||
// measured: `team` was null on 416/416 stored grades, so the park/weather
|
||||
// resolver could never find a venue and the environment axis fired on ZERO
|
||||
// rows while all 14 weather forecasts sat resolved and unused.
|
||||
//
|
||||
// The park depends on the GAME, not on the player's roster team, so binding
|
||||
// the game directly is both the fix and the more correct join: it does not
|
||||
// depend on a stats-resolve that can legitimately fail.
|
||||
if (winner.home_team == null && prop.home_team != null) winner.home_team = prop.home_team;
|
||||
if (winner.away_team == null && prop.away_team != null) winner.away_team = prop.away_team;
|
||||
return winner;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,3 +122,47 @@ describe('attachChallenger — reads opportunity off the grade (zero extra I/O)'
|
||||
expect(out[1].challenger_delta).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment context — joins on the GAME, not the roster team', () => {
|
||||
// Measured 2026-08-01: `team` was null on 416/416 stored grades, so an
|
||||
// environment resolver keyed only off the roster team could never find a
|
||||
// venue — the axis fired on ZERO rows while all 14 weather forecasts sat
|
||||
// resolved and unused. The park belongs to the GAME, so the game's own teams
|
||||
// are both the more reliable key and the more correct one.
|
||||
const envCtx = require('../../src/services/environmentContext');
|
||||
|
||||
it('abbrOf resolves both a full team name and an abbreviation', () => {
|
||||
expect(envCtx.abbrOf('Colorado Rockies')).toBe('COL');
|
||||
expect(envCtx.abbrOf('COL')).toBe('COL');
|
||||
});
|
||||
|
||||
it('a grade with no team AND no game resolves to nothing — never a default park', () => {
|
||||
// A fabricated park factor would be a silent multiplier on every prop.
|
||||
expect(envCtx.abbrOf(null)).toBeFalsy();
|
||||
expect(envCtx.abbrOf(undefined)).toBeFalsy();
|
||||
expect(envCtx.abbrOf('')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('gradeSlateService — the graded row carries its game', () => {
|
||||
it('binds home/away from the prop so the challenger can find the venue', async () => {
|
||||
const { gradeBestSide } = require('../../src/services/gradeSlateService').__internals;
|
||||
const grade = async (b) => ({
|
||||
player: b.player, stat_type: b.stat_type, line: b.line,
|
||||
direction: b.direction, grade: 'B', confidence: 60,
|
||||
});
|
||||
const out = await gradeBestSide(grade, {
|
||||
player: 'A', stat_type: 'hits', line: 1.5, book: 'draftkings',
|
||||
home_team: 'Colorado Rockies', away_team: 'Chicago Cubs',
|
||||
}, 'mlb', {});
|
||||
expect(out.home_team).toBe('Colorado Rockies');
|
||||
expect(out.away_team).toBe('Chicago Cubs');
|
||||
});
|
||||
|
||||
it('does not invent a game when the prop has none', async () => {
|
||||
const { gradeBestSide } = require('../../src/services/gradeSlateService').__internals;
|
||||
const grade = async (b) => ({ ...b, grade: 'B', confidence: 60 });
|
||||
const out = await gradeBestSide(grade, { player: 'A', stat_type: 'hits', line: 1.5 }, 'mlb', {});
|
||||
expect(out.home_team ?? null).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user