Files
vyndr/specs/LIVE-TRACKING.md
builtbykev b5d3fd14bb S11 (a1): live tracking — the read locked, the game watched
MLB statsapi + WNBA ESPN live boxscores -> per-player current values
(live:{sport}:{date} TTL 90s, /api/live/:sport + Next proxy). Pure
propState math (HIT / ON PACE / NEEDS N / HOLDS / LINE PASSED — never
red in-progress), attachLiveProgress strip join on nameKey+statType,
proximity-to-hit slate float, StatStrip LiveTracker in the ROW-GRAMMAR
outcome slot (spec amended + lock test updated). Grades never change
in-game — tracking, labeled as such. 2698 -> 2757 tests, web build 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:45:29 -04:00

6.8 KiB
Raw Permalink Blame History

VYNDR — LIVE TRACKING v1.0 (A1 board, Session 11)

The read is locked pre-game. The game is watched. GRADES NEVER CHANGE IN-GAME — this layer is TRACKING, labeled as such, per the Ledger ethos.

PRINCIPLES

  1. Zero out-of-pocket. Free feeds only: statsapi.mlb.com (MLB live boxscore) and the ESPN site API (WNBA summary boxscore). No new dependency, no quota key.
  2. Grades never change in-game. The locked pre-game read is untouched; live-progress marks are proto-outcomes rendered in the OUTCOME slot region (ROW-GRAMMAR §2 slot 6). Every live card carries the label "TRACKING — read locked pre-game" exactly once.
  3. One meaning per color. Green = on-pace or already-cleared; amber = needs-more / caution; red stays reserved for settled-negative truth (miss / DEAD from NOT-IN-LINEUP). An in-progress prop is NEVER red.
  4. Only real box-line values. A player not yet in the box score is ABSENT (no mark, no bar, no zero). Number(null) === 0 is the classic fabrication bug — every numeric path uses strict null guards.
  5. An under is never "hit" until final. Under semantics are HOLDS-IF: green HOLDING while the count sits under the line, amber LINE PASSED once the count reaches/passes it (counting stats can still be re-scored; nothing settles until the game is final — the settle pass owns the truth).
  6. DEAD is not declared here. An over that "can no longer hit" is only honestly declarable for NOT-IN-LINEUP (S5, already shipped). Live tracking tracks until final; it never over-claims.

DATA SOURCES (real shapes captured live, 2026-07-11)

  • MLBGET statsapi.mlb.com/api/v1/schedule?sportId=1&date=YYYY-MM-DD&hydrate=linescore identifies Live games (status.abstractGameState === 'Live') AND carries the per-game linescore (currentInning / inningState / scheduledInnings) in the same call. Per live game: GET /api/v1/game/{gamePk}/boxscoreteams.{home,away}.players.ID{personId}.stats.{batting,pitching}. A player who hasn't appeared has EMPTY stats objects → absent.
  • WNBA — ESPN scoreboard (already cached by scheduleService) identifies in-progress events (status.type.state === 'in', status.period). Per live event: GET site.api.espn.com/.../wnba/summary?event={id}boxscore.players[].statistics[0] with parallel keys + per-athlete stats string arrays. didNotPlay / empty stats → absent.

STAT MAPS

  • MLB (LOCAL map, LIVE_BOX_FIELD) — mirrors the idea of outcomeService.MLB_LOG_FIELD but is deliberately LOCAL: the live boxscore splits batting vs pitching into separate stat objects (the game log picks ONE group by position), and innings_pitched must be parsed in thirds ('5.2' = 5⅔ = 5.667) for live pace math. Wired stats: hits, total_bases, home_runs, rbi, runs, stolen_bases, doubles, walks (batting); strikeouts, earned_runs, innings_pitched, outs, hits_allowed (pitching).
  • WNBA (WNBA_BOX_KEY) — points, rebounds, assists, steals, blocks, turnovers, threes (made, parsed from "M-A"), pra (points+rebounds+assists, computed only when all three components are present).

PROP STATE (pure math, web/src/lib/liveProgress.js)

propState({ side, line, current, progress })

  • over, current > line → hit (✓ HIT — the over has already cleared; a counting stat cannot un-clear; still labeled TRACKING until the settle pass)
  • over, not cleared → needs N = floor(line) + 1 current (beats a push on integer lines); on_pace (green) when current / progress ≥ floor(line)+1, else needs (amber, "NEEDS N")
  • under, current < line → holding (green HOLDS chip — never ✓ until final)
  • under, current ≥ line → past (amber LINE PASSED — not red; nothing settled)
  • current == null → NO state (absent beats wrong) Progress fraction: MLB (inning (top ? 1 : 0.5)) / scheduledInnings; WNBA (period 0.5) / 4, clamped to [0, 1].

ENDPOINTS

  • GET /api/live/:sport (public, rate-limited 60/min, mounted in app.js) → { sport, date, hasLive, updated_at, games: [{ id, home, away, progress: { label, fraction, inning|period, half }, players: { [nameKey]: { name, values: { stat_type: number } } } }] }. mlb + wnba only; other sports → { hasLive: false, games: [] }.
  • POLLING RULE: the route reads live:{sport}:{date} (TTL 90s). On a cache MISS it consults the day's schedule; ONLY when the schedule shows live games does it fetch boxscores. Quota math: 1 schedule call + N boxscore calls per 90s window across ALL users (shared cache), during live windows only — zero boxscore calls when nothing is live.
  • Next proxy web/src/app/api/live/[sport]/route.ts (S25 rule — Express is not browser-reachable).

FRONTEND (LIVE SLATE MODE)

  • Slate polls /api/live/{sport} every 60s ONLY while live games are on screen (mlb/wnba, status 'in').
  • liveProgress.attachLiveProgress(strips, liveIndex) (pure) joins built player strips to live values by nameKey + canonical statType; only graded, unsettled, non-dead props get prop.live.
  • StatStrip renders the live mark in the OUTCOME slot region: 1/2 TB · ▲6th + a small game-progress bar + the state chip (ON PACE green / NEEDS 2 amber / HIT ✓ green filled / HOLDS green / LINE PASSED amber). Actions (parlay +, BOOK IT) are suppressed on live props — the pre-game market for the locked line is closed.
  • Live games with tracked props float to the top of the slate, ordered by proximity-to-hit (max over-fraction current / needed-total).
  • The card label "TRACKING — read locked pre-game" renders once per live card.

ACCEPTANCE CRITERIA

  1. Parsers reproduce per-player values from REAL captured feed fixtures; a player absent from the box yields NO entry (never 0).
  2. propState covers: over cleared / on-pace / needs-N / under-holds / under-passed / absent — unit-tested.
  3. /api/live/:sport cache behavior: hit → no fetch; miss + live games → boxscore fetch + cache write; miss + no live games → schedule check only.
  4. attachLiveProgress joins strips ↔ live index and never touches settled, dead, or ungraded props.
  5. Full jest suite green from the worktree; cd web && npm run build exit 0.
  6. If a game is genuinely live during the build, the service runs once against the real feed and the report shows a real tracked line.

TEST PLAN

  • tests/unit/liveTrackingService.test.js — MLB boxscore/linescore + WNBA summary parsers on real-shape fixtures; stat resolution incl. IP thirds; absent-player semantics; live-game identification from schedules.
  • tests/unit/liveProgress.test.js — propState math table; progress fractions; attachLiveProgress; proximity sort.
  • tests/integration/liveRoute.test.js — cache hit / miss+live / miss+idle / unknown sport, with injected deps (zero network).

— LIVE-TRACKING v1.0 · July 2026 —