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:
@@ -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 (proj−line)/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,
|
||||
};
|
||||
Reference in New Issue
Block a user