From c9d56d5668e06ffb425976ba72060ca5f1f8c686 Mon Sep 17 00:00:00 2001 From: Kev Date: Sat, 1 Aug 2026 04:01:57 -0400 Subject: [PATCH] Audit endpoint: where the env/matchup context chain drops off The arch-v1 environment and matchup axes fired on ZERO prod rows across 634 graded props while archetype axes fired normally, and buildContext works locally (15 games, 14 with weather, Coors composing to 1.241). So the failure is downstream of buildContext and has to be located, not inferred from an absence. Replays buildContext + contextFor against the CURRENT cached snapshot grades and counts the drop-off at each hop: team field present -> resolves to an abbr -> abbr matches a game -> environment produced; and bats / playerId / opposing-pitcher known -> matchup produced. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc --- src/routes/internal.js | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/routes/internal.js b/src/routes/internal.js index e968e90..e2ca9f5 100644 --- a/src/routes/internal.js +++ b/src/routes/internal.js @@ -652,4 +652,73 @@ router.get('/feature-coverage', async (req, res) => { } }); +/** + * GET /api/internal/env-context-audit (matchup/park audit, Step 0) + * + * READ-ONLY. The arch-v1 environment and matchup axes fired on ZERO prod rows + * while archetype axes fired normally, so the question is WHERE the resolution + * chain drops off. This replays buildContext + contextFor against the CURRENT + * cached snapshot grades and counts the drop-off at each hop, instead of + * inferring it from an absence. + */ +router.get('/env-context-audit', async (req, res) => { + try { + const sport = String(req.query.sport || 'mlb').toLowerCase(); + const { cacheGet } = require('../utils/redis'); + const envCtx = require('../services/environmentContext'); + + const snap = await cacheGet(`snapshot:${sport}:latest`); + const grades = (snap && Array.isArray(snap.grades) ? snap.grades : []).slice(0, 120); + + const ctx = await envCtx.buildContext(sport, { + origin: process.env.BACKEND_SELF_ORIGIN || 'http://localhost:3000', + }); + const i = ctx._internals || {}; + + const counts = { + grades_examined: grades.length, + with_team_field: 0, + team_resolves_to_abbr: 0, + abbr_matches_a_game: 0, + got_environment: 0, + with_bats: 0, + with_playerId: 0, + opp_pitcher_known: 0, + got_matchup: 0, + }; + const sampleTeams = []; + for (const g of grades) { + const team = g.team; + if (team) counts.with_team_field += 1; + const abbr = envCtx.abbrOf(team); + if (abbr) counts.team_resolves_to_abbr += 1; + if (sampleTeams.length < 8 && team) sampleTeams.push({ team, abbr: abbr || null }); + if (abbr && i.gameByTeam && i.gameByTeam.has(abbr)) counts.abbr_matches_a_game += 1; + if (g.bats) counts.with_bats += 1; + if (g.playerId != null) counts.with_playerId += 1; + if (abbr && i.oppPitcherByTeam && i.oppPitcherByTeam.get(abbr) != null) counts.opp_pitcher_known += 1; + const r = await ctx.contextFor(g); + if (r && r.environment) counts.got_environment += 1; + if (r && r.matchup) counts.got_matchup += 1; + } + + res.set('Cache-Control', 'no-store'); + return res.json({ + ok: true, + read_only: true, + build_context_stats: ctx.stats, + internals_sizes: { + gameByTeam: i.gameByTeam ? i.gameByTeam.size : null, + forecastByHome: i.forecastByHome ? i.forecastByHome.size : null, + oppPitcherByTeam: i.oppPitcherByTeam ? i.oppPitcherByTeam.size : null, + handById: i.handById ? i.handById.size : null, + }, + drop_off: counts, + sample_team_values: sampleTeams, + }); + } catch (err) { + return res.status(500).json({ ok: false, error: err && err.message, stack_head: String(err && err.stack || '').split('\n')[1] || null }); + } +}); + module.exports = router;