Price-layer token foundation + the price triplet, built natively on it
PHASE 0 finding, reported before building: the token layer this order asked
me to establish ALREADY EXISTS and already matches HANDOFF.md exactly.
web/src/app/globals.css :root carries the design's surfaces, borders, text
ramp, fonts and grade colours byte-for-byte (aligned 2026-07-16), and
lib/colorContract.js already encodes the green-is-edge-only and
glow-is-A-tier-only laws with a test enforcing them. The stack is Tailwind v4
CSS-first (no config file) with components styled by inline style={{}} reading
var(--x) — 1,916 such reads — so CSS custom properties are the only vehicle
the stack natively consumes. Creating a second parallel layer would have meant
two competing sources of truth, so this EXTENDS the existing one.
PHASE 1 — additive only. globals.css gains one colour the system did not have,
the priced-out blue (#8fb2de + tints), plus a tokenized A-tier glow and the
fair-leg tints. The block writes the LAWS into the token layer itself — green
= takeable edge only, glow = A-tier only, amber = caution + the fair leg, red
= miss/negative only, blue = edge priced out, JetBrains Mono = all data — and
a test asserts every newly-declared name is new (zero collisions, zero
overrides). No existing hardcoded style was touched and no live surface was
migrated: the diff over existing files is 149 insertions, 0 deletions.
PHASE 2 — lib/valueState.js is the single verdict function; the component
renders what it returns and never re-derives one. VALUE fires only on
ev >= 2 AND a takeable price, mirroring src/config/valueEngine.js with a test
that cross-checks both files and fails on drift. Five states: VALUE (green),
EDGE-NOT-TAKEABLE (blue), NO EDGE (grey, stated at full voice), QUARANTINE
(model leg withheld, book+fair stand), REFUSAL (nothing rendered). Free tier
is gated at the wire — tierGating strips model_odds and sets
model_price_locked, so the lock is real rather than a blur over data already
sent; book and fair pass through on every tier because the fair leg is never
the paywall. Wired into the landing hero (data was already on /api/hero-prop)
and the read card, where the projection block reads first and the triplet sits
beside it, not in place of it. Ledger and public profile are out of scope —
no fair-odds columns exist there.
PHASE 3 — induced all six states in a real browser and read back computed
styles, not just markup. Green resolves on VALUE alone: rgb(0,212,160) on the
model leg and verdict; the +11.7%-EV-at-210 row renders rgb(143,178,222) blue
and a white model leg; quarantine shows MODEL "—" with book and fair intact;
refusal renders no legs at all; free tier renders a lock bar with book -120 and
fair -104 still honest. Landing hero on live data: book -153, fair -129, model
-343, VALUE +21.1% vs fair at +28.1% EV. At a real 390px column the three legs
hold at 117px each with no horizontal overflow and fair no smaller than its
neighbours.
Induction caught a real bug that markup review would not have: the "VS FAIR"
figure compared BOOK to fair, printing "VALUE · -6.5% VS FAIR" — a
contradiction on screen. The design's own two worked examples pin the formula
as MODEL minus FAIR in implied-probability percentage points; modelVsFair now
reproduces both exactly (+2.9 and -1.8) and a test locks them. The figure is
shown only when its sign agrees with the verdict, so a row that clears the EV
bar on the book price while our price sits level with fair leads with the EV
instead of a number that reads as a contradiction.
Tests 3539 passed / 290 suites, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GradePill } from './GradeCard';
|
||||
import PriceTriplet from './vyndr/PriceTriplet';
|
||||
|
||||
/**
|
||||
* Daily hero prop card (Truth-Everywhere Part 2, item 5).
|
||||
@@ -28,6 +29,16 @@ type Hero = {
|
||||
book?: string | null;
|
||||
graded_at?: string | null;
|
||||
reasoning?: string | null;
|
||||
// Session 66 — the PRICE LAYER. /api/hero-prop (heroPropService.toHero) has
|
||||
// emitted these all along; the card just never declared or read them. All
|
||||
// nullable: a missing leg renders absent, never a sample number.
|
||||
book_odds?: number | null;
|
||||
fair_odds?: number | null;
|
||||
model_odds?: number | null;
|
||||
ev_pct?: number | null;
|
||||
value?: boolean | null;
|
||||
takeable?: boolean | null;
|
||||
quarantine_reason?: string | null;
|
||||
};
|
||||
|
||||
const SPORT_LABEL: Record<string, string> = { nba: 'NBA', wnba: 'WNBA', mlb: 'MLB', soccer: 'Soccer' };
|
||||
@@ -129,6 +140,33 @@ export default function LiveHeroProp() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ② PRICE · IS IT FAIR (Session 66 — the price triplet).
|
||||
The block above answers "will it clear" in STAT space; this answers
|
||||
"is it fair" in PRICE space. Additive — the projection comparison is
|
||||
untouched and the triplet sits beside it, per the design's two-question
|
||||
hierarchy. Self-hides when the feed carries no real prices. */}
|
||||
{(data.book_odds != null || data.fair_odds != null) && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div
|
||||
className="mono"
|
||||
style={{ fontSize: 9, letterSpacing: '0.14em', color: 'var(--text-3)', fontWeight: 700, marginBottom: 6 }}
|
||||
>
|
||||
PRICE · IS IT FAIR
|
||||
</div>
|
||||
<PriceTriplet
|
||||
compact
|
||||
showBody={false}
|
||||
data={{
|
||||
book_odds: data.book_odds,
|
||||
fair_odds: data.fair_odds,
|
||||
model_odds: data.model_odds,
|
||||
ev_pct: data.ev_pct,
|
||||
quarantine_reason: data.quarantine_reason,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Real timestamp — proves this was graded, and when. */}
|
||||
{data.graded_at && (
|
||||
<p className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)', margin: '0 0 12px', letterSpacing: '0.04em' }}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import PlayerAvatar from '@/components/vyndr/PlayerAvatar';
|
||||
import { type HeadshotSport } from '@/lib/playerHeadshot';
|
||||
import { gradeColor, gradeHex } from '@/lib/vyndrTokens';
|
||||
import { edgeColor, gradeGlows } from '@/lib/colorContract';
|
||||
import PriceTriplet, { type PriceTripletData } from './PriceTriplet';
|
||||
import { playerHref } from '@/lib/playerHref';
|
||||
|
||||
export interface GradeResultData {
|
||||
@@ -45,6 +46,10 @@ export interface GradeResultData {
|
||||
// below 3 real captured points). Fed by the snapshot pipeline's already-
|
||||
// emitted line history + public revision; the scan path leaves them absent.
|
||||
history?: Array<{ t: string; line: number }> | null;
|
||||
// Session 66 — the PRICE LAYER (book · fair · model). Optional + self-hiding:
|
||||
// absent means the engine supplied no prices, and the card shows the
|
||||
// projection alone rather than an empty gauge.
|
||||
priceTriplet?: PriceTripletData | null;
|
||||
revisedFrom?: string | null;
|
||||
gradedLine?: number | null;
|
||||
}
|
||||
@@ -189,6 +194,17 @@ export default function GradeResultCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Session 66 — TWO-QUESTION HIERARCHY (design ACT 01): the projection
|
||||
answers "will it clear" in STAT space and reads FIRST; the price
|
||||
triplet below answers "is it fair" in PRICE space. Two questions,
|
||||
two stacked blocks — the triplet sits BESIDE the projection, it does
|
||||
not replace it. This label is additive; the row below is untouched. */}
|
||||
{d.priceTriplet && (
|
||||
<div className="mono" style={{ fontSize: 9, letterSpacing: '0.14em', color: 'var(--text-3)', fontWeight: 700, padding: '10px 16px 0' }}>
|
||||
① PROJECTION · WILL IT CLEAR
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. PROJECTION ROW — DATA SEMANTICS (Session 58): LINE is a real
|
||||
market number (fact); MODEL/EDGE are model output. When the model
|
||||
has no projection they render an absent state ("—"), never the
|
||||
@@ -206,6 +222,19 @@ export default function GradeResultCard({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 4a. ② PRICE · IS IT FAIR (Session 66) — BOOK · FAIR · MODEL.
|
||||
Self-hides entirely when the engine supplied no price layer, so a
|
||||
read with no market prices shows the projection alone rather than an
|
||||
empty gauge. Every honesty ruling lives in lib/valueState. */}
|
||||
{d.priceTriplet && (
|
||||
<div style={{ padding: '12px 16px 14px', borderBottom: '1px solid var(--border)' }}>
|
||||
<div className="mono" style={{ fontSize: 9, letterSpacing: '0.14em', color: 'var(--text-3)', fontWeight: 700, marginBottom: 8 }}>
|
||||
② PRICE · IS IT FAIR
|
||||
</div>
|
||||
<PriceTriplet data={d.priceTriplet} ledgerHref="/ledger" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4b. LIVE GRADE-SHIFT (Wave 4B) — the line/grade movement timeline over
|
||||
the snapshot pipeline's already-emitted history. Self-hides below 3
|
||||
real captured points, so the scan path (no captured history) shows
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
STATES,
|
||||
deriveValueState,
|
||||
valueStateColor,
|
||||
modelVsFair,
|
||||
fmtOddsAmerican,
|
||||
TAKEABLE_ODDS_CEILING,
|
||||
TAKEABLE_ODDS_MAX,
|
||||
} from '@/lib/valueState';
|
||||
|
||||
/**
|
||||
* PriceTriplet (Session 66) — BOOK · FAIR · MODEL.
|
||||
*
|
||||
* The first component built natively on the price-layer tokens. Extends the
|
||||
* parlay's TRUE FAIR VALUE language from a 2-up grid to a 3-leg per-prop grid.
|
||||
*
|
||||
* DESIGN: `specs/design-reference/Vyndr Price Triplet.dc.html` (ACT 01) +
|
||||
* HANDOFF.md laws. Every colour resolves from a token — no literal hex.
|
||||
*
|
||||
* BOOK the market's offered price. Honest on every row.
|
||||
* FAIR the de-vigged number. THE HERO (amber). Never hidden — not even
|
||||
* when the model leg is poisoned, and never the paywall.
|
||||
* MODEL VYNDR's own price. Green ONLY for takeable edge, never a juiced
|
||||
* one. Suppressible on quarantined rows, lockable on free.
|
||||
*
|
||||
* LIVE DATA ONLY. The design file's numbers (+125 / +110 / +98) are a SPEC,
|
||||
* not data — nothing here carries a sample value. A leg with no real number
|
||||
* renders absent (a dash), never a zero and never a placeholder.
|
||||
*
|
||||
* The verdict is NOT computed here: `deriveValueState` owns it so there is one
|
||||
* place where "is this value?" is answered. This component only renders it.
|
||||
*/
|
||||
|
||||
export interface PriceTripletData {
|
||||
book_odds?: number | string | null;
|
||||
fair_odds?: number | string | null;
|
||||
model_odds?: number | string | null;
|
||||
ev_pct?: number | string | null;
|
||||
quarantine_reason?: string | null;
|
||||
refused?: boolean;
|
||||
/** Free tier: the model leg exists server-side but is not sent. */
|
||||
model_price_locked?: boolean;
|
||||
}
|
||||
|
||||
const ABSENT = '—';
|
||||
|
||||
/** Verdict copy, verbatim from the design file. */
|
||||
function verdictFor(state: string, vsFair: number | null, ev: number | null) {
|
||||
const pct = (n: number | null) => (n == null ? null : `${n >= 0 ? '+' : ''}${n}%`);
|
||||
switch (state) {
|
||||
case STATES.VALUE:
|
||||
// Show the VS FAIR gap only when it agrees in sign with the verdict.
|
||||
// A row can clear the EV bar on the BOOK price while our own price sits
|
||||
// level with fair; printing "VALUE · -0.4% VS FAIR" would read as a
|
||||
// contradiction, so we lead with the EV that actually drove the call.
|
||||
return {
|
||||
label: vsFair != null && vsFair > 0 ? `VALUE · ${pct(vsFair)} VS FAIR` : 'VALUE',
|
||||
sub: ev != null ? `${pct(ev)} EV` : null,
|
||||
body: 'Model prices it shorter than the honest number — the book is paying longer than the leg is worth. Real edge.',
|
||||
mark: null,
|
||||
};
|
||||
case STATES.PRICED_OUT:
|
||||
return {
|
||||
label: 'EDGE · NOT TAKEABLE',
|
||||
sub: ev != null ? `${pct(ev)} EV, priced out` : null,
|
||||
body: `The math shows edge — but this price is too juiced to bet. We won't call a juiced price a value, and we won't pretend the edge wasn't there. Takeable band ${TAKEABLE_ODDS_CEILING} to +${TAKEABLE_ODDS_MAX}.`,
|
||||
mark: '⊘',
|
||||
};
|
||||
case STATES.NO_EDGE:
|
||||
return {
|
||||
// Same sign-coherence rule inverted: a NO-EDGE row whose model happens
|
||||
// to sit above fair says so with the EV, not a positive-looking gap.
|
||||
label: vsFair != null && vsFair < 0 ? `NO EDGE HERE · ${pct(vsFair)} VS FAIR` : 'NO EDGE HERE',
|
||||
sub: ev != null ? `${pct(ev)} EV` : null,
|
||||
body: "Model lands short of fair — the honest number is already priced. We're not calling this a play. That “no” is the instrument working.",
|
||||
mark: null,
|
||||
};
|
||||
case STATES.QUARANTINE:
|
||||
return {
|
||||
label: 'MODEL READ WITHHELD',
|
||||
sub: null,
|
||||
body: 'We suppressed our own price on this row. Book and fair stand — we never hide the honest fair number because a different leg is poisoned.',
|
||||
mark: null,
|
||||
};
|
||||
case STATES.NO_VERDICT_LOCKED:
|
||||
return {
|
||||
label: 'MODEL PRICE ON ANALYST',
|
||||
sub: null,
|
||||
body: 'The honest de-vigged number stays free. Only VYNDR’s own price gates.',
|
||||
mark: null,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function Leg({
|
||||
label,
|
||||
value,
|
||||
note,
|
||||
color,
|
||||
hero,
|
||||
locked,
|
||||
compact,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | null;
|
||||
note?: string | null;
|
||||
color: string;
|
||||
hero?: boolean;
|
||||
locked?: boolean;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: compact ? '8px 10px' : '10px 12px',
|
||||
background: hero ? 'var(--fair-tint)' : 'transparent',
|
||||
border: `1px solid ${hero ? 'var(--fair-border)' : 'var(--border)'}`,
|
||||
borderRadius: 8,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: 7.5,
|
||||
letterSpacing: '0.14em',
|
||||
color: 'var(--text-2)',
|
||||
fontWeight: 700,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{hero ? '◆ ' : ''}
|
||||
{label}
|
||||
</div>
|
||||
{locked ? (
|
||||
// The lock is a TEASER, not a number. Nothing here can be mistaken
|
||||
// for a price the viewer wasn't given.
|
||||
<div
|
||||
aria-label="Model price locked"
|
||||
style={{
|
||||
marginTop: 4,
|
||||
height: compact ? 18 : 22,
|
||||
borderRadius: 4,
|
||||
background:
|
||||
'repeating-linear-gradient(90deg, var(--border-hi) 0 10px, transparent 10px 18px)',
|
||||
opacity: 0.55,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: compact ? 17 : 21,
|
||||
fontWeight: 800,
|
||||
color,
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
marginTop: 2,
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
{value ?? ABSENT}
|
||||
</div>
|
||||
)}
|
||||
{note && (
|
||||
<div
|
||||
className="mono"
|
||||
style={{ fontSize: 7.5, letterSpacing: '0.1em', color: 'var(--text-3)', fontWeight: 700 }}
|
||||
>
|
||||
{note}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PriceTriplet({
|
||||
data,
|
||||
compact = false,
|
||||
showBody = true,
|
||||
ledgerHref,
|
||||
}: {
|
||||
data: PriceTripletData | null | undefined;
|
||||
compact?: boolean;
|
||||
showBody?: boolean;
|
||||
ledgerHref?: string | null;
|
||||
}) {
|
||||
if (!data) return null;
|
||||
const state = deriveValueState(data);
|
||||
|
||||
const book = fmtOddsAmerican(data.book_odds);
|
||||
const fair = fmtOddsAmerican(data.fair_odds);
|
||||
const model = fmtOddsAmerican(data.model_odds);
|
||||
const ev = data.ev_pct == null || data.ev_pct === '' ? null : Number(data.ev_pct);
|
||||
const vsFair = modelVsFair(data.model_odds, data.fair_odds);
|
||||
const accent = valueStateColor(state);
|
||||
|
||||
// REFUSAL — nothing to price. No legs, no gauge, no fabricated number.
|
||||
if (state === STATES.REFUSAL) {
|
||||
return (
|
||||
<div
|
||||
className="price-triplet"
|
||||
style={{
|
||||
border: '1px dashed var(--border-hi)',
|
||||
borderRadius: 10,
|
||||
padding: compact ? 12 : 16,
|
||||
background: 'var(--bg-deep)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="mono"
|
||||
style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.12em', color: 'var(--text-2)' }}
|
||||
>
|
||||
CAN’T PRICE THIS ONE
|
||||
</div>
|
||||
{showBody && (
|
||||
<p style={{ fontSize: 12.5, color: 'var(--text-2)', margin: '6px 0 0', lineHeight: 1.55 }}>
|
||||
No fair price we’d defend yet — the inputs are too thin. When they clear, the
|
||||
triplet returns. We’d rather show nothing than a number we can’t stand
|
||||
behind.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const modelWithheld = state === STATES.QUARANTINE;
|
||||
const modelLocked = state === STATES.NO_VERDICT_LOCKED;
|
||||
// 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);
|
||||
|
||||
return (
|
||||
<div className="price-triplet">
|
||||
<div
|
||||
className="price-triplet-legs"
|
||||
style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}
|
||||
>
|
||||
<Leg label="BOOK" value={book} note="OFFERED" color="var(--text-0)" compact={compact} />
|
||||
<Leg
|
||||
label="FAIR"
|
||||
value={fair}
|
||||
note="DE-VIGGED"
|
||||
color="var(--amber)"
|
||||
hero
|
||||
compact={compact}
|
||||
/>
|
||||
<Leg
|
||||
label="MODEL"
|
||||
value={modelWithheld ? null : model}
|
||||
note={modelWithheld ? 'WITHHELD' : modelLocked ? 'ANALYST' : 'OUR PRICE'}
|
||||
color={modelColor}
|
||||
locked={modelLocked}
|
||||
compact={compact}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{verdict && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: compact ? '8px 10px' : '10px 12px',
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${
|
||||
state === STATES.PRICED_OUT ? 'var(--priced-out-border)' : 'var(--border)'
|
||||
}`,
|
||||
background:
|
||||
state === STATES.PRICED_OUT
|
||||
? 'var(--priced-out-tint)'
|
||||
: state === STATES.VALUE
|
||||
? 'rgba(0, 212, 160, 0.05)'
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span
|
||||
className="mono"
|
||||
style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.08em', color: accent }}
|
||||
>
|
||||
{verdict.mark ? `${verdict.mark} ` : ''}
|
||||
{verdict.label}
|
||||
</span>
|
||||
{verdict.sub && (
|
||||
<span
|
||||
className="mono"
|
||||
style={{
|
||||
fontSize: 10.5,
|
||||
color: 'var(--text-2)',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}}
|
||||
>
|
||||
{verdict.sub}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showBody && verdict.body && (
|
||||
<p style={{ fontSize: 12.5, color: 'var(--text-1)', margin: '6px 0 0', lineHeight: 1.55 }}>
|
||||
{verdict.body}
|
||||
</p>
|
||||
)}
|
||||
{modelLocked && (
|
||||
<a
|
||||
href="/pricing"
|
||||
className="mono"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginTop: 8,
|
||||
fontSize: 10.5,
|
||||
fontWeight: 800,
|
||||
letterSpacing: '0.08em',
|
||||
color: 'var(--g-a)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
UNLOCK ▸
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ledgerHref && (
|
||||
<a
|
||||
href={ledgerHref}
|
||||
className="mono"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginTop: 8,
|
||||
minHeight: 44,
|
||||
lineHeight: '44px',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.1em',
|
||||
color: 'var(--text-2)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
CLOSES TRACKED IN YOUR LEDGER ▸
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,3 +62,5 @@ export {
|
||||
gradeBadgeSize,
|
||||
} from '@/lib/vyndrTokens';
|
||||
export { default as ClvBadge } from './ClvBadge';
|
||||
export { default as PriceTriplet } from './PriceTriplet';
|
||||
export type { PriceTripletData } from './PriceTriplet';
|
||||
|
||||
Reference in New Issue
Block a user