# PropLine + Box Score + Pipeline Audit — 2026-07-10 (Session 56)

Ground-truth inventory of what data we can access, what our pipeline handles, and
the gaps. Live sections were fetched against real APIs (MLB Stats, ESPN, The Odds
API `/v4/sports`). PropLine has no dev keys, so its inventory is derived from the
authoritative code (`proplineAdapter.MARKETS` × `oddsNormalizer.MARKET_MAP`) —
that IS the set our pipeline requests + normalizes. Raw capture:
`specs/audit-data/propline-audit-raw.json`.

Baseline: **2276 tests** passing at session start.

---

## 0. What is in-season RIGHT NOW (The Odds API `/v4/sports`, live 2026-07-10)

| Sport | Active key(s) | Notes |
|-------|---------------|-------|
| MLB | `baseball_mlb` | ✅ full season — the live product |
| WNBA | `basketball_wnba` | ✅ in-season (3 games tonight per ESPN) |
| NBA | `basketball_nba_summer_league` only | ⚠️ regular NBA off until Oct; summer league live |
| NFL | `americanfootball_nfl`, `_nfl_preseason` | ⚠️ preseason lines starting |
| Soccer | 40+ leagues (`soccer_epl`, `soccer_usa_mls`, `soccer_fifa_world_cup`, La Liga, Serie A, …) | ✅ year-round via odds-api |
| NHL | (absent) | off-season — no lines |

Takeaway: **MLB + WNBA + soccer are the live opportunity today.** NBA/NFL are
pre-season shells; NHL is dark.

---

## 1. PropLine / odds inventory — what we REQUEST vs what we CAN normalize

`proplineAdapter.MARKETS` = the markets we actually ask for. `MARKET_MAP` = every
market we can normalize (superset). A market we request but don't map → **silent
zero**; a market we map but don't request → **unused capacity** (free to add — same
call).

### MLB
| Requested market | → stat_type | Grade whitelist? | Features (l5/l20)? | Outcome settle? |
|---|---|---|---|---|
| batter_hits | hits | ✅ | ✅ hits | ✅ hits |
| batter_home_runs | home_runs | ✅ | ✅ homeRuns | ✅ homeRuns |
| batter_total_bases | total_bases | ✅ | ✅ totalBases | ✅ totalBases |
| batter_rbis | **rbis** ❌ | ✅ (`rbi`) | ✅ (`rbi`) | ✅ (`rbi`) | ← **BUG: normalizes to `rbis`, whole chain keys on `rbi` → never grades/settles** |
| batter_stolen_bases | stolen_bases | ✅ | ✅ stolenBases | ✅ stolenBases |
| pitcher_strikeouts | strikeouts | ✅ | ✅ strikeOuts | ✅ strikeOuts |

**Mapped but NOT requested (free capacity — all supported downstream already):**
`batter_runs`→runs, `batter_walks`→walks, `pitcher_earned_runs`→earned_runs,
`pitcher_hits_allowed`→hits_allowed. **Mapped, not requested, and needs feature/
settle wiring:** `batter_doubles`→doubles, `pitcher_outs`→outs. Confirmed present
in the real boxscore (§2).

### WNBA / NBA
Requested: `player_points`, `player_rebounds`, `player_assists`, `player_threes`
(+ NBA adds `player_blocks`, `player_steals`). All grade-whitelisted. **Features:
NBA/WNBA game logs come from the Python nba_api service (usually OFFLINE in prod)
→ l5/l20 often empty; ESPN fallback exists for season avgs only. Outcomes: NOT
settled at all (outcomeService is MLB-only).** `player_turnovers`/`player_pra` are
mapped but not requested.

### Soccer
**PropLine has NO soccer entry** (`SPORT_KEYS`/`MARKETS` omit it) — soccer props
can only come from the odds-api backup. `MARKET_MAP` maps goals/shots/shots_on_
target/tackles/cards/corners/saves/passes/clean_sheet. Features: soccer extractor
exists; outcomes: NOT settled.

### NFL / NHL
NFL markets mapped + requested (pass/rush/rec yds, receptions, TDs) but the flow
isn't graded end-to-end yet. NHL mapped/requested but off-season.

---

## 2. MLB box score field map (REAL — Braves @ Pirates, 2026-07-09)

