Files
vyndr/specs/wiring-data-train.md
T
2026-07-13 02:55:43 -04:00

12 KiB

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.buildPlayerStripsFromPropsStatStrip.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 <img> → 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 <Slate> 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_artsalready 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).