Honesty pass: remove every live fabrication (REMOVE/HIDE only, no feature cut)

Six live untruths corrected — no grade/snapshot/scorer/pipeline touched:
1. /compare — hardcoded Jokic A+/Wembanyama A + fake VERDICT replaced with an
   honest in-development state; removed from Nav + BottomTabBar (route still
   resolves, never the sample). Real two-player build is later.
2. Pricing — founder Desk $34.99→$44.99 (matches lib/checkout.js), Analyst
   $14.99; removed the struck $19.99/$44.99 "regular" numbers and DeskShowcase's
   stale $34.99. First-100 counter is real (ClaimMeter → Stripe countFounderSeats);
   no fake "first 50" desk claim added (no such counter exists).
3. FAQ "NexaPay" → Stripe (verified: live checkout is Next→Express→checkout.stripe.com).
4. FAQ + Features "Brier/CLV published from day one" removed (not surfaced yet) —
   returns when real. Backend Brier compute untouched.
5. MobileEdgeBoard removed from the Slate — its edge% feed was a miscalibrated
   placeholder (masked >40% as "—"); phones now show the real game cards.
6. Price triplet — never-computed model/EV now derives NO_MODEL (honest absent,
   MODEL "—" / "NOT PRICED", no verdict) instead of QUARANTINE's false "we
   suppressed our price / a leg is poisoned" copy. Fixes grade card + LiveHeroProp.

