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
MISSION 1 — real sportsbook wordmarks (kills lowercase "betmgm"):
- books.js: add the 6 missing ALLOWED_BOOKS keys (fanatics/bet365/
hardrockbet/betrivers/pointsbet/pinnacle) with real brand names +
colors — no live book falls to neutral gray. Add `slug` fields +
bookSlug()/hasBookSvg() + BUNDLED_BOOK_SVGS.
- Bundle 8 self-authored styled-text wordmark SVGs under
web/public/books/{slug}.svg (draftkings/fanduel/betmgm/caesars/
bet365/pinnacle/hardrockbet/betrivers). NOT copied trademarked logo
glyphs — the book's NAME in brand weight+color; official press-kit
art can drop into the same paths with zero code change.
- BookWordmark: render the local SVG when bundled, else the brand-color
styled-text fallback (never a broken image; never a lowercase key).
- Import BookWordmark into the ledger row (page.tsx:368) + the identical
public-profile row, replacing bare {row.book} text. vyndr/GameCard
line-grid book cell now proper-cases via bookInfo().name (keeps the
preferred-book green highlight).
MISSION 2 — team-logo coverage gaps:
- teamMeta.js: add ESPN-schedule ball-sport abbr aliases the feed emits
that fell to monograms — SA→SAS, NY→NYK, WSH→WAS, BRK→BKN (NBA),
CONN→CON (WNBA). Real-abbr-first lookup means MLB WSH (Nationals) +
WNBA NY (Liberty) still resolve directly; NY in MLB stays null.
Tests: new tests/unit/bookWordmark.test.js (all 10 ALLOWED_BOOKS resolve
to a real brand+non-gray color; 8 bundled SVGs exist; BookWordmark
SVG-first + no-lowercase-leak; ledger/profile import + use BookWordmark).
entityLayer.test.js extended for the new aliases. Full suite green
(239 suites / 2891 tests); next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Work-order Phase 0 (Jul 10 live audit): every fabricated UI element deleted
or rewired to real data. Deletion sprint — no new product features.
0.1 Fake NBA game: root cause was scheduleService fetching the ESPN
scoreboard with no ?dates= param or date filter — off-season ESPN
returns the NEAREST slate (Jun 13 NYK@SA Finals rendered as tonight).
Now pinned to the requested ET date + defensive filter; undated events
dropped. Honest month-aware per-sport empty states (lib/emptyState.js).
0.2 Fake header counters: liveTick stripped to a bare 1s pulse (the
auto-incrementing "247 graded", sin-driven brain-%, aPlus/cascades are
dead). New GET /api/snapshot/summary (cache-only, before /:sport) +
Next proxy; HeartbeatBar shows the real graded count and SYNC =
elapsed since the last pipeline run (amber past 5 min).
0.3 Ticker: hardcoded fallback items deleted (real snapshot exhaust only);
MOVE kept — computeLineDeltas is real movement. <4 real items → no
ticker; bar publishes --ticker-h so the fixed header collapses cleanly.
0.4 /terminal retired: route redirects to /dashboard; VVI/injury-wire/
leaders layouts preserved unrouted as §12 content-engine templates.
Nav PRIMARY = Slate/Scan/Ledger; BottomTabBar Terminal→Explore; PWA
shortcut Terminal→Ledger; #terminal alias → /dashboard.
0.5 ›Query nav pill deleted (duplicate /scan link).
Backend 2289 → 2309 tests (199 suites), web build exit 0.
Spec: specs/phase-0-kill-the-lies.md. Next: work-order Phase 1 (ledger
persistence + settlement).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
- 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>
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>
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>
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>
VYNDR 2.0 conversion, Phase F (mobile is the PWA we launch first). Frontend-only;
zero backend changes.
- BottomTabBar rewritten to the §6 5-tab spec: Slate/Terminal/Scan/Ledger/More,
with Scan as the prominent raised grade-green action. Shown for anon too (only
mobile nav). Integrated More bottom sheet (sheet-up, backdrop dismiss, 48px mono
rows). iOS safe-area + 44px touch targets.
- Nav hamburger retired on mobile (tab bar owns nav).
- globals.css mobile section: tab-bar hidden >=768, main bottom padding,
grade-hero 80px, terminal-grid stacks, game-lines horizontal scroll.
- PWA: manifest shortcuts (Slate/Scan/Terminal) + categories; viewport-fit=cover.
Gotcha: `as const` on the TABS array broke type-check (distinct literal types);
fixed with a shared TabDef interface.
19 new tests. Backend 1853 -> 1872, 145 suites, zero regressions. Web build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>