498 Commits

Author SHA1 Message Date
builtbykev 18c8268f65 Merge Wave 5A (wiring/data): /u house-mode public model record (D2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:04:06 -04:00
builtbykev 11fc5a66d2 Wave 5B: Pitcher Arsenal via Baseball Savant (Statcast)
D4 — build the FREE Baseball Savant adapter for pitch-level identity
(mix / velo / usage% / whiff%), the missing layer statsapi doesn't carry.

- savantAdapter.getPitcherArsenal(id|name) — normalizes two public Savant
  CSV leaderboards (csv=true, NO parsing dependency): pitch-arsenal-stats
  (usage% + whiff% + K%) + pitch-arsenals avg_speed (velo). League-wide,
  cached 24h + in-memory mirror, indexed by MLBAM id. Defensive: null on any
  unrecognized shape; a missing velo/whiff is ABSENT (null), never 0.
  Injectable (fetchImpl/statsCsv/veloCsv/resolveId) → tests hit no network.
  Live endpoints VERIFIED (200, exact columns) from the sandbox.
- GET /api/stats/pitcher/:name/arsenal (stats.js) + Next proxy. MLB-only;
  an error/miss returns { found:false } so the card self-hides honestly.
- PitcherArsenal.tsx (+ barrel) — the mockup's PITCHER IDENTITY strip:
  pitch mix % + velo + whiff%, mono/tabular, ranked by usage, sharpest-whiff
  pitch highlighted green. Self-hides (heading included) when arsenal absent.
  Mounted on the MLB player profile (a pitcher surface). Context, not a
  graded market value.
- Tests: savantAdapter (fake CSV → ranked arsenal; unknown shape/blank cells
  → absent not 0; name→id resolve) + PitcherArsenal source locks (self-hide,
  mono/tabular, em-dash-not-zero). +2 suites / +17 tests (3012 → 3029).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:02:16 -04:00
builtbykev 76c289d4c1 Wave 5A: /u house-mode public profile (D2)
Reserved house handle (default 'vyndr', env HOUSE_HANDLE) resolves to the
PUBLIC model record — getModelAggregate() with no userId (user_id=NULL rows)
— WITHOUT a public_profiles row. It is the ONLY special case; every other
handle keeps the private-by-default, byte-identical-404 no-existence-leak
contract. The house profile is always public and never 404s (a fetch failure
degrades to an honest building state).

- src/routes/profiles.js: house short-circuit + sendHouseProfile (public
  aggregate + by_tier + public settled entries), reserved before the publish
  lookup so a user claim is shadowed.
- PublicProfile.tsx: house label 'VYNDR MODEL · PUBLIC RECORD' + hero/subtitle
  off data.house; keeps the CLV-VERIFIED record hero + TierRecord calibration
  + recent settled reads (misses included).
- opengraph-image.tsx (1200x630): house-branded eyebrow/heading.
- portrait/route.tsx: new 1080x1350 share crop (real aggregate or tagline
  fallback, never a fabricated number).
- Discoverability: 'VIEW AS PUBLIC PAGE ->' on the ledger MODEL header +
  'VIEW PUBLIC RECORD ->' under the landing ModelRecord, both to /u/vyndr.
- tests/unit/houseProfile.test.js: house resolves to user_id=NULL aggregate
  (no public_profiles row) + by_tier; unknown/unpublished user handles stay
  byte-identical 404; page renders house label + TierRecord + portrait crop.

3019 tests green (3012 -> 3019); next build EXIT=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:26:48 -04:00
builtbykev da144ecb5e Merge Wave 4B (wiring/data): Parlay Lab page + live grade-shift timeline
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:00:24 -04:00
builtbykev a99ba34e44 Merge Wave 4A (wiring/data): outlook mode + market-breadth consensus
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:00:24 -04:00
builtbykev bc8633466c Wave 4A: Outlook Mode (never-empty grid) + Market-Breadth consensus strip
Step 3 — OUTLOOK MODE. The game grid no longer dead-ends in a "NO SLATE" CTA.
When there are no live games (and it's not a network failure) it shows REAL,
always-available data: yesterday's PROVEN A-tier receipts (/api/ledger/model)
+ tomorrow's date-pinned ESPN schedule preview (free/cached). A network
fetchError stays a distinct ERROR state — never a fabricated outlook.
- lib/outlook.js (new, CommonJS, unit-tested): buildOutlook selection +
  mapTomorrowPreview (upcoming-only, drops incomplete matchups, never invents).
- Slate.tsx: OutlookSurface replaces the empty-grid CTA (dateOffset 0 only).
- dashboard/page.tsx: DashboardOutlook replaces the "Today's games" NO-SLATE CTA.

Step 4 — MARKET-BREADTH / CONSENSUS vs MODEL. Makes the DeskShowcase
"consensus vs model" claim REAL. Consensus = median book line across a prop's
per-book rows; the model's position is model_value vs consensus, signed by the
graded side. <2 distinct books → null (never fabricate a consensus); a
non-numeric line is ignored, never coerced to 0.
- lib/marketBreadth.js (new, CommonJS, unit-tested): median/computeBreadth/
  collectBreadth (strict null guards).
- components/vyndr/MarketBreadth.tsx (new): mono/tabular strip, colored by sign
  via colorContract.edgeColor, self-hides when nothing has >=2 books.
- Slate.tsx renders it above the grid (joins books + snapshot model_value).
- slateAdapter.js exports gradeKey for the join.
- DeskShowcase.tsx: the consensus claim is now backed by the shipped feature.

Tests: tests/unit/outlook.test.js + tests/unit/marketBreadth.test.js (23 cases).
Full suite 2984 passing (245 suites); next build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:59:44 -04:00
builtbykev 911cae4992 Wave 4B: Parlay Lab page + Live Grade-Shift timeline
TASK 1 — Parlay Lab (/parlay): a dedicated Correlation Builder with a
leg source INDEPENDENT of the live slate. Browses tonight's pre-graded
props from /api/snapshot/:sport (resolves players via /api/players/search),
adds legs through useParlay().addLeg (deduped by legKey), and renders the
PARLAY SLIP — combined grade, correlation, and payout read straight off
ParlayContext. Surfaces parlayService's correlation warning as the
CAUTION · CORRELATION FLAG, honors the tier leg-cap (free 2 / analyst 4 /
desk 6) and blurs the payout for free tier with the __goPaywall upsell.
Added /parlay to OPEN_ROUTES (free funnel, like scan/dashboard). Retired
the cleanly-dead ParlayTray.tsx (unmounted since Session 50). No new
proxies — reuses existing snapshot/search/parlay-grade endpoints.

TASK 2 — Live Grade-Shift timeline: web/src/lib/gradeShift.js (pure,
testable) builds a line/grade-movement timeline from already-emitted data
(intraday {t,line} history + revised_from_grade). Color law mirrors
ROW-GRAMMAR / StatStrip.LineSparkline: green = toward the graded side,
amber = against, dim = flat (never red). GradeShift.tsx renders it, shows
the original grade struck-through on a revision, and self-hides below 3
real points. Mounted in GradeResultCard (self-hides on the scan path,
which carries no captured history — honest, never fabricated).

Tests: tests/unit/gradeShift.test.js (15) + tests/unit/parlayLab.test.js
(13). Full suite 245 suites / 2989 tests green (baseline 243/2961).
Next build EXIT=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:36:28 -04:00
builtbykev 35007cde22 Merge Wave 3 (wiring/data): record by grade tier (Addition 2)
ONE shared TierRecord component (lib/tierRecord.js + TierRecord.tsx) on
dashboard + /u + ledger — per-tier calibration (A+ X-Y, A X-Y, …), W-L always,
hit-% only at n>=20 per tier. by_tier flows through all endpoints untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:04:21 -04:00
builtbykev 9bafc76092 Wave 3: Record by grade tier — shared TierRecord across dashboard, /u, ledger
Addition 2 (non-negotiable): the model's record must show PER GRADE TIER
(A+ went X-Y, A X-Y, …) everywhere the record appears. A blended % hides the
proof that higher grades win more — the tier calibration IS the credibility.

The backend already computed `by_tier` in getModelAggregate; this is display
propagation via ONE shared component (the class fix, not five one-offs).

- web/src/lib/tierRecord.js — testable CommonJS row-builder. W-L counts ALWAYS
  (honest at any n); hit-% only when the upstream n≥20 gate passed (hit_pct !=
  null), else "RECORD BUILDING · N settled". Order A+ A B C D F. A-tier is the
  only edge (green) tier — no glow below A, red reserved for outcomes.
- web/src/components/vyndr/TierRecord.tsx — the ONE shared table. Presentational
  (byTier) for /u + ledger; self-fetch (/api/ledger/model, sport-scoped) for the
  dashboard. Fully self-hides until a tier has a settled read.
- Ledger swaps its inline TierCalibration for the shared component (single
  source). /u PublicProfile renders it below the blended hero (by_tier added to
  the aggregate type; it already flows through the route + proxy untouched).
  Dashboard gains a compact per-tier surface.
- Endpoint/proxy audit: profiles.js + ledger.js return the full aggregate
  (by_tier included); both Next proxies pass the body through — no threading
  needed. No change to the gate or math in getModelAggregate.

Tests: tests/unit/tierRecord.test.js (row logic + tier order + edge contract +
source-assert all three surfaces render the shared component). ledgerService
test gains an A+-stands-alone bucketing case. Full suite green (243 suites /
2961 tests); web next build exits 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:58:01 -04:00
builtbykev 4303aa68a4 Wave 2 fix: correct bookWordmark test assertions (casing-only brands + bet365)
The merged suite exposed two self-defeating assertions in 2B's book test:
uppercasing the name before comparing to the key false-failed correct brand
names (DraftKings→DRAFTKINGS), and the lowercase-echo guard rejected bet365
whose official wordmark IS lowercase. bookInfo output was always correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:22:49 -04:00
builtbykev d3f18b6481 Merge Wave 2B (wiring/data): sportsbook SVG wordmarks + team-logo coverage
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:19:47 -04:00
builtbykev b5f5ba1a34 Merge Wave 2A (wiring/data): sport-agnostic headshot id thread-through
MLB MLBAM + NBA/WNBA ESPN athlete ids captured at ingestion (zero new I/O),
threaded onto grades → strips → PlayerAvatar across slate/scan/hotlist/search/
grade-card. Soccer honest-monogram (no free id). +11 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:19:47 -04:00
builtbykev 47ada9013c Wave 2A: real player headshots — sport-agnostic id threaded from ingestion
Threads a REAL athlete id from the snapshot's per-player stats resolve (zero
new I/O) → enriched grade → grades:{sport} → slate strip → PlayerAvatar. Real
photo where an id resolves; team-colored monogram (never a gray silhouette,
never a broken image) where it can't. Ids are never fabricated.

Ingestion (Addition 1):
- espnStatsAdapter.getSeasonAverages now RETURNS the resolved ESPN athlete id
  (was discarded) as espnId; non-numeric uid degrades to null.
- playerIntelService surfaces MLBAM playerId (MLB) / ESPN espnId (NBA/WNBA).
- snapshotService captures both per player and stores them on the enriched
  grade beside archetype/team (null when unresolved → monogram path).

Thread → component:
- slateAdapter.buildPlayerStripsFromProps carries playerId/espnId onto each
  strip; StatStrip → PlayerAvatar (accepts both ids; getHeadshotUrl routes by
  sport: MLB→mlbstatic, NBA/WNBA→a.espncdn).
- Silhouette surfaces rewired to PlayerAvatar (branded monogram on null):
  scan search dropdown (guarded MLBAM p.id) + tonight chips, SearchModal,
  HotListPanel, GradeResultCard header. Scan grade card feeds the picked
  MLBAM id through gradeAdapter.
- playerHeadshot pure URL logic extracted to CommonJS playerHeadshotUrl.js
  (unit-testable; the .ts re-exports it). nfl/nhl added to ESPN_SPORT_PATH.

Tests: tests/unit/headshotThread.test.js (per-league URL + id thread + monogram
null path) + extended snapshotService/espnStatsAdapter suites. Full suite
241 suites / 2915 green; next build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:18:59 -04:00
builtbykev 7d6dbc6cf3 Wave 2B: sportsbook wordmarks + team-logo coverage
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>
2026-07-13 11:44:15 -04:00
builtbykev b48dc2ed15 Merge Wave 1 (wiring/data): trust bugs + DeskShowcase copy
- billing renewal honest render (classifyRenewal — no far-future placeholder)
- James Wood nameKey-collision → teamHint disambiguation + streaks join-invariant
- DeskShowcase '$1M terminal' → deadpan 'Every grade, every alt line, live. $44.99/mo.'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 03:49:54 -04:00
builtbykev b6787af191 Wave 1: kill three trust bugs (billing renewal + namesake collision + Desk copy)
FIX 1 — Honest billing renewal render. VYNDR tiers are monthly, so a
`subscription_end` far in the future (the manually-seeded "RENEWS 6/9/2036"
founder row) is a comped/lifetime/seed value, not a renewal. New
web/src/lib/billingDisplay.js `classifyRenewal()` → date | none | lapsed |
unknown (strict Date.parse guard, MONTHLY_RENEWAL_MAX_DAYS=60). Profile page
renders the classified label for both the "Renews" stat and the
cancel-scheduled "Access ends" line — no raw far-future date. No DB row mutated.

FIX 2 — MLB namesake collision (James Wood → "Chicago Cubs"). searchPlayer now
collects ALL exact-nameKey matches instead of first-`.find`; a ≥2 collision
resolves ONLY via a confident teamHint (the prop's game participants, matched
against the cached /teams list with ESPN↔statsapi abbr reconciliation), else
refuses (null) — never guesses. The hint threads getPlayerStats →
resolvePlayerStats → snapshotService (built from each prop's home/away team).
Join invariant: a single-exact player whose team isn't in the hinted game has
its team DROPPED (null), so streaks/rosterlogs never tag a foreign team. Full
teamHint recovery shipped (not just the refuse fallback).

FIX 3 — DeskShowcase headline "A $1M terminal." → deadpan value-showing copy
"Every grade, every alt line, live." Prices ($44.99 / $34.99) unchanged.

Tests: billingDisplay.test.js (7), mlbNamesakeResolve.test.js (12,
disambiguation + join invariant + pure helpers), ds5PricingStates updated to
assert the new headline and no "$1M". Full suite green (237 suites / 2863
tests); web `next build` exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 03:48:58 -04:00
builtbykev 744eb68812 Wave 2 build plan locked: sport-agnostic id thread-through (MLB MLBAM + NBA/WNBA ESPN id)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 03:35:09 -04:00
builtbykev 93a220e0ca Design reference (mockup) + map updated: sport-agnostic entity, per-tier record, additions
- specs/design-reference/vyndr-system.html + support.js (global visual reference)
- D1 override (sport-agnostic athlete-id ingestion), D3 override (book SVGs in-wave)
- Addition 1 (ingestion-time id resolve+store), Addition 2 (record by grade tier),
  Addition 3 (mockup is global reference; live wordmark kept)
- revised 6-wave sequence

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 03:24:45 -04:00
builtbykev 69b1febb0f Step 0 MAP: wiring & data train — surface-by-surface data/asset map
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 02:55:43 -04:00
builtbykev 2217834e61 STATE.md: design train v2 SHIPPED to main + deployed live
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:58:16 -04:00
builtbykev e9c0a5961e Merge the design train v2 (DS0-DS5) into main
The 6-session presentation overhaul: entity layer (real logos/headshots/
wordmarks), speed+trust bugs (React #418, skeletons, sb-token class),
color contract (green=one meaning, enforced by tests), billboards (STREAKS/
grade reveal/CLV/\u profile), dashboard slate rebuild (never-empty hero),
pricing Desk-as-hero + ticker stillness + unified empty/error + archetype glyphs.

238 suites / 2885 tests green, next build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:44:54 -04:00
builtbykev 2394ba1b84 STATE.md: design train v2 complete (DS0-DS5), ready to merge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:33:55 -04:00
builtbykev 3d08390349 Merge DS5 (design): pricing Desk-as-hero, ticker stillness, empty/error unify, archetype glyphs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:32:28 -04:00
builtbykev a18a3f33fc DS5 — Pricing (Desk-as-hero) + Motion + Empty/Error + archetype glyphs
Part 6 #8 — Desk $44.99 is the hero tier. New DeskShowcase leads the
pricing page with the "$1M terminal · $44.99" story + real feature
visuals (alt-line ladder, quarter-Kelly, parlay φ, real-time feed) in a
balanced two-column layout (kills the dead right-half). Desk is the sole
highlighted tier / single primary CTA (color contract #9); Analyst is
secondary. Real prices: Free 5 scans, Analyst $14.99/$19.99, Desk
$34.99/$44.99. ClaimMeter + Stripe checkout wiring untouched.

Part 4 #7 — Ticker → punctuated stillness. The continuous marquee is
retired; the ticker now RESTS ≥4s on each ranked item and pulses only on
change. The EKG heartbeat is a static readout — the header's ONE idle
proof-of-life is the single SIGNAL-LIVE live-dot (the ticker dropped its
competing pulse). All durations tokenized (--motion-*, --ticker-hold);
prefers-reduced-motion kills the motion entirely.

Part 8 #20 — new EmptyState component modeled on the north-star 404
(scanlines + glitch wordmark + amber system voice + CTA hierarchy),
reused at the bare-red "Team not found", "Game not found", and the
ledger empties — one unified voice.

Part 5 — archetype glyph+chip propagated to STREAKS rows + ledger rows
(optional + self-hiding; absent beats fabricated); grade reveal already
carries ArchetypeBlend.

Tests: new tests/unit/ds5PricingStates.test.js (22) locks all four
workstreams; updated teamHubUI + vyndrDesignSystem for the new surfaces.
237 suites / 2864 tests green; next build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:31:35 -04:00
builtbykev fe294a5de3 DS2: Dashboard Slate Rebuild — one hero, pending collapse, never-empty hero
DESIGN-SPEC Parts 3 + 6 (audit #1, #13, #14). The founder's named #1 rebuild.

slateAdapter.js — the testable engine:
- selectTopGrades: rank tonight's grades by tier → confidence → edge so the
  row varies on a real signal, not identical-weight noise (#13).
- buildHeroReceipts: yesterday's PROVEN A-tier settled HITS (misses excluded),
  carrying the real result — the never-empty proof source (#1, Part 6).
- heroFallbackState: tonight wins, else receipts, else empty.
- pendingSummary: collapse an all-awaiting card's six "Grades post …" rows to
  ONE line count (#14).
- topReadForCard: the single best live graded read to promote (#2).

GameCard.tsx — ONE bold hero per card (large mono/tabular grade + player, rest
demoted); all-awaiting cards render one "N props pending · grade ~X ET" line via
nextRunLabelET instead of repeated filler. Real team logos + team-colored accent
already lead the card (DS0) — preserved.

dashboard/page.tsx — Top grades tonight ranked via selectTopGrades (+ % CONF the
varying signal); when tonight is empty, fetch /api/ledger/model and fall back to
yesterday's PROVEN A-tier receipts (✓ HIT + actual + CLV) so first paint always
proves the model. Honest nextRunLabelET copy kept for the truly-empty case (QA.22).

Tests: tests/unit/ds2Dashboard.test.js (21) — pure-fn + source assertions,
fail-before / pass-after. Full suite 237 suites / 2863 tests green (+21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:07:06 -04:00
builtbykev cf91c04e90 DS1 follow-up: close the sb-token trust-bug class across all surfaces
The OAuth-only 'sb-token' localStorage key was read by profile, slip,
dashboard (recent-scans), settings, and tracker for their authenticated
fetches. Email/password users never had that key, so those fetches sent
no Authorization header and silently returned nothing.

- web/src/lib/authToken.js — currentAccessToken() reads the REAL Supabase
  session (sb-<ref>-auth-token, v2 top-level or v1 currentSession), legacy
  fallback. CommonJS so Jest can unit-test it (5 tests).
- Swept all 5 pages to the helper (scan already session-first from DS1).
- lib/api.ts (0 callers) + ParlayTray (unmounted) left as dead code.

236 suites / 2842 tests green, next build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 23:41:13 -04:00
builtbykev bc5285d9e7 DS3+DS4 reconcile: edge coloring via the centralized colorContract enforcer
Both sessions fixed the negative-edge-green bug; kept DS3's centralized
colorContract.edgeColor helper (removed DS4's local const), updated DS4's
source-assertion test to match. Same semantics, one enforcer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:45:07 -04:00
builtbykev db8f570ae5 Merge DS3 (design): the color contract — one meaning, enforced by tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	web/src/components/vyndr/GradeResultCard.tsx
2026-07-12 19:36:07 -04:00
builtbykev 49a3323c20 DS3 (design): the color contract — one meaning, enforced by tests
Signal-green #00D4A0 now means exactly ONE thing (edge/active/A-tier/CTA),
locked by tests that fail on violation. Part 1 of DESIGN-SPEC v2.

- web/src/lib/colorContract.js — pure CommonJS helpers: edgeColor(value)
  colors edge/CLV/delta by SIGN (neg=var(--miss), pos=var(--g-a), 0=neutral);
  gradeTierColor() (A/A+ green, B blue, C amber, D/F red, in lockstep with
  vyndrTokens.gradeColor); gradeGlows() (A/A+ only); deltaE()/isSignalGreen()
  CIE76 gate so no archetype hue dilutes the signal.
- GradeResultCard: edge confidence-strip + EDGE row route through edgeColor
  (a -33.3% edge was rendering GREEN — audit #3); grade-hero glow gated to
  A/A+ via gradeGlows (a glowing C devalued the cue); VYNDR INTELLIGENCE
  panel de-flooded (neutral border, Form/Rest neutral not green — #15).
- LiveHeroProp: negative edge now muted red, not neutral (sign completeness).
- Archetype dedup off signal-green (both archetypes.js + archetypeService.js,
  kept matched): DUAL THREAT/MOTOR #00D4A0, MIRROR #34D399, ARTILLERY/RANGE/
  GHOST/BLADE #2DD4BF, BRUSH #3DDC84 shifted to distinct non-green hues (all
  ΔE>=44 from #00D4A0). Within-sport uniqueness preserved.
- tests/unit/colorContract.test.js — 21 tests: helper units + source-grep
  violation locks + archetype-green-dedup. QA.20-22 kept green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:34:12 -04:00
builtbykev 32929c0052 DS4 merge: update QA.20 color lock to the rebuilt STREAKS billboard structure
Semantics preserved (step-up amber / step-down green); DS4 moved the
coloring into a sev object, so the source-lock regex is updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:28:17 -04:00
builtbykev 73325665ec Merge DS4 (design): billboards — STREAKS row, grade reveal, CLV reframe, /u profile
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

# Conflicts:
#	web/src/app/ledger/page.tsx
2026-07-12 19:27:15 -04:00
builtbykev 9d903ee4aa Merge DS1 (design): speed + trust bugs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:26:12 -04:00
builtbykev 45bafbc01a DS4 (design): billboards — STREAKS row, grade reveal, CLV reframe, /u profile
P0 billboards (the timeline is customer #1). Pixel-level craft: one bold hero
among muted context, real entities from DS0.

- STREAKS row: rebuilt from a log line into a Bloomberg alert. Streak LENGTH is
  now the mono/tabular HERO (38px, the largest figure in the row); real
  PlayerAvatar identity; muted "built vs [opponents]" lens; ONE severity accent
  (step-up amber / step-down green); grade badge tier-gated (the READ is paid).
- Grade reveal: edge is now sign-colored — negative edge uses var(--miss),
  never green (color contract #3). Fixed in BOTH the confidence strip and the
  MODEL/LINE/EDGE row; that row is now mono + tabular. Letter stays the hero.
- CLV reframe: new pure lib/clvDisplay.js (clvMode flat/spread/none). A near-
  flat distribution (73/74) now renders a confident VOICE line — "CLV flat — we
  grade the outcome, not the close" — instead of a broken-looking histogram;
  bars show only on real spread.
- /u profile: hit% is the bold record hero (56px mono tabular) + beat-close
  secondary; honest CLV-VERIFIED badge (only when closing value is tracked);
  real PlayerAvatar identity on cards; OG image elevated to carry the real
  record at 1200x630 social crop (graceful tagline fallback).

Tests: +23 (tests/unit/ds4Billboards.test.js). Full suite green (2732).
web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:25:57 -04:00
builtbykev 1c681df5d3 DS1 (design): speed + trust bugs
Fixes the three DESIGN-SPEC Part 4 + #17 audit findings.

1. React #418 hydration mismatch (landing → dashboard entry). The
   `maybeSignedIn` value was computed in a useState INITIALIZER that reads
   localStorage during render: server (no window) → false → emits the
   marketing tree; a signed-in visitor's first CLIENT render → true → emits
   the loading placeholder. Whole-subtree server/client mismatch → React
   discarded and re-rendered the page. Deferred behind a mounted flag so the
   first client render matches the server; the stored-session check flips
   post-mount. SSR HTML is no longer discarded.

2. Loading walls → skeletons. New tokenized Skeleton primitive
   (.vyndr-skeleton, reduced-motion-safe via the global rule). Swapped into
   every text-wall loader: dashboard slate load ("Loading the slate…"), /desk
   ("Assembling the pack…"), /ledger ("Loading…"), scan ("Loading the model…"),
   and the landing redirect placeholder. No bare text loader remains.

3. scan→ledger persistence. Root cause: the scan page read its bearer token
   from localStorage['sb-token'] — a key written ONLY by the OAuth callback —
   so email/password users posted /api/scan anonymously and the ledger write
   (gated on an authed user) was silently skipped. Now uses the authoritative
   session.access_token (matching the ledger read path). Extracted the row
   builder to web/src/lib/ledgerRow.js (shared, testable).

Tests: +17 (scanLedgerPersistence write→mine round-trip + scope + idempotency;
ds1SpeedTrust hydration/skeleton/persistence source invariants). Full suite
233 suites / 2793 green; web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:25:32 -04:00
builtbykev 2f6aaad366 STATE.md: design train v2 in flight (DS0 done, Wave 1 building)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:07:51 -04:00
builtbykev 24af247b29 DS0 (Design v2): the Entity Layer — teams/players/books render as themselves
Founder's #1 priority. A single cached asset+rendering system, swapped into
every surface, so entities stop being flat gray strings (DESIGN-SPEC Part 2).

- web/src/lib/teamMeta.js: static registry for ALL 4 sports — 30 MLB, 30 NBA,
  13 WNBA teams + 48 World Cup national teams, each with real colors + the
  ESPN logo/flag CDN abbr. resolveTeam (abbr/full-name/nickname/alias),
  teamLogoUrl (statsapi->ESPN mapping: AZ->ari, CWS->chw; soccer via the
  countries/ flag CDN), accentColor (picks the VISIBLE color of the pair so a
  #000000 primary never renders an invisible accent on #06060B). Colors +
  abbrs sourced once from ESPN's team API — stable public facts, zero-latency
  static data, no paid dependency.
- TeamLogo: real ESPN-CDN logo with a team-colored MONOGRAM fallback (never a
  gray box / bare abbr). PlayerAvatar: real headshot with a team-colored
  monogram fallback (kills the gray silhouette). BookWordmark: brand-color
  wordmark, proper casing (DraftKings, not 'draftkings').
- Swapped into the class-level shared components so it propagates to ALL
  surfaces: GameCard (team logos + team-colored accent edge), StatStrip
  (player identity avatar), StreaksPanel (P0 billboard avatars), TeamHub
  header (the team's real crest leads its hub). Barrel-exported.

ZERO OUT-OF-POCKET: ESPN logo/flag CDN + league headshot CDNs, all verified
200 image/png across MLB/NBA/WNBA/soccer.

ACCEPTANCE (SSR render proof): /entity-demo harness server-rendered the exact
real asset URLs across all 4 sports — mlb/500/nyy.png, mlb/500/chw.png (White
Sox, correct ESPN abbr), nba/500/lal.png, wnba/500/ny.png, countries/500/
usa.png + bra/eng/arg/jpn flags, real mlbstatic/nba headshots (Judge 592450,
LeBron 2544), DraftKings/FanDuel wordmarks. Each URL curl-verified 200
image/png. Harness removed post-proof (not a product surface). Pixel
screenshot blocked by WSL2<->Windows-Chrome localhost networking, not code.

2757 -> 2776 tests (+13 entityLayer, +6 boot resilience), web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:02:18 -04:00
builtbykev b452db727c DESIGN-SPEC.md v2 — the raised standard (governs the design train)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:52:13 -04:00
builtbykev 1f5a0552ca STATE.md: P0 resolved — API on 17fb981, all fingerprints confirmed live
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:44:36 -04:00
builtbykev 17fb981f99 P0 fix: content/ not in image crashed API boot; harden garnish + preflight
ROOT CAUSE: the Dockerfile copied src/poller/scripts/supabase but NOT
content/. mediaEngine.js read content/stark-lines.json with an unguarded
module-load readFileSync; ENOENT in the image threw at require time, and
via app.js → routes/desk → deskService → mediaEngine that crashed the
ENTIRE API at boot. The Coolify healthcheck rolled back to the last
healthy image (4d2b27d), so every deploy since 219167e silently served a
14-hour-old build — S11 live tracking, S6 API code, the settlement boot
line, and SNAPSHOT_EXPECTED_INTERVAL were all merged but NOT running.

FIX (one train):
1. Dockerfile COPYs content/ into the runner image.
2. mediaEngine: stark-lines.json is OPTIONAL (garnish, never load-bearing)
   — loadStark() try/catch → {} → posts render without the Stark kicker,
   never a crash. Belt AND suspenders with #1.
3. src/preflight.js (§A4): boot prints '[preflight] OK' or 'DEGRADED'
   naming exactly what content/env is missing — before the healthcheck
   can fail silently. Run first in server.js.
4. Full fragility sweep: mediaEngine was the ONLY unguarded module-load
   file read; coachSignals (config/coaches.json) was already lazy +
   try/catch + copied. No others.

Verified: requiring app.js + deskService + mediaEngine with
stark-lines.json ABSENT now boots clean (reproduced the exact prod
failure). 2757 -> 2763 tests (tests/unit/bootResilience.test.js).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:42:18 -04:00
builtbykev 4dc22c9bc9 specs/STATE.md — state of the world as of ec5ee3d (A1 board complete)
The session-opening doc: 3 trains shipped (2757 tests), live infra map,
pending Coolify env vars + entity placeholders, honest open items.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:17:34 -04:00
builtbykev ec5ee3dcaf Merge S11 (a1): live tracking — the read locked, the game watched
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 00:46:32 -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 14dc9cf270 Merge S6 (a1): display — the full picture under the grammar
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:09:23 -04:00
builtbykev d3637e7abd S6 (a1): display — the full picture under the grammar
- specs/ROW-GRAMMAR.md: the row grammar law (slot order, color law, mark
  law, mobile stacking, no-truncation), locked by tests/unit/rowGrammar.
  StatStrip violations fixed: MovementChip before the grade (market
  context before model output); ViabilityChips after the archetype
  (identity is one contiguous run).
- Line-movement sparklines: intradayRefreshService.trackHistory captures
  real {t,line} points per grade (seeded with the lock, deduped when
  flat, capped 24) inside the snapshot write-back; StatStrip.LineSparkline
  renders at >=3 points (green toward / amber against / dim flat).
- Last-10 dot strips: services/last10Dots (streaksService accessors) ->
  /api/snapshot/:sport attaches last10_dots from rosterlogs:{sport};
  StatStrip.DotStrip renders vs the LOCKED line, newest first.
- CLV distribution: getModelAggregate emits clv_distribution (7 signed
  buckets, outliers clamped) only past the centralized n>=20 gate;
  ledger MODEL header renders the bar strip.
- Global search: SearchModal (cmd-K via GlobalHosts + window.__search),
  players via /api/players/search per sport + static lib/teams.js
  (soccer deliberately absent); Nav search icon + Search first in the
  mobile More sheet. Explore tab untouched.
- Landing LCP: fade-up floored at opacity .6 (hero h1 contentful on
  first frame, S33 visible-floor rule) + IBM Plex Mono preload:false
  (4 decorative font files off the slow-4G critical path).

2654 -> 2698 tests (226 suites) green; web build exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:08:24 -04:00
builtbykev 219167eebf A1: migration 021 — partner attribution (pre-apply commit, per docs/PARTNERS.md)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:54:16 -04:00
builtbykev 1d46b446c9 Merge S10 (a1): public ledger profiles v1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	BUILD-STATE.md
#	CLAUDE.md
2026-07-11 19:40:43 -04:00
builtbykev caf09840d5 Merge S9 (a1): slip reader — zero-API OCR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:39:56 -04:00
builtbykev c18dd42067 S5 fix: viability JSDoc type (JS default-param inference broke the build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:39:21 -04:00
builtbykev 02c17a65c3 S5 (a1): prop viability — lineups, injury wire, date navigation
- lineupService: statsapi hydrate=lineups (live shape verified) →
  CONFIRMED (batting slot) / NOT_IN (team posted without the player) /
  PROJECTED (not posted). 10-min cache, pure parser, injectable.
- NOT_IN visibly KILLS the grade on the slate: struck through + NOT IN
  LINEUP chip, parlay/book actions suppressed. The locked ledger read is
  untouched — honesty is showing the read is dead, not deleting it.
- injuryService: ESPN injuries feed → OUT/GTD/PROB chips (unknown status
  → no chip, never invented). Chips on slate strips via ViabilityChips.
- Date navigation on the Slate: YESTERDAY (results surface — finals +
  THE SETTLE panel of that date's settled reads w/ outcome + CLV chips,
  via new ?date= filter on /api/ledger/model) / TODAY / TOMORROW
  (schedule until lines post). Odds/grades/pitcher layers are TODAY's
  and never fake other dates; 60s poll only refreshes today.
- Routes /api/schedule/:sport/lineups + /injuries + Next proxies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:38:37 -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