Grade-board sort: signed signal, takeable-gated p_win, missing sorts LAST

Display ORDERING only. No grade, ledger row, lock_line, scoring, or edge_pct
scale/display change. Push scoring untouched.

Two defects removed from selectTopGrades (wrong at ANY scale, independent of
edge_pct's separate retirement):
  1. edge: Math.abs(numOr(g.edge, -Infinity)) — abs() on an already-
     direction-signed value ranked the model's strongest DISAGREEMENTS level
     with its strongest agreements (177 public ledger rows carry a negative
     edge; positive = the model AGREES with the graded side).
  2. Math.abs(-Infinity) === Infinity, so a row with NO edge sorted FIRST —
     absent data presented as the top pick (the Number(null) class).

New key: grade -> confidence -> takeable-gated p_win (nulls LAST) -> SIGNED
edge (nulls LAST) -> input order. Scales are never mixed in one comparator.
Takeable band = web valueState.isTakeable, asserted byte-equal to the hero's
config/valueEngine.isTakeable (-160..+200) incl. strict-null.

Alt-line ladder (analyzeViaEngine1:506) no longer sorts by edge_pct: ordered
highest-p_win-first derived analytically at zero added compute — P(stat >= k)
is monotone non-increasing in k, so p_win-desc is line-ASC for an over and
line-DESC for an under. base stays marked; no consumer depends on
alt_lines[0]; deskShowcaseService.rungsOf already re-sorted by line.

THREE PREMISE BREAKS found report-first, before code:
  - /api/props/top-graded 404s in prod (absent from src/) so the dashboard
    board renders receipts/empty — the edge sort orders nothing there today.
    The prior order's "97.3% of rows tie" was a LEDGER measurement wrongly
    extrapolated to that board. Fix is correct-in-itself and lands when the
    feed is restored.
  - p_win cannot be a client-side key for all tiers: snapshotGating strips it
    for unentitled tiers ("shipping p_win is shipping the model price").
    Verified live: prod /api/snapshot carries p_win on 0/8 MLB, 0/25 WNBA.
  - Ladder rungs carry no per-rung price, so the takeable gate is inapplicable.

Verified on real data, both sports, both paths: unentitled — WNBA (n=25)
ordering CHANGED, MLB (n=8) unchanged, signed edge non-increasing in every
(grade,confidence) tie group (20 pairs, 0 violations); entitled — 40 real
ledger rows with p_win+locked_odds, p_win-descending, untakeable chalk NOT
promoted (Trea Turner .757 @-275 does not beat Rhyne Howard .745 @-120)
(36 pairs, 0 violations).

Hero consistency, stated honestly: same signal + same gate, different
precedence BY CONTRACT (board = grade-tier-first "top GRADES"; hero =
p_win-first "top read"). Identical within the leading tier (verified); across
tiers the board may lead with an A the hero doesn't pick. Not a contradiction.

Floor: 310 suites / 3864 tests green, web build exit 0. Dashboard + Desk
visuals are auth/feed-gated -> tagged for the Chrome audit, no visual faked.

Held: edge_pct rescale/display retirement (Order B); building the missing
/api/props/top-graded selector; exposing p_win to unentitled tiers.

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 21:13:51 -04:00
parent 9b5235cf99
commit b85b351993
9 changed files with 886 additions and 11 deletions
+162
View File
@@ -139,6 +139,11 @@ scorer, pipeline, or real feature touched. Updated HONEST cells:
## KNOWN HONESTY GAPS (not fixed this order — logged, not fabrication)
- **Hit rate 59% (n=763) shown without ROI/CLV** — thin, not false. ROI/CLV surfacing is a later build.
- **"0 pushes = mis-scoring" — RETIRED 2026-07-29 as a false alarm** (premise re-verified, report-only).
The displayed hit/miss denominators are NOT corrupted by a hidden push bug: the feed is still 100%
half-numbers (0 whole lines in 117,970 captured market lines / 6,050 snapshots / 1,141 ledger rows /
173 lock_lines), all 992 settled actuals are integers, and the smallest actual-vs-line gap in the
whole ledger is 0.5. Expected pushes = exactly 0. See the verdict block below.
- **CLV instrument REPAIRED 2026-07-28** (commit 6552281). Was: 59 usable closing_prob. Now: **406** (MLB 248, WNBA 158) — the collapse was `attachClosingProb`'s `.limit(50000)`/no-ORDER-BY read + write-once `market_unavailable`, NOT capture (95% per-prop coverage) or the join (0 key mismatches). **CLV finding, straight: MLB unders lag the close (mean 9.1 prob-pts, 74% lose); MLB overs +2.0; WNBA flat.** → the +4.57% MLB-C and over/under asymmetry are substantially stale-line artifacts. This UNBLOCKS the proof order (proj-v1.1), which gates promoting p_win/ev to served grades.
**Honest state after this order: "no KNOWN live fabrications" — not "provably none."** The audit was thorough (repo + prod), but absence of a claim of falsehood is not a proof of universal truth.
@@ -191,3 +196,160 @@ Gates the champion's over-CLV signal (partial r=0.375, p≈0.003, n=62 takeable
# HERO RANKING FIX — 2026-07-29 (commit 41b86e3, deployed)
The landing/hero (matrix row 1) selection was silently broken: it ranked on `ev_pct`, which is NULL on served grades, and **`Number(null) === 0`** made every prop tie at EV 0 → the "top read" was the FIRST takeable A/B prop in cache order — **arbitrary, dressed as ranked** (prod served Kelsey Mitchell, the #6 read by p_win). FIXED: rank by the **champion's p_win** (the only promising edge signal) among A/B **takeable-priced** reads (`isTakeable` 160..+200, same band as the proof/audit); strict-null guard; takeable filter excludes chalk; **no backfill** → honest empty state when nothing qualifies. p_win is ranking-only (never exposed; the route strips it). Display-only — reads caches, writes to nothing. No proven-edge/+EV/best-bet claim, no CLV/ROI/edge number. This makes the champion's p_win a real (display) consumer for the first time. Fingerprint VERIFIED: hero is the max-p_win read across sports (WNBA A), not old code's first-in-order MLB pick (Schanuel 135); untakeable chalk excluded. Visual auth-gated → data fingerprint.
---
# PUSH-SCORING PREMISE VERIFY — verdict 2026-07-29 (report-only, read-only)
Tested the standing ruling "push scoring is correct — do not touch." That ruling rested on
"100% half-number lines → pushes structurally impossible," which was true for the data it was
made on. If whole-number lines had entered the feed since, 0 pushes across settled rows would be
a real mis-scoring bug the ruling was shielding. **The premise HOLDS — the ruling stands.**
**Phase 1 — feed distribution, 4 independent populations, per sport AND per market (never blended):**
| population | what it covers | rows with a line | whole-number lines |
|---|---|---|---|
| `closing_captures` | raw captured market lines, 5 books, `book`+`sharp`, Jul 20-29 continuous | **117,970** | **0** |
| `model_snapshots` | every graded prop **incl. grader refusals** (not survivorship-filtered) | 6,050 | **0** |
| `ledger_entries` (public) | the settled public record, 11 markets | 1,141 | **0** |
| `lock_lines` | TODAY's lock-time per-book lines (freshest feed, migration 033) | 173 | **0** |
Per-market: MLB hits / doubles / rbi / total_bases / stolen_bases / runs / strikeouts / home_runs /
walks / earned_runs / outs / hits_allowed and WNBA points / rebounds / assists / threes — **every
market's min AND max line ends in `.5`** (e.g. MLB strikeouts 2.5-8.5, WNBA points 5.5-26.5, MLB
outs 3.5-19.5). No whole-number market is hiding inside a blended fraction.
**Phase 2 — the push branch would fire.** `outcomeService.js:151` `if (a === l) return 'push'`,
reached **after** `Number()` + `Number.isFinite` guards on both operands — a sound numeric compare,
not the `Number(null) === 0` string-vs-number class that hit the hero. It is the **single scoring
chokepoint** (`ledgerService.js:31` imports `settleResult`; no parallel hit/miss derivation exists
in `src/`), it is **unit-tested live** (`outcomeService.test.js:38`, `nbaSettlement.test.js:104`),
and both `ledger_entries.outcome` and `outcomes.result` CHECK constraints **include `'push'`** — a
real push would score, write, and persist end-to-end.
**Phase 2.6 — the decisive number.** Across 992 settled rows carrying an actual: **0 exact ties, 0
fractional actuals, and the smallest actual-vs-line gap is 0.5** — the arithmetic minimum between an
integer result and a half-number line.
**VERDICT: RULING HOLDS.** Expected push rate is **exactly 0 (P = 0), not "low"** — 0/992 is
*forced*, not chance. The "implausible" flag mistook an arithmetic impossibility for a suspicious
absence; the row closes honestly. Stale n corrected: the flag said 470 settled, it is now **1,097**
(593 hit / 399 miss / 105 void / 44 unsettled-today). Nothing modified — no scoring, settlement,
re-settle, or backfill.
**No latent bug either.** Because the branch is correct and covered, a whole-number market entering
later (NFL/NHL are code-wired but out of season; whole-number strikeout props exist at some books)
would be scored as a push automatically. The residual is a **monitoring** gap, not a scoring gap:
nothing alerts on the first whole-number line to enter the feed. Logged, not built.
---
# EDGE_PCT SCALE DIAGNOSIS — 2026-07-29 (report-only, read-only). Fork REPORTED, not chosen.
**What it is (0.1).** `analyzeViaEngine1.js:265-270``edge_pct = ((projection line) / line) × 100`,
signed by direction, where `projection = l5_avg ?? l20_avg ?? {stat}_per_90 ?? xg_per_90`.
**Independent of `p_win`** (so NOT tainted by the overconfidence that damns `ev_pct`) but it takes
**no price input at all**, so it cannot express a betting edge. **Arithmetically correct, MISLABELLED:**
honest as "% the projection differs from the line," **a lie at any scale as "EDGE."** Two independent
implementations — backend `edgePctFor` and `web/src/lib/gradeAdapter.js:25-31 computeEdge`; the grade
card renders the WEB one, so a backend-only fix would miss it.
**The cap (0.2).** `SANE_EDGE_MAX = 40` (`deskShowcaseService.js:31`: *"beyond this the (model-line)/line
value isn't a market edge"*), mirrored in `slateAdapter.js:613` and `MobileEdgeBoard.tsx:45`. A
self-declared plausibility bound from an earlier order, not a derived statistical one.
**Mechanism (0.3) = SMALL-DENOMINATOR EXPLOSION** — not units, not inversion, not a missing ×100.
`line` is the denominator and **86% of MLB rows (562/655) sit at line 0.5**. Max 620 = a ~3.6 projection
on a 0.5 line.
| population | n | >cap 40 | >100 | median | p95 | max | min |
|---|---|---|---|---|---|---|---|
| **MLB** | 655 | 65.8% (>50) | 13.6% | **60** | 180 | **620** | 86.7 |
| **WNBA** | 486 | 4.9% (>50) | **0%** | 12 | 49 | 77.8 | 51.7 |
| blended | 1,141 | **44.0% (502)** | 7.8% | — | — | 620 | — |
Per line (the proof): MLB 0.5 → 73.0% over cap, max 620 · MLB 1.5 → 43.8%, max 153 · WNBA 12.5 → 6.7%
· **WNBA 26.5 → max 1.9.** Matrix figures re-verified: **51.5% is stale → 44.0%; worst 620 is exact.**
**Shape: structurally broken for MLB, sane for WNBA** — and the scale is a *function of line size*, so
the metric is incomparable across markets **by construction**. No rescaling fixes that.
**Surfaces (Phase 2) — the "~13" count is NOT confirmed. Three surfaces RENDER it:**
| surface | live | access | role | user sees at 620 |
|---|---|---|---|---|
| `GradeResultCard.tsx:182,216,325` | YES (3 importers) | **auth-gated `/scan`****TAGGED FOR CHROME AUDIT** | display | **"+620% edge", raw + GREEN** |
| `DeskShowcase.tsx:40` | YES | **PUBLIC** `/pricing` | display | **"—"** (already honest) |
| `SoccerGradeResult.tsx:229` | YES | orphan `/soccer` (0 nav links, public by URL) | display | raw uncapped `X.X% edge` |
| `MobileEdgeBoard.tsx:47` | **DEAD** (0 importers) | — | sort+display | "—" (pulled in honesty pass) |
| `PropRow:45`, `GradeCard:32`, `ledger/page:40` | live components | — | **type-only, never rendered** | nothing |
| `contentTemplateService.js:164` | public `/api/content` | API | string | uncapped — **no page fetches it** |
**🔴 IT DRIVES TWO LIVE SORTS (the fork's load-bearing answer).**
1. `slateAdapter.selectTopGrades:469-471``grade → confidence → |edge| desc` → **dashboard TOP GRADES
top-10** (`dashboard/page.tsx:419`). **97.3% of rows (1110/1141) sit in a (date,sport,grade,confidence)
tie group of ≥2** (biggest 56), so |edge| is operative for essentially the whole slate — the **de facto
ordering** of that leaderboard.
2. `analyzeViaEngine1.js:506` — the Desk **alt-line ladder** is sorted by `edge_pct` desc.
**Two scale-INDEPENDENT defects inside that sort** (`Math.abs(numOr(g.edge, -Infinity))`): **(i) abs()**
on an already-direction-signed value ranks the model's strongest *disagreements* equal to its strongest
agreements (**177 negative-edge rows**: 58 B / 118 C / 1 F, worst 86.7); **(ii)** `Math.abs(-Infinity)
= Infinity` → a **missing edge sorts FIRST**. The `Number(null)` fabrication class again, new costume.
**Phase 4 correction — "nothing renders `ev_pct`" is WRONG.** `PriceTriplet.tsx:60,67,76` renders
`${pct(ev)} EV`, live and wired (scan → `gradeAdapter:143`). It shows nothing only because `ev_pct` is
NULL on served grades → `valueState.js:121` falls to **NO_MODEL honest-absent**. The right metric already
has a live honest render site, **starved of data, not unwired** — and the card's "EDGE" row sits exactly
where a price-aware number belongs.
**THE FORK (reported, not chosen).**
- **FIX** — dishonest: no rescaling turns a price-free projection gap into an edge (renaming, not fixing);
it silently re-ranks the dashboard top-10 (the hero-class bug just fixed); needs BOTH implementations.
- **HIDE** — cheap: 3 render sites, each already has a null branch (no layout breaks), and DeskShowcase
already proves the honest "—" pattern in-product. Not load-bearing for layout anywhere.
- **RECOMMENDED: HIDE the number and re-point the sort at `p_win`** — the hero order established p_win is
on 100% of recent ledger rows and is the only signal that survived an adversarial audit. Repairing a key
that is a 0.5-line artifact is not worth it. End state: **p_win ranks · ev_pct displays · edge_pct retires.**
The abs()/null-first sort defects deserve their own small order either way.
*Nothing changed: no edge_pct, scale, surface, sort, grade, ledger, or accruing edge touched.*
---
# GRADE-BOARD SORT FIX — 2026-07-29 (spec `specs/grade-board-sort.md`, shipped)
Display ORDERING only. Fixes two defects that were wrong at ANY scale, independent of edge_pct's
separate retirement (Order B, still held).
**Defects removed.** `selectTopGrades` ranked on `Math.abs(numOr(g.edge, -Infinity))`:
`abs()` on an already-direction-signed value ranked the model's strongest **disagreements** level with
its agreements (177 public ledger rows carry a negative edge); and `Math.abs(-Infinity) === Infinity`
made a **missing** signal sort **FIRST** — absent data as the top pick. Now: `grade → confidence →
takeable-gated p_win (nulls LAST) → SIGNED edge (nulls LAST) → input order`, scales never mixed.
The alt-line ladder (`analyzeViaEngine1:506`) no longer sorts by `edge_pct`; it is ordered
highest-p_win-first via the monotonic line rule (line-ASC for an over, line-DESC for an under) at zero
added compute.
**Three premise breaks found report-first.** (1) **`/api/props/top-graded` 404s in prod** — the
dashboard board's feed does not exist, so that board renders receipts/empty and the sort orders nothing
there today; the prior order's "97.3% of rows tie → the edge key decides the board" was a ledger
measurement wrongly extrapolated to it. (2) **p_win is stripped for unentitled tiers by design**
(`snapshotGating`, Session 67 — "shipping p_win is shipping the model price"); verified live, prod
`/api/snapshot` carries p_win on **0/8 MLB and 0/25 WNBA** grades, so the browser path uses the signed
edge and only entitled callers rank on p_win. (3) **Ladder rungs carry no per-rung price**, so the
hero's takeable gate is inapplicable there.
**Verified on real data, both sports, both paths.** Unentitled: WNBA (n=25) ordering CHANGED, MLB (n=8)
unchanged; signed edge non-increasing within every (grade,confidence) tie group — 20 pairs, 0
violations. Entitled: 40 real ledger rows with p_win+locked_odds — p_win-descending, untakeable chalk
not promoted, 36 pairs, 0 violations.
**Hero consistency, honestly:** same signal + same gate, different precedence by contract (board =
grade-tier-first "top GRADES"; hero = p_win-first "top read"). They agree exactly **within** the
leading tier (verified); across tiers the board may lead with an A the hero doesn't pick. Not a
contradiction — do not "fix" it by making the board ignore grade.
**Floor:** 310 suites / 3864 tests green, web build exit 0. Dashboard + Desk visuals are auth/feed-gated
→ 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.