Session 56: Full audit — PropLine + boxscore + pipeline + sport coverage (2289 tests)
Research (verified against live MLB Stats / ESPN / The Odds APIs): - specs/propline-audit.md — every stat_type mapped against our 4-layer pipeline; real MLB boxscore fields; sport coverage status; pipeline gap analysis. - specs/vyndr-roadmap.md — priority-ordered Sessions 57–64 + coverage targets. - scripts/propline-audit.js + specs/audit-data/ (raw capture). Headline bug: oddsNormalizer mapped batter_rbis → 'rbis' while the whole grade/feature/outcome chain keys on 'rbi' — every PropLine RBI prop silently failed to grade AND settle. Fixed (+ regression test). Phase 4 — wired missing MLB stats end-to-end: - PropLine MLB markets 6 → 12 (+runs, walks, doubles, earned_runs, hits_allowed, outs — same request, no extra quota). - doubles/outs/triples added to featureCache + outcomeService MLB_LOG_FIELD and all three grade whitelists (analyze/scan/validation.py). Phase 6 — pipeline resilience: - opsNotify.js: ntfy alerts (never throws, test-disabled). Snapshot success/ stale/failure alerts; retry-once on hard odds error (not on empty slate). - Missed-cron watchdog (mostRecentExpectedSlot/isSnapshotOverdue); status probe now returns `overdue`. Coverage truth: MLB is the only end-to-end-live sport; outcome settlement is MLB-only (WNBA/NBA/soccer never settle) — documented as the #1 roadmap gap. Backend 2276 → 2289 tests (+13). Web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -184,18 +184,36 @@ async function runSnapshot(sport, opts = {}) {
|
||||
cacheSet: opts.cacheSet || require('../utils/redis').cacheSet,
|
||||
now: opts.now || (() => new Date().toISOString()),
|
||||
nowMs: opts.nowMs || (() => Date.now()),
|
||||
// Session 56 — ops alerting (ntfy) + retry-once on a hard odds failure.
|
||||
notify: opts.notify || require('../utils/opsNotify').notify,
|
||||
sleep: opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms))),
|
||||
retryDelayMs: opts.retryDelayMs != null ? opts.retryDelayMs : 60_000,
|
||||
};
|
||||
const start = deps.nowMs();
|
||||
const ts = deps.now();
|
||||
|
||||
// Session 56 — retry ONCE on a hard failure (thrown error / null response = a
|
||||
// transient PropLine/network blip). A successful-but-empty slate is NOT a
|
||||
// failure (off-hours), so it is not retried — that would waste quota + latency.
|
||||
let odds;
|
||||
try {
|
||||
odds = await deps.getOdds(sp);
|
||||
if (odds == null) throw new Error('null odds response');
|
||||
} catch (e) {
|
||||
return { sport: sp, status: 'error', reason: e.message, gradeCount: 0 };
|
||||
try {
|
||||
await deps.sleep(deps.retryDelayMs);
|
||||
odds = await deps.getOdds(sp);
|
||||
if (odds == null) throw new Error('null odds response (retry)');
|
||||
} catch (e2) {
|
||||
await deps.notify(`❌ ${sp.toUpperCase()} snapshot FAILED: ${e2.message}`, { title: 'VYNDR pipeline', priority: 'high', tags: ['x'] });
|
||||
return { sport: sp, status: 'error', reason: e2.message, gradeCount: 0 };
|
||||
}
|
||||
}
|
||||
const props = (odds && Array.isArray(odds.props)) ? odds.props : [];
|
||||
if (props.length === 0) return { sport: sp, status: 'skipped', reason: 'no props', gradeCount: 0 };
|
||||
if (props.length === 0) {
|
||||
await deps.notify(`⚠️ ${sp.toUpperCase()} snapshot: 0 props (odds unavailable)`, { title: 'VYNDR pipeline', priority: 'low', tags: ['warning'] });
|
||||
return { sport: sp, status: 'skipped', reason: 'no props', gradeCount: 0 };
|
||||
}
|
||||
|
||||
// Grade the slate via the existing service; capture the envelope instead of
|
||||
// letting it write (we re-write an ENRICHED version below).
|
||||
@@ -273,6 +291,18 @@ async function runSnapshot(sport, opts = {}) {
|
||||
const events = generateTickerEvents(sp, enriched, deltas, ts);
|
||||
await pushTickerItems(events, deps);
|
||||
|
||||
// Session 56 — success alert, enriched with the rolling accuracy (if settled).
|
||||
let accPart = '';
|
||||
try {
|
||||
const acc = await deps.cacheGet(`accuracy:${sp}`);
|
||||
const pct = acc && acc.overall && acc.overall.pct;
|
||||
if (pct != null) accPart = `, ${pct}% accuracy (30d)`;
|
||||
} catch { /* accuracy is best-effort in the alert */ }
|
||||
await deps.notify(
|
||||
`✅ ${sp.toUpperCase()} snapshot: ${enriched.length} props graded, ${deltas.length} deltas${accPart}`,
|
||||
{ title: 'VYNDR pipeline', tags: ['white_check_mark'] },
|
||||
);
|
||||
|
||||
return {
|
||||
sport: sp,
|
||||
status: 'ok',
|
||||
|
||||
Reference in New Issue
Block a user