Item 5 — daily hero prop is a live RULE (biggest model-vs-market disagreement)

The landing hero was a static Jokic "Example" card with a name-length pick and a
hardcoded A- 73% +6.2% fallback. Now it's deterministic and live:

- heroPropService.pickHeroProp reads the pre-graded snapshot and selects the
  prop with the LARGEST |projection - line| gap among A/B grades (conviction,
  not noise) — the read where VYNDR disagrees most with the market, the card
  that makes a stranger argue. No curation, no grading (reads cache → no API
  credits). GET /api/hero-prop (backend) + repointed Next proxy.
- The card shows the disagreement EXPLICITLY: the book's line vs VYNDR's model,
  side by side (model in green), with the real grade timestamp ("Graded 2:14
  PM"). The EXAMPLE chip is gone.
- Empty slate → the MOST RECENT real graded read (flagged "LATEST READ", real
  date). Nothing cached → { available:false } and the card HIDES. No
  hand-written fallback — the Jokic card is deleted. Survives a dead night: a
  live rule shows tonight's real MLB read, never a phantom July NBA card.

7 service tests lock the rule (max-gap, A/B gate, projection/line required,
empty→recent, hidden, cross-sport). colorContract updated to the new
disagreement display. Change-affected suites green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-18 01:29:55 -04:00
parent 89a2977f57
commit 9b9aab4262
8 changed files with 345 additions and 460 deletions
+93
View File
@@ -0,0 +1,93 @@
'use strict';
/**
* heroPropService (Truth-Everywhere Part 2, item 5) — the DAILY HERO PROP.
*
* The landing card is a live rule, not a hand-picked example: the graded prop
* where VYNDR disagrees MOST with the market — the largest |model - consensus|
* gap — gated to A/B grades (conviction, not noise). Highest-confidence would
* just be the model agreeing loudly with an undisputed number; the disagreement
* is the read that makes a stranger argue.
*
* Deterministic, no curation. Reads the pre-graded snapshot caches (no grading,
* no API credits). Empty slate → the MOST RECENT real graded read with its real
* date. Never a hand-written fallback. Truly nothing cached → { available:false }
* and the card hides.
*/
const DEFAULT_SPORTS = ['mlb', 'wnba', 'nba', 'soccer'];
// A/B tiers only — the conviction gate.
const isAB = (g) => /^[AB]/.test(String(g || '').trim().toUpperCase());
function toHero(g, sport, gap, isRecent) {
const at = g.gradedAt || {};
return {
available: true,
is_recent: !!isRecent, // true = the empty-slate "most recent real read"
sport,
player: g.player_name || g.player || null,
stat_type: g.stat_type || g.stat || null,
line: g.line ?? at.line ?? null, // the book's number (consensus)
direction: g.direction || 'over',
projection: g.projection ?? null, // VYNDR's number (model)
grade: g.grade || null,
book: g.book || null,
graded_at: at.timestamp || null, // real timestamp — "graded 2:14 PM"
gap: gap == null ? null : Math.round(gap * 100) / 100,
team: g.team || null,
reasoning: (g.reasoning && g.reasoning.summary) || null, // blurred paywall teaser
};
}
// A prop is a valid hero candidate only with a real projection AND a real line
// (both required to compute an honest gap; a missing value never counts as 0).
function candidate(g) {
const line = Number(g && g.line);
const proj = Number(g && g.projection);
return Number.isFinite(line) && line > 0 && Number.isFinite(proj) && proj > 0;
}
async function pickHeroProp(deps = {}) {
const cacheGet = deps.cacheGet || require('../utils/redis').cacheGet;
const sports = deps.sports || DEFAULT_SPORTS;
const all = [];
for (const sport of sports) {
let grades = null;
const snap = await cacheGet(`snapshot:${sport}:latest`);
if (snap && Array.isArray(snap.grades)) grades = snap.grades;
else {
const env = await cacheGet(`grades:${sport}`);
if (env && Array.isArray(env.grades)) grades = env.grades;
}
for (const g of grades || []) {
if (!g || !g.grade || g.insufficient_data) continue;
all.push({ g, sport });
}
}
// The hero: largest |projection - line| among A/B candidates.
let hero = null, heroGap = -1;
for (const { g, sport } of all) {
if (!isAB(g.grade) || !candidate(g)) continue;
const gap = Math.abs(Number(g.projection) - Number(g.line));
if (gap > heroGap) { heroGap = gap; hero = { g, sport }; }
}
if (hero) return toHero(hero.g, hero.sport, heroGap, false);
// Empty slate → the MOST RECENT real graded read (any grade), by timestamp.
let recent = null, recentTs = '';
for (const { g, sport } of all) {
const ts = (g.gradedAt && g.gradedAt.timestamp) || '';
if (ts && ts > recentTs) { recentTs = ts; recent = { g, sport }; }
}
if (recent) {
const gp = candidate(recent.g) ? Math.abs(Number(recent.g.projection) - Number(recent.g.line)) : null;
return toHero(recent.g, recent.sport, gp, true);
}
// Truly nothing cached — the card hides. Never a fabricated fallback.
return { available: false };
}
module.exports = { pickHeroProp, __internals: { isAB, candidate, DEFAULT_SPORTS } };