# VYNDR — Build State ## Last Updated 2026-06-18 ## Current Phase SHIP BUILD v46.0 — Grade-card intel (MLB game logs) + player-name normalization + MLB starting pitchers. Three focused P1 fixes on the Session-45 snapshot model. ## Session 46 (2026-06-18) — SHIPPED ✅ P1 FIXES Backend 2100 → **2122 tests** (+22), 176 suites. Web build clean (exit 0). ### Phase 1 — grade card intel (ROOT CAUSE) The STAT CONTEXT + VYNDR INTELLIGENCE sections were empty for MLB because `gameLogService.getGameLogs` is NBA/WNBA-only (offline Python service) — MLB props NEVER got `l5_avg`/`l20_avg`, so `buildIntelFields` always returned `{}`. FIX: `featureCache.gameLogFeatures` now has an MLB branch that derives l5/l10/l20 averages from `mlbStatsAdapter.getPlayerStats` (free statsapi.mlb.com) via the pure `mlbGameLogFeatures` + an MLB stat_type→game-log-field map. `buildIntelFields(features, opts)` also gained `playerStats`/`projection` fallbacks so partial intel renders (Sessions 43/44 had the wiring; the engine just never produced the values for MLB). ### Phase 2 — player name normalization `src/utils/playerName.js` + `web/src/lib/playerName.js` (identical; cross-checked by a test): `normalizeName(raw)` → `{ display, key }`. Strips periods, de-dots suffixes, accent-folds the key. Applied in `snapshotService` (grouping/deltas), `slateAdapter` (grade index + `buildPlayerStripsFromProps` merges variants, displays the longest), and `playerIntelService.sanitizePlayerName`/`normName`. "A.J. Ewing"/"AJ Ewing" and "Jazz Chisholm"/"Jazz Chisholm Jr." now merge. ### Phase 3 — MLB starting pitchers The ESPN schedule lacks probable pitchers. NEW `GET /api/schedule/:sport/pitchers` (MLB) → `probablePitchers` service wrapping `mlbStatsAdapter.getScheduleWithPitchers` + best-effort season ERA. The Slate fetches it, builds a team→pitcher map (`slateAdapter.buildPitcherMap`/`pitchersForGameTeams`, matched by full name + mascot), and attaches `pitchers` to MLB GameCardData. + Next proxy. ### Phase 4 — verify `computeLineDeltas` confirmed structurally sound (S45 tests); `deltas: 0` in the audit was just the first snapshot (no previous to diff). BACKEND_HANDOFF.md updated. ## Session 45 (2026-06-18) — SHIPPED ✅ SNAPSHOT PIPELINE ## Session 45 (2026-06-18) — SHIPPED ✅ SNAPSHOT PIPELINE The on-demand grade model is retired. Backend 2061 → **2100 tests** (+39), 173 suites. Web build clean (exit 0). ### Phase 1 — snapshotService (orchestration of existing services) `src/services/snapshotService.js` `runSnapshot(sport)`: getOdds (PropLine rotation) → gradeAndCacheSlate (captured via injected cacheSet) → classify each player's archetype (resolvePlayerStats + archetypeService, pure math) → attach `gradedAt {line, odds, timestamp}` (LOCKED) → compute line deltas vs the previous snapshot → write `snapshot:{sport}:latest|previous` + `grades:{sport}` → emit ticker events. `runAllSnapshots()` loops mlb/nba/wnba/soccer. Everything injectable → fully unit-tested with zero network. Deltas: `toward` = market confirming our side, `away` = opposing; noise filtered <0.5. ### Phase 2 — internal API + cron + ticker API - `POST /api/internal/snapshot/:sport` + `/snapshot/all` (existing `requireInternalAuth`; `/all` registered first so it isn't captured as a sport). - `GET /api/ticker` (public, cache-only, merges `TICKER_MANUAL` pins) + Next proxy. - In-process cron `src/snapshotScheduler.js` (gated `SNAPSHOT_CRON=1`, UTC hours 14,19,22,1,3 = 10AM/3PM/6PM/9PM/11PM ET), armed in server.js. No new dep — 1-min unref'd interval, fires once per slot. Prefer external n8n cron hitting the internal endpoint for multi-replica deploys. ### Phase 3 — GameCard swap (THE product shift) The live Slate now renders `vyndr/GameCard` (legacy kept for TYPES only). Reads `GET /api/snapshot/:sport` (cache-only, public; falls back to `grades:{sport}`), overlays the locked grades onto each game's odds-derived props via `slateAdapter.buildPlayerStripsFromProps` → player name ONCE + archetype badge + locked grade + "Graded Xh ago at -115 · Current 2.5 · ▲ TOWARD +1.0" line delta. Ungraded props → "Awaiting next scan" (NO Read button). The on-demand onGrade flow was deleted. ### Phase 4 — live ticker `vyndr/Ticker` now polls `/api/ticker` every 30s (graceful: keeps the passed hardcoded items as initial/fallback, never blanks). Tag colors: A+/A/SCAN green, MOVE/CASCADE amber, ALERT white. ### Phase 5 — NBA/WNBA ESPN fallback `espnStatsAdapter.getSeasonAverages(name, sport)` — free public ESPN stats, DEFENSIVE (`parseAthleteStats` returns null on any unrecognized shape → caller degrades to found:false; never a wrong-but-confident number). Wired as the NBA/WNBA fallback in `resolvePlayerStats` after the offline Python service. Note: the live ESPN athlete-stats shape may need tuning against production — the parser is tolerant and tested against a representative payload. ### Env vars (set in Coolify) - `PROPLINE_API_KEY_1/2/3` — PropLine props rotation (snapshot odds source). - `VYNDR_INTERNAL_KEY` — the internal-auth secret (header `x-internal-key`) for the snapshot trigger. (This is the existing key, NOT a new INTERNAL_AUTH_TOKEN.) - `SNAPSHOT_CRON=1` — arm the in-process scheduler (omit if using n8n). - `TICKER_MANUAL` — JSON array of editorial pins, e.g. `[{"tag":"ALERT","text":"VYNDR 2.0 is live."}]`. ## Session 44 (2026-06-18) — SHIPPED ✅ MAKE IT VISIBLE ## Session 44 (2026-06-18) — SHIPPED ✅ MAKE IT VISIBLE Wired existing backend work into the pages users see + renamed archetypes to VYNDR Originals. Backend 2045 → **2061 tests** (+16), 167 suites. Web build clean (exit 0). `BACKEND_HANDOFF.md` added as the canonical data contract. ### VYNDR Original archetype rename (proprietary names) All 41 archetypes renamed in `archetypeService.js` + `lib/archetypes.js` + `ArchetypeBadge`, each keeping `legacyName`/`legacy` (never displayed; resolves for stale data). NBA: TORCH, CONDUCTOR, FORTRESS, ARTILLERY, SURGE, DUAL THREAT, CONNECTOR, FASTBREAK, PAINT BOSS, LOCKDOWN, SWITCHBOARD, ARCHITECT, PISTON, SENTINEL, IGNITER. WNBA-unique: DISTRIBUTOR, SHIELD, RANGE, SPARK, ANCHOR. MLB: BOMBER, BRUSH, DRIVER, ALPHA, WHIFF, GHOST, HYBRID, FLEX, WORKHORSE, CATALYST, MIRROR, HAMMER, SINKER, BRIDGE, SWITCH. Soccer: FINISHER, MAESTRO, TOWER, MOTOR, BLADE, WALL. **MLB note:** the old taxonomy had two power hitters (POWER PULL/POWER SLUGGER) but the new set has one power name (BOMBER), so BOMBER now fires for any high-HR bat (Judge → BOMBER per the audit checklist) and the freed slot became a real WHIFF strikeout-artist pitcher (improves pitcher coverage). `getArchetype`/`archetypeInfo` resolve legacy names → VYNDR Originals. ### Phase 2 — grade-card intel now populates (the real bug) The chain engine→tierGating→/api/scan proxy already PRESERVED the intel fields (all spread `...result`/`...data`). The ONE broken link: `scan/page.tsx` called `mapScanToGradeResult` with a hardcoded field subset and DROPPED season_avg/ form/usage/matchup_grade/etc. Now forwards them (+ extended `ScanResponse`), so STAT CONTEXT + VYNDR INTELLIGENCE sections light up on a real MLB grade. ### Phase 3 — stale-game filtering `slateAdapter.isRelevantGame(game, now)`: upcoming/live always show; a COMPLETED game is dropped once >24h old (no more 5-day-old FINALs). Applied in `Slate`'s `filteredGames`. Schedule TTL was already 60s (≤30min) — no change. ### Phase 4 — landing copy `Features.tsx` rewritten: no more "Point-biserial", "Zone 14", "ABS intelligence", "Auto-calibrating", "Phi-coefficient". User-facing benefit copy (Player DNA archetypes / Self-improving model / Lineup intel before tip-off / Deep pitcher-batter matchups). ### Phase 5 — depth chart proxies (404 fix) Added the missing Next proxies `/api/stats/lineup/[team]`, `/depth/[team]`, `/cascade/[player]` → return JSON, not 404. ### Phase 1 — GameCard swap DEFERRED (decision: Kev) The live Slate keeps the legacy "Read"/on-demand grade card as a **temporary bridge**. The vyndr/GameCard swap lands WITH the snapshot pipeline (next session): the on-demand "Read" model is being retired for a pre-graded snapshot model (full slate graded at scheduled intervals, grades locked to the line), and the new card is designed for that. Swapping now would remove grading + show blank cards (grades cache unpopulated). BookChip brand colors already render in the legacy card (S43). ## Session 43 (2026-06-18) — SHIPPED ✅ DATA PIPELINE + AUDIT FIXES ## Session 43 (2026-06-18) — SHIPPED ✅ DATA PIPELINE + AUDIT FIXES Post-S42 Chrome audit: architecture renders but no real data flowed (every player `found:false`, leaders empty, grade intel hidden) + a P0 dropdown bug. Backend 2011 → **2045 tests** (+34), 163 suites. Web build clean (exit 0). ### Phase 1 — P0 bug fixes - **Avatar/More dropdown z-index** — the `