d09a06c054
Product overhaul core — the two transformative, differentiated systems: Self-learning loop (Phase 2): outcomeService settles locked snapshot grades against real MLB Stats API results → hit/miss/push, rolling accuracy by grade tier (30d window). Idempotent, injectable, unit-tested. New GET /api/accuracy + /api/ledger/accuracy + internal settle triggers + cron hook. AccuracyBadge (dashboard/scan/landing) is honest — "LEARNING" below MIN_SAMPLE, never a fake number. Settled HIT/MISS chips overlay the live slate. Real-time layer (Phase 1): Slate silent 60s auto-refresh (no flash, no wipe on transient blips) + "SIGNAL LIVE · UPDATED Xs ago" freshness strip; Ticker LIVE badge that flashes on fresh events. Landing (Phase 3): TopSignals shows tonight's real top-3 A-rated grades + live accuracy — the product shown, not described. Founder pricing: FOUNDER_CODE_EXPIRY default 2026-06-30 → 2026-12-31 (had lapsed, disabling every founder code + the ClaimMeter pitch). That expiry — not a tier change — was the real cause of the 4 stripe test failures. Backend 2255 (4 failing) → 2274 (all green; +19 new, +4 fixed). Web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
209 lines
9.1 KiB
Markdown
209 lines
9.1 KiB
Markdown
# VYNDR — Backend / Frontend Data Contract (BACKEND_HANDOFF.md)
|
||
|
||
**Canonical frontend↔backend data contract for the Player Intelligence System.**
|
||
Every future session references this. Authored Session 44 from the shipped
|
||
implementation (Sessions 42–44). When an endpoint or component shape changes,
|
||
update this file in the same commit.
|
||
|
||
> Naming: archetypes are **VYNDR Originals** (TORCH, BOMBER, ALPHA, …). The old
|
||
> descriptive labels live on each archetype as `legacyName` and are NEVER shown.
|
||
|
||
---
|
||
|
||
## 1. Archetypes (`src/services/archetypeService.js`)
|
||
|
||
`classify(sport, stats) → { sport, primary, secondary|null, blend }`
|
||
|
||
```
|
||
primary | secondary : {
|
||
name, legacyName, tag, sport, color (#hex), glyph (key),
|
||
description, propDNA: { reliable: string[], volatile: string[] }, education
|
||
}
|
||
blend : [{ archetype: <NAME>, weight: 0..1 }] // normalized, top 4, sums ~1
|
||
```
|
||
|
||
**Roster (41):** see `ARCHETYPES` registry. Colors are unique *within* a sport,
|
||
reused *across* sports. Frontend visual map (color/glyph/desc) is mirrored in
|
||
`web/src/lib/archetypes.js` — keep colors identical (a test enforces it).
|
||
|
||
- **NBA (15):** TORCH, CONDUCTOR, FORTRESS, ARTILLERY, SURGE, DUAL THREAT,
|
||
CONNECTOR, FASTBREAK, PAINT BOSS, LOCKDOWN, SWITCHBOARD, ARCHITECT, PISTON,
|
||
SENTINEL, IGNITER
|
||
- **WNBA-unique (5):** DISTRIBUTOR, SHIELD, RANGE, SPARK, ANCHOR (plus reused NBA)
|
||
- **MLB (15):** BOMBER, BRUSH, DRIVER, ALPHA, WHIFF, GHOST, HYBRID, FLEX,
|
||
WORKHORSE, CATALYST, MIRROR, HAMMER, SINKER, BRIDGE, SWITCH
|
||
- **Soccer (6):** FINISHER, MAESTRO, TOWER, MOTOR, BLADE, WALL
|
||
|
||
Classifier input shapes:
|
||
- NBA/WNBA: `{ ppg, rpg, apg, bpg, spg, threes, usg, fg3a, pos, bench }`
|
||
- MLB hitter: `{ avg, hr, rbi, sb, ops, runs, k_rate, doubles }`
|
||
- MLB pitcher: `{ era, k9, whip, ip_per_start, saves, role: 'SP'|'RP'|'CL' }`
|
||
|
||
---
|
||
|
||
## 2. Stats API (`src/routes/stats.js`, mounted at `/api/stats`)
|
||
|
||
All player-intelligence endpoints are public, rate-limited 60/min, and ALWAYS
|
||
return 200 with a valid (possibly empty) shape — the UI must never hard-fail.
|
||
Browser must hit the **Next proxy** under `web/src/app/api/stats/...`, never
|
||
Express directly.
|
||
|
||
### `GET /api/stats/player/:name?sport=nba`
|
||
```
|
||
{
|
||
player, sport, team, found: boolean,
|
||
archetype: { primary, secondary, blend }, // §1
|
||
propDNA: { reliable, volatile },
|
||
education: string,
|
||
season: [{ k, v, lg? }], // display rows (mono)
|
||
last10: [{ d, opp, res?, stat }],
|
||
splits: [{ k, a, b }],
|
||
gradeHistory: [{ grade, prop, hit?, miss? }],
|
||
activeProps: [{ stat, line, side: 'O'|'U', grade, confidence }],
|
||
intel: [{ label, kind: 'form'|'grade'|'plain', value, score?, color }],
|
||
injury: { label, note, cascade } | null
|
||
}
|
||
```
|
||
`found` is true when real season stats OR tonight's graded props exist. MLB stats
|
||
come from `mlbStatsAdapter.getPlayerStats(name)`; NBA/WNBA from `nbaStatsClient`
|
||
(degrades to `found:false` when the Python service is offline).
|
||
|
||
### `GET /api/stats/leaders?sport=mlb&stat=hits&limit=10`
|
||
```
|
||
{ sport, stat|null, leaders: [{ player, team, stat, line, side, grade, confidence }] }
|
||
```
|
||
Source: the `grades:{sport}` cache (tonight's graded slate, by confidence desc).
|
||
|
||
### `GET /api/stats/game/:id?sport=nba`
|
||
ESPN game summary (injuries, leaders, ESPN Bet odds, box score) or `{error}`.
|
||
|
||
### `GET /api/stats/lineup/:team?sport=mlb` (Session 43)
|
||
`{ sport, team, lineup: [{ player, position, battingOrder, projectedMinutes }] }`
|
||
MLB returns the probable starting pitcher from the schedule.
|
||
|
||
### `GET /api/stats/depth/:team?sport=nba`
|
||
`{ sport, team, positions: [{ position, starter, backup, thirdString }] }`
|
||
|
||
### `GET /api/stats/cascade/:player?sport=nba&team=SA`
|
||
`{ sport, player, cascade: [{ player, stat, delta: '+x%', reason }] }`
|
||
Usage redistribution weighted by archetype (SURGE benefits most).
|
||
|
||
---
|
||
|
||
## 3. Grade Result Card (`web/src/components/vyndr/GradeResultCard.tsx`)
|
||
|
||
`GradeResultData` (built by `web/src/lib/gradeAdapter.js` `mapScanToGradeResult`):
|
||
core fields (player, team, sport, stat, line, side, grade, confidence, edge,
|
||
projection, signals[], killConditions[], books[], altLadder[]) **plus** optional
|
||
Player-Intelligence fields that self-hide when absent:
|
||
```
|
||
archetypeBlend?: [{ archetype, weight }]
|
||
propDNA?: { reliable: string[], volatile: string[] }
|
||
statContext?: { season?, last10?, vsOpp? }
|
||
vyndrIntel?: { form?, usage?, matchup?, rest? }
|
||
```
|
||
The engine (`analyzeViaEngine1`) attaches snake_case fields
|
||
(`season_avg`/`last10_avg`/`form`/`usage`/`matchup_grade`/`rest`, and when a
|
||
season line is available, `archetype`/`archetype_blend`/`prop_dna`); the adapter
|
||
maps them in. Archetype at grade time requires a multi-stat season line — until
|
||
the snapshot pipeline supplies it, the archetype strip stays hidden.
|
||
|
||
---
|
||
|
||
## 4. Game Card (`web/src/components/vyndr/GameCard.tsx`)
|
||
|
||
`GameCardData` core (id, sport, away/home, time, venue, lines[], …) **plus**:
|
||
```
|
||
playerStrips?: [{ player, team, archetype?: {primary, secondary?}, stats: [{label,value}], props: [{stat,line,side,grade}] }]
|
||
pitchers?: { away: {name, era, archetype?}, home: {name, era, archetype?} }
|
||
```
|
||
Built by `slateAdapter.mapScheduleToGameCards` (`groupPropsByPlayer` +
|
||
`mapPitchers`). The card prefers `playerStrips` (name once, horizontal) over
|
||
legacy per-prop rows. Book chips use `web/src/lib/books.js` brand colors.
|
||
|
||
---
|
||
|
||
## 5. Stat Strip (`web/src/components/vyndr/StatStrip.tsx`)
|
||
|
||
`compact` (game cards) | `expanded` (profile hero / grade result). HARD RULE: the
|
||
player name appears ONCE; stats flow horizontally in JetBrains Mono. Props render
|
||
inline with `GradeBadge`. `onPlayerClick` → `/player/:name?sport=` (`lib/playerHref.js`).
|
||
|
||
---
|
||
|
||
## 5a. Normalization is applied at the SNAPSHOT source (Session 48)
|
||
|
||
`snapshotService.runSnapshot` normalizes every grade's player name (de-dotted
|
||
display) and dedupes to ONE grade per `nameKey|stat_type` (highest confidence)
|
||
BEFORE writing `grades:{sport}` + `snapshot:{sport}:latest`. Every downstream
|
||
consumer (GameCard overlay, `/api/snapshot`, `/leaders`, profile `activeProps`)
|
||
therefore receives clean, merged names. New consumers don't need their own
|
||
normalization — but UI lists built from RAW odds (e.g. the scan player grid)
|
||
must group by `nameKey` + display `normalizeName().display`.
|
||
|
||
## 5b. Player name normalization (Session 46)
|
||
|
||
`src/utils/playerName.js` (+ identical `web/src/lib/playerName.js`) is the ONE
|
||
source of truth for comparing/deduping names. `normalizeName(raw)` →
|
||
`{ display, key }`: `display` strips periods + de-dots the suffix (keeps accents
|
||
+ casing); `key` is accent-folded, lowercased, suffix-stripped for comparison.
|
||
Used by snapshot grouping, `slateAdapter` (grade index + player strips), and
|
||
`playerIntelService` so "A.J. Ewing"/"AJ Ewing" and "Jazz Chisholm"/"Jazz
|
||
Chisholm Jr." collapse to one player.
|
||
|
||
## 5c. MLB intel features (Session 46)
|
||
|
||
`buildIntelFields` (grade-card STAT CONTEXT + VYNDR INTELLIGENCE) reads
|
||
`l5_avg`/`l10_avg`/`l20_avg`/`opp_rank_stat`/`rest_days` from the feature vector.
|
||
MLB game logs are now wired into `featureCache.gameLogFeatures` via
|
||
`mlbStatsAdapter.getPlayerStats` (the old Python game-log path was NBA/WNBA-only,
|
||
so MLB props had no intel). `buildIntelFields(features, { playerStats, projection })`
|
||
also accepts fallbacks so partial intel still renders.
|
||
|
||
## 5d. MLB probable pitchers (Session 46)
|
||
|
||
`GET /api/schedule/:sport/pitchers` (MLB only) → `{ sport, date, games:
|
||
[{ home:{team,pitcher,era}, away:{...} }] }` from `probablePitchers` /
|
||
`mlbStatsAdapter.getScheduleWithPitchers` (the ESPN schedule lacks them). The
|
||
Slate builds a team→pitcher map (`slateAdapter.buildPitcherMap` /
|
||
`pitchersForGameTeams`) and attaches `pitchers` to MLB GameCardData.
|
||
|
||
## 6. Freshness & caching
|
||
|
||
- Schedule cache TTL ≤ 30 min (`scheduleService`). Frontend filters completed
|
||
games older than 24h out of the slate (`slateAdapter.isRelevantGame`).
|
||
- Player props: PropLine primary (3-key rotation), The-Odds-API backup.
|
||
- `grades:{sport}` cache (TTL 2h) is written by `gradeSlateService` on a fresh
|
||
odds fetch; it feeds `/leaders` + the player card's `activeProps`.
|
||
|
||
---
|
||
|
||
## 7. Accuracy / self-learning loop (`outcomeService`, Session 55)
|
||
|
||
The system's track record — settled grades vs real results. Written by
|
||
`outcomeService.settleAllOutcomes()` (cron, before grading); read-only endpoints.
|
||
|
||
### `GET /api/accuracy` (public, cached 5m)
|
||
```
|
||
{
|
||
overall: { // aggregate across sports
|
||
sport: 'overall', updated_at, window_days: 30, sample,
|
||
overall: { hits, misses, pushes, total, pct|null }, // pct excludes pushes
|
||
byGrade: { 'A+':{…}, 'A':{…}, 'B':{…}, 'C':{…}, 'D':{…}, 'F':{…} }
|
||
} | null,
|
||
sports: { mlb?: <record>, nba?: <record>, … }, // same shape per sport
|
||
min_sample: 8, // below this, show "LEARNING" not a %
|
||
updated_at: string | null
|
||
}
|
||
```
|
||
|
||
### `GET /api/ledger/accuracy` (public) — the ledger `buckets` shape
|
||
```
|
||
{ buckets: [{ grade, hits, total, pct|null }], overall, updated_at }
|
||
```
|
||
|
||
### Settled outcome overlay
|
||
`GET /api/snapshot/:sport` grades may carry `outcome: { result:'hit'|'miss'|
|
||
'push', actual:number }` once the game is final. `AccuracyBadge` +
|
||
`StatStrip.OutcomeChip` render these. Frontend proxy: `web/src/app/api/accuracy`.
|