Frontend + wiring only. Wires existing backend into the pages users see. - VYNDR Original archetype rename (41) across archetypeService.js + lib/ archetypes.js + ArchetypeBadge, each keeping legacyName (resolves stale data). Judge -> BOMBER. Old POWER PULL slot -> WHIFF strikeout-artist pitcher. - BACKEND_HANDOFF.md: canonical frontend<->backend data contract. - Grade card intel: scan/page.tsx now forwards the engine's intel fields (season_avg/form/usage/matchup_grade/archetype/...) into mapScanToGradeResult -> STAT CONTEXT + VYNDR INTELLIGENCE sections populate. The chain already preserved them (tierGating + /api/scan spread); the page was dropping them. - Schedule freshness: slateAdapter.isRelevantGame drops completed games >24h old; Slate.filteredGames applies it. (TTL already 60s.) - Landing: Features.tsx rewritten to user-facing copy (no Point-biserial/Zone 14/ABS/Phi-coefficient). - Depth chart Next proxies added (/api/stats/lineup|depth|cascade) - were 404. - GameCard swap DEFERRED (Kev): legacy on-demand card stays as a bridge until the snapshot pipeline populates the grades cache; vyndr/GameCard swaps in then. Backend 2045 -> 2061 tests (+16), 167 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 KiB
Executable File
VYNDR — Claude Code Project Context
What This Is
Sports betting intelligence SaaS. Real software product. Three tiers: Free (5 scans), Analyst ($19.99 / $14.99 founder), Desk ($49.99 / $34.99 founder).
Tech Stack
- Backend: Node.js / Express
- Database: Supabase (PostgreSQL)
- Frontend: React Native (built in Cursor)
- Data: The Odds API ($30/mo), nba_api (free, Python wrapper)
- Caching: Redis — 15min for odds, 24hr for season averages, 1hr for recent games
- Payments: Stripe
Critical Rules — Non-Negotiable
1. NO CODE WITHOUT A SPEC
Every feature requires a spec file in specs/ before any code is written.
Spec must include: endpoints, data shapes, acceptance criteria, test plan.
Get approval before building.
2. WSL2 HEREDOC RULE
WSL2 corrupts heredoc for files over 10 lines.
ALWAYS use Python file-writing: python3 with triple-quoted strings.
This applies to every file creation operation.
3. 5 QUALITY GATES (all must pass before any feature is marked complete)
- Unit tests pass
- Integration tests pass
- Acceptance criteria met (from spec)
- PR description written
- CLAUDE.md updated if anything new learned
4. BUILD-STATE.md
Update after every session. What shipped, what's next, any blockers.
5. BLOCKERS.md
If you hit something you cannot resolve: log it. Don't guess. Don't skip.
Folder Structure
vyndr/
├── src/
│ ├── routes/ # Express route handlers
│ ├── models/ # Supabase data models
│ ├── services/ # Business logic (prop analysis, odds normalization)
│ ├── middleware/ # Auth, rate limiting, scan counting
│ └── utils/ # Helpers, formatters, validators
├── tests/ # Unit + integration tests
├── docs/ # API docs, architecture notes
├── specs/ # Feature specs (write BEFORE code)
├── build-briefs/ # Session summaries
├── CLAUDE.md # This file
├── ROADMAP.md # Feature roadmap with phases
├── BUILD-STATE.md # Current build status
├── BLOCKERS.md # Unresolved blockers
└── DECISIONS.md # Architecture decisions log
All-Day Intelligence Layer (Session 23)
Free/cheap content that keeps the platform alive when odds-api props are empty. NONE of these spend odds-api credits:
/api/schedule/:sport— cache-aside ESPN scoreboard (scheduleService), self-heals on cache miss. Per-gamehasOdds/hasGameLinesflags peek at other caches without fetching./api/gamelines/:sport— Tank01 book-by-book lines (RAPID_API_KEY quota)./api/streaks/:sport+/api/hotlist/:sport— PURE engines (streaksService,hotListService) computed from cached game logs. NO API calls. Logs loaded byrosterLogs.js(prefetch blob, else Redis SCAN overgamelogs:{sport}:{player}:{count}). Empty roster = valid empty state.?stat=filters narrow streaks/hotlist; categories inconfig/statFilters.js(mirrorweb/src/config/statFilters.ts). Discovery:/api/stats/filters/:sport.- Dead providers: set
status: 'dead'inconfig/providers.jsto drop a provider from fallback chains + configured list (ParlayAPI host is dead).
Provider Strategy (Session 30)
Player props now have abundance, not rationing.
- Player props — PRIMARY: PropLine (
proplineAdapter, 3 keysPROPLINE_API_KEY_1/2/3, 3,000 req/day FREE, rotates per-key; registryproplinepriority 1). BACKUP: The Odds API (ODDS_API_KEY, 500/month, priority 2, conserve).getOdds()tries PropLine first when keys present, falls back to odds-api; the response + cache carry aproviderfield. PropLine is The-Odds-API-compatible → reusesutils/oddsNormalizer. MLB market keys (batter_hits,pitcher_strikeouts, …) were added toMARKET_MAP— without them MLB props normalize to zero. - MLB stats —
mlbStatsAdapter→ statsapi.mlb.com. FREE, no auth, unlimited. Game logs, season averages, BvP, probable pitchers. Does NOT use the gateway (no quota). Registrymlb-stats(noAuth: true). - Game enrichment —
scheduleService.getGameSummary(sport, eventId)→ ESPN summary (injuries, ESPN Bet odds, ATS, leaders, box score). Free. - Game-level odds — Tank01 (unchanged). Tank01 PLAYER PROPS = empty, do not wire.
Grades Content Pipeline (Session 32)
Closes the content pipeline: contentTemplateService reads a grades:{sport}
cache "when present" but nothing wrote it, so slate/POTD never reached
dataLevel: 'full'.
- Writer —
gradeSlateService.gradeAndCacheSlate(sport, props, opts)dedupes props to unique player+stat+line (cap 25, concurrency 5), grades BOTH sides viaanalyzeViaEngine1(engine1 is direction-aware — keeps the higher-confidence side), sorts by confidence desc, writesgrades:{sport}={ grades, updated_at, source }(TTL 2h). The legacy grade shape already matchesnormalizeGrade— no remap needed. - Trigger — fire-and-forget inside
oddsService.recordDownstream(runs on a fresh odds fetch / cache MISS, NOT on cache hits). Does NOT hold the odds HTTP response. Gated byshouldGradeSlate(): ON by default, OFF whenNODE_ENV==='test'(its feature-compute fan-out would pollute call-count assertions), override withGRADE_SLATE_ON_FETCH=1/0.0is the operator kill-switch if the per-prop feature-compute cost needs shedding.
NFL/NHL Props (Session 32)
NFL + NHL wired end-to-end. the-odds-api keys are americanfootball_nfl
and icehockey_nhl (full-name prefix like basketball_nba) — NOT
football_nfl/hockey_nhl (those are PropLine's keys in proplineAdapter).
oddsService.SPORT_KEYS + SPORT_MARKETS carry both; proplineAdapter.MARKETS
filled. NHL keys (player_shots_on_goal, goalie_saves) added to
MARKET_MAP so NHL props don't silently normalize to zero in-season.
Public Route Rate Limiting (Session 32)
middleware/rateLimit (createRateLimit, in-memory per-IP, independent
bucket per call site) now mounts via router.use at the top of every public
cached router: odds + parlay = 30/min, schedule/gamelines/streaks/hotlist/
content/lines/books = 60/min. /api/analyze keeps its own 10/min.
Frontend ↔ Backend Wiring (Session 25 — non-obvious)
A new Express route under /api/* is NOT reachable from the browser until
a matching Next.js proxy route exists at web/src/app/api/.../route.ts
that forwards to ${BACKEND_URL}/api/.... The browser hits the Next origin,
not Express directly. This bit us: schedule/gamelines/streaks/hotlist
endpoints worked on Express but 404'd in the UI for two sessions. When
adding a backend endpoint the frontend calls, ALWAYS add the proxy too
(pattern: web/src/app/api/odds/nba/route.ts).
Tank01 betting-odds real shape: sportsbooks are TOP-LEVEL keys on each
game object ({ awayTeam, homeTeam, bet365:{...} }), not a sportsBooks
array. Filter NON_BOOK_KEYS to extract books (see gameLines.js).
VYNDR 2.0 Design System (Session 33 — Phase A+B)
Multi-session frontend conversion of the claude.ai/design "VYNDR 2.0" handoff
(NOT a backend change). Foundation shipped; pages/mobile/systems are Sessions
34+. Source of truth = the prototype's vyndr.css + VYNDR_HANDOFF.md.
- Tokens live in
web/src/app/globals.css:root. The NEW canonical set is the short names: grades--g-ap/--g-a/--g-b/--g-c/--g-d, sports--s-nba/--s-mlb/--s-wnba/--s-soccer,--amber,--live/--hit/--miss,--scan-op(0.04),--glitch(1),--sans(Inter),--mono(JetBrains Mono). The legacy alias block (--grade-a,--nba,--accent, …) is KEPT — do not delete it until every consumer is migrated. - Two hard brand rules: (1) JetBrains Mono (
var(--mono)/.mono) for ALL data — odds, %, timestamps, book names, stat lines, grades; Inter (var(--sans)) for everything else. (2) Glitch animations apply ONLY to chrome (wordmark, headers-on-hover, dividers, loaders, living layer). DATA NEVER GLITCHES. - Glitch keyframes (§4) are appended to globals.css after the legacy block —
later-wins where names collide, so the appended VYNDR-2.0 definitions are
authoritative. Entrance keyframes floor at the visible state (
fade-infrom opacity .6) so a paused frame is never invisible. - Shared components:
@/components/vyndr/*(Wordmark, GradeBadge, SportBadge, TerminalInput, SectionHead, VBtn, Card, Sparkline, Ticker), helpers inweb/src/lib/vyndrTokens.js. The NEW Wordmark is.wmmarkup at@/components/vyndr/Wordmark; the legacy@/components/Wordmark(.wordmark) is still used by Nav until pages convert.vyndrTokens.jsis CommonJS so it's importable by.tsx(allowJs) AND requireable by the plain-JS Jest suite — keep helper logic there so it stays genuinely unit-testable. - a11y layer (§10):
<html data-contrast|data-text|data-cb|data-font| data-motion>overrides in globals.css. Wiring the toggles to these attrs is Phase G (Session 38).
VYNDR 2.0 App Shell (Session 34 — Phase C)
The frame every page sits in. Frontend-only.
- Routing config =
web/src/lib/routes.js(CommonJS so it's unit-testable):GATED_ROUTES,OPEN_ROUTES,HASH_ALIASES,isGatedRoute(),resolveHashAlias(). GATED is deliberately narrow — only personal surfaces (ledger/tracker/account/profile/settings/notifications/invite). dashboard + scan stay OPEN (the free-scan funnel); gating them would be a monetization regression. - Auth gate = CLIENT-side (
components/AuthGate.tsx, mounted around<main>in layout). Our Supabase session lives in localStorage, not an httpOnly cookie, andmiddleware.tsis locale-only — a server middleware can't read it. The gate usesuseAuth().loading/user+isGatedRoute()and redirects to/login?next=<path>(the param/loginalready consumes). Do NOT try to move this to middleware without first adding@supabase/auth-helpers-nextjs+ cookie sessions (a separate, larger change). - Hash deep-links:
components/vyndr/HashRedirect.tsxtranslates#scan/#terminal/… → real Next routes once on mount. We keep file-based routing; hashes are just redirect aliases for old share links / PWA shortcuts. - Nav (
components/Nav.tsx): uses the new@/components/vyndrWordmark (.wm), mono uppercase links, active =--g-a, a More dropdown, and a Ticker under the bar. The fixed header is 60px nav + 32px ticker, so layoutmainpaddingTop is 96 (not 64) — keep that in sync if the header height changes. - Footer (
components/Footer.tsx) is mounted GLOBALLY in the layout (not per-page). System voice + "BUILT BY KEVON BUTLER · DETROIT". - 404 (
app/not-found.tsx) is the north star — scanlines, crt-sweep, glitch wordmark, amber 404. Interactive CTAs live in clientNotFoundActionsso the page stays a server component (keeps its metadata export). - RouteStub (
components/vyndr/RouteStub.tsx) backs not-yet-built routes (terminal/compare/invite/help/about/notifications). Never stub over a route that already has real content.
VYNDR 2.0 Core Screens (Session 35 — Phase D)
- Grade Result Card =
@/components/vyndr/GradeResultCard(the product's core moment) +ProcessingGrade(the factor-ignite reveal that precedes it). Feed them the §7 contract vialib/gradeAdapter.js(mapScanToGradeResult). The adapter is CommonJS (unit-testable) and tier-gates content: free = 3-signal teaser, no kill conditions, no alt ladder; analyst = full signals + kill conditions; desk = + alt ladder. Keep that gating — the card itself shows whatever it's given, so the giveaway-prevention lives in the adapter. GradeResultCard returns the inferred JS-object type loosely, so castmapScanToGradeResult(...) as GradeResultDataat call sites (the JS adapter has no TS types). - Scan grade flow (
app/scan/page.tsx) now renders ProcessingGrade→ GradeResultCard. The existing search/limit/parlay logic,markReadCompletereads tracking, andnoopener noreferrersportsbook deep-links were preserved — don't drop those when iterating. - GameCard =
@/components/vyndr/GameCard(Bloomberg best/worst line cells: best =rgba(0,212,160,.13)+ green left border, worst = subtle red). Built + tested but NOT yet swapped into the livedashboard/Slate.tsx— that data-mapping swap is a pending task; don't assume the dashboard uses it yet. - Terminal (
app/terminal/page.tsx) is a real page now (server component, sample intel data) — no longer a RouteStub. Real-data wiring is later. - ClaimMeter =
@/components/vyndr/ClaimMeter(founder-seat scarcity), on the landing under the Hero.
VYNDR 2.0 Remaining Screens (Session 36 — Phase E)
- Dashboard lines —
lib/slateAdapter.js(parseAmericanOdds,detectBestLines,mapScheduleToGameCards) is the testable best/worst-line engine. The LEGACYcomponents/GameCard.tsx(used by the live Slate, with inline grading) was RESKINNED to render its game-lines grid viadetectBestLines(best = green tint + green left border, worst = subtle red)- SportBadge. IMPORTANT: the live Slate still uses the legacy GameCard, NOT
vyndr/GameCard— a full swap needs inline grading ported into the new component first (slateAdapter + vyndr/GameCard are ready for it).
- SportBadge. IMPORTANT: the live Slate still uses the legacy GameCard, NOT
- Real pages (were RouteStubs):
compare,invite,help,about. Only/notificationsis still a RouteStub (keep the Session-34 stub test in sync if you convert it). - Reskinned (logic preserved):
login(scanlines + "ACCESS THE SIGNAL"),pricing(+ ClaimMeter).accountredirects to/profile.
VYNDR 2.0 Mobile Parity (Session 37 — Phase F)
- Bottom tab bar =
components/BottomTabBar.tsx: 5 tabs (Slate/Terminal/ Scan/Ledger/More), Scan is the prominent raised grade-green action, More opens an integrated bottom sheet. Shown for ALL users (anon included — it's the only mobile nav); hidden on auth flows + landing viaHIDE_ON, and hidden ≥768px via the.mobile-tab-barrule in globals.css. The Nav hamburger is retired on mobile (the tab bar owns nav); the Nav's mobile panel is now dead code. - Mobile CSS lives in the "MOBILE PARITY" section of globals.css:
mainbottom-padding clears the 64px bar + safe-area <768px;.grade-hero(the GradeResultCard letter) → 80px <640px;.terminal-gridstacks <768px;.game-lines-gridhorizontal-scroll. Class hooks:grade-hero,terminal-grid. - PWA: manifest has shortcuts (Slate/Scan/Terminal) + categories
[sports,finance,productivity]; layout viewport sets
viewportFit: 'cover'. - BUILD GOTCHA: don't use
as conston heterogeneous config arrays (TABS) — it makes each entry a distinct literal type and optional props fail type-check. Use a shared interface. And the build worker exits code 1 on type errors — check the build EXIT CODE, not just a| tailof its output.
VYNDR 2.0 Systems (Session 38 — Phase G)
Testable CommonJS modules in lib/ + thin React glue:
lib/parlayMath.js— frontend correlation model (player 0.62 / team 0.34 / league 0.06 / cross-sport 0),parlayGradepenalty, grade→odds. Backend parlayService (S28) still owns server combined odds.lib/oddsFormat.js—fmtOdds(value, format). CRITICAL: only signed-int strings + integer numbers are odds; totals/lines/spreads pass through UNCHANGED (parseMoneyline). This is intentionally stricter than the prototype's parseAm (which would mis-convert 228.5) — keep it that way.lib/prefs.js—applyPrefssets<html data-*>(the S33 CSS layer keys off these), load/save tolocalStorage('vyndr_prefs').lib/liveTick.js— single tick store; never auto-starts (SSR/test-safe),start()runs the unref'd 1s interval,tick()emits a fresh state object.lib/checkout.js—checkoutUrl(plan).components/vyndr/LiveLayer.tsx(useLive/LiveNumber/HeartbeatBar) +GlobalHosts.tsx(mounted in layout — applies prefs, registerswindow.__prefs/__goPaywall/__checkout, hosts Prefs + Paywall modals).- Header is now 124px tall (nav 60 + ticker 32 + heartbeat 30): layout
mainpaddingTop = 124, Slate stickytop= 122. Keep them in sync if header changes. - GOTCHA: don't return a
Set.delete-based unsub directly fromuseEffect(returns boolean ≠ valid cleanup) — wrap as() => { unsub(); }.
VYNDR 2.0 Conversion COMPLETE (Session 39 — Phase H QA)
The 7-session design conversion (33–39) is done and parity-verified against §13.
- Parity invariants are locked by
tests/unit/vyndrParityQA.test.js— if you later add a glitch class to a data component, a raw#00ffb8/sport hex, a deadonClick={}, or break the gated-route list, that suite fails. Keep it green. - INTENTIONAL hex (do NOT "fix" to tokens):
var(--token, #fallback)fallbacks, Next metadatathemeColor, and the bespoke intel-surface/red-tint text shades (#e8fff4/#bdf5e2/#ff8a8a/#ff8b7a/#ffb0a4/#ffd9a8/#04140f) ported from the prototype — no token equivalent. - The GradeResultCard hero LETTER is intentionally
var(--sans)(display), matching the prototype; all grade DATA rows + the GradeBadge chip are mono. - DEFERRED (never in 33–39 scope): a true ⌘K command palette (Nav
›Query currently links to /scan). - TEST DE-FLAKE:
soccerFeatureExtractorCascadegetsjest.setTimeout(20000)— it falls through to live adapters on cache miss and flaked at Jest's 5s default under full-suite load (same family as the S32 pipeline test).
P0 Audit Fixes (Session 41 — non-obvious)
- THREE stat_type whitelists must stay in sync. A prop's
stat_typeis gated insrc/routes/analyze.js(/prop+/batch),src/routes/scan.js(parlay legs), ANDsrc/services/python/utils/validation.py. Adding a sport's stats to one without the others silently 400s. The S41 MLB bug was exactly this: Python had the MLB set; both Node gates didn't.mlbGrader.jsis DEAD CODE (required nowhere) — the live MLB grade path is the generic engine1 feature pipeline, which keys off the Python validator's exact stat names (rbi/runs/innings_pitched, NOTrbis/runs_scored/outs_recorded). tests/integration/analyze.test.jssits exactly at the 10-req/min IP rate limit (createRateLimit max:10, no reset hook). Adding any HTTP test there 429s the later cases. Put new analyze-route HTTP tests in a SEPARATE file (jest isolates module state per file → fresh limiter): seetests/integration/analyzeMlbStats.test.js.- Fonts are self-hosted via
next/font/google(layout.tsx): Inter→--font-sans, JetBrains_Mono→--font-mono, IBM_Plex_Mono→--font-ibm, all set on<html className>. globals.css:rootmaps--sans/--mono/--ibm-monoonto them. CRITICAL: next/font OBFUSCATES family names, so literal'JetBrains Mono'/'IBM Plex Mono'in CSS or inlinefontFamilyno longer resolve — always reference the variable. The oldfonts.googleapis.comCDN<link>is GONE (it 503'd in prod). ShareCard canvas still uses literal names (canvas can't read CSS vars) — knowingly left. - Redirect routes use server-component
redirect()fromnext/navigation(/settings→/profile,/report→/blog)./settings/securityis a REAL MFA enrollment page — do NOT clobber it into a redirect. - Profile tier reads from
useAuth().tier(the nav's source), not the/api/user/profilefetch, so the two can't disagree.
Player Intelligence System (Session 42 — non-obvious)
Built from the Claude Design "VYNDR Player Intelligence" bundle.
- Archetypes (41, NOT 45) — 15 NBA + 5 WNBA-unique + 15 MLB + 6 soccer. The
canonical registry is
src/services/archetypeService.js(ARCHETYPESkeyed by UPPERCASE name; classify/getArchetype). The frontend visual map (color + glyph SVG + desc) is duplicated inweb/src/lib/archetypes.jsbecause the browser can't import the backend; a test asserts the colors MATCH. Colors are unique WITHIN a sport but REUSED across sports (a TWO-WAY archetype is purple in NBA + WNBA) — don't "dedupe" them.classify(sport, stats)is a feature-scoring classifier →{ primary, secondary|null, blend: [{archetype, weight}] }. - StatStrip rule — player name appears ONCE; stats are horizontal mono runs.
Never stack the name per stat.
components/vyndr/StatStrip.tsx(compact + expanded).vyndr/GameCardprefersplayerStrips(grouped) over legacy per-prop rows; both kept for back-comat. - Stats API —
src/routes/stats.jsALREADY existed (filters/public/live); Session 42 ADDED/player/:name,/leaders,/game/:id(don't recreate the file). Aggregation logic is insrc/services/playerIntelService.js(testable; injectcacheGet). The name param is sanitized there (strip non-name chars, cap 60). Readsgrades:{sport}cache for a player's props. Frontend needs the Next proxy (app/api/stats/player|leaders/route.ts) — Express isn't reachable from the browser directly (same rule as S25). - Player links — always via
web/src/lib/playerHref.js→/player/:name?sport=. - GradeResultCard / gradeAdapter — new card sections (archetypeBlend, propDNA,
statContext, vyndrIntel) are OPTIONAL + self-hiding;
gradeAdapter.buildIntelFieldsonly populates them when the engine suppliesarchetype/season_avg/form/etc. They light up once the Session-43 data pipeline feeds them — no empty boxes now. - Settings —
/settingsis now a REAL page (replaced the S41 redirect). It LINKS to/settings/security(the real MFA page) — never overwrite that. Danger zone delete is gated ondeleteText === 'DELETE'; there's NO backend deletion endpoint yet, so the confirmed action surfaces an honest "email support" message rather than faking success. - Components added to the barrel: ArchetypeBadge, ArchetypeBlend, StatStrip,
BookChip (
@/components/vyndr). Book brand map =web/src/lib/books.js.
Data Pipeline Wiring (Session 43 — non-obvious)
- Dropdown z-index P0 — the
<nav>usesbackdrop-filter, which creates a stacking context. The living-layer bars (Ticker,HeartbeatBar) render as siblings AFTER it, so dropdowns that overflow below the 60px nav got covered. The nav carriesposition:relative; zIndex:2to float above them — don't remove it, and keep dropdown menus atzIndex:100. - MLB real stats —
mlbStatsAdapteris id-keyed; Session 43 addedsearchPlayer(name)(resolves via the cached season player list) +getPlayerStats(name).playerIntelService.resolvePlayerStats(name, sport)normalizes the raw statsapi.mlb.com object into the archetype classifier's input shape + display rows.getPlayerIntelclassifies from REAL stats now. Adapters are injectable (opts.mlbAdapter/opts.resolveStats) — tests never hit the network. NBA/WNBA usesnbaStatsClient(Python service, usually offline in prod → degrades tofound:false, NOT an error). - Grade-card intel —
analyzeViaEngine1.buildIntelFields(features)computes stat-context + form/usage/matchup/rest from the EXISTING feature vector (no extra I/O) andObject.assigns them onto the legacy result.gradeAdaptermaps them into the card. Archetype is deliberately NOT attached at grade time (the per-prop feature vector has no multi-stat season line) — that strip waits for the Session-44 pipeline. Keep grade-time I/O at zero (slate grades in tight loops; this is why archetype isn't fetched per-prop). - slateAdapter now emits
playerStrips(groupPropsByPlayer) +pitchers(mapPitchers) on every card. The LIVE slate still uses the legacycomponents/GameCard(which ignores those + rendersBookChipin its line grid); the full swap tovyndr/GameCardis still pending (needs inline grading ported).mapPitchersreturns undefined for non-MLB / no probables. - depthChartService (
getLineup/getDepthChart/getCascadeProjection) +/api/stats/lineup|depth|cascadeare the foundation; all graceful + injectable.matchesTeammust guard empty names (t.includes('')is always true) — and the schedulefindchecksg.home/g.away, not the game object.
VYNDR Original Archetypes + Make-It-Visible (Session 44 — non-obvious)
- Archetype names are VYNDR Originals (TORCH/BOMBER/ALPHA/…), NOT the old
descriptive labels. The registry keys in
archetypeService.js+ the map keys inlib/archetypes.jsare the new names; each carrieslegacyName/legacy(the old label, NEVER displayed).getArchetype()+archetypeInfo()+badgeStyle()resolve legacy names → the canonical VYNDR name (defends stale cached data). Theclassify()scorer objects use the new keys too — if you edit a threshold, use the new key. Full mapping is inBACKEND_HANDOFF.md. - MLB power merge: there's ONE power archetype (BOMBER) — it fires for any high-HR bat (incl. high-K sluggers like Judge → BOMBER). The old POWER PULL slot became WHIFF (strikeout-artist pitcher). Don't re-split power.
BACKEND_HANDOFF.md(repo root) is the canonical frontend↔backend data contract — update it in the same commit when an endpoint/component shape changes.- Grade-card intel gotcha: the engine→tierGating→/api/scan chain already
preserves the intel fields (everything spreads
...result/...data). The bug wasscan/page.tsxpassing a hardcoded field subset tomapScanToGradeResult— it must forward season_avg/last10_avg/form/usage/matchup_grade/rest/archetype /archetype_blend/prop_dna (andScanResponsemust type them) or the card sections stay hidden. - Stale games:
slateAdapter.isRelevantGame(game, now)drops completed games24h old;
Slate.filteredGamesapplies it. Schedule TTL is 60s. - GameCard swap is DEFERRED (Kev's call): the live Slate keeps the legacy on-demand "Read" card as a bridge until the snapshot pipeline lands. The on-demand grade model is being retired for a pre-graded snapshot model; the vyndr/GameCard (playerStrips/pitchers) is built for that and swaps in then. Don't swap it before the grades cache is populated (cards would be blank).
Active Skills
- vyndr-voice (all user-facing output)
- prop-analysis (grading methodology)
- monetization-system (scan-5 pitch, tier conversion)