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
+1 -1
View File
@@ -32,7 +32,7 @@ const PRICE_UNCONFIGURED = '__unconfigured__';
// codes distributed before the rebrand keep redeeming during the transition.
const VALID_FOUNDER_CODES = (process.env.FOUNDER_CODES || 'FOUNDER2026,VYNDR,BETONBLK,EARLYBIRD').split(',');
// Session 55 — founder pricing is still an active launch lever (the ClaimMeter
// scarcity meter + the $14.99/$34.99 founder tiers advertise it), so the default
// scarcity meter + the $14.99/$44.99 founder tiers advertise it), so the default
// window runs through 2026. The 2026-06-30 default had silently lapsed (current
// date 2026-07-10), disabling every founder code. Operators override via env.
const FOUNDER_EXPIRY = new Date(process.env.FOUNDER_CODE_EXPIRY || '2026-12-31');
+10 -7
View File
@@ -36,13 +36,16 @@ describe('Pricing — Desk is the hero, real prices, single primary CTA', () =>
expect(tierBlock(pricing, 'desk')).toBe('true');
});
test('real prices only — Desk $34.99 founder / $44.99 regular, Analyst $14.99/$19.99, Free 5 reads', () => {
expect(pricing).toMatch(/id: 'desk'[\s\S]*?price: '\$34\.99'/);
expect(pricing).toMatch(/id: 'desk'[\s\S]*?originalPrice: '\$44\.99'/);
test('real founder prices only — Desk $44.99, Analyst $14.99, no struck/unwired "regular" number, Free 5 reads', () => {
// Honesty pass: founder Desk = $44.99 (matches lib/checkout.js); Analyst = $14.99.
// The struck "regular" numbers ($19.99 / $34.99) were removed — the post-founder
// price is not wired, and we do not promise a figure that isn't.
expect(pricing).toMatch(/id: 'desk'[\s\S]*?price: '\$44\.99'/);
expect(pricing).toMatch(/id: 'analyst'[\s\S]*?price: '\$14\.99'/);
expect(pricing).toMatch(/id: 'analyst'[\s\S]*?originalPrice: '\$19\.99'/);
expect(pricing).toContain('5 reads to try the model');
// no fabricated legacy price
// no stale/fabricated founder numbers anywhere in the grid
expect(pricing).not.toContain('$34.99');
expect(pricing).not.toContain("originalPrice: '$19.99'");
expect(pricing).not.toContain("originalPrice: '$24.99'");
expect(pricing).not.toContain("originalPrice: '$49.99'");
});
@@ -55,8 +58,8 @@ describe('Pricing — Desk is the hero, real prices, single primary CTA', () =>
expect(showcase).toContain('Every grade, every alt line, live.');
expect(showcase).not.toContain('$1M');
expect(showcase).not.toContain('1M terminal');
expect(showcase).toContain('$44.99'); // the conversion hook figure
expect(showcase).toContain('$34.99'); // the founder price
expect(showcase).toContain('$44.99'); // the founder price (first 100)
expect(showcase).not.toContain('$34.99'); // stale founder price — removed
// real feature visuals fill the right half (kills the dead half, #8)
expect(showcase).toContain('ALT LINE LADDER');
expect(showcase).toContain('QUARTER-KELLY');
+13 -7
View File
@@ -157,9 +157,13 @@ describe('deriveValueState — the single verdict function', () => {
expect(vs.deriveValueState({})).toBe(vs.STATES.REFUSAL);
});
it('a missing model price or EV withholds the verdict — never asserts NO EDGE', () => {
expect(vs.deriveValueState({ ...base, model_odds: null })).toBe(vs.STATES.QUARANTINE);
expect(vs.deriveValueState({ ...base, ev_pct: null })).toBe(vs.STATES.QUARANTINE);
it('a missing model price or EV → NO_MODEL (absent leg), never NO EDGE and never QUARANTINE', () => {
// Honesty pass: never-computed is NOT a deliberate quarantine, so it must
// not wear QUARANTINE's "we suppressed / a leg is poisoned" copy.
expect(vs.deriveValueState({ ...base, model_odds: null })).toBe(vs.STATES.NO_MODEL);
expect(vs.deriveValueState({ ...base, ev_pct: null })).toBe(vs.STATES.NO_MODEL);
// A real quarantine_reason still quarantines (distinct from never-computed).
expect(vs.deriveValueState({ ...base, model_odds: null, quarantine_reason: 'x' })).toBe(vs.STATES.QUARANTINE);
});
it('a LOCKED model leg is neither quarantine nor refusal', () => {
@@ -174,7 +178,7 @@ describe('valueStateColor — green appears on exactly one state', () => {
expect(vs.valueStateColor(vs.STATES.VALUE)).toBe('var(--g-a)');
const others = [
vs.STATES.PRICED_OUT, vs.STATES.NO_EDGE, vs.STATES.QUARANTINE,
vs.STATES.REFUSAL, vs.STATES.NO_VERDICT_LOCKED,
vs.STATES.REFUSAL, vs.STATES.NO_VERDICT_LOCKED, vs.STATES.NO_MODEL,
];
for (const s of others) expect(vs.valueStateColor(s)).not.toBe('var(--g-a)');
});
@@ -263,11 +267,13 @@ describe('PriceTriplet component', () => {
// ── 6b. CONSUMERS MUST FORWARD THE LOCK FLAG ─────────────────────────────
describe('a gated model leg never wears quarantine copy', () => {
it('deriveValueState separates GATED from WITHHELD', () => {
it('deriveValueState separates GATED (locked) from NEVER-COMPUTED — neither is QUARANTINE', () => {
const gated = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null, model_price_locked: true };
const withheld = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null };
const neverComputed = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null };
expect(vs.deriveValueState(gated)).toBe(vs.STATES.NO_VERDICT_LOCKED);
expect(vs.deriveValueState(withheld)).toBe(vs.STATES.QUARANTINE);
// Honesty pass: a never-computed model leg is NO_MODEL (absent), NOT quarantine
// — it must not claim we "suppressed" a price we never produced.
expect(vs.deriveValueState(neverComputed)).toBe(vs.STATES.NO_MODEL);
});
it('LiveHeroProp forwards model_price_locked (live bug: it did not)', () => {
+3 -1
View File
@@ -43,9 +43,11 @@ describe('Phase F — BottomTabBar (5-tab spec)', () => {
describe('Phase F — More bottom sheet', () => {
const src = read('components/BottomTabBar.tsx');
it('lists the secondary routes', () => {
['/compare', '/tracker', '/blog', '/invite', '/pricing', '/account', '/help', '/about', '/responsible-gambling'].forEach((href) =>
// Compare removed from nav (honesty pass — in development, no real feed).
['/tracker', '/blog', '/invite', '/pricing', '/account', '/help', '/about', '/responsible-gambling'].forEach((href) =>
expect(src).toContain(href),
);
expect(src).not.toContain('/compare');
});
it('slides up with a dismissible backdrop', () => {
expect(src).toContain('sheet-up');
+13 -20
View File
@@ -182,23 +182,14 @@ describe('M1 — mobile foundation is wired (structural, not visual)', () => {
describe('M4 — mobile structural rules locked (source, not visual)', () => {
const css = read('app/globals.css');
it('the flat EDGE BOARD is the mobile board; game cards are desktop-only', () => {
// Design screen 01: <768px shows the flat edge-ranked board, ≥768px keeps
// the game-grouped cards.
expect(css).toMatch(/\.edge-board-mobile\s*\{\s*display:\s*none/);
expect(css).toMatch(/@media\s*\(max-width:\s*767px\)[^]*?\.edge-board-mobile\s*\{\s*display:\s*block/);
expect(css).toMatch(/\.edge-board-desktop\s*\{\s*display:\s*none\s*!important/);
it('the fabricated mobile edge board is REMOVED — phones show the real game cards', () => {
// Honesty pass: MobileEdgeBoard's edge% feed was a miscalibrated placeholder
// (it masked any value >40% to "—"). It renders nowhere until a real edge
// feed exists. The Slate no longer imports or renders it; mobile now shows
// the same real game cards as desktop.
const slate = read('components/Slate.tsx');
expect(slate).toMatch(/MobileEdgeBoard/);
expect(slate).toMatch(/flattenToEdgeBoard/);
});
it('the mobile edge board renders matchup chips + tier grade + one hero edge%', () => {
const b = read('components/vyndr/MobileEdgeBoard.tsx');
expect(b).toMatch(/TeamChip/); // Rev 3 matchup chips
expect(b).toMatch(/GradeBadge/); // tier-colored grade
expect(b).toMatch(/var\(--miss\)/); // negative edge = red by sign
expect(b).toMatch(/rampOpacity/); // Design's ranked opacity ramp
expect(slate).not.toMatch(/MobileEdgeBoard/);
expect(slate).not.toMatch(/flattenToEdgeBoard/);
});
it('document still never scrolls sideways on phones (overflow contained)', () => {
@@ -290,12 +281,14 @@ describe('P1-7 — edge board guards the broken edge placeholder', () => {
});
});
// ── P1-8 lock — Compare verdict makes no unverifiable "tonight" claim ────────
describe('P1-8 — Compare verdict is time-honest (form, not a phantom game)', () => {
// ── P1-8 lock — Compare ships NO fabrication (honest in-development state) ────
describe('P1-8 — Compare carries no hardcoded grades or phantom verdict', () => {
const compare = read('app/compare/page.tsx');
it('the sample verdict never claims "tonight" (NBA has 0 games in July)', () => {
it('no sample players, no fake verdict, no "tonight" claim — honest in-dev', () => {
expect(compare).not.toMatch(/edge tonight/i);
expect(compare).toMatch(/on current form/i);
expect(compare).not.toMatch(/Joki|Wembanyama/);
expect(compare).not.toContain('VYNDR VERDICT');
expect(compare).toMatch(/IN DEVELOPMENT/i);
});
});
+4 -3
View File
@@ -84,10 +84,11 @@ describe('Phase E.3 — stubs are now real pages', () => {
it.each(pages)('/%s is no longer a RouteStub', (p) => {
expect(read(`app/${p}/page.tsx`)).not.toContain('RouteStub');
});
it('compare renders two player inputs + a verdict', () => {
it('compare shows an honest in-development state — no sample grades, no fake verdict', () => {
const src = read('app/compare/page.tsx');
expect((src.match(/TerminalInput/g) || []).length).toBeGreaterThanOrEqual(2);
expect(src).toContain('VYNDR VERDICT');
expect(src).toMatch(/IN DEVELOPMENT/i);
expect(src).not.toContain('VYNDR VERDICT');
expect(src).not.toMatch(/Joki|Wembanyama/); // no hardcoded sample players
});
it('invite renders the 3-friends referral messaging', () => {
expect(read('app/invite/page.tsx')).toContain('3 friends');
+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)';
}