Commit Graph

55 Commits

Author SHA1 Message Date
builtbykev 528cb1a6d0 Layer 1: Statcast mechanism-data ingestion (backfill + nightly refresh)
The data foundation for the archetype and projection layers, built as the
pattern every sport inherits. Layers 2 and 3 are not touched.

PHASE 0 GATE — both match rates measured live, both 100%. Batters 40/40;
PITCHERS 66/66 across five real rosters (CLE, DET, MIN, NYY, LAD) joined by
MLBAM id against the 713-pitcher Savant feed. Zero honest-absent on identity,
because the join is an integer both systems use natively — and the snapshot
pipeline already stores it per graded row.

SOURCE — five Baseball Savant CSV leaderboards, free and public, pulled with
axios and the CSV parser savantAdapter already runs in prod. pybaseball is
deliberately NOT used: it is an MIT wrapper over these same URLs, and adding it
would reintroduce a Python runtime in a stack where the existing Python service
is already offline. min=1 on every feed, not Savant's default min=q, so the
long tail arrives and OUR minimum-sample gate decides what is thin — explicit
and testable rather than silently dropped upstream.

Measured: 1,354 rows per season (604 batters, 750 pitchers), all five feeds in
about five seconds. Pitcher mechanism includes arm angle, GB/FB/LD, chase and
whiff; batters get exit velo, launch angle, barrel and hard-hit, chase and
z-swing. Handedness rides in free on the movement feed (677 pitchers); batter
handedness stays absent pending a roster join rather than being guessed.

BACKFILL AND REFRESH ARE THE SAME CALL — a full re-pull upserted on
(sport, season, source_id). Idempotent and self-healing: a missed night
self-corrects on the next run, with no incremental who-played bookkeeping to
drift out of sync. At 1,354 rows the simple thing is also the robust one.

HONESTY RULES, each with a test: a metric the feed did not carry is null and
never 0; a thin sample is STORED and flagged rather than dropped or inflated,
because thin and missing are different claims; an unjoined player is stored
with a null player_key and joins later; and if every feed comes back empty the
job REFUSES to write, so a bad night can never blank a good table.

Freshness is treated as a truth property. updated_at on every row, and the
scheduler pages on a failed run AND on silent staleness — a job that stops
being scheduled never produces a failure, so staleness has to alarm on its own.
Never-built is deliberately not stale: different condition, different fix, and
paging on a fresh install teaches the operator to ignore the alarm.

Nightly at STATCAST_HOUR_UTC (default 11 UTC, after every game is final), kill
switch STATCAST=0, and induce-able at POST /api/internal/statcast/refresh with
a freshness probe at /statcast/status — we verify a refresh by running it, not
by waiting for the slot.

Migration 030 applied. Promoted columns for the classification-critical metrics
plus a metrics JSONB carrying every raw field, so Layer 2 can reach something we
did not promote without a re-ingest. Raw per-pitch stays out of Postgres on
purpose: one season is ~0.85 GB against a 500 MB plan ceiling, and it is
re-pullable from the free source if Layer 3 ever needs it.

Pattern documented in docs/MECHANISM-DATA.md for NBA tracking and NFL Next Gen.

Tests 3581 passed / 292 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 21:37:30 -04:00
builtbykev 4e2f488341 Forward model_price_locked to the hero — a paywall was wearing poison's copy
Live induction on the deployed landing page caught this; markup review and the
unit suite both passed it. With the model leg stripped for anonymous visitors,
LiveHeroProp forwarded book/fair/model/ev/quarantine to PriceTriplet but NOT
model_price_locked, so deriveValueState fell through to the missing-model-price
branch and the card rendered "MODEL READ WITHHELD" — the quarantine state,
whose copy says we suppressed our own price because a leg is poisoned. Nothing
was poisoned. The real reason was the paywall, and the two must never share a
face: one says our data is untrustworthy, the other says you don't have this
tier.

Now forwarded, with a test asserting both the separation in deriveValueState
and the forwarding at the call site.

Tests 3559 passed / 291 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 19:56:21 -04:00
builtbykev aaa41134d4 Close the second leak path: /api/hero-prop bypassed the snapshot gate
The landing hero reads the snapshot from Redis DIRECTLY via heroPropService,
so it never passed through routes/snapshot.js and was still serving model_odds,
ev_pct, value and takeable to anonymous visitors after the first fix. Same
strip, same tier resolution, same private-cache rule for authenticated callers;
the Next proxy now forwards the bearer token.

