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

4.2 KiB
Raw Blame History

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.