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
+29
View File
@@ -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;