# VYNDR — Build State ## Last Updated 2026-06-18 ## Current Phase SHIP BUILD v48.0 — Name normalization applied at EVERY layer (snapshot source, game-card strips, scan grid) + profile usage/rest wired. The dup-player arc closed. ## Session 48 (2026-06-19) — SHIPPED ✅ NORMALIZATION AT EVERY LAYER The normalizer functions were correct (S47) but raw names still flowed through paths that skipped them. Traced top-down, fixed each. Backend 2149 → **2156 tests** (+7), 181 suites. Web build clean (exit 0). ### The trace (3 leaking paths) 1. **Snapshot grades** stored RAW player names + no dedup → Explore/leaders/ profile/GameCard-overlay all saw variants. 2. **Game-card strips** merged the player but pushed EVERY variant prop → "Ks 5.5 AND Ks 3.5" duplicate rows. 3. **Scan player grid** (`tonightsPlayers`) grouped by raw `p.player` → variant tiles. ### Fixes - **2a (source chokepoint):** `snapshotService.runSnapshot` now normalizes each grade's player to the de-dotted display AND dedupes to ONE grade per normalized player+stat (highest confidence) BEFORE writing `grades:{sport}` + `snapshot:{sport}:latest`. Every consumer now gets clean, merged names for free. - **2b:** `buildPlayerStripsFromProps` dedupes a player's props by stat (prefers the graded over an awaiting row) → one row per stat. - **2c:** scan `tonightsPlayers` groups by `nameKey`, displays `normalizeName().display`. - **3 (profile usage/rest):** the "+0%"/"—" was the PLAYER PROFILE's `buildIntel` (separate from the grade card's `buildIntelFields`, which already works — the full feature merge carries ab_per_game/rest_days, NOT filtered by FEATURE_NAMES which is meta-only). `resolvePlayerStats` (MLB) now attaches real `usage` (AB/G) + `rest` (B2B/Xd) and `buildIntel` renders them; REST default is now "—" not "+0%". ## Session 47 (2026-06-19) — SHIPPED ✅ NAME NORM + INTEL + TICKER POLISH ## Session 47 (2026-06-19) — SHIPPED ✅ NAME NORM + INTEL + TICKER POLISH Backend 2122 → **2149 tests** (+27), 179 suites. Web build clean (exit 0). ### Phase 1 — complete name normalization `playerName.js` (both copies) gained: parenthetical-tag strip ("Jose Fermin (STL)" → "Jose Fermin"), a ~50-entry NICKNAMES table resolved in `nameKey` ("Matt"↔ "Matthew", "Mike"↔"Michael"), and the existing accent-fold now verified for Iván/Ivan, José/Jose. The slate strip now DISPLAYS the normalized de-dotted name (`buildPlayerStripsFromProps` uses `normalizeName().display`) — "A.J. Ewing" shows as "AJ Ewing", not the raw PropLine string. ### Phase 2 — complete VYNDR INTELLIGENCE for MLB The audit showed only Form. `mlbGameLogFeatures` now also derives `rest_days` (days off between the two most recent game-log dates; 0 = B2B, matching the NBA convention) and `ab_per_game` (the MLB "usage" equivalent). `buildIntelFields` renders `usage` as "X AB/G", `rest` as "B2B"/"Xd rest", and a matchup grade from `bvp_advantage` when no opp rank. So MLB grade cards now show Form + Usage + Rest (+ Matchup when available). ### Phase 3 — ticker SCAN dedup `pushTickerItems` keeps only the LATEST SCAN per sport (drops prior SCAN events for any sport with a fresh scan; parses the sport from a `sport` field or the text prefix for legacy items). MOVE/GRADE events preserved; cap stays 50. ### Phase 4 — archetype threshold BOMBER's HR thresholds prorated for mid-season (`hr>=15` strong / `hr>=10` moderate, was `>=20`/`>=15`), so June sluggers (Schwarber/Harper ~17-18 HR) classify as BOMBER instead of FLEX/DRIVER. `computeLineDeltas` re-verified structurally sound (deltas populate on the 2nd+ snapshot). ## Session 46 (2026-06-18) — SHIPPED ✅ P1 FIXES ## 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 `