Session 7d: Audit fixes - rate limiting, error leak, parallel parlays, analyze cache, bundle analyzer

This commit is contained in:
Kev
2026-06-10 03:12:20 -04:00
parent d954e4d952
commit 6f4a353de9
18 changed files with 913 additions and 72 deletions
+11
View File
@@ -1,3 +1,14 @@
// DEPRECATED — Session 7c audit flagged this for unification with the
// new Engine 1 (src/services/intelligence/engine1.js). Session 7d deferred
// the rewire because the output shapes are incompatible:
// - Legacy: 4-letter grade (A|B|C|D), 0-100 confidence, kill_conditions,
// reasoning.steps. Consumed by /api/analyze, /api/scan, /api/bets
// and the frontend GradeCard + DemoScan components.
// - New: 11-step grade (F..A+), 0-1 confidence, factors array. Consumed
// by /api/grading/pipeline only.
// Migration plan in docs/SYSTEM-MANIFEST.md §8 ARCH-1. Do not extend this
// file — new features land in engine1.js. Remove this file when the legacy
// route set retires.
function computeGrade(stepResults) {
const {
seasonDelta,
@@ -31,6 +31,12 @@ function inactive(reason) {
// Normalize player names for matching across data sources. ParlayAPI may
// emit "Brunson, Jalen" while ESPN emits "Jalen Brunson" — strip case,
// punctuation, suffixes, and collapse whitespace so equivalence works.
//
// DUP-1 (Session 7c): a near-identical implementation lives in
// scripts/populate-player-ids.js. The script's variant keeps digits
// (some legacy roster fields encode jersey numbers); this one strips
// them because trap matches go by player name only. If the script
// stops needing digits, consolidate to a shared util.
function normalizeName(name) {
if (!name) return '';
return String(name)
+39 -27
View File
@@ -23,12 +23,22 @@ async function scanParlay(user, legs) {
}
}
// Analyze all legs
const legResults = [];
for (const leg of legs) {
const result = await analyzeProp(leg);
legResults.push(result);
}
// PERF-2 (Session 7d): analyze legs in parallel. Each call is an
// independent upstream lookup, so a 6-leg parlay is ~6x faster here
// than the old sequential loop. allSettled preserves leg order and
// lets a single failed leg surface as an error stub instead of
// crashing the whole parlay.
const settled = await Promise.allSettled(legs.map((leg) => analyzeProp(leg)));
const legResults = settled.map((s, i) => {
if (s.status === 'fulfilled') return s.value;
return {
...legs[i],
error: s.reason?.message || 'analysis_failed',
grade: 'F',
confidence: 0,
reasoning: { summary: 'Analysis failed for this leg.' },
};
});
// Fetch odds data for correlation detection (spreads, game context)
let spreads = [];
@@ -81,28 +91,30 @@ async function scanParlay(user, legs) {
correlationFlags
);
// Write to database
const pickIds = [];
for (const leg of legResults) {
const { data: pick, error } = await supabase
// PERF-2 (Session 7d): one batched insert for every leg's pick row
// instead of N sequential inserts. Supabase preserves insert order in
// the returned data array so pickIds line up with legResults.
const pickRows = legResults.map((leg) => ({
user_id: user.id,
player: leg.player,
stat_type: leg.stat_type,
line: leg.line,
book: leg.book || 'unknown',
direction: leg.direction,
grade: leg.grade,
edge_pct: leg.edge_pct,
reasoning: leg.reasoning?.summary || '',
kill_conditions: (leg.kill_conditions_triggered || []).map((k) => k.code),
confidence: leg.confidence,
}));
let pickIds = [];
if (pickRows.length > 0) {
const { data: picksData, error: picksErr } = await supabase
.from('picks')
.insert({
user_id: user.id,
player: leg.player,
stat_type: leg.stat_type,
line: leg.line,
book: leg.book || 'unknown',
direction: leg.direction,
grade: leg.grade,
edge_pct: leg.edge_pct,
reasoning: leg.reasoning?.summary || '',
kill_conditions: (leg.kill_conditions_triggered || []).map((k) => k.code),
confidence: leg.confidence,
})
.select('id')
.single();
if (pick) pickIds.push(pick.id);
.insert(pickRows)
.select('id');
if (picksErr) console.warn('[VYNDR] picks batch insert failed:', picksErr.message);
pickIds = (picksData || []).map((p) => p.id);
}
// Write scan session