Build /api/props/top-graded server selector: rank with p_win, serve without it

New READ endpoint. No grade, ledger row, lock_line, or scoring write. Push
scoring untouched.

REVIEW ZERO CORRECTED THE PREMISE: the handler NEVER EXISTED in any commit
(searched git rev-list --all for a /top-graded definition in src/ — zero hits).
Not "removed" — the three axios callers (cheatsheetGenerator, gradeOfTheDay,
widget) and the Next proxy were written against a phantom endpoint, so those
three content generators have silently received [] for their entire life.
Contract recovered from the four consumers, not guessed: {props:[...]},
?sport=UPPERCASE (absent = all sports, which gradeOfTheDay relies on) + ?limit,
rows carrying player/stat/line/direction/sport/grade/confidence? plus the
player_name/stat_type aliases and game_id.

POPULATED-PATH RISK FOUND: the board's populated branch had never run in prod,
and dashboard/page.tsx:463 calls g.stat.replace(/_/g,' ') UNGUARDED (g.player
also feeds the row key, /scan URL and heading; sport must be UPPERCASE for
SportPill). toRow requires non-empty string player+stat and a finite line,
uppercases sport, and DROPS unrenderable rows — a shorter board beats a broken
one.

THE LEAK BOUNDARY (why this is server-side): the browser cannot rank on p_win
for all tiers because stripModelPrice deliberately withholds it from unentitled
tiers. Order of operations is
  read cache -> RANK with p_win (every tier) -> map rows incl. model fields
    -> stripModelPrice(rows, tier) -> serialize
so a free caller receives the paid RANKING without the paid VALUES. Tier comes
from resolveTierFromRequest, which FAILS CLOSED to 'free'. Cache-Control is
private under a bearer token, public otherwise (the /api/snapshot precedent).

ONE SHARED DEFINITION, no drift: new src/utils/gradeRanking.js
(takeablePWin/descNullsLast/rankGrades). heroPropService now imports
takeablePWin instead of its inline copy (behaviour unchanged — it was that
logic verbatim); the selector imports rankGrades; web/src/lib/slateAdapter
keeps its mirror (the browser cannot import src/, S25) and a test cross-checks
the two on identical fixtures (playerName.js precedent). Board is grade-first
("top GRADES"), hero is p_win-first ("top read") — they differ BY DESIGN and
agree within the leading tier.

HONEST LIMIT: the Next proxy (cachedBackendJson) sends no Authorization header
and caches under a shared key, so via the dashboard every viewer gets the
free-tier payload — correct order, no paid values. That is the SAFE behaviour;
forwarding auth into a shared cache is exactly how a paid payload leaks to
anonymous viewers. Per-tier delivery through the proxy needs a tier-keyed cache
and is not done here.

Verified on real prod snapshot data (anonymous path): MLB 8 props, WNBA 10,
0 paid-field leaks, render-contract safe on every row, sport uppercase.

Floor: 311 suites / 3882 tests green (18 new — leak test uses POPULATED p_win,
not today's nulls: entitled gets p_win and it drove the order, unentitled gets
a byte-identical order with all five MODEL_FIELDS absent and no trace in
JSON.stringify, while book/fair market facts survive). Web build exit 0.
Dashboard visual is auth-gated -> tagged for the Chrome audit, not faked.

