Per-sport rank guard + edge diagnostic-only display + delta report

DELTA MEASURED on live prod grades (live ordering unchanged): MLB 7/8
props move (87.5%), mean 2.5 places, TOP READ CHANGES (corey seager hits
1.5 under -> jake burger hits 0.5 over). WNBA 25/25 move, mean 4.1, max 12.
This is a large re-ordering, not a tweak.

Caveat recorded rather than buried: MLB had only 8 graded props at
measurement time. The percentages are real; the sample is one small slate.
Re-run before the flip -- it is one call.

PER-SPORT DOCTRINE ENFORCED IN CODE. WNBA moves the most and must NOT
adopt this: its p_win is anti-predictive, so ranking that board by p_win
would sort it by a signal measured to point the WRONG WAY -- worse than
the incumbent, not better. A comment would not have stopped a future flip
from going global, so FORECAST_RANKED_SPORTS = Set(['mlb']) gates the
forecast_rank stamp, with tests asserting no sport inherits MLB's result.
A sport joins only by passing its own holdout.

EDGE IS NOW DIAGNOSTIC-ONLY IN DISPLAY. MobileEdgeBoard.EdgeCell rendered
green (--g-a) for positive edge and red (--miss) for negative. Two things
were wrong: green/red IS a quality claim on a quantity that does not
predict, and ROW-GRAMMAR reserves red for settled-negative ONLY -- a
negative diagnostic is not a settled loss. Now neutral mono with a
diagnostic tooltip; header reads "MKT GAP · DIAGNOSTIC". The number is
still shown -- no display went blank. DeskShowcase neutralised likewise.

PINNACLE LOGGED, NOT ENSHRINED. Per the order, "market-not-sharp" is
PENDING-RECOVERY rather than a confirmed permanent limitation. The single
question for PropLine is in BLOCKERS.md with its evidence, and MASTER-PLAN
now carries the pending status instead of the permanent claim.

Live sorts remain byte-identical: selectTopGrades, flattenToEdgeBoard and
topGradedService all still call the incumbent.

