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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
Kev
2026-08-01 04:01:57 -04:00
parent 48a2f764ac
commit c9d56d5668
+69
View File
@@ -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;