Full suite 3833 green, web build exit 0. Tests updated to the new honest contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
This commit is contained in:
Kev
2026-07-27 06:04:00 -04:00
parent be6b4a2849
commit 6bc18d823c
16 changed files with 100 additions and 132 deletions
+9 -50
View File
@@ -1,65 +1,24 @@
'use client';
import { useState } from 'react';
import SectionHead from '@/components/vyndr/SectionHead';
import GradeBadge from '@/components/vyndr/GradeBadge';
import TerminalInput from '@/components/vyndr/TerminalInput';
// Head-to-head player comparison (§6). Sample data for now — real player
// resolution wires to /api/players/search + game logs in a later pass.
const SAMPLE = {
a: { name: 'Nikola Jokić', team: 'DEN', rows: { 'L10 PTS': '28.4', 'L10 REB': '12.1', 'L10 AST': '9.8', 'Usage%': '29.1', 'Grade': 'A+' } },
b: { name: 'Victor Wembanyama', team: 'SA', rows: { 'L10 PTS': '26.9', 'L10 REB': '10.4', 'L10 AST': '4.2', 'Usage%': '31.0', 'Grade': 'A' } },
};
const ROW_KEYS = ['L10 PTS', 'L10 REB', 'L10 AST', 'Usage%', 'Grade'];
const num = (v: string) => parseFloat(v.replace(/[^\d.]/g, ''));
// Head-to-head player comparison (§6). IN DEVELOPMENT — real two-player
// resolution wires to /api/players/search + live game logs in a later pass.
// Until then this surface shows an honest in-development state: NO sample
// matchups, NO hardcoded grades, no fabricated verdict.
export default function ComparePage() {
const [a, setA] = useState('Nikola Jokić');
const [b, setB] = useState('Victor Wembanyama');
return (
<section style={{ maxWidth: 860, margin: '0 auto', padding: '28px 16px 96px' }}>
<SectionHead accent="var(--g-a)"> HEAD TO HEAD</SectionHead>
<h1 className="mono" style={{ fontSize: 28, fontWeight: 800, letterSpacing: '-0.02em', margin: '8px 0 18px' }}>COMPARE</h1>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 20 }}>
<TerminalInput prompt="" value={a} onChange={setA} placeholder="Player A" />
<TerminalInput prompt="" value={b} onChange={setB} placeholder="Player B" amber />
</div>
<div style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 140px 1fr', padding: '14px 16px', background: 'var(--bg-2)', borderBottom: '1px solid var(--border)', alignItems: 'center' }}>
<div style={{ fontSize: 16, fontWeight: 800, textAlign: 'right' }}>{SAMPLE.a.name} <span className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>{SAMPLE.a.team}</span></div>
<div className="label" style={{ textAlign: 'center' }}>VS</div>
<div style={{ fontSize: 16, fontWeight: 800 }}>{SAMPLE.b.name} <span className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>{SAMPLE.b.team}</span></div>
</div>
{ROW_KEYS.map((k) => {
const av = SAMPLE.a.rows[k as keyof typeof SAMPLE.a.rows];
const bv = SAMPLE.b.rows[k as keyof typeof SAMPLE.b.rows];
const isGrade = k === 'Grade';
const aWins = !isGrade && num(av) >= num(bv);
const bWins = !isGrade && num(bv) >= num(av);
return (
<div key={k} style={{ display: 'grid', gridTemplateColumns: '1fr 140px 1fr', padding: '11px 16px', borderBottom: '1px solid var(--border)', alignItems: 'center' }}>
<div className="mono" style={{ textAlign: 'right', fontSize: 15, fontWeight: 700, color: aWins ? 'var(--g-a)' : 'var(--text-0)' }}>
{isGrade ? <GradeBadge grade={av} size="sm" glow /> : av}
</div>
<div className="label" style={{ textAlign: 'center' }}>{k}</div>
<div className="mono" style={{ fontSize: 15, fontWeight: 700, color: bWins ? 'var(--g-a)' : 'var(--text-0)' }}>
{isGrade ? <GradeBadge grade={bv} size="sm" glow /> : bv}
</div>
</div>
);
})}
</div>
<div className="intel-surface scanlines" style={{ marginTop: 16, borderRadius: 10, padding: '16px 18px', position: 'relative', overflow: 'hidden' }}>
<div className="intel-surface scanlines" style={{ borderRadius: 10, padding: '22px 20px', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'relative', zIndex: 2 }}>
<div className="label" style={{ color: 'rgba(232,255,244,.6)', marginBottom: 6 }}>VYNDR VERDICT</div>
<div className="label" style={{ color: 'rgba(232,255,244,.6)', marginBottom: 8 }}>IN DEVELOPMENT</div>
<div className="mono" style={{ fontSize: 14, color: '#e8fff4', lineHeight: 1.6 }}>
{SAMPLE.a.name} carries the higher floor on scoring, boards, and playmaking on current form, the edge tilts his way.
Head-to-head comparison is being wired to live player game logs. We are not
shipping sample matchups here when the real two-player read is ready it grades
from the same pipeline as every other card. Check back soon.
</div>
</div>
</div>
+1 -1
View File
@@ -83,7 +83,7 @@ export default function DeskShowcase() {
Claim a Founder Desk
</a>
<span className="mono" style={{ fontSize: 12.5, color: 'var(--text-tertiary)' }}>
<span style={{ color: 'var(--g-a)', fontWeight: 700 }}>$34.99</span> locked for the first 100 · then $44.99
<span style={{ color: 'var(--g-a)', fontWeight: 700 }}>$44.99</span> locked for the first 100 founders · the rate holds for life
</span>
</div>
</div>
+2 -1
View File
@@ -35,7 +35,8 @@ const TABS: TabDef[] = [
];
const MORE_ITEMS = [
{ label: 'Compare', href: '/compare' },
// Compare removed from nav — in development, no real feed yet (route still
// resolves to an honest in-dev state, never the old sample matchup).
{ label: 'Tracker', href: '/tracker' },
{ label: 'The Report', href: '/blog' },
{ label: 'Invite Friends', href: '/invite' },
+3 -3
View File
@@ -9,7 +9,7 @@ const FAQS = [
},
{
q: 'How accurate is the model?',
a: 'Check the ledger. Every grade, every result, updated nightly. We don\'t hide misses. Brier score and CLV are tracked from day one and published.',
a: 'Check the ledger. Every grade, every result, updated nightly. We don\'t hide misses. Hit rate is published by grade tier once the sample is large enough to mean something.',
},
{
q: 'What sports do you cover?',
@@ -21,11 +21,11 @@ const FAQS = [
},
{
q: 'What is the Founder Access price?',
a: 'First 100 users lock $14.99/mo for life. After that the price moves to $24.99/mo and never comes back to $14.99. Locked-in pricing carries if you maintain continuous subscription.',
a: 'First 100 users lock $14.99/mo for life. When the 100 founder seats are gone, standard pricing applies. Locked-in pricing carries if you maintain continuous subscription.',
},
{
q: 'How is payment processed?',
a: 'We use NexaPay. You pay with Visa, Mastercard, Apple Pay, or Google Pay. We never see your card data. We are PCI-out-of-scope.',
a: 'We use Stripe. You pay with Visa, Mastercard, Apple Pay, or Google Pay. We never see your card data. We are PCI-out-of-scope.',
},
];
+1 -1
View File
@@ -37,7 +37,7 @@ const FEATURES = [
{
icon: '⌦',
title: 'The honest ledger',
body: 'Every grade. Every result. No hiding. No deletion. Brier score and CLV from day one. Public accuracy by tier.',
body: 'Every grade. Every result. No hiding. No deletion. Public accuracy by grade tier, once the sample is large enough to mean something.',
},
];
+2 -1
View File
@@ -26,7 +26,8 @@ const MORE = [
{ label: 'Explore', href: '/explore' },
// Session 60 (4.4) — discoverable Parlay Lab entry (opens the drawer).
{ label: 'Parlay Lab', href: '#parlay', action: 'parlay' as const },
{ label: 'Compare', href: '/compare' },
// Compare is in development (no real two-player feed yet) — no nav entry until
// it renders real data. The route still resolves to an honest in-dev state.
{ label: 'Tracker', href: '/tracker' },
{ label: 'The Report', href: '/blog' },
{ label: 'Invite', href: '/invite' },
+6 -4
View File
@@ -68,8 +68,10 @@ const TIERS: TierConfig[] = [
{
id: 'analyst',
name: 'Analyst',
// Founder price. First 100 seats lock $14.99 for life (real Stripe-synced
// counter via ClaimMeter). No struck "regular" number: the post-founder
// price is not wired, and we do not promise a figure that isn't.
price: '$14.99',
originalPrice: '$19.99',
cadence: '/mo',
badge: 'Founder Access',
headline: 'The full intelligence layer.',
@@ -91,11 +93,11 @@ const TIERS: TierConfig[] = [
{
// DS5 (Part 6, #8) — Desk is THE hero tier. It carries the value-showing
// story and the single primary CTA on the grid (color contract #9: never
// two competing green CTAs). $44.99 regular, $34.99 for founders.
// two competing green CTAs). Founder Desk = $44.99 (matches lib/checkout.js).
// No struck "regular" number: the post-founder desk price is not wired.
id: 'desk',
name: 'Desk',
price: '$34.99',
originalPrice: '$44.99',
price: '$44.99',
cadence: '/mo',
badge: 'Founder Desk',
headline: 'The professional terminal. Everything the model knows.',
+15 -28
View File
@@ -7,8 +7,7 @@ import { useRouter } from 'next/navigation';
import VyndrGameCard, { type GameCardData } from '@/components/vyndr/GameCard';
import type { SlateSport, GameLines, GameStreak } from '@/components/GameCard';
import { PropRowProp, Tier } from '@/components/PropRow';
import { isRelevantGame, detectBestLines, indexGrades, indexDeltas, buildPlayerStripsFromProps, formatGameTime, buildPitcherMap, pitchersForGameTeams, gradeKey, flattenToEdgeBoard, edgeBoardBreadth } from '@/lib/slateAdapter';
import MobileEdgeBoard from '@/components/vyndr/MobileEdgeBoard';
import { isRelevantGame, detectBestLines, indexGrades, indexDeltas, buildPlayerStripsFromProps, formatGameTime, buildPitcherMap, pitchersForGameTeams, gradeKey } from '@/lib/slateAdapter';
// Wave 4A (Step 3) — OUTLOOK MODE: the never-empty grid. When there are no
// live games (and it's not a fetch failure) the grid shows REAL data —
// yesterday's proven receipts + tomorrow's date-pinned schedule.
@@ -1205,34 +1204,22 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook
{dateOffset === 0 && <MarketBreadth items={breadthItems} />}
{(() => {
// Build each game's card once, then present two ways (Rev 3, screen 01):
// MOBILE = Design's FLAT edge-ranked board (all props, one list, sorted
// by edge); DESKTOP = the game-grouped cards. Same data, different IA —
// toggled by width in globals.css (.edge-board-mobile / -desktop).
// The game-grouped cards, shown identically on mobile and desktop.
// The FLAT mobile "edge board" was removed (honesty pass): its edge%
// feed was a miscalibrated placeholder, so it renders nowhere until a
// real edge feed exists. Phones now show the same real game cards.
const cards = orderedGames.map((g) => slateGameToCardData(g, gradeIndex, deltaIndex, pitcherMap, activeStat, viability, liveIndex));
const edgeRows = flattenToEdgeBoard(cards);
const breadth = edgeBoardBreadth(edgeRows, cards);
const hasBoard = edgeRows.length > 0;
return (
<>
{hasBoard && (
<div className="edge-board-mobile" style={{ background: 'var(--bg-1)', border: '1px solid var(--border)', borderRadius: 12, overflow: 'hidden' }}>
<MobileEdgeBoard rows={edgeRows} breadth={breadth} />
</div>
)}
{/* Cards: on mobile they hide ONLY when the flat board is present
(an ungraded slate still shows the game cards on phones). */}
<div className={hasBoard ? 'edge-board-desktop' : ''} style={{ display: 'grid', gap: 16 }}>
{cards.map((card, i) => (
<VyndrGameCard
key={`${card.sport}-${card.away.abbr}-${card.home.abbr}-${i}`}
game={card}
preferredBooks={preferredBooks}
onOpen={() => router.push('/scan')}
/>
))}
</div>
</>
<div style={{ display: 'grid', gap: 16 }}>
{cards.map((card, i) => (
<VyndrGameCard
key={`${card.sport}-${card.away.abbr}-${card.home.abbr}-${i}`}
game={card}
preferredBooks={preferredBooks}
onOpen={() => router.push('/scan')}
/>
))}
</div>
);
})()}
+4 -1
View File
@@ -229,6 +229,9 @@ export default function PriceTriplet({
const modelWithheld = state === STATES.QUARANTINE;
const modelLocked = state === STATES.NO_VERDICT_LOCKED;
// Honesty pass: model never computed → absent leg, honest note, NO verdict
// (verdictFor returns null for this state), never QUARANTINE's "suppressed" copy.
const modelAbsent = state === STATES.NO_MODEL;
// GREEN = TAKEABLE EDGE ONLY. The model leg is green on exactly one state.
const modelColor = state === STATES.VALUE ? 'var(--g-a)' : 'var(--text-0)';
const verdict = verdictFor(state, vsFair, Number.isFinite(ev as number) ? (ev as number) : null);
@@ -251,7 +254,7 @@ export default function PriceTriplet({
<Leg
label="MODEL"
value={modelWithheld ? null : model}
note={modelWithheld ? 'WITHHELD' : modelLocked ? 'ANALYST' : 'OUR PRICE'}
note={modelWithheld ? 'WITHHELD' : modelLocked ? 'ANALYST' : modelAbsent ? 'NOT PRICED' : 'OUR PRICE'}
color={modelColor}
locked={modelLocked}
compact={compact}
+13 -3
View File
@@ -52,6 +52,13 @@ const STATES = Object.freeze({
// Not a verdict — a display state. The model leg exists and is honest, the
// viewer just isn't entitled to it (free tier). Book + fair render normally.
NO_VERDICT_LOCKED: 'NO_VERDICT_LOCKED',
// Honesty pass: the model price was never COMPUTED (the EV layer isn't
// producing p_win/model_odds yet) — NOT quarantined, NOT locked. The model
// leg renders absent ('—'), book + fair stand, and NO verdict is claimed.
// This exists so a never-priced row never wears QUARANTINE's "we suppressed
// our own price / a leg is poisoned" copy, which asserts a deliberate act
// that did not happen.
NO_MODEL: 'NO_MODEL',
});
/** Strict numeric parse — `Number(null) === 0` is the fabrication bug this
@@ -107,9 +114,11 @@ function deriveValueState(row = {}) {
const model = num(row.model_odds);
const ev = num(row.ev_pct);
// Without a model price or an EV there is no verdict to render — treat as a
// withheld model leg rather than asserting "no edge" we didn't compute.
if (model == null || ev == null) return STATES.QUARANTINE;
// Without a model price or an EV there is no verdict to render. This is an
// absent leg, NOT a quarantine: we never computed it, so we must not claim we
// "suppressed" or "poisoned" it. Book + fair still stand; the model leg goes
// honestly absent and no verdict is asserted.
if (model == null || ev == null) return STATES.NO_MODEL;
// 3. The three real verdicts.
if (isValue(book, ev)) return STATES.VALUE;
@@ -130,6 +139,7 @@ function valueStateColor(state) {
case STATES.NO_EDGE:
case STATES.REFUSAL:
case STATES.NO_VERDICT_LOCKED:
case STATES.NO_MODEL:
default:
return 'var(--text-2)';
}