Gates: 4,041 tests / 323 suites green; next build exit 0.

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-08-01 01:29:20 -04:00
parent 86d123945c
commit ef4ac60b81
8 changed files with 242 additions and 10 deletions
+153
View File
@@ -0,0 +1,153 @@
# RANK ON p_win — CHALLENGER DELTA + EDGE RETIREMENT
**Date:** 2026-08-01 · **Live ordering byte-identical** · challenger measured on
live prod grades · edge retired from decisions and from display quality-signalling.
**Gates:** 4,039 tests / 323 suites green · `next build` exit 0 · delta recorded ·
live sorts untouched.
---
## WHY (the measurement that dictates this)
| instrument | corr with outcome, n=200 settled MLB |
|---|---:|
| **`p_win`** | **+0.26** |
| `p_win fair_prob` (v1 single-book ruler) | 0.010 |
| `p_win fair_prob` (v2 consensus ruler) | 0.022 |
**Subtracting the market destroys the signal, under both rulers.** A quantity
that does not predict must not rank, gate, or decide.
---
## THE CHALLENGER — `rankByForecast`
**Order:** takeable-gated `p_win` → grade tier → confidence → stable input order.
**No edge term anywhere** (a test flips edge from 99 to +99 and asserts the
order does not move).
**`p_win` leads and the letter follows — deliberately.** The grade letter measured
**r ≈ 0.005** against outcomes and is **inverted** (B 52.4% < C 56.9%), while
`p_win` measures **+0.26**. Leading with the letter would sort the board by the
weaker signal and use the stronger one only to break ties.
The takeable gate is unchanged and mandatory: raw `p_win` crowns 300 chalk.
### One thing worth stating precisely
**Isotonic calibration is a MONOTONE transform, so ranking on raw `p_win` and on
calibrated `p_win` produce the SAME ORDER.** Calibration matters when `p_win` is
*displayed* or *thresholded* — it cannot change a ranking. The order asked to
"rank on calibrated p_win"; for ranking specifically, that is a no-op relative to
raw. Recorded in the code so nobody re-derives it.
---
## THE DELTA (live prod grades, nothing flipped)
`GET /api/internal/ranking-delta` · `live_ordering_unchanged: true`
| | MLB | WNBA |
|---|---:|---:|
| graded props | 8 | 25 |
| `p_win` coverage | 100% | 100% |
| **props that move** | **7/8 (87.5%)** | **25/25 (100%)** |
| mean \|move\| | 2.5 places | 4.1 places |
| max move | 5 | 12 |
| top-10 overlap | 80% | 80% |
| **top read changes** | **YES** | **YES** |
MLB #1: `corey seager | hits 1.5 under``jake burger | hits 0.5 over`
(Seager falls 1 → 6).
**This is a large re-ordering, not a tweak.** Nearly every row moves and the
headline read changes.
**Caveat, stated because it matters:** MLB's slate had only **8 graded props** at
measurement time. The percentages are real but the sample is a single small
slate — re-run the endpoint on a full slate before the flip. It is one call.
---
## PER-SPORT DOCTRINE — ENFORCED IN CODE, NOT IN A COMMENT
**WNBA moves the most (100% of rows, mean 4.1 places) and must NOT adopt this.**
WNBA's `p_win` is **anti-predictive** on its own data — it abstains. Ranking that
board by `p_win` would sort it by a signal measured to point the *wrong way*:
worse than the incumbent, not better.
A comment would not have stopped a future flip from applying this globally, so:
```js
const FORECAST_RANKED_SPORTS = Object.freeze(new Set(['mlb']));
```
`ranksOnForecast(sport)` gates the `forecast_rank` stamp, and tests assert **no
sport inherits MLB's result** — a sport joins only by passing its **own** holdout
(honest calibration AND surviving resolution).
**The WNBA number above is informational only.** It is in the report to show what
the guard is preventing.
---
## WHAT CHANGED, WHAT DID NOT
### Changed now (not rankings, so not gated on the flip)
- **`altLineScanner.compareToBookImplied`** no longer returns
`value_detected: edge > 0`. **Edge is still computed and returned** — losing the
record would be worse than mis-using it — but the verdict is `null` with
`value_basis: 'retired:edge_does_not_predict'`.
- **`scanAltLines`** no longer filters to `edge > 0` nor calls the survivor
`optimal_line`. The whole ladder returns, ranked, labelled
`price_gap_diagnostic_unvalidated`. **The module has zero callers** (verified) —
unwired like `mlbGrader.js`; left in place and made honest rather than deleted.
- **`MobileEdgeBoard.EdgeCell`** no longer renders green for positive edge and red
for negative. Two things were wrong with that: green/red **is** a quality claim
on a quantity that does not predict, and **ROW-GRAMMAR reserves red for
settled-negative only** (miss/dead/stale/faded) — a negative diagnostic is not a
settled loss. Now neutral mono with a diagnostic tooltip; the column header
reads **`MKT GAP · DIAGNOSTIC`**. **The number is still shown** — no display went
blank.
- **`DeskShowcase`** edge colour neutralised for the same reason.
### An honest asymmetry I did not paper over
**Ranking props against each other must not use edge. Choosing between RUNGS of
the same prop is inherently price-relative** — ranking rungs by model probability
alone would always pick the lowest line, because P(over 0.5) > P(over 2.5) by
construction. So the gap stays the rung key in `scanAltLines`, **explicitly
labelled unvalidated**, rather than being replaced by something that would look
principled and be degenerate.
### NOT changed (challenger-first)
- **`rankGrades`** — the incumbent (grade-first, edge as 4th key) is untouched,
and tested as untouched.
- **Every live sort** still calls the incumbent. `selectTopGrades`,
`flattenToEdgeBoard`, `topGradedService` — all byte-identical.
- **`forecast_rank`** is stamped additively on MLB snapshot grades, **before**
`stripModelPrice`, so every tier would receive the correct order without the
paid values (the `topGradedService` precedent — an ordinal travels where the
magnitude cannot). **Nothing sorts by it yet.**
- **Edge stays stored** in the ledger, as required.
---
## THE FLIP, WHEN YOU WANT IT
One decision, three edits: `selectTopGrades` and `flattenToEdgeBoard` sort by
`forecast_rank` when present; `rankGrades` drops its edge key. **Re-run the delta
on a full MLB slate first** — 8 props is not a slate.
## PINNACLE — LOGGED, NOT ENSHRINED
Per the order: **"market-not-sharp" is PENDING-RECOVERY, not a confirmed permanent
limitation.** The question for PropLine is logged in `BLOCKERS.md`:
> *Why did Pinnacle MLB player-prop coverage stop on 2026-07-31?* Captures ran
> 103,940 over the prior 10 days, then 4,022 → 0 while every other book continued.
Until answered, the plan must not record "no sharp anchor exists" as permanent.