Files
vyndr/specs/top-graded-selector.md
builtbykev 72a14dc4cd 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
2026-07-29 22:05:30 -04:00

5.1 KiB
Raw Permalink Blame History

SPEC — /api/props/top-graded server selector (rank with p_win, serve without it)

Status: built 2026-07-29. New READ endpoint. No grade/ledger/lock_line/scoring write. Push scoring untouched. Spec companion: specs/grade-board-sort.md.

1. Review Zero findings

  • 0.1 CORRECTED — the handler NEVER EXISTED. Not "removed": searched every commit (git rev-list --all) for a /top-graded definition in src/zero hits. The three axios callers (content/cheatsheetGenerator:21, content/gradeOfTheDay:17, routes/widget:76) and the Next proxy were written against a phantom endpoint, so those three content generators have silently received [] for their entire life. The contract is therefore recoverable ONLY from consumers, which is what this spec builds to.
  • CONTRACT (VERIFIED, union of four consumers). Envelope { props: [...] } (all three callers do Array.isArray(res.data?.props); the Next proxy returns the same). Query: sport (UPPERCASE NBA|MLB|WNBA — the proxy validates that set; absent = all sports, which gradeOfTheDay relies on) and limit. Row fields REQUIRED by dashboard/page.tsx TopGrade: player, stat, line, direction ('over'|'under'), sport, grade, confidence?. Callers additionally read player_name || player, stat_type || stat, and game_id (cheatsheet's gameCount).
  • 0.5 POPULATED-PATH RISK — FLAGGED. The board's populated branch has effectively never run in prod. dashboard/page.tsx:463 calls g.stat.replace(/_/g,' ') UNGUARDED — a row without a string stat THROWS and takes out the board. g.player is used in the key, the /scan URL and the <h3>; sport must be UPPERCASE (type Sport = 'NBA'|'MLB'|'WNBA', fed to SportPill). confidence is the only null-guarded field. The selector therefore emits player/stat as non-empty strings and uppercases sport, and DROPS any row that cannot satisfy that (a crashed board is worse than a shorter board).
  • 0.2 VERIFIED — the strip can run server-side, after ranking. utils/requestTier.resolveTierFromRequest(req) is purpose-built for a PUBLIC endpoint (bearer token when present, else 'free') and FAILS CLOSED — any error returns the LEAST entitled tier, so a resolution failure can only withhold, never leak. utils/snapshotGating.stripModelPrice(rows, tier) deletes model_odds|p_win|ev_pct|value|takeable for unentitled tiers and stamps model_price_locked where a book+fair pair survives.
  • 0.3 VERIFIED LIVE — the server HAS p_win right now. GET /api/hero-prop returns available:true (Brionna Jones, B, wnba), and the hero rule REQUIRES a non-null p_win AND a takeable price, so the snapshot cache carries both server-side. The public /api/snapshot shows p_win on 0/8 MLB + 0/25 WNBA rows only because it is stripped on the way out.
  • 0.4 ONE SHARED DEFINITION. Extracted to src/utils/gradeRanking.js (takeablePWin, descNullsLast, rankGrades). heroPropService now imports takeablePWin instead of its inline copy, and the new selector imports rankGrades. The browser cannot import src/ (S25 rule), so web/src/lib/slateAdapter keeps its mirror — a test cross-checks the two on identical fixtures, the playerName.js precedent. Takeable band = config/valueEngine.isTakeable (160..+200), strict-null.

2. The order of operations (the leak surface)

read snapshot cache  →  RANK with p_win (all tiers, server-side)
  →  map to contract rows INCLUDING model fields
  →  stripModelPrice(rows, tier)        ← the boundary
  →  serialize

Ranking happens BEFORE the strip, so a free caller gets the SAME order an entitled caller gets, without the paid values. Cache-Control follows the /api/snapshot precedent: private when a bearer token is present, public otherwise — a CDN must never hand a paid payload to an anonymous viewer.

3. Rank order

grade → confidence → takeable-gated p_win (nulls LAST) → SIGNED edge (nulls LAST) → stable input order. Grade-first because this is "top GRADES", not "top read" — it is INTENDED that this board and the hero can lead with different picks (the hero is p_win-first). They agree within the leading grade tier.

4. Acceptance criteria

  1. Entitled request: ranked by takeable-gated p_win; p_win PRESENT in the payload.
  2. Unentitled request: byte-identical ORDER; p_win/ev_pct/model_odds/value/ takeable ABSENT. Proven with POPULATED p_win, not today's nulls.
  3. Nulls sort LAST server-side; untakeable chalk never tops the board.
  4. Grade tier dominates every signal.
  5. Thin/empty slate → 200 { props: [] }, never 404, never filler.
  6. Every emitted row satisfies the populated-render contract (string player+stat, uppercase sport).
  7. Hero / client / server use ONE definition — cross-checked by test.
  8. No grade/ledger/lock_line/scoring write. Suite green, web build exit 0.

5. Held

edge_pct rescale or display retirement (Order B) · changing the board's columns or contract · making the board and hero lead with the same pick.