Files
vyndr/specs/truth-everywhere-train.md

56 lines
8.7 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 — TRUTH-EVERYWHERE + OFFSEASON HUB · STEP 0 MAP
### The map before the build. Governing: accountable-before-wide; never fabricate a grade/settle/market value; absent-beats-wrong; n≥20 gate; the record includes the misses. Design ref = `specs/design-reference/vyndr-system.html` (live wordmark kept).
## THE HEADLINE FINDING (this reorders the train)
The work order assumed grades are MADE everywhere but only CHECKED for MLB. **That's not the situation. Only MLB actually GRADES in prod.** NBA/WNBA/soccer silently refuse their entire slate; combat/NFL/NHL aren't in the grade loop at all. So the record isn't "MLB wearing a platform badge because only MLB settles" — it's MLB because **only MLB produces grades in the first place.**
**The first domino is GRADING, not settlement.** You cannot settle what never grades. Wave 0 (grade-first) is not a quick gate — it is the actual unlock, and it's the highest-leverage work in the product.
## PER-SPORT GRADE → SETTLE → LEARN MATRIX
| Sport | Grades in prod? | Why | Free settle path? |
|---|---|---|---|
| **MLB** | ✅ yes | `featureCache` MLB branch → statsapi.mlb.com (free, no auth) → real l5/l20 → projection | ✅ live (`outcomeService` via statsapi last10) |
| **NBA** | ❌ refuses ALL | `featureCache` non-MLB branch → `gameLogService` hits ONLY the Python nba_api (`:8000`), **usually offline in prod, no fallback** → null logs → no projection → refuse. The espnStatsAdapter "fallback" is NOT wired into the grade path (only profile/savant). | ✅ **viable** — ESPN boxscore; `liveTrackingService.parseWnbaBoxscore` + `WNBA_BOX_KEY` ALREADY parse it |
| **WNBA** | ❌ refuses ALL | same Python-only root cause (in-season now, still refuses) | ✅ same as NBA (identical ESPN shape) |
| **Soccer** | ❌ errors THEN refuses | (1) `getOdds('soccer')` throws `Unknown sport: soccer``SPORT_KEYS` has `soccer_wc/soccer_epl…` not bare `soccer` → snapshot errors pre-grade; (2) `soccerFeatureExtractor` reads prefetch Redis keys that **nothing writes** (prefetch script unarmed) → starved → refuse | ⚠️ api-football only (free 100/day, key-gated `API_FOOTBALL_KEY`), NOT ESPN cleanly |
| **Combat/MMA** | ❌ not graded | v1 is a standalone surface; no engine branch, no projection field, not in `ACTIVE_SPORTS` | ML: ✅ free (ESPN `winner` boolean, already parsed); round-total: ❌ no free method/round feed |
| **NFL/NHL** | ❌ dormant | not in `ACTIVE_SPORTS`; no `featureCache` gameLog branch either | ✅-pattern (ESPN box, same shape) when in-season |
## REVISED WAVE PLAN (grade → settle → learn, per sport)
### WAVE 0 — GRADE-FIRST (the real unlock; do FIRST)
Make NBA/WNBA/soccer actually produce grades in prod off a FREE source.
- **NBA/WNBA:** wire a free per-game-log source into `featureCache.gameLogFeatures` (mirroring the MLB branch). **CRUX/RISK to verify first:** the source must give **per-game logs (last 5/20), not just season averages**`l5_avg`/`l20_avg` can't come from a season mean. ESPN's per-athlete gamelog endpoint (`.../athletes/{id}/gamelog`) is the candidate; confirm it returns per-game rows before building. If ESPN gamelogs work, NBA (off-season) + WNBA (in-season NOW) grade for free.
- **Soccer:** (a) fix `getOdds('soccer')` to resolve the `soccer_*` league keys; (b) arm the soccer prefetch (or wire api-football live) so `soccerFeatureExtractor` isn't starved. Soccer is the hardest (double-blocked + key-gated) — consider deferring to last.
- Combat/NFL/NHL: out of scope for grading now (no engine branch; combat stays its standalone surface).
### WAVE 1 — SETTLEMENT EVERYWHERE (once a sport grades)
The plumbing is ALREADY sport-agnostic — the ONLY gate is `defaultGetPlayerStats` (MLB-only, **duplicated in `outcomeService.js` AND `ledgerService.js` — both must change**) + a per-sport box-field map.
- **NBA/WNBA:** a game-centric settle branch — sweep yesterday's `status.state==='post'` ESPN summaries, `parseWnbaBoxscore` each (reuse verbatim), match graded players by `nameKey`. Add a WNBA/NBA `BOX_KEY` local map (mirror `MLB_LOG_FIELD`; keep the 3-map split intentional per S11).
- **MMA moneyline:** settles free from the `winner` boolean already parsed. Requires combat to first WRITE grades/locks (it's not in the snapshot loop) — smaller than a box feed. Round-total = honest gap (no free source).
- **Soccer:** api-football per-fixture stats, quota-bound + key-gated → defer with grading.
- **Observability (non-negotiable):** boot announce per sport (`[settle:nba] armed`); add the sport to `opsWatch.SETTLEABLE_SPORTS` **only with a real-finals probe** (count yesterday's ESPN `state==='post'` events for the sport) so an off-day (0 games) never false-pages "settled 0". Thrown-error paging already covers all sports.
- Records need ZERO new plumbing — `accuracy:{sport}` + `getModelAggregate` `by_tier` already flow per-sport; the Wave-3 `TierRecord` lights up automatically.
### WAVE 2 — OFFSEASON / NEVER-DARK HUB
- **News/transactions/injury wire:** injuries already ingested (`injuryService`, mlb/wnba/nba). NEWS is a gap — ESPN `/news` feed is FREE, unused. Build `newsService` (mirror `injuryService`) + `/api/news/:sport` + proxy. Signings appear as prose headlines (ESPN has no typed transactions feed — honest limitation).
- **Futures/outrights:** genuine gap (zero support today). **⚠️ PAID-QUOTA DECISION (D1 below):** The Odds API carries `outrights` on the ALREADY-paid key but each futures sport-key call spends the scarce 500/mo quota; PropLine has none. Needs new `FUTURES_KEYS` + `markets=outrights` param + a new `normalizeOutrights` branch (outrights have `name`+`price`, no `point` — can't reuse `normalizeProps`) + long TTL/offseason gating.
- **Line-movement on futures:** high reuse — `computeLineDeltas`/`trackHistory`/`signedDelta` port cleanly with `line``odds` (price). News→move causal tie ("signing → win total +2.5") is NEW small join code.
- **Render:** extend `/explore` (`ExploreHub`) + the offseason-aware `outlook.js`/`emptyState.js`; **reskin the retired `TerminalTemplates` injury-wire/leader LAYOUTS with REAL data** (they're SAMPLE data today — never route as-is). New `/futures` route only if wanted.
### WAVE 3 — FUTURES HONESTY LINE
Season-long grading is a different model (projection+roster+schedule, not last-5). The hub SHOWS + TRACKS futures + news + movement honestly, and GRADES only where the model genuinely supports it — "tracked, not graded" everywhere else. The deliberate futures-grading model is a LATER train.
## DECISIONS — LOCKED (2026-07-13)
- **D1 = BUILD FUTURES, quota-disciplined.** Separate `FUTURES_KEYS` path, 612h/daily TTL, offseason-month gated, never touches the daily player-prop budget.
- **D2 = DEFER SOCCER to the end / its own follow-up.** NBA/WNBA are the free high-value wins first; soccer wired when `API_FOOTBALL_KEY` is provided.
- **D3 = ANSWERED (green).** ESPN per-athlete gamelog verified live: NBA returns 73 per-game rows + stat labels; WNBA endpoint 200. Free per-game logs exist → NBA/WNBA can grade. Endpoint: `site.web.api.espn.com/apis/common/v3/sports/basketball/{nba|wnba}/athletes/{id}/gamelog``events[]` + `labels`/`names`.
## ORIGINAL DECISION RECOMMENDATIONS (superseded by LOCKED above)
- **D1 — Futures quota.** Futures spend the already-paid odds-api 500/mo quota, competing with game-prop credits. **Rec: YES but disciplined — futures move slowly, so a 612h/daily TTL + offseason-month gating keeps the cost tiny; wire them on a separate `FUTURES_KEYS` path so they never touch the daily player-prop budget.** (Alt: skip futures, ship only the free news wire + line-movement on existing markets.)
- **D2 — Soccer.** Grading + settling soccer both need `API_FOOTBALL_KEY` (free tier, 100/day) which YOU provision (I can't create accounts/keys), plus arming the prefetch. **Rec: defer soccer to the END of the train (or its own follow-up) — it's the only double-blocked, key-gated sport; NBA/WNBA are the high-value free wins. Provide the key when convenient and I'll wire it.**
- **D3 — NBA/WNBA grade source.** Wave 0 hinges on a FREE per-game-log feed. **Rec: verify ESPN's per-athlete gamelog endpoint returns per-game rows FIRST (a spike), before building the featureCache branch — if ESPN only gives season averages, we need another free source or NBA/WNBA can't honestly grade yet.** This is the single technical risk of the whole train; I'll de-risk it in Step 0.5.
## SEQUENCE
Wave 0 (NBA/WNBA grade via ESPN gamelogs — verify the feed first) → Wave 1 (NBA/WNBA settle via existing box parser + observability; MMA moneyline) → Wave 2 (news wire free + futures per D1) → Wave 3 (futures honesty). Soccer deferred per D2. Each wave: green → merge → deploy → fingerprint → report.