7969a4971a
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>
420 lines
26 KiB
Markdown
Executable File
420 lines
26 KiB
Markdown
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)
|
||
1. Unit tests pass
|
||
2. Integration tests pass
|
||
3. Acceptance criteria met (from spec)
|
||
4. PR description written
|
||
5. 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-game `hasOdds`/`hasGameLines` flags 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 by `rosterLogs.js` (prefetch blob, else Redis SCAN
|
||
over `gamelogs:{sport}:{player}:{count}`). Empty roster = valid empty state.
|
||
- `?stat=` filters narrow streaks/hotlist; categories in `config/statFilters.js`
|
||
(mirror `web/src/config/statFilters.ts`). Discovery: `/api/stats/filters/:sport`.
|
||
- Dead providers: set `status: 'dead'` in `config/providers.js` to 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 keys
|
||
`PROPLINE_API_KEY_1/2/3`, 3,000 req/day FREE, rotates per-key; registry
|
||
`propline` priority 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 a `provider` field.
|
||
PropLine is The-Odds-API-compatible → reuses `utils/oddsNormalizer`.
|
||
MLB market keys (`batter_hits`, `pitcher_strikeouts`, …) were added to
|
||
`MARKET_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). Registry `mlb-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 via `analyzeViaEngine1` (engine1 is direction-aware — keeps the
|
||
higher-confidence side), sorts by confidence desc, writes
|
||
`grades:{sport}` = `{ grades, updated_at, source }` (TTL 2h). The legacy
|
||
grade shape already matches `normalizeGrade` — 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 by `shouldGradeSlate()`: ON by default, OFF when
|
||
`NODE_ENV==='test'` (its feature-compute fan-out would pollute call-count
|
||
assertions), override with `GRADE_SLATE_ON_FETCH=1`/`0`. `0` is 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-in` from
|
||
opacity .6) so a paused frame is never invisible.
|
||
- **Shared components**: `@/components/vyndr/*` (Wordmark, GradeBadge,
|
||
SportBadge, TerminalInput, SectionHead, VBtn, Card, Sparkline, Ticker), helpers
|
||
in `web/src/lib/vyndrTokens.js`. The NEW Wordmark is `.wm` markup at
|
||
`@/components/vyndr/Wordmark`; the legacy `@/components/Wordmark` (`.wordmark`)
|
||
is still used by Nav until pages convert. `vyndrTokens.js` is 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,
|
||
and `middleware.ts` is locale-only — a server middleware can't read it. The
|
||
gate uses `useAuth().loading/user` + `isGatedRoute()` and redirects to
|
||
`/login?next=<path>` (the param `/login` already 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.tsx` translates
|
||
`#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/vyndr` Wordmark
|
||
(`.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 layout
|
||
`main` paddingTop 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 client `NotFoundActions` so 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 via `lib/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 cast
|
||
`mapScanToGradeResult(...) as GradeResultData` at 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, `markReadComplete`
|
||
reads tracking, and `noopener noreferrer` sportsbook 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 live `dashboard`/`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 LEGACY `components/GameCard.tsx` (used by the live Slate, with
|
||
inline grading) was RESKINNED to render its game-lines grid via
|
||
`detectBestLines` (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).
|
||
- **Real pages** (were RouteStubs): `compare`, `invite`, `help`, `about`. Only
|
||
`/notifications` is 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). `account` redirects 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 via `HIDE_ON`, and hidden ≥768px
|
||
via the `.mobile-tab-bar` rule 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: `main`
|
||
bottom-padding clears the 64px bar + safe-area <768px; `.grade-hero` (the
|
||
GradeResultCard letter) → 80px <640px; `.terminal-grid` stacks <768px;
|
||
`.game-lines-grid` horizontal-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 const` on 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 `| tail` of 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), `parlayGrade` penalty, 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`** — `applyPrefs` sets `<html data-*>` (the S33 CSS layer keys
|
||
off these), load/save to `localStorage('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, registers
|
||
`window.__prefs`/`__goPaywall`/`__checkout`, hosts Prefs + Paywall modals).
|
||
- Header is now 124px tall (nav 60 + ticker 32 + heartbeat 30): layout `main`
|
||
paddingTop = 124, Slate sticky `top` = 122. Keep them in sync if header changes.
|
||
- GOTCHA: don't return a `Set.delete`-based unsub directly from `useEffect`
|
||
(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 dead
|
||
`onClick={}`, or break the gated-route list, that suite fails. Keep it green.
|
||
- INTENTIONAL hex (do NOT "fix" to tokens): `var(--token, #fallback)` fallbacks,
|
||
Next metadata `themeColor`, 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: `soccerFeatureExtractorCascade` gets `jest.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_type` is
|
||
gated in `src/routes/analyze.js` (`/prop` + `/batch`), `src/routes/scan.js`
|
||
(parlay legs), AND `src/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.js` is 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`, NOT `rbis`/`runs_scored`/`outs_recorded`).
|
||
- **`tests/integration/analyze.test.js` sits 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): see
|
||
`tests/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 `:root` maps `--sans`/`--mono`/
|
||
`--ibm-mono` onto them. CRITICAL: next/font OBFUSCATES family names, so literal
|
||
`'JetBrains Mono'`/`'IBM Plex Mono'` in CSS or inline `fontFamily` no longer
|
||
resolve — always reference the variable. The old `fonts.googleapis.com` CDN
|
||
`<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()` from `next/navigation`
|
||
(`/settings`→`/profile`, `/report`→`/blog`). `/settings/security` is 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/profile` fetch, 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` (`ARCHETYPES` keyed by
|
||
UPPERCASE name; classify/getArchetype). The frontend visual map (color + glyph
|
||
SVG + desc) is duplicated in `web/src/lib/archetypes.js` because 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/GameCard` prefers `playerStrips` (grouped) over legacy
|
||
per-prop rows; both kept for back-comat.
|
||
- **Stats API** — `src/routes/stats.js` ALREADY existed (filters/public/live);
|
||
Session 42 ADDED `/player/:name`, `/leaders`, `/game/:id` (don't recreate the
|
||
file). Aggregation logic is in `src/services/playerIntelService.js` (testable;
|
||
inject `cacheGet`). The name param is sanitized there (strip non-name chars,
|
||
cap 60). Reads `grades:{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.buildIntelFields`
|
||
only populates them when the engine supplies `archetype`/`season_avg`/`form`/etc.
|
||
They light up once the Session-43 data pipeline feeds them — no empty boxes now.
|
||
- **Settings** — `/settings` is 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 on `deleteText === '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>` uses `backdrop-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 carries `position:relative; zIndex:2` to float above them — don't
|
||
remove it, and keep dropdown menus at `zIndex:100`.
|
||
- **MLB real stats** — `mlbStatsAdapter` is id-keyed; Session 43 added
|
||
`searchPlayer(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. `getPlayerIntel` classifies from REAL stats now.
|
||
Adapters are injectable (`opts.mlbAdapter`/`opts.resolveStats`) — tests never
|
||
hit the network. NBA/WNBA uses `nbaStatsClient` (Python service, usually
|
||
offline in prod → degrades to `found: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) and `Object.assign`s them onto the legacy result. `gradeAdapter`
|
||
maps 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 legacy
|
||
`components/GameCard` (which ignores those + renders `BookChip` in its line
|
||
grid); the full swap to `vyndr/GameCard` is still pending (needs inline grading
|
||
ported). `mapPitchers` returns undefined for non-MLB / no probables.
|
||
- **depthChartService** (`getLineup`/`getDepthChart`/`getCascadeProjection`) +
|
||
`/api/stats/lineup|depth|cascade` are the foundation; all graceful + injectable.
|
||
`matchesTeam` must guard empty names (`t.includes('')` is always true) — and
|
||
the schedule `find` checks `g.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
|
||
in `lib/archetypes.js` are the new names; each carries `legacyName`/`legacy`
|
||
(the old label, NEVER displayed). `getArchetype()` + `archetypeInfo()` +
|
||
`badgeStyle()` resolve legacy names → the canonical VYNDR name (defends stale
|
||
cached data). The `classify()` scorer objects use the new keys too — if you
|
||
edit a threshold, use the new key. Full mapping is in `BACKEND_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
|
||
was `scan/page.tsx` passing a hardcoded field subset to `mapScanToGradeResult`
|
||
— it must forward season_avg/last10_avg/form/usage/matchup_grade/rest/archetype
|
||
/archetype_blend/prop_dna (and `ScanResponse` must type them) or the card
|
||
sections stay hidden.
|
||
- **Stale games:** `slateAdapter.isRelevantGame(game, now)` drops completed games
|
||
>24h old; `Slate.filteredGames` applies 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)
|