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

115 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
- **MLB** — `GET 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}/boxscore`
`teams.{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 —