Held: edge_pct rescale/retirement (Order B); board columns/contract unchanged;
tier-keyed proxy caching.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
Kev
2026-07-29 22:05:30 -04:00
parent 69feab4d25
commit 72a14dc4cd
10 changed files with 672 additions and 8 deletions
+41
View File
@@ -2,9 +2,50 @@ const express = require('express');
const { requireAuth } = require('../middleware/auth');
const { checkGracePeriod } = require('../middleware/gracePeriod');
const { getSupabaseServiceClient } = require('../utils/supabase');
const { createRateLimit } = require('../middleware/rateLimit');
const { resolveTierFromRequest } = require('../utils/requestTier');
const { getTopGraded } = require('../services/topGradedService');
const router = express.Router();
/**
* GET /top-graded — the dashboard TOP GRADES board (specs/top-graded-selector.md).
*
* PUBLIC + tier-aware, NOT `requireAuth`: this board must keep serving anonymous
* callers (it is the free funnel), so entitlement is resolved best-effort and
* FAILS CLOSED to 'free'. Ranking happens server-side WITH `p_win` for every
* tier; `stripModelPrice` then removes the paid signal for unentitled callers —
* so a free caller gets the CORRECT ORDER without the paid VALUES.
*
* A thin slate is `200 { props: [] }`, never a 404 and never filler: the board
* renders its own honest empty state, and a 404 is what left it blank for months.
*
* Cache-Control follows the /api/snapshot precedent — `private` once a bearer
* token is present, so a CDN can never hand a paid payload to an anonymous viewer.
*/
router.get('/top-graded', createRateLimit({ windowMs: 60_000, max: 60 }), async (req, res) => {
const deps = router.__deps || {};
const sportParam = req.query.sport ? String(req.query.sport) : null;
try {
const tier = await (deps.resolveTier || resolveTierFromRequest)(req);
const out = await (deps.getTopGraded || getTopGraded)({
sport: sportParam,
limit: req.query.limit,
tier,
...(deps.cacheGet ? { cacheGet: deps.cacheGet } : {}),
});
res.set('Cache-Control', req.headers.authorization ? 'private, max-age=60' : 'public, max-age=60');
return res.json(out);
} catch (err) {
console.error('[props/top-graded]', err.message);
// Honest empty — the board's empty state, never a 404.
return res.status(200).json({ props: [], sport: sportParam ? sportParam.toUpperCase() : null, updated_at: null });
}
});
// Test seam — lets route tests inject without Redis/Supabase (slips.js precedent).
router.__internals = { setDeps: (d) => { router.__deps = d; } };
// GET /joint-history — joint outcome history and phi coefficient
router.get('/joint-history', requireAuth, checkGracePeriod, async (req, res) => {
const { player_a, stat_a, player_b, stat_b } = req.query;
+9 -7
View File
@@ -88,16 +88,18 @@ async function pickHeroProp(deps = {}) {
// product. Floor = the project's `isTakeable` band (-160..+200), same definition
// the takeable-edge proof/audit used.
// - NO BACKFILL: nothing qualifies → honest empty state, never a weak recent read.
const { isTakeable } = require('../config/valueEngine');
// Strict null: `Number(null) === 0` is the exact fabrication this hero fell to.
const num = (v) => (v == null || v === '' ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
// ONE SHARED DEFINITION (2026-07-29, specs/top-graded-selector.md): the
// takeable-gated p_win primitive now lives in `utils/gradeRanking` so the hero
// (p_win-FIRST, "top read"), the server board (`topGradedService`, grade-FIRST,
// "top GRADES") and the client board cannot drift apart. Behaviour here is
// UNCHANGED — `takeablePWin` is this hero's former inline logic verbatim:
// strict-null p_win, price = book_odds ?? gradedAt.odds, the `isTakeable` band.
const { takeablePWin } = require('../utils/gradeRanking');
let hero = null; let heroP = -Infinity;
for (const { g, sport } of all) {
if (!isAB(g.grade) || !candidate(g)) continue;
const p = num(g.p_win);
if (p == null) continue; // no champion p_win → cannot rank
const price = num(g.book_odds) ?? num(g.gradedAt && g.gradedAt.odds);
if (price == null || !isTakeable(price)) continue; // takeable price only — excludes chalk
const p = takeablePWin(g); // null → no champion p_win, or an untakeable (chalk) price
if (p == null) continue;
if (p > heroP) { heroP = p; hero = { g, sport }; }
}
if (hero) {
+135
View File
@@ -0,0 +1,135 @@
'use strict';
/**
* topGradedService (2026-07-29, specs/top-graded-selector.md) — the SERVER
* selector behind `GET /api/props/top-graded`.
*
* WHY THIS EXISTS: the endpoint the dashboard TOP GRADES board fetches was
* NEVER IMPLEMENTED in any commit (searched all of `git rev-list --all`) — the
* three axios callers (cheatsheetGenerator, gradeOfTheDay, widget) and the Next
* proxy have always received `[]`, so the board rendered its empty fallback.
*
* THE POINT OF DOING IT SERVER-SIDE: the browser CANNOT rank on `p_win` for
* every tier, because `utils/snapshotGating.stripModelPrice` deliberately
* withholds it from unentitled tiers ("shipping p_win is shipping the price in a
* different base", Session 67). Here the server ranks WITH p_win and then strips
* it, so a free caller receives the CORRECT ORDER without the paid SIGNAL.
*
* read cache → RANK with p_win → map rows (model fields included)
* → stripModelPrice(rows, tier) ← the boundary
* → serialize
*
* Everything is injectable so route tests never touch Redis.
*/
const { rankGrades } = require('../utils/gradeRanking');
const { stripModelPrice } = require('../utils/snapshotGating');
const DEFAULT_SPORTS = ['nba', 'mlb', 'wnba'];
const DEFAULT_LIMIT = 10;
const MAX_LIMIT = 50;
/**
* The populated-render contract, recovered from the four consumers (0.1/0.5).
*
* `dashboard/page.tsx:463` calls `g.stat.replace(/_/g,' ')` UNGUARDED and uses
* `g.player` in the row key, the `/scan` link and the heading — so a row missing
* either would CRASH the board. We therefore require both as non-empty strings
* and DROP any row that cannot satisfy it: a shorter board beats a broken one.
* `sport` is UPPERCASED because the dashboard types it `'NBA'|'MLB'|'WNBA'` and
* feeds it to `SportPill`.
*/
function toRow(g, sport) {
const player = g.player_name || g.player;
const stat = g.stat_type || g.stat;
if (typeof player !== 'string' || !player.trim()) return null;
if (typeof stat !== 'string' || !stat.trim()) return null;
const line = Number(g.line != null ? g.line : (g.gradedAt && g.gradedAt.line));
if (!Number.isFinite(line)) return null;
const direction = String(g.direction || 'over').toLowerCase() === 'under' ? 'under' : 'over';
const row = {
// ── the dashboard TopGrade contract ──
player,
stat,
line,
direction,
sport: String(sport || g.sport || '').toUpperCase(),
grade: g.grade,
confidence: g.confidence != null ? g.confidence : null,
// ── what the other three callers read ──
player_name: player,
stat_type: stat,
game_id: g.game_id != null ? g.game_id : null,
// ── market facts (never gated — the fair leg is never the paywall) ──
book: g.book != null ? g.book : null,
book_odds: g.book_odds != null ? g.book_odds : null,
fair_odds: g.fair_odds != null ? g.fair_odds : null,
projection: g.projection != null ? g.projection : null,
edge: (g.edge != null ? g.edge : (g.edge_pct != null ? g.edge_pct : null)),
graded_at: (g.gradedAt && g.gradedAt.timestamp) || null,
};
// ── MODEL fields: included here ON PURPOSE so stripModelPrice can remove
// them per entitlement AFTER ranking. Never serialize this row un-stripped.
if (g.p_win != null) row.p_win = g.p_win;
if (g.ev_pct != null) row.ev_pct = g.ev_pct;
if (g.model_odds != null) row.model_odds = g.model_odds;
if (g.value != null) row.value = g.value;
if (g.takeable != null) row.takeable = g.takeable;
return row;
}
/** Load one sport's graded rows: the locked snapshot first, else the envelope. */
async function loadGrades(sport, cacheGet) {
const snap = await cacheGet(`snapshot:${sport}:latest`);
if (snap && Array.isArray(snap.grades)) return snap.grades;
const env = await cacheGet(`grades:${sport}`);
if (env && Array.isArray(env.grades)) return env.grades;
return [];
}
/**
* getTopGraded({ sport, limit, tier, cacheGet })
* → { props, sport, updated_at } — ALWAYS 200-shaped; a thin slate is
* `props: []`, never an error and never filler.
*
* `sport` omitted → every sport merged (gradeOfTheDay calls it that way).
*/
async function getTopGraded(opts = {}) {
const cacheGet = opts.cacheGet || require('../utils/redis').cacheGet;
const tier = opts.tier || 'free';
const requested = opts.sport ? String(opts.sport).toLowerCase() : null;
const sports = requested ? [requested] : DEFAULT_SPORTS;
const n = Number(opts.limit);
const limit = Number.isFinite(n) && n > 0 ? Math.min(Math.floor(n), MAX_LIMIT) : DEFAULT_LIMIT;
const collected = [];
let updatedAt = null;
for (const sp of sports) {
let grades = [];
try { grades = await loadGrades(sp, cacheGet); } catch { grades = []; }
for (const g of grades) {
// A refusal is not a read — `insufficient_data` rows never reach a board.
if (!g || !g.grade || g.insufficient_data) continue;
collected.push({ g, sport: sp });
}
}
// RANK FIRST, with p_win, for EVERY tier — the whole reason this is server-side.
const ranked = rankGrades(collected.map((c) => c.g), null);
const sportOf = new Map(collected.map((c) => [c.g, c.sport]));
const rows = [];
for (const g of ranked) {
if (rows.length >= limit) break;
const row = toRow(g, requested ? requested : sportOf.get(g));
if (row) rows.push(row); // unrenderable rows are dropped, not faked
if (!updatedAt && row && row.graded_at) updatedAt = row.graded_at;
}
// THE BOUNDARY — strip AFTER ranking, BEFORE serialization.
const props = stripModelPrice(rows, tier);
return { props, sport: requested ? requested.toUpperCase() : null, updated_at: updatedAt };
}
module.exports = { getTopGraded, __internals: { toRow, loadGrades, DEFAULT_SPORTS, DEFAULT_LIMIT, MAX_LIMIT } };
+99
View File
@@ -0,0 +1,99 @@
'use strict';
/**
* gradeRanking (2026-07-29, specs/top-graded-selector.md) — THE ONE definition of
* how a board of graded props is ordered. Extracted so the hero, the server
* selector, and the client board cannot drift apart.
*
* WHO USES THIS:
* - `services/heroPropService` — imports `takeablePWin` (its "top READ" rule
* is p_win-FIRST, so it uses the primitive, not `rankGrades`).
* - `services/topGradedService` — imports `rankGrades` (its "top GRADES" board
* is grade-FIRST). Those two leading picks may legitimately differ; they
* agree WITHIN the leading grade tier.
* - `web/src/lib/slateAdapter.selectTopGrades` — a MIRROR, because the browser
* cannot import `src/` (the Session-25 rule). `tests/unit/gradeBoardSort`
* cross-checks the two on identical fixtures — the `playerName.js` precedent.
* If you change the order here, change it there IN THE SAME COMMIT.
*
* WHY p_win AND NOT ev_pct/edge_pct: ev_pct is NULL on served grades and
* `Number(null) === 0` made every prop tie at 0 (the hero bug); edge_pct is a
* price-free (projline)/line artifact whose scale is a function of line size.
* p_win is the only signal whose takeable-MLB-over CLV survived the skew audit.
*/
const { isTakeable } = require('../config/valueEngine');
const GRADE_RANK = Object.freeze({
'A+': 0, A: 1, 'A-': 2, 'B+': 3, B: 4, 'B-': 5, 'C+': 6, C: 7, 'C-': 8, D: 9, F: 10,
});
/** Grade letter → sortable tier rank (lower = better). Unknown → 99. */
function gradeRankOf(g) {
const k = String(g == null ? '' : g).trim().toUpperCase();
return GRADE_RANK[k] !== undefined ? GRADE_RANK[k] : 99;
}
/** Strict numeric read — `Number(null) === 0` is the recurring fabrication bug. */
function strictNum(v) {
if (v == null || v === '') return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
/**
* The takeable-gated champion probability for one grade row, or null.
*
* The takeable filter is MANDATORY: raw p_win crowns 300 chalk, which is not
* the product. Band = `config/valueEngine.isTakeable` (160..+200), the same
* definition the takeable-edge proof and the over-side skew audit used.
* Price falls back to the LOCKED odds (`gradedAt.odds`) when `book_odds` is absent.
*/
function takeablePWin(g) {
const p = strictNum(g && g.p_win);
if (p == null) return null;
const price = strictNum(g && g.book_odds) ?? strictNum(g && g.gradedAt && g.gradedAt.odds);
if (price == null || !isTakeable(price)) return null;
return p;
}
/** Descending comparator that always sorts a null signal LAST (never first). */
function descNullsLast(a, b) {
if (a == null && b == null) return 0;
if (a == null) return 1;
if (b == null) return -1;
return b - a;
}
/**
* rankGrades — "top GRADES" order: grade tier → confidence → takeable-gated
* p_win → SIGNED edge → stable input order. Nulls sort LAST on both signals.
*
* SCALES ARE NEVER MIXED: p_win (0..1) is only ever compared against p_win and
* edge (%) only against edge. Comparing 0.62 against 62 is not a comparison.
*
* Rows without a grade are dropped (a board of ungraded rows is not a board).
* `limit` omitted → the whole ranked list (callers slice).
*/
function rankGrades(grades, limit) {
const arr = (Array.isArray(grades) ? grades : []).filter((g) => g && g.grade);
const scored = arr.map((g, idx) => ({
g,
idx,
rank: gradeRankOf(g.grade),
conf: strictNum(g.confidence) == null ? -1 : strictNum(g.confidence),
pWin: takeablePWin(g),
edge: strictNum(g.edge != null ? g.edge : g.edge_pct),
}));
scored.sort((a, b) => a.rank - b.rank
|| b.conf - a.conf
|| descNullsLast(a.pWin, b.pWin)
|| descNullsLast(a.edge, b.edge)
|| a.idx - b.idx);
const out = scored.map((s) => s.g);
return limit == null ? out : out.slice(0, Math.max(0, limit));
}
module.exports = {
GRADE_RANK, gradeRankOf, strictNum, takeablePWin, descNullsLast, rankGrades,
};