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:
@@ -24,7 +24,12 @@ const MARKET_MAP = {
|
||||
batter_hits: 'hits',
|
||||
batter_home_runs: 'home_runs',
|
||||
batter_total_bases: 'total_bases',
|
||||
batter_rbis: 'rbis',
|
||||
// Session 56 audit — must be 'rbi' (singular): the grade whitelists
|
||||
// (analyze/scan/validation.py), featureCache MLB_LOG_FIELD, and outcomeService
|
||||
// all key on 'rbi'. Normalizing to 'rbis' silently dropped every PropLine RBI
|
||||
// prop from grading AND settlement. The streaks/hotlist path uses its own
|
||||
// 'rbis' key built from raw MLB stats — independent of this normalizer.
|
||||
batter_rbis: 'rbi',
|
||||
batter_runs: 'runs',
|
||||
batter_stolen_bases: 'stolen_bases',
|
||||
batter_singles: 'singles',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* opsNotify (Session 56) — pipeline alerting via ntfy.sh.
|
||||
*
|
||||
* Push a one-line operational alert (snapshot success / failure / stale / missed
|
||||
* cron) to an ntfy topic so a silent pipeline never goes unnoticed. Fire-and-
|
||||
* forget: NEVER throws, NEVER blocks the pipeline on a notify failure.
|
||||
*
|
||||
* Config:
|
||||
* NTFY_URL (default https://ntfy.sh)
|
||||
* NTFY_TOPIC (default vyndr-pipeline-kev2026)
|
||||
* PIPELINE_ALERTS=0 → disable entirely
|
||||
* Disabled automatically under NODE_ENV==='test' unless a fetchImpl is injected
|
||||
* (so the unit tests can assert the call without hitting the network).
|
||||
*/
|
||||
|
||||
const NTFY_URL = () => process.env.NTFY_URL || 'https://ntfy.sh';
|
||||
const NTFY_TOPIC = () => process.env.NTFY_TOPIC || 'vyndr-pipeline-kev2026';
|
||||
|
||||
function enabled(opts = {}) {
|
||||
if (opts.fetchImpl) return true; // tests inject → always "enabled"
|
||||
if (process.env.PIPELINE_ALERTS === '0') return false;
|
||||
if (process.env.NODE_ENV === 'test') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an ops alert. `opts`: { title, priority ('min'|'low'|'default'|'high'|
|
||||
* 'urgent'), tags (string[]), fetchImpl }. Resolves { sent: boolean } — never rejects.
|
||||
*/
|
||||
async function notify(message, opts = {}) {
|
||||
if (!enabled(opts)) return { sent: false, reason: 'disabled' };
|
||||
const doFetch = opts.fetchImpl || fetch;
|
||||
const headers = {};
|
||||
if (opts.title) headers.Title = opts.title;
|
||||
if (opts.priority) headers.Priority = opts.priority;
|
||||
if (Array.isArray(opts.tags) && opts.tags.length) headers.Tags = opts.tags.join(',');
|
||||
try {
|
||||
await doFetch(`${NTFY_URL()}/${NTFY_TOPIC()}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: String(message == null ? '' : message),
|
||||
signal: typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(6000) : undefined,
|
||||
});
|
||||
return { sent: true };
|
||||
} catch (err) {
|
||||
// Alerting must never break the pipeline.
|
||||
if (process.env.NODE_ENV !== 'test') console.warn('[opsNotify] failed:', err.message);
|
||||
return { sent: false, reason: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { notify, __internals: { enabled, NTFY_URL, NTFY_TOPIC } };
|
||||
Reference in New Issue
Block a user