TRUTH FIX FIRST. row.clv/clv_result derive from the overwritable
closing_line — 637/699 rows had closing == locked — so of the 578 rendered
chips, 522 "flat"s encoded a CAPTURE FAILURE as a held line. That number
was live on FIVE surfaces, not the one the order assumed:
1. ledger card ClvChip@319 (authed)
2. public profile /u/ duplicate chip@260 (PUBLIC, shareable)
3. dashboard "· CLV BEAT"@508 (authed)
4. board / Slate "CLV BEAT/FADED"@422 (FREE surface)
5. board / Slate "✓ HIT · CLV BEAT"@517 (FREE surface)
Removing it from the ledger alone would have left the lie live on three
surfaces including two public ones, defeating the stated PRIMARY GOAL, so
the removal covers all five. That is a deliberate extension beyond the
"scoped to ledger card" guardrail and is flagged as such — the guardrail
protected against feature creep, and this is the same defect at four more
addresses.
DIRECTIONAL BADGE installed in the old ledger slot. Three time states now
read distinctly on the row: entry (locked_odds, at-grade) · close (badge,
at-close) · outcome (hit/miss, final). The RESULT stays the row hero.
The PUBLIC profile deliberately gets NO badge — it never receives dclv
data (Analyst+Desk, server-gated), so that surface now shows the settled
result alone.
HONEST ABSENCE, tested: the 578 formerly-chipped rows now render NOTHING —
not a "flat", which is the old chip's lie in subtler form. Rows settled
before capture existed will never get dclv, and permanent silence is the
correct output. All six states verified in place; a 40-row dense ledger
stays legible (27 badges, max 40 chars, one line, consistent slot after
OutcomeChip); no CLV sort or filter exists.
FIELD REMOVAL DEFERRED as a separate scoped cleanup: /api/ledger/mine and
/api/profiles still SEND clv/clv_result, and dashboard/Slate still type
them. Display removal is local and safe; stripping the fields mid-swap
could break a response consumer.
Suite 289/3485 green, build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
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>
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>
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>
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>
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>
Overnight sprint for the Saturday 10 AM ET deploy gate — day one of the
public ledger record locks against freshly posted lines.
Task A — ledger team/opponent (migration 020, applied at 0 rows):
populated in both write paths from the real feed; opponent only when the
player's team matches a game participant (never guessed). Roadmap: Phase
4.5 WNBA ESPN-boxscore settlement (due ~Jul 24) + Phase 5 per-tier
calibration logged.
Task B — work-order 1.6 CLOSED (canonical player keys):
- searchPlayer resolves via nameKey; the old matcher deleted accents
("Sanchez" with acute -> "snchez") and substring-guessed onto the WRONG
player (the mismatched last-10 bug). Ambiguous -> null, never guess.
- Slate JOIN INVARIANT: a graded prop whose player's real team isn't in
the game is dropped (TB player can't render under MIL@PIT) — locked by
tests that fail the suite on regression.
- grades:{sport} TTL 2h -> 6h (expired between 5h cron gaps — the real
cause of /team "No active props" for slate players).
Task C — Phase 2 slate UX: tabs are THE filter (URL ?sport=, deep-linkable,
duplicate legacy tablist removed); cards cap at 6 graded props sorted
A+->F with ALL N READS in-place expander; waiting states show the real
next pipeline run ("Grades post ~6:00 PM ET").
Task D — Phase 3 mobile P0: root cause of vanished 390px nav was HIDE_ON
including '/' (landing had zero navigation) — fixed; html/body overflow-x
contained; GAME LINES collapses to best-line summary + "N BOOKS" expander
below 640px; venue drops before time/pitchers ever truncate.
Live verification: raw ESPN today STILL returns the Jun 13 NYK@SA Finals
game without a date pin; the pinned fetch returns 0 games, 0 off-date.
Backend 2327 -> 2352 tests (202 suites), web build exit 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>