PRODUCT CONSEQUENCE, FLAGGED RATHER THAN BURIED: the landing hero is served to
anonymous visitors, so it now renders BOOK and FAIR with the model leg LOCKED
instead of the full triplet it showed this morning. That follows the stated
free-tier rule exactly, but it does trade a strong marketing moment (VALUE
+21.1% VS FAIR on the shop window) for consistency of the gate. Reversing is
one line — add 'model_price' to the free tier in src/config/tiers.js, or
special-case the hero route — and is a product call, not a correctness one.

Tests 3557 passed / 291 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 19:52:00 -04:00
builtbykev fbcb00b7b1 Close the public model-price leak; wire the read card's price layer
PHASE 0.5 GATE — the three checks, and one correction.

`fairLine` does not exist. Zero hits across src/ and web/src. Option A as
written had no referent, but it resolves better than feared: `fair_odds` is
already a real de-vigged American price on every graded snapshot row, so there
is nothing to derive.

  Gate 1 (is it a price): PASS. fair_odds is American odds from
  impliedProbToAmerican inside devigTwoWay; fair_prob is the probability. Both
  distinct from `line`, the stat threshold.

  Gate 2 (numeric match): PASS, 8/8 exact. Recomputed fair_odds and fair_prob
  independently from the stored raw over/under prices; every value matched the
  stored one to the integer and to 3dp. Same de-vig, same numbers the component
  was proven against.

  Gate 3 (poison independence): PASS, and proven on the quarantined cohort
  itself. devigTwoWay's inputs are (over_odds, under_odds) — market prices
  only, no model term is reachable. The 8 rows recomputed above are all
  wrong_opponent_grade rows, and their fair prices reproduce exactly from the
  market. The poison is in the grade, not the price. Quarantine therefore
  suppresses the MODEL leg only; the fair leg stands, as designed.

THE LEAK WAS REAL AND ALREADY LIVE. GET /api/snapshot/:sport is public and
unauthenticated, and it was serving model_odds, p_win, ev_pct, value and
takeable to anonymous callers on every graded row — 25 of 25 on the live wnba
board. The Session-66 gate on /api/analyze was bypassed entirely by this
endpoint.

The strip covers more than model_odds, because model_odds is not the only way
to read the model price: p_win IS the price in another base, and ev_pct is
INVERTIBLE — ev is a function of p_win and book_odds, and book_odds is public,
so leaving ev behind hands the price over. All five model-derived fields go.
book_odds, fair_odds, fair_prob, overround and devig_method stay on every tier:
the fair leg is never the paywall. Rows that keep a book+fair pair are stamped
model_price_locked so a gated price is never mistaken for a missing one.

Tier comes from resolveTierFromRequest, which reads a bearer token when one is
present and otherwise returns 'free'. It FAILS CLOSED on every error path, so a
resolution failure can only ever withhold the price. The response now varies by
entitlement, so the /:sport handler downgrades Cache-Control to private for
authenticated callers and the browser proxy forwards the bearer token —
otherwise a CDN could hand a paid payload to an anonymous viewer, or every
request would look anonymous and paid users would lose the leg.

READ CARD — a manual scan carries no market. The request is {player, stat,
line, direction}, so the engine has no over/under prices to de-vig and
book_odds/fair_odds are legitimately absent from its response; that is why the
triplet was hidden there. lookupSnapshotPrices recovers them from the
pre-graded snapshot via the same cache-only read this route already performs
for locked odds and team. The join is exact on player + stat + line + side
(fair_odds is side-specific), and returns nothing unless book and fair are BOTH
present — a user-chosen line the board never graded has no market attached, so
the triplet stays hidden rather than borrowing another line's price.

FAIR-LEG ABSENCE, measured before shipping: 636 graded rows, 636 with book,
636 with fair, 0 one-sided. Absence rate 0.0%. The hero number is not a
sometimes-number on current data.

Tests 3556 passed / 291 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 19:47:42 -04:00
builtbykev f5156dd16d Un-claim CLV on the public profile; un-fabricate player-page FORM
Two Truth-Law fixes found by auditing the product logged-out.

FIX 1 — /u/[handle] claimed a "CLV-verified record" with "closing-line
value included" while ZERO closing-line value renders there. Verified
live: GET /api/profiles/vyndr returns beat_close_pct null (gated behind
CLV_CAPTURE_RELIABLE, unset while C4 is open). Eight instances found —
two of them (the OG + portrait "CLV-VERIFIED RECORD · 30D" eyebrows)
only by the post-removal residual sweep; two more printed the claim in
exactly the no-record branch.

Copy now describes what the page shows. The gated CLV-VERIFIED badge and
the BEAT CLOSE figure are removed from the public profile, OG card and
portrait card. DISPLAY ONLY: beat_close_pct, clvCaptureReliable() and
the whole CLV data path are untouched, and the earned directional badge
stays Analyst+Desk. The claim returns when CLV genuinely renders here.

