From 6bc18d823c5e683b667b961e8f6359425bf4a884 Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 27 Jul 2026 06:04:00 -0400 Subject: [PATCH] Honesty pass: remove every live fabrication (REMOVE/HIDE only, no feature cut) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1 --- src/services/stripeService.js | 2 +- tests/unit/ds5PricingStates.test.js | 17 ++++--- tests/unit/priceTriplet.test.js | 20 +++++--- tests/unit/vyndrMobile.test.js | 4 +- tests/unit/vyndrParityQA.test.js | 33 +++++-------- tests/unit/vyndrPhaseE.test.js | 7 +-- web/src/app/compare/page.tsx | 59 ++++------------------- web/src/app/pricing/DeskShowcase.tsx | 2 +- web/src/components/BottomTabBar.tsx | 3 +- web/src/components/FAQ.tsx | 6 +-- web/src/components/Features.tsx | 2 +- web/src/components/Nav.tsx | 3 +- web/src/components/Pricing.tsx | 10 ++-- web/src/components/Slate.tsx | 43 ++++++----------- web/src/components/vyndr/PriceTriplet.tsx | 5 +- web/src/lib/valueState.js | 16 ++++-- 16 files changed, 100 insertions(+), 132 deletions(-) diff --git a/src/services/stripeService.js b/src/services/stripeService.js index 80f3f34..1f551a1 100644 --- a/src/services/stripeService.js +++ b/src/services/stripeService.js @@ -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'); diff --git a/tests/unit/ds5PricingStates.test.js b/tests/unit/ds5PricingStates.test.js index b10acf2..e7207ed 100644 --- a/tests/unit/ds5PricingStates.test.js +++ b/tests/unit/ds5PricingStates.test.js @@ -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'); diff --git a/tests/unit/priceTriplet.test.js b/tests/unit/priceTriplet.test.js index e65afa0..09035b8 100644 --- a/tests/unit/priceTriplet.test.js +++ b/tests/unit/priceTriplet.test.js @@ -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)', () => { diff --git a/tests/unit/vyndrMobile.test.js b/tests/unit/vyndrMobile.test.js index 1f564eb..1e972a3 100644 --- a/tests/unit/vyndrMobile.test.js +++ b/tests/unit/vyndrMobile.test.js @@ -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'); diff --git a/tests/unit/vyndrParityQA.test.js b/tests/unit/vyndrParityQA.test.js index a1de704..5680dd5 100644 --- a/tests/unit/vyndrParityQA.test.js +++ b/tests/unit/vyndrParityQA.test.js @@ -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); }); }); diff --git a/tests/unit/vyndrPhaseE.test.js b/tests/unit/vyndrPhaseE.test.js index f976e8c..ec531c9 100644 --- a/tests/unit/vyndrPhaseE.test.js +++ b/tests/unit/vyndrPhaseE.test.js @@ -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'); diff --git a/web/src/app/compare/page.tsx b/web/src/app/compare/page.tsx index 78ac651..af67ea2 100644 --- a/web/src/app/compare/page.tsx +++ b/web/src/app/compare/page.tsx @@ -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 (
▚ HEAD TO HEAD

COMPARE

-
- - -
- -
-
-
{SAMPLE.a.name} {SAMPLE.a.team}
-
VS
-
{SAMPLE.b.name} {SAMPLE.b.team}
-
- {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 ( -
-
- {isGrade ? : av} -
-
{k}
-
- {isGrade ? : bv} -
-
- ); - })} -
- -
+
-
VYNDR VERDICT
+
IN DEVELOPMENT
- {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.
diff --git a/web/src/app/pricing/DeskShowcase.tsx b/web/src/app/pricing/DeskShowcase.tsx index c284536..68be74f 100644 --- a/web/src/app/pricing/DeskShowcase.tsx +++ b/web/src/app/pricing/DeskShowcase.tsx @@ -83,7 +83,7 @@ export default function DeskShowcase() { Claim a Founder Desk → - $34.99 locked for the first 100 · then $44.99 + $44.99 locked for the first 100 founders · the rate holds for life
diff --git a/web/src/components/BottomTabBar.tsx b/web/src/components/BottomTabBar.tsx index 53f7e1c..3d76111 100644 --- a/web/src/components/BottomTabBar.tsx +++ b/web/src/components/BottomTabBar.tsx @@ -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' }, diff --git a/web/src/components/FAQ.tsx b/web/src/components/FAQ.tsx index 6dd3dce..6457b09 100644 --- a/web/src/components/FAQ.tsx +++ b/web/src/components/FAQ.tsx @@ -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.', }, ]; diff --git a/web/src/components/Features.tsx b/web/src/components/Features.tsx index 5a7d8c1..76edbb1 100644 --- a/web/src/components/Features.tsx +++ b/web/src/components/Features.tsx @@ -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.', }, ]; diff --git a/web/src/components/Nav.tsx b/web/src/components/Nav.tsx index 98fecb3..90c3093 100644 --- a/web/src/components/Nav.tsx +++ b/web/src/components/Nav.tsx @@ -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' }, diff --git a/web/src/components/Pricing.tsx b/web/src/components/Pricing.tsx index 593a24f..c8cf023 100644 --- a/web/src/components/Pricing.tsx +++ b/web/src/components/Pricing.tsx @@ -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.', diff --git a/web/src/components/Slate.tsx b/web/src/components/Slate.tsx index 833fb91..c06679a 100644 --- a/web/src/components/Slate.tsx +++ b/web/src/components/Slate.tsx @@ -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 && } {(() => { - // 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 && ( -
- -
- )} - {/* Cards: on mobile they hide ONLY when the flat board is present - (an ungraded slate still shows the game cards on phones). */} -
- {cards.map((card, i) => ( - router.push('/scan')} - /> - ))} -
- +
+ {cards.map((card, i) => ( + router.push('/scan')} + /> + ))} +
); })()} diff --git a/web/src/components/vyndr/PriceTriplet.tsx b/web/src/components/vyndr/PriceTriplet.tsx index bd89b3f..57d6f17 100644 --- a/web/src/components/vyndr/PriceTriplet.tsx +++ b/web/src/components/vyndr/PriceTriplet.tsx @@ -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({