Files
vyndr/specs/grade-board-sort.md
T
builtbykev b85b351993 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
2026-07-29 21:13:51 -04:00

74 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC — Fix the grade-board sort (display ordering only)
**Status:** built 2026-07-29. Report-first found three premise breaks — see §2.
**Scope:** display ORDERING only. No grade, ledger row, lock_line, scoring, model,
or edge_pct scale/display change. Push scoring untouched.
## 1. The two defects (wrong at ANY scale, independent of edge_pct's retirement)
`web/src/lib/slateAdapter.js selectTopGrades` ranked on
`edge: Math.abs(numOr(g.edge, -Infinity))`:
1. **abs() on an already-signed value.** `edge` is signed BY DIRECTION upstream
(`edgePctFor`: `over ? projline : lineproj`), so **positive = the model AGREES
with the graded side**. Taking `|edge|` ranked the model's strongest
DISAGREEMENTS equal to its strongest agreements. 177 public ledger rows carry a
negative edge (58 B / 118 C / 1 F, worst 86.7).
2. **`Math.abs(-Infinity) === Infinity`** → a row with NO edge sorted **FIRST**.
Absent data presented as the top pick — the `Number(null)` fabrication class.
`src/services/intelligence/analyzeViaEngine1.js:506` sorted the Desk alt-line
ladder by `edge_pct` desc — the same price-free 0.5-line artifact.
## 2. REPORT-FIRST — three premise breaks found before building
- **B1. Site 1's board is fed by a 404.** `/api/props/top-graded` **does not exist**
in `src/` (only three axios *callers* reference it) and returns **404 in prod**.
The Next proxy catches → `{props: []}``topGrades = []``selectTopGrades([])`
→ the dashboard renders `proofMode` (yesterday's receipts) or honest empty copy.
**The edge sort orders nothing on that surface today.** CORRECTION to the prior
order: its "97.3% of rows tie → the edge key decides the board" was a LEDGER
population measurement extrapolated to this board; the board has no rows to order.
The fix is still correct-in-itself and lands the moment the feed is restored.
- **B2. p_win CANNOT be the client-side sort key.** `src/utils/snapshotGating.js`
(Session 67) strips `p_win`/`ev_pct`/`model_odds`/`value`/`takeable` for
unentitled tiers, with the explicit rationale *"shipping p_win is shipping the
price in a different base."* `selectTopGrades` runs in the BROWSER. Ranking there
by p_win for all tiers would REVERSE that gate. Implemented instead:
**p_win is used WHEN THE ROW CARRIES IT** (entitled tiers / server callers),
takeable-gated identically to the hero; unentitled tiers fall through to the
signed edge uniformly. Scales are NEVER mixed in one comparator.
- **B3. The ladder cannot take the takeable gate, and p_win order there is
ANALYTICALLY the line order.** Ladder rungs carry **no per-rung price** (books
price each line differently; we do not fetch them), and `isTakeable` is a
property of price alone → inapplicable per rung. And `P(stat ≥ k)` is monotone
non-increasing in `k`, so **p_win-desc ≡ line-asc for an over, line-desc for an
under**. Implemented as the direction-aware line order = exactly p_win-desc, at
zero added compute. (`deskShowcaseService.rungsOf:40` already re-sorted rungs by
line, discarding the edge order — so only `GradeResultCard` ever showed it.)
## 3. What ships
- `selectTopGrades`: `grade → confidence → takeable-gated p_win (nulls last) →
SIGNED edge (nulls last) → input order`. No `abs()`. Missing signal ranks LAST,
rows are never dropped.
- Alt-line ladder: ordered by highest p_win first, computed via the monotonic
line equivalence (direction-aware). No `edge_pct` in the sort.
- The takeable band is `web/src/lib/valueState.isTakeable` (160..+200), which
mirrors `src/config/valueEngine.isTakeable` — the hero's exact definition.
## 4. Acceptance criteria
1. A disagreement (negative-edge) prop does NOT outrank an agreement at equal
grade+confidence.
2. A missing-signal row sorts LAST and is still PRESENT.
3. A takeable p_win row outranks a higher-p_win UNTAKEABLE (chalk) row.
4. p_win and edge are never compared against each other.
5. Ladder rungs are ordered highest-p_win-first for both over and under.
6. No grade/ledger/lock_line/scoring write. Full suite green, web build exit 0.
## 5. Held (NOT this order)
edge_pct rescale or display retirement (Order B) · building the missing
`/api/props/top-graded` server selector · exposing p_win to unentitled tiers.