Also fixes the doubled "· VYNDR · VYNDR" title (layout's '%s · VYNDR'
template already supplies the suffix); verified on composed output by
serving the build and reading the real HTML, not on source.

FIX 2 — the player page's FORM was `70 + 4 × (count of tonight's graded
props)`. Nothing on the HTTP path ever sets stats.form, so that fallback
WAS the live number: Josh Bell's "74" is 70 + 4×1 prop, confirmed
against his live payload. MATCHUP was gradeFromForm(that number), with a
hardcoded 'B' on the no-archetype branch — both fabricated letters with
no opponent input on the path. Systemic: buildIntel is the unconditional
path for every player and sport.

FORM and MATCHUP now render "—" (kind 'plain', so no bar width or colour
is computed off a null). gradeFromForm is deleted and the prop count is
no longer passed into buildIntel. computeFormScore's hardcoded 75 now
returns undefined. Induced across MLB/NBA/WNBA: all render cleanly, and
real values (USAGE 3.6 AB/G, REST B2B) still render.

Neither form value feeds the grade — engine1 reads raw l5_avg/l20_avg
against the line and never a form key; buildIntelFields decorates the
already-graded object. Grade inputs are byte-identical.

Held (needs a per-sport headline-stat design call): a real player-level
form metric + label disambiguation.

Tests 3491 passed / 289 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 17:05:04 -04:00
builtbykev b5d3fd14bb S11 (a1): live tracking — the read locked, the game watched
MLB statsapi + WNBA ESPN live boxscores -> per-player current values
(live:{sport}:{date} TTL 90s, /api/live/:sport + Next proxy). Pure
propState math (HIT / ON PACE / NEEDS N / HOLDS / LINE PASSED — never
red in-progress), attachLiveProgress strip join on nameKey+statType,
proximity-to-hit slate float, StatStrip LiveTracker in the ROW-GRAMMAR
outcome slot (spec amended + lock test updated). Grades never change
in-game — tracking, labeled as such. 2698 -> 2757 tests, web build 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:45:29 -04:00
builtbykev b20145c215 S10 (a1): public ledger profiles v1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:32:22 -04:00
builtbykev 2ae8a5697e Session 56: Full audit — PropLine + boxscore + pipeline + sport coverage (2289 tests)
Research (verified against live MLB Stats / ESPN / The Odds APIs):
- specs/propline-audit.md — every stat_type mapped against our 4-layer pipeline;
  real MLB boxscore fields; sport coverage status; pipeline gap analysis.
- specs/vyndr-roadmap.md — priority-ordered Sessions 57–64 + coverage targets.
- scripts/propline-audit.js + specs/audit-data/ (raw capture).

Headline bug: oddsNormalizer mapped batter_rbis → 'rbis' while the whole
grade/feature/outcome chain keys on 'rbi' — every PropLine RBI prop silently
failed to grade AND settle. Fixed (+ regression test).

Phase 4 — wired missing MLB stats end-to-end:
- PropLine MLB markets 6 → 12 (+runs, walks, doubles, earned_runs, hits_allowed,
  outs — same request, no extra quota).
- doubles/outs/triples added to featureCache + outcomeService MLB_LOG_FIELD and
  all three grade whitelists (analyze/scan/validation.py).

Phase 6 — pipeline resilience:
- opsNotify.js: ntfy alerts (never throws, test-disabled). Snapshot success/
  stale/failure alerts; retry-once on hard odds error (not on empty slate).
- Missed-cron watchdog (mostRecentExpectedSlot/isSnapshotOverdue); status probe
  now returns `overdue`.

Coverage truth: MLB is the only end-to-end-live sport; outcome settlement is
MLB-only (WNBA/NBA/soccer never settle) — documented as the #1 roadmap gap.

Backend 2276 → 2289 tests (+13). Web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:00:29 -04:00
builtbykev d09a06c054 Session 55: Self-learning loop + real-time layer (2274 tests)
Product overhaul core — the two transformative, differentiated systems:

Self-learning loop (Phase 2): outcomeService settles locked snapshot grades
against real MLB Stats API results → hit/miss/push, rolling accuracy by grade
tier (30d window). Idempotent, injectable, unit-tested. New GET /api/accuracy +
/api/ledger/accuracy + internal settle triggers + cron hook. AccuracyBadge
(dashboard/scan/landing) is honest — "LEARNING" below MIN_SAMPLE, never a fake
number. Settled HIT/MISS chips overlay the live slate.

Real-time layer (Phase 1): Slate silent 60s auto-refresh (no flash, no wipe on
transient blips) + "SIGNAL LIVE · UPDATED Xs ago" freshness strip; Ticker LIVE
badge that flashes on fresh events.

Landing (Phase 3): TopSignals shows tonight's real top-3 A-rated grades + live
accuracy — the product shown, not described.

Founder pricing: FOUNDER_CODE_EXPIRY default 2026-06-30 → 2026-12-31 (had
lapsed, disabling every founder code + the ClaimMeter pitch). That expiry — not
a tier change — was the real cause of the 4 stripe test failures.

Backend 2255 (4 failing) → 2274 (all green; +19 new, +4 fixed). Web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 15:39:13 -04:00
builtbykev 8629021774 Session 54: Audit cleanup — name edges + polish (2255 tests)
P1 name edge cases (BOTH playerName.js copies, kept identical):
- normalizeName strips hyphens (display+key): "Jung-hoo Lee" === "Jung Hoo Lee".
- nameKey strips single-letter MIDDLE tokens: "Josh H Smith" === "Josh Smith"
  (keeps first+last; real middle names + collapsed initials untouched).
- richie -> richard added to NICKNAMES.

P2 polish:
- Team Hub names normalized at the source (teamService.getTeamHub) so
  "J.C. Escarra" renders as "JC Escarra" like the dashboard.
- snapshotService dedup keeps the highest-confidence GRADE but the richest
  DISPLAY (accented "José" over "Jose") so prop rows match the pitcher line.
- correlationWarning names the game: "2 legs from the same game (NYY @ BOS)".

Backend 2246 -> 2255 tests (+9), 194 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:45:07 -04:00
builtbykev b012da13f8 Session 53: Social preview fix — OG meta tags + image (2246 tests)
The link preview still showed engineer-speak ("Bayesian intelligence", "kill
conditions") because the OG meta tags were never updated after the S44 landing
cleanup.

- layout.tsx: de-jargoned the openGraph + main description + titles → "Pre-graded
  player props with proprietary archetypes. Correlation-aware Parlay Lab.
  Real-time line movement tracking. Built in Detroit." (Used "proprietary
  archetypes" not "45" — codebase has 41.)
- NEW app/opengraph-image.tsx (+ twitter-image.tsx re-export): dynamically
  generated 1200x630 card — VYNDR wordmark, "The books have every advantage. /
  We built this to give it back." + feature row. Node runtime (not edge —
  self-hosted standalone). Both routes prerender to a real PNG.
- Removed the stale /og-image.png metadata ref so the file convention owns the
  image (no duplicate og:image tag).

Backend 2239 -> 2246 tests (+7), 193 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:25:05 -04:00
builtbykev cdedecf55b Session 52: Coming Soon teaser + infrastructure verification (2239 tests)
Phase 1 — Push-to-Book teaser (feature not live; teaser only):
- StatStrip: "BOOK IT ⟶" per graded prop (hover: "Push-to-Book coming soon").
- GradeResultCard: "PUSH-TO-BOOK · COMING SOON" footer.

Phase 2 — infrastructure verification:
- snapshotScheduler logs armed AND disarmed state (incl SNAPSHOT_CRON) so
  container logs disambiguate off-vs-crashed.
- NEW GET /api/internal/snapshot/status (internal-key gated): cron_armed,
  cron_hours_utc, last_snapshot per sport (gradeCount/deltaCount), redis_keys
  existence map, ticker_count. The post-deploy pipeline health probe.
- Finding: Redis AOF/RDB persistence is a server-side (Coolify) config the app
  can't set/verify — documented.

Phase 3 — delta pipeline (verified sound, no fix needed):
- runSnapshot already rotates :latest->:previous and diffs locked lines; added
  opt-in SNAPSHOT_DEBUG=1 [deltas] log + a trace test asserting :previous is
  preserved verbatim and the delta math is correct.

Backend 2234 -> 2239 tests (+5), 192 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:55:39 -04:00
builtbykev f0674ca07d Session 51: Complete Team Hub (2234 tests)
Research-depth team view: /team/[abbr] with roster, archetypes, stats, props.

- Team API: mlbStatsAdapter.getTeams/resolveTeam/getTeamRoster (statsapi, abbr→id
  + active roster, cached). teamService.getTeamHub assembles roster → per-player
  season stats (bounded concurrency) + archetype (snapshot grade or classify) +
  tonight's graded props from grades:{sport}; whole hub cached 15min. MLB real;
  NBA/WNBA graceful snapshot roster. GET /api/team/:abbr (404 unknown) + proxy.
- Team Hub page: server page.tsx (generateMetadata) + TeamHub client — header,
  sort (archetype/graded/A-Z), archetype filter chips, roster rows (archetype +
  player link + position + stats + graded props + parlay "+"), "No active props"
  greyed state, loading/error.
- Game cards: team abbreviations are now TeamLinks → /team/:abbr?sport= (green
  hover, stops propagation). Team Hub has "← Back to Slate".

Backend 2215 -> 2234 tests (+19), 190 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:55:10 -04:00
builtbykev f1956dc953 Session 50: Complete Parlay Lab (2215 tests)
Correlation-aware combined parlay grading — the Desk-tier differentiator.

- Correlation model (parlayService.js, added to S28 funcs): correlationScore
  (game-aware 0.7/0.4/0.2/0.0), combinedGrade (avg penalized by avgCorr*0.5),
  estimatedPayout (fair-odds product * (1-avgCorr) discount), correlationWarning,
  gradeParlay.
- POST /api/parlay/grade (public, 2-6 legs) -> {combined,correlation,payout,legs}.
  Fixed the Next proxy (was forwarding to /api/scan/parlay).
- ParlayContext: legs gained team/game/archetype; tier-aware maxLegs; auto-grades
  the slip (debounced) when legs>=2 -> live combined/correlation/payout; hasLeg/
  legKey/atCap.
- "+" button on every graded prop: StatStrip onAddLeg/isLegActive, wired by
  vyndr/GameCard via useParlay (builds leg w/ team + game). GradeResultCard feeds
  the same context from the scan page.
- ParlayPanel (replaces legacy ParlayTray): bottom slide-up w/ legs, combined
  grade, correlation warning, est payout, CLEAR ALL + floating leg-count badge.
  Tier-gated: free 2 legs (payout blurred -> Desk upsell), Analyst 4, Desk 6.

Backend 2185 -> 2215 tests (+30), 187 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:25:14 -04:00
builtbykev 3b47b783dc Session 49: Complete onboarding flow + name micro-fixes (2185 tests)
Name micro-fixes (close the normalization arc):
- collapseInitials merges "J C Escarra" -> "JC Escarra" (display + key); both
  playerName.js copies. Added mickey:michael nickname.

Onboarding flow (end-to-end, complete):
- Storage: Supabase user_metadata.preferences (no migration).
- API: src/routes/preferences.js GET/POST (requireAuth, admin getUserById/
  updateUserById, partial merge + sanitize) + Next /api/preferences proxy.
- Page: web onboarding/page.tsx — 3 steps (sports >=1 / books skip / bankroll
  presets+custom+skip) -> SIGNAL ACTIVE -> POST onboarding_complete:true -> 2s
  -> /dashboard. Redirects to login when unauthenticated.
- Redirect: dashboard fetches /api/preferences fresh; new+incomplete users
  (created_at >= cutoff) -> /onboarding; never while auth loading; existing
  users exempt.
- Personalization: Slate default tab = prefs.sports[0]; preferred books glow in
  the card lines grid (lib/books isPreferredBook, threaded dash->Slate->GameCard).
- Settings: PREFERENCES section loads + edits + saves sports/books/limit.

Backend 2156 -> 2185 tests (+29), 184 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:20:55 -04:00
builtbykev 91b03c4044 Session 48: Name normalization at every layer + usage field (2156 tests)
Trace-first: the normalizer functions were correct (S47) but raw names still
flowed through paths that skipped them. Fixed each leaking path.

- 2a (source chokepoint): snapshotService.runSnapshot normalizes each grade's
  player to the de-dotted display AND dedupes to one grade per nameKey|stat
  (highest confidence) before writing grades:{sport} + snapshot:latest. Every
  consumer (GameCard, Explore, leaders, profile) now gets clean merged names.
- 2b: buildPlayerStripsFromProps dedupes a player's props by stat (graded >
  awaiting) → one row per stat (kills "Ks 5.5 AND Ks 3.5" variant dupes).
- 2c: scan tonightsPlayers grid groups by nameKey, displays normalized name.
- 3: profile VYNDR INTELLIGENCE "+0%"/"—" was buildIntel's defaults (separate
  from the grade card's buildIntelFields, which already works). resolvePlayerStats
  now attaches real usage (AB/G) + rest (B2B/Xd); buildIntel renders them; REST
  default is now "—".

Backend 2149 -> 2156 tests (+7), 181 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 02:42:13 -04:00
builtbykev 78db55d499 Session 47: Name normalization + grade intel + ticker polish (2149 tests)
- Name normalization completed: NICKNAMES table (Matt↔Matthew, Mike↔Michael...)
  resolved in nameKey, parenthetical team-tag strip "(STL)", verified accent-fold
  (Iván/Ivan, José/Jose). Slate strip now DISPLAYS the normalized de-dotted name
  ("AJ Ewing" not "A.J. Ewing") via buildPlayerStripsFromProps.
- Complete MLB VYNDR INTELLIGENCE: mlbGameLogFeatures derives rest_days (days off
  between latest games; 0=B2B) + ab_per_game (usage). buildIntelFields renders
  usage as "X AB/G", rest as B2B/Xd, matchup from bvp_advantage fallback.
- Ticker SCAN dedup: pushTickerItems keeps one SCAN per sport (sport field or
  text-prefix parse for legacy); MOVE/GRADE preserved; cap 50.
- BOMBER threshold prorated for mid-season (hr>=15 strong / >=10 mod) so June
  sluggers classify BOMBER not FLEX/DRIVER.

Backend 2122 -> 2149 tests (+27), 179 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 01:45:36 -04:00
builtbykev c8fc9f577e Session 46: Grade card intel + name normalization + pitchers (2122 tests)
Three focused P1 fixes on the Session-45 snapshot model.

- Grade card intel ROOT CAUSE: gameLogService is NBA/WNBA-only (offline Python),
  so MLB props never got l5_avg/l20_avg and buildIntelFields returned {}. Wired
  MLB game logs into featureCache.gameLogFeatures via mlbStatsAdapter.getPlayerStats
  (pure mlbGameLogFeatures + MLB stat_type->field map). buildIntelFields gained
  playerStats/projection fallbacks for partial intel.
- Player name normalization: src/utils/playerName.js (+ web/src/lib copy):
  normalizeName -> {display,key}. Strips periods, de-dots suffix, accent-folds
  the key. Applied in snapshotService grouping, slateAdapter grade index +
  player-strip merge (variants collapse, longest name shown), and
  playerIntelService. "A.J. Ewing"/"AJ Ewing" + "Jazz Chisholm"/"Jr." now merge.
- MLB starting pitchers: new GET /api/schedule/:sport/pitchers (probablePitchers
  service wrapping mlbStatsAdapter.getScheduleWithPitchers + best-effort ERA).
  Slate fetches it, builds a team->pitcher map (full name + mascot match),
  attaches pitchers to MLB GameCardData. + Next proxy.

Backend 2100 -> 2122 tests (+22), 176 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:56:26 -04:00
builtbykev f8b120c0aa Session 45: Snapshot pipeline + GameCard swap + live ticker (2100 tests)
The on-demand "Read" grade model is RETIRED. A scheduled pipeline pre-grades the
slate, locks grades to the line, tracks movement; the dashboard shows them already
there. Orchestrates existing services — nothing rebuilt.

- snapshotService.runSnapshot(sport): getOdds → gradeAndCacheSlate → classify
  archetype per player → lock gradedAt → line deltas vs previous snapshot → write
  snapshot:{sport}:latest/previous + grades:{sport} → ticker events. Fully
  injectable, zero-network unit tests. runAllSnapshots = cron entrypoint.
- Internal trigger POST /api/internal/snapshot/:sport + /all (requireInternalAuth).
  In-process cron (SNAPSHOT_CRON=1, UTC 14,19,22,1,3) in server.js, no new dep.
- Public reads: GET /api/snapshot/:sport (cache-only) + GET /api/ticker (merges
  TICKER_MANUAL pins) + Next proxies.
- GameCard swap: live Slate renders vyndr/GameCard (legacy kept for types only),
  overlays locked grades onto game props → player name once + archetype badge +
  "Graded Xh ago at -115 · Current 2.5 · ▲ TOWARD +1.0". Ungraded → "Awaiting next
  scan", NO Read button. On-demand onGrade flow deleted.
- Ticker polls /api/ticker every 30s, graceful fallback to hardcoded items.
- NBA/WNBA: espnStatsAdapter free fallback (defensive parse → found:false on shape
  mismatch) wired into resolvePlayerStats after the offline Python service.

Env: PROPLINE_API_KEY_1/2/3, VYNDR_INTERNAL_KEY, SNAPSHOT_CRON=1, TICKER_MANUAL.
Backend 2061 -> 2100 tests (+39), 173 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:34:29 -04:00
builtbykev 7969a4971a Session 44: Make it visible — VYNDR archetype names, grade intel, schedule fix, landing page (2061 tests)
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>
2026-06-18 20:15:38 -04:00
builtbykev 80683e71b4 Session 43: Data pipeline + audit fixes + depth chart foundation (2045 tests)
P0 fixes + wiring real data into the S42 Player Intelligence architecture.

- P0 dropdown z-index: the nav's backdrop-filter stacking context let the
  Ticker/HeartbeatBar paint over the avatar/More dropdowns and eat clicks.
  nav now position:relative zIndex:2; menus zIndex:100. Avatar Settings ->
  /settings.
- Real MLB stats: mlbStatsAdapter.searchPlayer + getPlayerStats (name->id->
  season+gamelog). playerIntelService.resolvePlayerStats normalizes into the
  archetype classifier; getPlayerIntel returns found:true + real season +
  archetype classified from real stats. NBA via nbaStatsClient (degrades).
- Game cards: slateAdapter.groupPropsByPlayer (playerStrips, name once) +
  mapPitchers (MLB probables), folded into mapScheduleToGameCards. Legacy
  GameCard line grid renders BookChip (brand colors) not grey text.
- Grade card intel: analyzeViaEngine1.buildIntelFields computes stat-context +
  form/usage/matchup/rest from the existing feature vector (zero extra I/O);
  gradeAdapter lights up the card sections. Archetype deferred (needs season
  line at grade time).
- Depth chart foundation: depthChartService (getLineup/getDepthChart/
  getCascadeProjection) + /api/stats/lineup|depth|cascade, graceful + injectable.
- Mobile: player hero name overflow-wrap + 24px on <=640px (was clipping).

Backend 2011 -> 2045 tests (+34), 163 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:38:06 -04:00
builtbykev 8bc79f3c38 Session 42: Player Intelligence System — archetypes, stat strips, player profile, enhanced cards (2011 tests)
Built from the Claude Design "VYNDR Player Intelligence" bundle (10 sections).

- Archetypes: src/services/archetypeService.js — 41 archetypes (15 NBA / 5
  WNBA-unique / 15 MLB / 6 soccer), classify -> primary+secondary+blend.
  Frontend visual map web/src/lib/archetypes.js (colors verified == backend).
  ArchetypeBadge (full/ghost/tint + glyphs) + ArchetypeBlend (DNA bar).
- StatStrip (compact/expanded): player name once, horizontal mono stats,
  inline GradeBadge props, onPlayerClick -> profile.
- Stats API: extended src/routes/stats.js with /player/:name, /leaders,
  /game/:id (rate-limited). Aggregation in playerIntelService.js (sanitizes
  name param; grades cache; graceful on cold cache). Next proxies added.
- Player Profile /player/[name]: all 9 design sections, graceful empty states.
- Enhanced GameCard (MLB pitchers + player-grouped StatStrips) + GradeResultCard
  (archetype strip + stat context + VYNDR intelligence, optional/self-hiding via
  gradeAdapter.buildIntelFields). Player-name links wired everywhere.
- Settings page replaces the S41 redirect (account/subscription/notifications/
  display/responsible-play/danger-zone with DELETE-gated delete). LINKS to the
  real /settings/security MFA page — does not replace it. + BookChip.
- Bonus: Stats Explorer /explore (real /api/stats/leaders leaderboard); added
  Explore + Settings to Nav MORE.

Deferred (need data pipelines, Session 43): Team Hub, Offseason Intel, Slate
redesign, Stats Explorer sub-panels.

Backend 1940 -> 2011 tests (+71), 157 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:12:24 -04:00
builtbykev 32069863dc Session 41: P0 audit fixes — MLB stat_types, broken routes, tier mismatch, self-hosted fonts (1940 tests)
- Backend: whitelist MLB stat_types in analyze.js + scan.js gates (mirrors
  python validation.py); fixes MLB scans 400ing.
- Routes: /settings -> /profile, /report -> /blog redirect pages.
- Profile: read tier from useAuth().tier (nav's source) to kill the
  Free-vs-DESK mismatch.
- Fonts: self-host Inter/JetBrains Mono/IBM Plex Mono via next/font, drop the
  503ing fonts.googleapis.com <link>; rewire literal font-family refs to vars.
- Kept /settings/security (real MFA page) intact — NOT clobbered to a redirect.
- +33 tests (1907 -> 1940), 149 suites; web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:55:40 -04:00
builtbykev a3351e2135 Sessions 29-30: Content templates + PropLine 3-key adapter + MLB Stats API + ESPN summary (1694 tests) 2026-06-14 22:29:01 -04:00
builtbykev 927c4a5c65 Session 29: Content generation templates — slate threads, POTD, recaps, matchup previews (1660 tests) 2026-06-13 21:30:57 -04:00
builtbykev c48aecd510 Session 28: Parlay builder, line movement tracker, book comparison — 3 features, zero credits (1623 tests) 2026-06-13 12:37:08 -04:00
builtbykev 66fafd8429 Session 27: PWA autopilot — NetworkFirst cache policy, stale bucket cleanup, offline fallback, push helper, manifest polish, tier fix (1584 tests) 2026-06-13 10:57:49 -04:00
builtbykev f8a51cd9d0 Session 26: Cross-sport tab counts, scan copy fix, game card visual polish, empty section auto-hide (1579 tests) 2026-06-12 20:18:55 -04:00
builtbykev 956cdb863a Session 25: Fix all data rendering — proxy routes, Tank01 normalizer, box-score bridge, inline streaks (1579 tests) 2026-06-12 17:58:55 -04:00
builtbykev 433e827103 Session 24: Connect everything — Slate wired to all sources, copy fixed, nav fixed, startup prefetch, language button removed (1571 tests) 2026-06-12 15:45:19 -04:00
builtbykev 0538205fab Session 23: All-day intelligence layer — schedule, game lines, streaks, hot lists, stat filtering, ParlayAPI dead (1567 tests) 2026-06-12 11:16:58 -04:00
builtbykev 6ab49d4c37 Session 22: Tracker-driven quota guard, configurable cache TTL (1hr default), opt-in odds prewarmer (1505 tests) 2026-06-12 02:41:51 -04:00
builtbykev ea848e327e Session 21: All adapters through gateway, ntfy alerts, provider registry correction (1486 tests) 2026-06-12 02:06:22 -04:00
builtbykev 9b10bb4138 Session 20: Provider intelligence — quota tracker, gateway with fallback cascade, admin quota dashboard (1476 tests) 2026-06-12 00:54:39 -04:00
builtbykev 56392ec8f4 Session 19: Sports design overhaul — player cards with headshots, game card redesign, scan page tonight's players, odds diagnostic logging, tier gate utility (1444 tests) 2026-06-12 00:30:13 -04:00
builtbykev 0e3839a90a Session 18: Admin dashboard + Tank01 prefetch endpoint (1443 tests) 2026-06-11 22:29:38 -04:00
builtbykev beaf8b2a61 Session 17: Audit response — checkout 401 fix, hero prop 404 fix, Slate parsing fix, ALL tab cascade isolation, cookie/nav/footer/autocomplete polish (1438 tests) 2026-06-11 21:22:59 -04:00
builtbykev 73b65a0248 Session 16: Live hero prop, sport-specific markets fix, soccer weather, Sentry CSP (1429 tests) 2026-06-11 18:15:25 -04:00
builtbykev 167996d99a Session 15: Intelligence hardening — park factors, weather, Tank01 prefetch, pace factors, signal audit, founder pricing fix (1405 tests)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 16:21:18 -04:00
builtbykev f5d79cf70d Session 14: Africa checkout, Tank01 NBA/MLB wiring, WNBA+MLB odds proxies, OAuth icons, loading skeletons (1330 tests) 2026-06-11 10:06:49 -04:00
builtbykev 10159209fa Session 13: The Slate, Africa geo-restriction, OAuth providers, PropRow + GameCard (1311 tests) 2026-06-11 03:48:07 -04:00
builtbykev d957dee17b Session 12: i18n (10 languages, cookie-based), Africa tier .99, locale switcher, RTL Arabic (1305 tests) 2026-06-10 22:24:40 -04:00
builtbykev e5c45ecc8e Session 10: Internal auth refactor, prefetch cascade keys, Sentry, welcome email (1286 tests) 2026-06-10 20:45:05 -04:00
builtbykev b55dcbd614 Session 9: api-football + FootApi + Tank01 adapters, grace period middleware, cookie consent, /pricing page, OOM fix documented (1240 tests) 2026-06-10 19:41:37 -04:00
builtbykev 4db1c1c539 Session 8: Frontend Stripe cutover, soccer pages, sport selector, grade result cards, beta badge 2026-06-10 15:34:23 -04:00
builtbykev ad5ea8d5a8 Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total) 2026-06-10 14:50:13 -04:00
builtbykev b9084408bf Session 7i: Stripe test coverage gaps filled, dual-provider cutover documented (1042 tests) 2026-06-10 13:55:59 -04:00
builtbykev d4e5e76452 Session 7h: Stripe products, tier config, scan limits, response gating, free tier 2026-06-10 13:24:11 -04:00
builtbykev 4e18eb1efe Sessions 7e-7g: Grading path unified - adapter, computeFeatures, analyzeViaEngine1, all routes migrated, dead code removed 2026-06-10 10:23:55 -04:00
builtbykev 4815ceac03 Sessions 7e+7f: Grade adapter, normalize consolidation, computeFeatures, analyzeViaEngine1, scan/parlay migrated to engine1 2026-06-10 09:28:30 -04:00