Batting fields present: `runs, doubles, triples, homeRuns, strikeOuts,
baseOnBalls, hits, atBats, stolenBases, totalBases, rbi, sacFlies, …`
Pitching fields present: `strikeOuts, baseOnBalls, hits, inningsPitched,
earnedRuns, outs, battersFaced, …`

**OUTCOME_MAP (MLB) — stat_type → game-log/boxscore field:**
```
hits           → hits            total_bases → totalBases    home_runs → homeRuns
rbi            → rbi             runs        → runs           walks     → baseOnBalls
stolen_bases   → stolenBases     strikeouts  → strikeOuts     earned_runs → earnedRuns
hits_allowed   → hits (pitching) innings_pitched → inningsPitched
doubles        → doubles  ⟵ NEW  triples → triples ⟵ NEW      outs → outs ⟵ NEW
batter_strikeouts → strikeOuts ⟵ (batter Ks — roadmap)
```
Every field above is real in the boxscore/game log → all are settleable today.

## 2b. ESPN box score (WNBA/soccer — the non-MLB settle path)
WNBA settlement source = ESPN summary `boxscore.players[].statistics[]`
(`labels`/`names` arrays: PTS/REB/AST/3PM/STL/BLK/TO). No completed game at fetch
time (all 3 tonight were Scheduled), so labels weren't captured — wiring the ESPN
settle path is a roadmap item (`espnStatsAdapter` already parses this shape for
season avgs).

---

## 3. Pipeline flow + gaps

**Current flow:** cron (`SNAPSHOT_CRON=1`, UTC 14,19,22,1,3) → per sport:
`settleAllOutcomes()` (S55, MLB only) → `snapshotService.runSnapshot` → PropLine
(3-key rotation) / odds-api backup → `gradeAndCacheSlate` → per-player archetype +
features → Redis (`snapshot:{sport}:latest`, `grades:{sport}`) → ticker.

**Gaps found:**
1. **RBI silent failure** — `batter_rbis → rbis` mismatch (§1). FIX this session.
2. **Under-requesting MLB markets** — only 6 of ~12 supported; runs/walks/doubles/
   earned_runs/hits_allowed/outs are free in the same call. EXPAND this session.
3. **Outcome settlement is MLB-only** — WNBA/NBA/soccer grades NEVER settle, so
   `accuracy` is MLB-only. Biggest roadmap item (needs the ESPN settle path).
4. **No pipeline alerting** — a failed/empty/missed cron run is silent. ADD ntfy
   this session.
5. **No retry** — one PropLine blip marks a sport unavailable for the whole cycle.
   ADD retry-once this session.
6. **No missed-cron detector** — nothing notices if a slot didn't fire. ADD.
7. **NBA/WNBA features depend on an offline Python service** — l5/l20 often empty;
   ESPN gives season avgs only. Roadmap: an ESPN game-log feature path.
8. **Soccer has no PropLine source** — odds-api backup only; document + monitor.

---

## 4. Sport coverage status (verified 2026-07-10)

| Sport | Odds props | Grade pipeline | Features | Outcomes | Status |
|-------|-----------|----------------|----------|----------|--------|
| MLB | ✅ PropLine + odds-api | ✅ | ✅ real (statsapi) | ✅ (S55) | **LIVE** |
| WNBA | ✅ odds-api/PropLine | ✅ | ⚠️ Python offline → thin | ❌ not settled | **PARTIAL** |
| NBA | ⚠️ summer league only | ✅ | ⚠️ offline | ❌ | **OFF-SEASON** |
| Soccer | ✅ odds-api (no PropLine) | ⚠️ extractor exists | ⚠️ | ❌ | **PARTIAL** |
| NFL | ⚠️ preseason | ⚠️ mapped, not e2e | ❌ | ❌ | **PRE-SEASON** |
| NHL | ❌ off-season | ⚠️ mapped | ❌ | ❌ | **DARK** |

---

## 5. Fixes shipped this session (Phase 4/6)
- `batter_rbis → rbi` (unblocks a market we already request).
- PropLine MLB MARKETS expanded: +batter_runs, +batter_walks, +batter_doubles,
  +pitcher_earned_runs, +pitcher_hits_allowed, +pitcher_outs.
- `doubles` + `outs` wired into featureCache + outcomeService + all three grade
  whitelists (analyze.js / scan.js / validation.py).
- Pipeline alerting (ntfy) + PropLine retry-once + missed-cron detector.

See `specs/vyndr-roadmap.md` for the priority-ordered plan that follows from this.
