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;