Session 50: Complete Parlay Lab (2215 tests)

Correlation-aware combined parlay grading — the Desk-tier differentiator.

- Correlation model (parlayService.js, added to S28 funcs): correlationScore
  (game-aware 0.7/0.4/0.2/0.0), combinedGrade (avg penalized by avgCorr*0.5),
  estimatedPayout (fair-odds product * (1-avgCorr) discount), correlationWarning,
  gradeParlay.
- POST /api/parlay/grade (public, 2-6 legs) -> {combined,correlation,payout,legs}.
  Fixed the Next proxy (was forwarding to /api/scan/parlay).
- ParlayContext: legs gained team/game/archetype; tier-aware maxLegs; auto-grades
  the slip (debounced) when legs>=2 -> live combined/correlation/payout; hasLeg/
  legKey/atCap.
- "+" button on every graded prop: StatStrip onAddLeg/isLegActive, wired by
  vyndr/GameCard via useParlay (builds leg w/ team + game). GradeResultCard feeds
  the same context from the scan page.
- ParlayPanel (replaces legacy ParlayTray): bottom slide-up w/ legs, combined
  grade, correlation warning, est payout, CLEAR ALL + floating leg-count badge.
  Tier-gated: free 2 legs (payout blurred -> Desk upsell), Analyst 4, Desk 6.

Backend 2185 -> 2215 tests (+30), 187 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-19 11:25:14 -04:00
parent 3b47b783dc
commit f1956dc953
14 changed files with 706 additions and 48 deletions
+23
View File
@@ -533,6 +533,29 @@ snapshot, locked to the line, and read from cache.
- **Name micro-fix:** `playerName.js` `collapseInitials` merges "J C" → "JC"
(display + key) so space-separated initials dedupe.
## Parlay Lab (Session 50 — non-obvious)
- **Correlation model** lives in `src/services/parlayService.js` (ADDED to the
S28 categorical matrix — both coexist). `correlationScore(l1,l2)` is numeric +
GAME-aware (0.7 same-player/game, 0.4 same-team/game, 0.2 same-game, 0.0 diff-
game). `combinedGrade` penalizes the leg-grade avg by `avgCorrelation*0.5`;
`estimatedPayout` = Πfair-odds × (1avgCorr) discount. `gradeParlay` is the
bundle the route returns. Leg shape: `{ player, team, game, stat, grade }`.
- **POST /api/parlay/grade** (`src/routes/parlay.js`) — public, 26 legs, returns
`{ combined, correlation, payout, legs }`. The Next proxy was pointed at the
WRONG upstream (`/api/scan/parlay`); it now forwards to `/api/parlay/grade`.
- **ParlayContext auto-grades** the slip (debounced 250ms) via that endpoint
whenever legs change (≥2). `combined`/`correlation`/`payout` are live on the
context — don't call the endpoint from components, read the context.
- **Leg cap is tier-aware via the context.** `maxLegs` defaults 6; `ParlayPanel`
sets it from `useAuth().tier` (free 2 / analyst 4 / desk 6). `addLeg` reads a
ref so it stays a stable callback. The "+" buttons no-op at the cap.
- **"+" wiring:** `StatStrip` takes `onAddLeg`/`isLegActive`; `vyndr/GameCard`
provides them via `useParlay` (it owns the game id + team). `legKey` =
`player|stat|line|direction` is the dedupe key (exported from the context).
- **ParlayPanel** (mounted in layout, REPLACED `ParlayTray` which called a
then-missing `/grade` endpoint). Floating badge bottom-right when closed; free
tier blurs the payout with a `window.__goPaywall` upsell.
## Active Skills
- vyndr-voice (all user-facing output)
- prop-analysis (grading methodology)