From 69b1febb0f460c0ed2e5de68abfc435d1fcfc914 Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 13 Jul 2026 02:55:43 -0400 Subject: [PATCH] =?UTF-8?q?Step=200=20MAP:=20wiring=20&=20data=20train=20?= =?UTF-8?q?=E2=80=94=20surface-by-surface=20data/asset=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- specs/wiring-data-train.md | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 specs/wiring-data-train.md diff --git a/specs/wiring-data-train.md b/specs/wiring-data-train.md new file mode 100644 index 0000000..9ecaf2a --- /dev/null +++ b/specs/wiring-data-train.md @@ -0,0 +1,99 @@ +# VYNDR β€” WIRING & DATA TRAIN Β· STEP 0 MAP +### Branch `wiring/data-train`. The map before the build. Governing law: the $1M/mo feeling is PRODUCED, never claimed. Honesty guardrail: real asset where genuinely sourceable; honest fallback (monogram/text/absent) where not. Never fabricate or scrape a real person's face/data to fill a hole. + +Legend: **βœ… wire-able now** (data already in the system) Β· **πŸ”§ sourceable free** (real source exists, needs a new adapter, zero-out-of-pocket) Β· **β›” blocked** (no honest source today β†’ keep fallback) Β· **πŸ†• net-new**. + +--- + +## STEP 1 β€” TWO TRUST BUGS + +### 1a. Billing "RENEWS 6/9/2036" β€” βœ… fixable +- **Render:** `web/src/app/profile/page.tsx:128` renders `subscription_end` verbatim from the Supabase `user_profiles` row. Honest render, bad data β€” no hardcoded 2036 in source. +- **Root cause:** the live web payment path is **NexaPay, not Stripe**. `web/src/app/api/webhook/nexapay/route.ts:41-50` writes `subscription_end = now + 30 days` (a synthetic guess). The 2036 value is a **manually-seeded/comped founder-account row** the UI trusts blindly. The *real* Stripe `current_period_end` exists at `GET /api/stripe/status` (`stripeService.js:278`) but the UI never consumes it. +- **Task:** point the "Renews" stat at a provider-asserted value (consume `/api/stripe/status.current_period_end`, or have the NexaPay webhook persist its real next-bill timestamp instead of now+30); correct/clear the stale 2036 row. Guard: never render a `subscription_end` the provider didn't assert β†’ fall back to `β€”`. + +### 1b. James Wood β†’ "Chicago Cubs" / builds vs AL East β€” βœ… fixable (nameKey collision) +- **Root cause:** `mlbStatsAdapter.js:192` `people.find(p => nameKey(p.fullName) === targetKey)` returns the **first** exact-nameKey match with **no namesake disambiguation**. A second "James Wood" in the statsapi season list (a Cubs-affiliate namesake) wins β†’ wrong `currentTeam`. The S59 substring guard only covered *fuzzy* mismatches; it never covered two players sharing an exact name β€” **not a regression**, an uncovered case. +- **Propagation:** wrong team enters `snapshotService.js:315` `teamByPlayer` + rosterlogs opponents β†’ `streaksService.js:229` `team` + opponents β†’ `streakLens.js:79-82` "built vs" / tonight's-opponent. The S59 slate JOIN INVARIANT guards only Slate cards; the **streaks/rosterlogs path bypasses it**. +- **Task:** add an optional `teamHint` to `searchPlayer`/`getPlayerStats`/`resolveStats`; on multiple exact-nameKey matches pick the one whose `currentTeam` matches the hint, else return null (never guess β€” S59 doctrine). In `snapshotService.js:311` pass the prop's game team as the hint and drop `stats.team`/rawLog if it disagrees with the prop's game participants. +- **Test:** join-invariant β€” a resolved player's `team` must equal / be a participant of the prop's game; a rosterlogs entry's `team` must match the graded prop's team β†’ omit on mismatch, never tag a foreign team. + +--- + +## STEP 2 β€” ENTITY LAYER, FULLY FED + +**Root-cause verdict:** team **logos already render live**. Player **headshots break at a data-threading gap, not a helper bug** β€” the MLBAM id is fetched then severed before it reaches the browser. + +| Surface | Asset | Status | Task | +|---|---|---|---| +| Game-card team logos | ESPN logo CDN | βœ… live (`vyndr/GameCard.tsx:121` `TeamLogo`) | none; add any missing abbrs to `teamMeta.js` | +| MLB slate headshots | `img.mlbstatic.com/…/people/{id}/headshot` | βœ… id fetched (`playerIntelService.js:141`) but **severed at `snapshotService.js:331-336`** | thread `playerId` β†’ enriched grade β†’ `slateAdapter.buildPlayerStripsFromProps` β†’ `StatStrip.tsx:381` `playerId={ps.playerId}` | +| MLB scan-search headshots | same | βœ… id already on `/api/players/search` `p.id` | pass `playerId:p.id` at `scan/page.tsx:566` (+ `SearchModal.tsx`) | +| MLB hot-list headshots | same | βœ… id via `rosterlogs` | `HotListPanel.tsx:68` uses raw silhouette `` β†’ swap for `PlayerAvatar` (branded monogram on null) | +| NBA/WNBA headshots | cdn.nba.com / ESPN athlete id | β›” **no real id in data** (search returns synthetic `sport-i-key`; `resolvePlayerStats` NBA branch emits none) | source a real id first, or keep honest monogram. NBA/WNBA don't settle yet β†’ low ROI now | +| Soccer headshots | β€” | β›” no central CDN (by design) | honest monogram | +| Ledger book wordmarks | `BookWordmark` (brand-color text) | βœ… built but **never imported**; renders raw lowercase `row.book` at `ledger/page.tsx:368` | import `BookWordmark`; add 6 missing keys to `books.js` (`fanatics/bet365/hardrockbet/betrivers/pointsbet/pinnacle` fall to gray) | + +- **Sportsbook wordmarks:** the work order asks for **local SVGs**. Current `BookWordmark` is brand-color *text*. Decision below (D3) β€” text-wordmark ships today; SVGs are an upgrade. +- **Headshot normalization:** `PlayerAvatar` already enforces size/round treatment; extend to guarantee consistent crop when a real photo loads. `onError`β†’monogram is first-class (no broken-image icon ever). + +**Wire-able now: MLB headshots (3 surfaces) + ledger wordmarks. Blocked: NBA/WNBA headshots (no id source).** + +--- + +## STEP 3 β€” OUTLOOK MODE (never-empty slate) β€” βœ… wire-able now + +- DS2's `heroFallbackState`/`buildHeroReceipts` (`slateAdapter.js:467-495`) covers ONLY the "Top grades tonight" hero (`dashboard/page.tsx:314-418`) β€” never blank there. **Gap:** the `` game grid (`Slate.tsx:1001-1013` `emptyStateCopy`) AND the dashboard's separate "Today's games" section (`page.tsx:421-439`) each independently fall to a dead-end "NO SLATE" card. +- The `"No games available…"` string at `Slate.tsx:609` is the **network `fetchError`** state, distinct from empty-slate. +- **Fill data already fetched:** yesterday's settled receipts (`/api/ledger/model`, already loaded), tomorrow's date-pinned schedule (`scheduleService`), ticker items, month-aware `emptyStateCopy`. Only the wiring into the grid branch is new. +- **Task:** extend the DS2 fallback below the hero β€” when `games.length===0`, render receipts / tomorrow's schedule preview inside the grid slot in both `Slate.tsx:1001` and `dashboard/page.tsx:424`, not a CTA card. + +--- + +## STEP 4 β€” MISSING SURFACES + +| Surface | Status | Verdict | +|---|---|---| +| **Live grade-shift timeline** | βœ… wire-able now | backend done (`intradayRefreshService` revisions + `revised_from_grade` + line history); reaches UI as per-row chip (`StatStrip.tsx:171`) + `LineSparkline`. Missing only a **timeline VIEW** assembled from already-emitted movement/history data. No new backend. | +| **Market-breadth / consensus-vs-model** | βœ… wire-able now | per-prop `books[]` (`Slate.tsx:373`) + `model_value` + snapshot line-deltas already present. Missing only a component that computes median-book-vs-model. `DeskShowcase.tsx:89` currently *advertises* it as copy β€” must become real or the claim goes. | +| **Parlay Lab builder** | βœ… math done, πŸ†• UI | `parlayService`/`/api/parlay/grade`/`ParlayContext` all live; combined grade + correlation + payout render in `ParlayPanel`. "Empty tray" because the **only leg source is the "+" on live slate rows** and there's **no `/parlay` page**. Task: build a Parlay Lab page with a prop search/browse leg source independent of the slate. | +| **Pitcher arsenal** (mix/velo/usage/whiff%) | πŸ”§ sourceable free / πŸ†• feed | **NOT in statsapi** β€” this is Statcast / Baseball Savant (free, public). Needs a genuinely new Savant adapter, not wiring. | +| **/u public profile** | βœ… route live, β›” no record | route + aggregate machinery work; `/u/vyndr` 200s to an honest not-found because **no `public_profiles` row is claimed+published** AND `getModelAggregate({userId})` needs **β‰₯20 settled rows** under that user. Structural note: it scopes to a *user's own* ledger β€” the **house model record is `user_id=NULL`** and isn't surfaceable as a user profile without a "house profile" option. | + +**Wire-able now:** grade-shift timeline, consensus strip, Parlay Lab UI. **New free feed:** pitcher arsenal (Savant). **Data-gated:** /u real record (needs a published house profile β€” see D2). + +--- + +## STEP 5 β€” COMBAT (net-new, largest lift, sequenced last) + +- **Verdict: genuinely net-new.** Only inert placeholders exist (`sports.js` `mma:{active:false,comingSoon}`, a share-card color, a dormant ESPN URL in `ESPNAdapter.js`, Pinnacle id 22). No pipeline/route/service/archetype/grade path. **No combat spec exists in the repo** β€” it must be authored as Step 5's first artifact. +- **PropLine has no combat** β†’ combat cannot ride the abundant-props path; it's **odds-api-only**, which changes the quota story. +- **Free source option space (report-only, no pick):** + - **ESPN MMA** (`site.api.espn.com/…/mma/ufc/scoreboard|summary`) β€” free JSON, no auth, **same family VYNDR already uses**; fight cards/results/method/round + some bio. Thinner stat depth than ufcstats. The URL already sits dormant in `ESPNAdapter.js`. + - **ufcstats.com** β€” richest tale-of-the-tape + striking/grappling granularity, but **HTML scraping β†’ needs a parser dep (cheerio) + fragile**; UFC-only. + - **The Odds API `mma_mixed_martial_arts`** β€” **already paid** (`ODDS_API_KEY`); realistically **moneyline + round totals** only. Method-of-victory/round/props are **thin-to-absent** on the standard feed β†’ data-constrained, not just build-constrained. + - Wikipedia/Wikidata β€” bio backstop only. +- **Honesty caveats:** never ingest fighter photos (likeness/rights β€” same rule as headshots); scraping must degrade to *absent*, never fabricated 0; if only ML+round-total are free, the UI must not imply a full method/prop board. +- **Biggest architectural departure:** combat's unit is a **matchup, not a per-player prop** β†’ the current `analyzeViaEngine1` per-prop feature-vector doesn't fit; a **style-matchup grade engine** is closer to a new engine than a config add. Settlement needs a new ESPN-MMA result path (like the pending WNBA box-score work). +- **Cheapest honest v1:** ESPN-MMA (free) + odds-api ML/round-totals (paid, existing) + a net-new style-blend archetype set + a matchup-grade path; method/round/props flagged data-limited. + +--- + +## DECISIONS FOR THE FOUNDER (my honest defaults in **bold**) + +- **D1 β€” NBA/WNBA headshots:** no real id source in the data today, and those sports don't settle yet. **Default: ship MLB headshots now; keep honest branded monograms for NBA/WNBA until an id source is added.** (Alt: invest in sourcing cdn.nba.com/ESPN ids now.) +- **D2 β€” /u real record:** the CLV-verified profile can't show a house record as a *user* profile (house = `user_id=NULL`). **Default: add a "house/model" profile mode that reads the public `user_id=NULL` aggregate** (the partner-pitch weapon shows the real 55-19), keeping user profiles gated at nβ‰₯20. (Alt: claim+publish a house account and let it accrue 20 settled reads β€” slower, but no code path added.) +- **D3 β€” Book wordmarks:** **Default: ship the existing brand-color text `BookWordmark` now (kills lowercase "betmgm"), bundle local SVGs as a follow-up wave** (SVGs are the work order's ask but a bigger drop-in). (Alt: source + bundle the ~8 SVGs before shipping Step 2.) +- **D4 β€” Pitcher arsenal:** Baseball Savant is free β†’ **Default: build the Savant adapter (zero-out-of-pocket).** Flag any field Savant doesn't cover as absent. +- **D5 β€” Combat scope:** **Default: author the combat spec + ship the free honest v1 (ESPN-MMA + odds-api ML/round-totals), flagging method/round/props as data-limited; defer the full matchup-grade engine to its own sub-wave.** Confirm before adding any scraping dep (ufcstats/cheerio). + +--- + +## PROPOSED WAVE SEQUENCE (ship on green, per step) +1. **Wave 1 β€” Step 1 trust bugs** (billing date + Wood nameKey collision + join-invariant test). Quick, independent. +2. **Wave 2 β€” Step 2 entity layer** (MLB headshot thread-through on 3 surfaces + ledger wordmark import + books.js keys). **Eyeball gate: report when this lands so real logos/headshots can be verified before the rest builds on it.** +3. **Wave 3 β€” Step 3 outlook mode** (never-empty grid) + Step 4 wire-able-now surfaces (grade-shift timeline, consensus strip, Parlay Lab page). +4. **Wave 4 β€” Step 4 new feed** (pitcher arsenal via Savant) + /u house-profile mode (pending D2). +5. **Wave 5 β€” Step 5 combat** (spec first, then free v1 pending D5). + +Also queued (from the work order preamble): remove the literal "A $1M terminal" DeskShowcase headline β†’ on-voice deadpan (it claims premium instead of showing it).