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:
@@ -216,6 +216,7 @@ app.use('/api/internal', internalRoutes);
|
||||
// requireInternalAuth); no Next proxy on purpose — never browser-facing.
|
||||
app.use('/api/partners', require('./routes/partners'));
|
||||
app.use('/api/founders', require('./routes/founders'));
|
||||
app.use('/api/hero-prop', require('./routes/heroProp'));
|
||||
|
||||
// Session 10 — Sentry's Express error handler catches uncaught
|
||||
// errors from every route mounted above. Must come AFTER routes but
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GET /api/hero-prop (Truth-Everywhere Part 2, item 5) — the daily hero prop:
|
||||
* the live read where VYNDR disagrees most with the market (largest
|
||||
* |model - consensus| gap, A/B only). Public, cache-only (reads the pre-graded
|
||||
* snapshot) → never triggers grading, never spends API credits. Empty slate →
|
||||
* most recent real graded read; nothing cached → { available:false } (hides).
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const { createRateLimit } = require('../middleware/rateLimit');
|
||||
const heroPropService = require('../services/heroPropService');
|
||||
|
||||
const router = express.Router();
|
||||
router.use(createRateLimit({ windowMs: 60_000, max: 60 }));
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const hero = await heroPropService.pickHeroProp({});
|
||||
res.set('Cache-Control', 'public, max-age=300');
|
||||
return res.json(hero);
|
||||
} catch (err) {
|
||||
console.error('[hero-prop]', err.message);
|
||||
return res.status(200).json({ available: false });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -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 } };
|
||||
Reference in New Issue
Block a user