diff --git a/outputs/VYNDR-COMPLETION-MATRIX.md b/outputs/VYNDR-COMPLETION-MATRIX.md index 12e1d42..8a97bf5 100644 --- a/outputs/VYNDR-COMPLETION-MATRIX.md +++ b/outputs/VYNDR-COMPLETION-MATRIX.md @@ -359,3 +359,36 @@ OVER @0.5 → `0.5•(C) 1(C) 1.5(F)` (line-ASC PASS), UNDER @1.5 → `2.5(C) 2( (line-DESC PASS). Only the *rendered* Desk card remains unverified anonymously → tagged for the Chrome audit, no visual faked. **Held:** edge_pct rescale/display retirement, building the missing `/api/props/top-graded` selector, exposing p_win to unentitled tiers. + +--- + +# /api/props/top-graded SERVER SELECTOR — 2026-07-29 (spec `specs/top-graded-selector.md`, shipped) + +The dashboard TOP GRADES board had no feed. **The handler NEVER existed in any commit** (searched +`git rev-list --all`) — so the three axios callers (cheatsheetGenerator, gradeOfTheDay, widget) plus +the Next proxy had always received `[]`. Contract recovered from those consumers, not guessed. + +**The leak boundary — the whole point of doing it server-side.** The browser cannot rank on `p_win` +for all tiers because `stripModelPrice` deliberately withholds it from unentitled tiers ("shipping +p_win is shipping the price in a different base", S67). Order of operations: +`read cache → RANK with p_win (every tier) → map rows incl. model fields → stripModelPrice(rows, tier) +→ serialize`. A free caller gets the paid RANKING without the paid VALUES. Tier resolution FAILS +CLOSED to `free`; `Cache-Control` is `private` under a bearer token, `public` otherwise. + +**Populated-path risk found and handled:** the board's populated branch had never run in prod, and +`dashboard/page.tsx:463` calls `g.stat.replace(/_/g,' ')` **unguarded** — `toRow` requires string +`player`+`stat`, a finite `line`, uppercases `sport` for `SportPill`, and drops unrenderable rows. + +**One shared ranking definition:** new `src/utils/gradeRanking.js`; `heroPropService` now imports +`takeablePWin` (was an inline copy, behaviour unchanged), the selector imports `rankGrades`, and the +web mirror is cross-checked by test. 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 forwards no Authorization and caches under a shared key, so via the +dashboard every viewer receives the free-tier payload — correct order, no paid values. That is the safe +default (forwarding auth into a shared cache is how paid payloads leak); per-tier delivery through the +proxy needs a tier-keyed cache and is NOT done here. + +**Verified:** 311 suites / 3882 tests green (18 new, leak test on POPULATED p_win), web build exit 0, +and induced on real prod snapshot data — MLB 8 / WNBA 10 props, 0 paid-field leaks, render-contract +safe. Dashboard visual is auth-gated → tagged for the Chrome audit, not faked. diff --git a/specs/STATE.md b/specs/STATE.md index 92f126b..c8f9fb2 100644 --- a/specs/STATE.md +++ b/specs/STATE.md @@ -264,6 +264,56 @@ > exposing p_win to unentitled tiers. **Dashboard + Desk visuals are auth/feed-gated → TAGGED FOR > THE CHROME AUDIT, no visual faked.** +> ## 🟢 `/api/props/top-graded` SERVER SELECTOR BUILT 2026-07-29 (spec `specs/top-graded-selector.md`) +> The dashboard TOP GRADES board finally has a feed. New READ endpoint; no grade/ledger/ +> lock_line/scoring write. **0.1 CORRECTS the premise: the handler NEVER EXISTED** — searched +> every commit (`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** — a second, previously-unnoticed casualty now fixed. +> **CONTRACT recovered from consumers, not guessed:** envelope `{props:[...]}`; params `sport` +> (UPPERCASE NBA|MLB|WNBA, absent = all sports, which `gradeOfTheDay` relies on) + `limit`; rows carry +> `player/stat/line/direction/sport/grade/confidence?` plus the `player_name`/`stat_type` aliases and +> `game_id` the other callers read. +> **🔴 0.5 POPULATED-PATH RISK FOUND (the board's populated branch had never run in prod):** +> `dashboard/page.tsx:463` calls **`g.stat.replace(/_/g,' ')` UNGUARDED**, and `g.player` feeds the row +> key + `/scan` URL + heading, and `sport` must be UPPERCASE for `SportPill` (`type Sport = +> 'NBA'|'MLB'|'WNBA'`). A row missing any of those would have CRASHED the board on first populated +> render. `toRow` therefore requires non-empty string `player`+`stat` and a finite `line`, uppercases +> `sport`, and **DROPS** an unrenderable row — a shorter board beats a broken one. +> **THE ORDER OF OPERATIONS (the leak surface):** read cache → **RANK with `p_win` for EVERY tier +> server-side** → map rows *including* model fields → **`stripModelPrice(rows, tier)`** → serialize. So +> a free caller receives the paid RANKING without the paid VALUES. Tier via +> `resolveTierFromRequest` which **FAILS CLOSED to 'free'** (a resolution failure can only withhold, +> never leak). `Cache-Control` = `private` with a bearer token, `public` otherwise (the `/api/snapshot` +> precedent — a CDN must never hand a paid payload to an anonymous viewer). +> **ONE SHARED DEFINITION, no drift:** extracted `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 (browser can't import `src/`, S25) and a +> test **cross-checks the two on identical fixtures** — the `playerName.js` precedent. +> **0.3 VERIFIED LIVE that the server HAS p_win:** `/api/hero-prop` returns `available:true` (Brionna +> Jones, B, wnba) and the hero rule REQUIRES non-null p_win + a takeable price. Public +> `/api/snapshot` shows 0/8 MLB + 0/25 WNBA only because it is stripped on the way out. +> **⚠️ HONEST LIMIT — via the dashboard, EVERY viewer gets the free-tier payload.** The Next proxy +> (`cachedBackendJson`) sends only `{Accept}` — **no Authorization header** — and caches under a +> SHARED key (`todayKey(sport,'top_graded')`). That is the SAFE behaviour: forwarding auth into a shared +> cache is exactly how a paid payload leaks to anonymous viewers. So the board shows the correct ORDER +> with no paid values for everyone; entitled payloads are served on a direct authenticated API call +> (proven by route test). Wiring per-tier delivery through the proxy would need a tier-keyed cache — NOT +> this order. +> **VERIFIED ON REAL PROD SNAPSHOT DATA** (anonymous path, what the board will actually render): +> MLB 8 props (B c57 Chandler Simpson edge 140 → …), WNBA 10 (B c69 Rhyne Howard u17.5 points → …), +> **0 paid-field leaks, render-contract safe on every row, sport uppercase**. +> **FLOOR: 311 suites / 3882 tests green (18 new), web build exit 0.** The leak test uses POPULATED +> p_win fixtures (not today's nulls): entitled → p_win present and it drove the order; unentitled → +> **byte-identical order, all five MODEL_FIELDS absent, `JSON.stringify` carries no trace**, while +> market facts (book/fair) SURVIVE — the fair leg is never the paywall. Also locked: chalk (p_win .95 +> @−300) never tops the board, nulls last but PRESENT, refusals excluded, unrenderable rows dropped, +> thin slate → `200 {props:[]}` never a 404, and board-vs-hero differ by design yet agree within tier. +> **HELD:** edge_pct rescale/retirement (Order B) · board columns/contract unchanged · tier-keyed proxy +> caching. Dashboard visual is auth-gated → TAGGED FOR THE CHROME AUDIT, not faked. + - **Redirect EXISTS + WIRED:** `closingCapture.buildCaptureRows`→`closing_captures` (append-only, provenance: captured_at/book/line_type/both-prices/missed_reason) via `intradayRefreshService:221` + internal endpoint; `ledgerService.attachClosingProb`→`closing_prob` (de-vigs both raw sides, diff --git a/specs/top-graded-selector.md b/specs/top-graded-selector.md new file mode 100644 index 0000000..06c7555 --- /dev/null +++ b/specs/top-graded-selector.md @@ -0,0 +1,82 @@ +# 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 `