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:
Kev
2026-07-20 18:22:00 -04:00
parent f549422758
commit 8cf9cbfc26
11 changed files with 1027 additions and 1 deletions
+8
View File
@@ -77,6 +77,10 @@ const TIERS = Object.freeze({
// Session 64 — per-read directional-CLV badge. Analyst+Desk only; // Session 64 — per-read directional-CLV badge. Analyst+Desk only;
// Free never RECEIVES the data (gated server-side, not hidden client-side). // Free never RECEIVES the data (gated server-side, not hidden client-side).
clv_badge: true, clv_badge: true,
// Session 66 — VYNDR's OWN price (model_odds) on the price triplet.
// Analyst+Desk. Free keeps BOOK and FAIR: the de-vigged honest number is
// the hook and is NEVER the paywall — only our own price gates.
model_price: true,
}), }),
desk: Object.freeze({ desk: Object.freeze({
scans_per_day: Infinity, scans_per_day: Infinity,
@@ -94,6 +98,10 @@ const TIERS = Object.freeze({
// Session 64 — per-read directional-CLV badge. Analyst+Desk only; // Session 64 — per-read directional-CLV badge. Analyst+Desk only;
// Free never RECEIVES the data (gated server-side, not hidden client-side). // Free never RECEIVES the data (gated server-side, not hidden client-side).
clv_badge: true, clv_badge: true,
// Session 66 — VYNDR's OWN price (model_odds) on the price triplet.
// Analyst+Desk. Free keeps BOOK and FAIR: the de-vigged honest number is
// the hook and is NEVER the paywall — only our own price gates.
model_price: true,
}), }),
}); });
+14
View File
@@ -55,6 +55,20 @@ function applyTierGating(result, tierName) {
if (!canAccess(tierName, 'alt_line_ladder')) delete deskGated.alt_lines; if (!canAccess(tierName, 'alt_line_ladder')) delete deskGated.alt_lines;
if (!canAccess(tierName, 'kelly_sizing')) delete deskGated.kelly; if (!canAccess(tierName, 'kelly_sizing')) delete deskGated.kelly;
// Session 66 — the PRICE TRIPLET's model leg. Free never RECEIVES
// `model_odds`; it is stripped here, at the wire, not blurred in the client.
// `book_odds` / `fair_odds` / `ev_pct` deliberately pass through on EVERY
// tier: the de-vigged fair number is the free-tier hook, and the fair leg is
// never the paywall. `model_price_locked` tells the card to render the lock
// teaser rather than an absent leg, so a gated price is never mistaken for a
// missing one.
if (!canAccess(tierName, 'model_price')) {
delete deskGated.model_odds;
if (deskGated.book_odds != null && deskGated.fair_odds != null) {
deskGated.model_price_locked = true;
}
}
if (canAccess(tierName, 'reasoning_visible')) { if (canAccess(tierName, 'reasoning_visible')) {
// Paid tier — everything else passes through unchanged. // Paid tier — everything else passes through unchanged.
return deskGated; return deskGated;
+301
View File
@@ -0,0 +1,301 @@
/* ============================================================
Session 66 — THE PRICE LAYER: token layer + the five honesty states.
Two things are locked here:
1. The TOKEN LAYER is additive and complete — the design bundle's values
exist in globals.css and match HANDOFF.md byte-for-byte.
2. The LAWS are enforceable, not folklore — the verdict function is the
single place "is this value?" is answered, and it can never say VALUE
on a juiced price.
============================================================ */
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..', '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
const CSS = read('web/src/app/globals.css');
const HANDOFF = read('specs/design-reference/HANDOFF.md');
const vs = require('../../web/src/lib/valueState');
const backendValue = require('../../src/config/valueEngine');
// ── 1. TOKEN LAYER — the design's values, present and correct ────────────
describe('token layer — HANDOFF values resolve in globals.css', () => {
// Each entry: [css custom property, expected hex, why it exists]
const TOKENS = [
['--bg-0', '#06060B'], // void
['--bg-1', '#0E0E14'], // card
['--bg-2', '#14141E'], // elevated
['--hairline', '#101018'], // row hairline
['--bg-deep', '#0A0A10'], // deep panel
['--border', '#1E1E2A'],
['--border-hi', '#2A2A38'],
['--text-0', '#F0F0F0'],
['--text-1', '#B8BCC8'],
['--text-2', '#707080'],
['--text-3', '#4a4a58'],
['--g-a', '#00d4a0'], // signal green
['--amber', '#ffb347'],
['--miss', '#FF4757'],
['--priced-out', '#8fb2de'], // NEW this session
];
test.each(TOKENS)('%s is declared as %s', (name, hex) => {
const m = CSS.match(new RegExp(`${name}\\s*:\\s*([^;]+);`));
expect(m).toBeTruthy();
expect(m[1].trim().toLowerCase()).toBe(hex.toLowerCase());
});
it('every hex above also appears in HANDOFF.md (design is the source)', () => {
// The priced-out blue comes from the triplet file, not HANDOFF's token list.
for (const [, hex] of TOKENS.filter(([n]) => n !== '--priced-out')) {
expect(HANDOFF.toLowerCase()).toContain(hex.toLowerCase());
}
});
it('grade colors follow the design: A green, B white, C grey, D/F red', () => {
expect(CSS).toMatch(/--g-a:\s*#00d4a0/i);
expect(CSS).toMatch(/--g-b:\s*#F0F0F0/i);
expect(CSS).toMatch(/--g-c:\s*#B8BCC8/i);
expect(CSS).toMatch(/--g-d:\s*#FF4757/i);
});
it('fonts: Inter for chrome, JetBrains Mono for ALL data', () => {
expect(CSS).toMatch(/--sans:[^;]*Inter/);
expect(CSS).toMatch(/--mono:[^;]*JetBrains Mono/);
});
it('the laws are written INTO the token layer, not left as folklore', () => {
const block = CSS.slice(CSS.indexOf('SESSION 66 — PRICE-LAYER TOKENS'));
expect(block).toMatch(/GREEN = EDGE ONLY/);
expect(block).toMatch(/GLOW = A-TIER ONLY/);
expect(block).toMatch(/AMBER = CAUTION/);
expect(block).toMatch(/RED = MISS \/ NEGATIVE only/i);
expect(block).toMatch(/BLUE = EDGE PRICED OUT/);
expect(block).toMatch(/JETBRAINS MONO = ALL DATA/);
});
it('is ADDITIVE — the new block declares only new names', () => {
// Bound the block to the Session-66 section itself: globals.css continues
// past :root with a11y/media layers that legitimately re-declare tokens.
const start = CSS.indexOf('SESSION 66 — PRICE-LAYER TOKENS');
const block = CSS.slice(start, CSS.indexOf('\n}', start));
const declared = [...block.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gim)].map((m) => m[1]);
expect(declared.length).toBeGreaterThan(0);
// None of the newly-declared names may collide with a pre-existing token.
const before = CSS.slice(0, CSS.indexOf('SESSION 66 — PRICE-LAYER TOKENS'));
for (const name of declared) {
expect(before).not.toMatch(new RegExp(`^\\s*${name}\\s*:`, 'm'));
}
});
});
// ── 2. THE BAND — frontend law mirrors the backend engine ────────────────
describe('takeable band + EV threshold stay in sync with the backend', () => {
it('mirrors src/config/valueEngine.js exactly', () => {
expect(vs.TAKEABLE_ODDS_CEILING).toBe(backendValue.TAKEABLE_ODDS_CEILING);
expect(vs.TAKEABLE_ODDS_MAX).toBe(backendValue.TAKEABLE_ODDS_MAX);
expect(vs.VALUE_EV_THRESHOLD).toBe(backendValue.VALUE_EV_THRESHOLD);
});
it('agrees with the backend on isTakeable / isValue across the band', () => {
const prices = [-400, -210, -161, -160, -110, 100, 200, 201, 500, null, undefined, ''];
for (const p of prices) {
expect(vs.isTakeable(p)).toBe(backendValue.isTakeable(p));
for (const ev of [-58.7, 0, 1.9, 2, 11.7, 26.5]) {
expect(vs.isValue(p, ev)).toBe(backendValue.isValue(p, ev));
}
}
});
it('a missing price is NEVER takeable (Number(null) === 0 guard)', () => {
expect(vs.isTakeable(null)).toBe(false);
expect(vs.isTakeable('')).toBe(false);
expect(vs.isTakeable(undefined)).toBe(false);
});
});
// ── 3. THE FIVE HONESTY STATES ───────────────────────────────────────────
describe('deriveValueState — the single verdict function', () => {
const base = { book_odds: -120, fair_odds: -104, model_odds: -196, ev_pct: 21.4 };
it('VALUE — real edge at a takeable price (the live Petey Halpin row)', () => {
expect(vs.deriveValueState(base)).toBe(vs.STATES.VALUE);
});
it('PRICED OUT — positive EV at a juiced price (the live Josh Bell row)', () => {
// book -210, +11.7% EV: the engine refuses to call this value, and so do we.
const row = { book_odds: -210, fair_odds: -173, model_odds: -311, ev_pct: 11.7 };
expect(vs.deriveValueState(row)).toBe(vs.STATES.PRICED_OUT);
});
it('NEVER calls raw positive EV "value" — the whole point of state 2', () => {
for (const [book, ev] of [[-210, 11.7], [-190, 26.5], [-400, 60], [250, 30]]) {
const st = vs.deriveValueState({ book_odds: book, fair_odds: -150, model_odds: -300, ev_pct: ev });
expect(st).not.toBe(vs.STATES.VALUE);
expect(st).toBe(vs.STATES.PRICED_OUT);
}
});
it('EV below the threshold is NO EDGE even at a takeable price', () => {
expect(vs.deriveValueState({ ...base, ev_pct: 0.2 })).toBe(vs.STATES.NO_EDGE);
expect(vs.deriveValueState({ ...base, ev_pct: 1.99 })).toBe(vs.STATES.NO_EDGE);
expect(vs.deriveValueState({ ...base, ev_pct: 2 })).toBe(vs.STATES.VALUE); // boundary
});
it('QUARANTINE — model leg withheld, book + fair still stand', () => {
const row = { ...base, quarantine_reason: 'wrong_opponent_grade' };
expect(vs.deriveValueState(row)).toBe(vs.STATES.QUARANTINE);
});
it('REFUSAL — no fair price we would defend', () => {
expect(vs.deriveValueState({ ...base, fair_odds: null })).toBe(vs.STATES.REFUSAL);
expect(vs.deriveValueState({ ...base, book_odds: null })).toBe(vs.STATES.REFUSAL);
expect(vs.deriveValueState({ ...base, refused: true })).toBe(vs.STATES.REFUSAL);
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 LOCKED model leg is neither quarantine nor refusal', () => {
const row = { ...base, model_price_locked: true };
expect(vs.deriveValueState(row)).toBe(vs.STATES.NO_VERDICT_LOCKED);
});
});
// ── 4. THE COLOR LAW ─────────────────────────────────────────────────────
describe('valueStateColor — green appears on exactly one state', () => {
it('VALUE is the ONLY green', () => {
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,
];
for (const s of others) expect(vs.valueStateColor(s)).not.toBe('var(--g-a)');
});
it('PRICED OUT is the blue carve-out; NO EDGE is never red', () => {
expect(vs.valueStateColor(vs.STATES.PRICED_OUT)).toBe('var(--priced-out)');
// An honest "no" is the instrument working, not a loss.
expect(vs.valueStateColor(vs.STATES.NO_EDGE)).not.toBe('var(--miss)');
});
it('QUARANTINE is amber (caution), per the token law', () => {
expect(vs.valueStateColor(vs.STATES.QUARANTINE)).toBe('var(--amber)');
});
});
// ── 5. HONEST ABSENCE ────────────────────────────────────────────────────
describe('never prints a number it cannot stand behind', () => {
it('fmtOddsAmerican returns null for absence — never 0, never a placeholder', () => {
expect(vs.fmtOddsAmerican(null)).toBeNull();
expect(vs.fmtOddsAmerican('')).toBeNull();
expect(vs.fmtOddsAmerican(undefined)).toBeNull();
expect(vs.fmtOddsAmerican('abc')).toBeNull();
expect(vs.fmtOddsAmerican(-210)).toBe('-210');
expect(vs.fmtOddsAmerican(125)).toBe('+125');
});
it('modelVsFair is null unless BOTH legs are real', () => {
expect(vs.modelVsFair(null, -110)).toBeNull();
expect(vs.modelVsFair(-110, null)).toBeNull();
expect(typeof vs.modelVsFair(98, 110)).toBe('number');
});
it('reproduces the design file\'s own worked examples EXACTLY', () => {
// The design states these two figures; the formula was derived from them.
// STATE 1: book +125 · fair +110 · model +98 → "+2.9% VS FAIR"
expect(vs.modelVsFair(98, 110)).toBe(2.9);
// STATE 3: book +118 · fair +104 · model +112 → "-1.8% VS FAIR"
expect(vs.modelVsFair(112, 104)).toBe(-1.8);
});
it('compares MODEL to FAIR — not book to fair (the induction bug)', () => {
// Petey Halpin, live: book -120 · fair -104 · model -196. Our price is far
// shorter than fair, so the gap is strongly POSITIVE. Comparing the book
// to fair instead produced -6.5% on a VALUE row — a visible contradiction.
expect(vs.modelVsFair(-196, -104)).toBeGreaterThan(10);
});
});
// ── 6. THE COMPONENT carries the rulings ─────────────────────────────────
describe('PriceTriplet component', () => {
const src = read('web/src/components/vyndr/PriceTriplet.tsx');
it('uses ONLY tokens — no literal hex anywhere', () => {
const hexes = src.match(/#[0-9a-f]{3,8}\b/gi) || [];
expect(hexes).toEqual([]);
});
it('does not re-derive the verdict — it renders deriveValueState', () => {
expect(src).toContain('deriveValueState');
expect(src).not.toMatch(/ev_pct\s*>=\s*2/);
});
it('FAIR is the amber hero and is never locked', () => {
expect(src).toMatch(/label="FAIR"[\s\S]{0,200}var\(--amber\)/);
expect(src).not.toMatch(/label="FAIR"[\s\S]{0,200}locked/);
});
it('only the MODEL leg is lockable / withheld', () => {
expect(src).toMatch(/label="MODEL"[\s\S]{0,300}locked=\{modelLocked\}/);
});
it('carries no sample numbers from the design file', () => {
// Strip comments first — the docblock names the design's sample values in
// order to say they are a SPEC and must never ship as data.
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
for (const sample of ['1,188', '1,115.5', '+125', '+110', '+98', '+130', '+116']) {
expect(code).not.toContain(sample);
}
});
it('REFUSAL renders no legs and no gauge', () => {
const refusal = src.slice(src.indexOf('STATES.REFUSAL'));
expect(refusal).toContain("CAN&rsquo;T PRICE THIS ONE");
});
});
// ── 7. THE SERVER GATE — fair is never the paywall ───────────────────────
describe('free-tier gate strips the model price at the wire', () => {
const { applyTierGating } = require('../../src/utils/tierGating');
const row = {
grade: 'B', book_odds: -120, fair_odds: -104, model_odds: -196, ev_pct: 21.4,
};
it('free: model_odds is REMOVED, book + fair survive', () => {
const out = applyTierGating(row, 'free');
expect(out.model_odds).toBeUndefined();
expect(out.book_odds).toBe(-120);
expect(out.fair_odds).toBe(-104);
expect(out.model_price_locked).toBe(true);
});
it('analyst + desk: the full triplet passes through', () => {
for (const tier of ['analyst', 'desk']) {
const out = applyTierGating(row, tier);
expect(out.model_odds).toBe(-196);
expect(out.model_price_locked).toBeUndefined();
}
});
it('the adapter turns a stripped row into a LOCK, not an absent leg', () => {
const { buildPriceTriplet } = require('../../web/src/lib/gradeAdapter');
const gated = applyTierGating(row, 'free');
const t = buildPriceTriplet(gated).priceTriplet;
expect(t.model_odds).toBeNull();
expect(t.model_price_locked).toBe(true);
expect(vs.deriveValueState(t)).toBe(vs.STATES.NO_VERDICT_LOCKED);
});
it('no book/fair → no price story at all (section hidden, not empty)', () => {
const { buildPriceTriplet } = require('../../web/src/lib/gradeAdapter');
expect(buildPriceTriplet({ grade: 'B' })).toEqual({});
expect(buildPriceTriplet({ book_odds: -120 })).toEqual({});
});
});
+49
View File
@@ -144,6 +144,55 @@
--forest: var(--acc-0); --forest: var(--acc-0);
--forest-dark: #0A2A20; --forest-dark: #0A2A20;
--forest-light: var(--acc-1); --forest-light: var(--acc-1);
/* ═════════════════════════════════════════════════════════
SESSION 66 — PRICE-LAYER TOKENS (ADDITIVE).
The Jul-20 design bundle (`Vyndr Price Triplet.dc.html`) introduces one
colour the system did not have. Everything else it specifies already
resolves above and is byte-identical to HANDOFF.md — nothing here
re-declares or overrides an existing token, and no existing component
changes because these are added.
THE LAWS THESE TOKENS CARRY (machine-readable mirror in
`web/src/lib/colorContract.js`; enforced by colorContract.test.js and
priceTriplet.test.js — a component must be able to read the law, not
just the hex):
--g-a (#00D4A0) GREEN = EDGE ONLY. edge / active / A-tier /
primary CTA. Never decorative. On the price layer:
green fires ONLY on takeable edge (ev >= 2 AND the
price inside the takeable band) — never on a juiced
price, never on raw positive EV.
--glow-a GLOW = A-TIER ONLY (A / A+). The scarcest cue in the
system; gate every glow through gradeGlows().
--amber AMBER = CAUTION / CORRELATION only — plus, from this
bundle, the FAIR leg of the price triplet (the
de-vigged number is the hero of the price layer).
--miss RED = MISS / NEGATIVE only. Never "no edge" (an
honest no is the instrument working, not a loss).
--priced-out BLUE = EDGE PRICED OUT. The carve-out that lets a row
say "there is edge" WITHOUT green saying "take it":
positive EV at a price outside the takeable band.
Used nowhere else in the system.
--mono JETBRAINS MONO = ALL DATA (tabular-nums). Inter is
chrome only. Prices are data.
═════════════════════════════════════════════════════════ */
/* A-tier glow, tokenized. The law (glow = A/A+ only) lived in
colorContract.gradeGlows() with no token to hand back; this is that value. */
--glow-a: 0 0 12px rgba(0, 212, 160, 0.45);
/* EDGE PRICED OUT — muted blue. Exact values from the design bundle. */
--priced-out: #8fb2de;
--priced-out-dim: #7a93b0;
--priced-out-tint: rgba(106, 147, 200, 0.08);
--priced-out-tint-2: rgba(106, 147, 200, 0.12);
--priced-out-border: rgba(106, 147, 200, 0.30);
--priced-out-border-hi: rgba(106, 147, 200, 0.34);
/* FAIR leg surface tints (amber hero of the price layer). */
--fair-tint: rgba(255, 179, 71, 0.055);
--fair-border: rgba(255, 179, 71, 0.22);
} }
/* ───────────────────────────────────────────────────────── /* ─────────────────────────────────────────────────────────
+17
View File
@@ -67,6 +67,14 @@ interface ScanResponse {
archetype?: string; archetype?: string;
archetype_blend?: { archetype: string; weight: number }[]; archetype_blend?: { archetype: string; weight: number }[];
prop_dna?: { reliable: string[]; volatile: string[] }; prop_dna?: { reliable: string[]; volatile: string[] };
// Session 66 — the PRICE LAYER. `model_odds` is derived from p_win by the
// engine and STRIPPED for unentitled tiers, which is what sets
// `model_price_locked` — so a locked leg and a missing leg stay distinct.
book_odds?: number | null;
fair_odds?: number | null;
model_odds?: number | null;
ev_pct?: number | null;
model_price_locked?: boolean;
} }
const NBA_STATS = [ const NBA_STATS = [
@@ -742,6 +750,15 @@ export default function ScanPage() {
archetype: result.archetype, archetype: result.archetype,
archetype_blend: result.archetype_blend, archetype_blend: result.archetype_blend,
prop_dna: result.prop_dna, prop_dna: result.prop_dna,
// Session 66 — the PRICE LAYER. Without these forwarded the
// triplet stays hidden no matter what the engine computed
// (the Session-44 lesson: this call site is a hardcoded
// whitelist, not a spread).
book_odds: result.book_odds,
fair_odds: result.fair_odds,
model_odds: result.model_odds,
ev_pct: result.ev_pct,
model_price_locked: result.model_price_locked,
}) as GradeResultData} }) as GradeResultData}
onAddToParlay={() => { onAddToParlay={() => {
addLeg({ addLeg({
+38
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { GradePill } from './GradeCard'; import { GradePill } from './GradeCard';
import PriceTriplet from './vyndr/PriceTriplet';
/** /**
* Daily hero prop card (Truth-Everywhere Part 2, item 5). * Daily hero prop card (Truth-Everywhere Part 2, item 5).
@@ -28,6 +29,16 @@ type Hero = {
book?: string | null; book?: string | null;
graded_at?: string | null; graded_at?: string | null;
reasoning?: 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' }; const SPORT_LABEL: Record<string, string> = { nba: 'NBA', wnba: 'WNBA', mlb: 'MLB', soccer: 'Soccer' };
@@ -129,6 +140,33 @@ export default function LiveHeroProp() {
</div> </div>
</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. */} {/* Real timestamp — proves this was graded, and when. */}
{data.graded_at && ( {data.graded_at && (
<p className="mono" style={{ fontSize: 10, color: 'var(--text-tertiary)', margin: '0 0 12px', letterSpacing: '0.04em' }}> <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 { type HeadshotSport } from '@/lib/playerHeadshot';
import { gradeColor, gradeHex } from '@/lib/vyndrTokens'; import { gradeColor, gradeHex } from '@/lib/vyndrTokens';
import { edgeColor, gradeGlows } from '@/lib/colorContract'; import { edgeColor, gradeGlows } from '@/lib/colorContract';
import PriceTriplet, { type PriceTripletData } from './PriceTriplet';
import { playerHref } from '@/lib/playerHref'; import { playerHref } from '@/lib/playerHref';
export interface GradeResultData { export interface GradeResultData {
@@ -45,6 +46,10 @@ export interface GradeResultData {
// below 3 real captured points). Fed by the snapshot pipeline's already- // below 3 real captured points). Fed by the snapshot pipeline's already-
// emitted line history + public revision; the scan path leaves them absent. // emitted line history + public revision; the scan path leaves them absent.
history?: Array<{ t: string; line: number }> | null; 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; revisedFrom?: string | null;
gradedLine?: number | null; gradedLine?: number | null;
} }
@@ -189,6 +194,17 @@ export default function GradeResultCard({
)} )}
</div> </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 {/* 4. PROJECTION ROW — DATA SEMANTICS (Session 58): LINE is a real
market number (fact); MODEL/EDGE are model output. When the model market number (fact); MODEL/EDGE are model output. When the model
has no projection they render an absent state ("—"), never the has no projection they render an absent state ("—"), never the
@@ -206,6 +222,19 @@ export default function GradeResultCard({
))} ))}
</div> </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 {/* 4b. LIVE GRADE-SHIFT (Wave 4B) — the line/grade movement timeline over
the snapshot pipeline's already-emitted history. Self-hides below 3 the snapshot pipeline's already-emitted history. Self-hides below 3
real captured points, so the scan path (no captured history) shows real captured points, so the scan path (no captured history) shows
+345
View File
@@ -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 VYNDRs 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&rsquo;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&rsquo;d defend yet the inputs are too thin. When they clear, the
triplet returns. We&rsquo;d rather show nothing than a number we can&rsquo;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>
);
}
+2
View File
@@ -62,3 +62,5 @@ export {
gradeBadgeSize, gradeBadgeSize,
} from '@/lib/vyndrTokens'; } from '@/lib/vyndrTokens';
export { default as ClvBadge } from './ClvBadge'; export { default as ClvBadge } from './ClvBadge';
export { default as PriceTriplet } from './PriceTriplet';
export type { PriceTripletData } from './PriceTriplet';
+40 -1
View File
@@ -105,6 +105,45 @@ function mapScanToGradeResult(input = {}) {
// Session 42 — Player Intelligence additions. All OPTIONAL + self-hiding: // Session 42 — Player Intelligence additions. All OPTIONAL + self-hiding:
// they only render once the engine supplies them (Session 43 data pipeline). // they only render once the engine supplies them (Session 43 data pipeline).
...buildIntelFields(input), ...buildIntelFields(input),
// Session 66 — the PRICE LAYER (book · fair · model). Self-hiding: absent
// unless the engine supplied real prices, so a read with no market prices
// renders the projection alone rather than an empty gauge. NEVER
// fabricated — the design file's numbers are a spec, not a fallback.
...buildPriceTriplet(input),
};
}
/**
* buildPriceTriplet(input) — the optional price-layer block for the card.
*
* Returns `{}` (section stays hidden) unless the engine supplied at least a
* book price AND a fair price: with no honest de-vigged number there is no
* price story to tell, and a lone book price is just the market repeated back.
*
* `model_price_locked` is set by the SERVER (utils/tierGating strips
* `model_odds` for unentitled tiers and flags it) — the free tier never
* receives the model price, so the lock is real, not a client-side blur over
* data that was already sent. Book and fair always pass through: the fair leg
* is never the paywall.
*/
function buildPriceTriplet(input) {
const n = (v) => {
if (v == null || v === '') return null;
const x = typeof v === 'number' ? v : Number(v);
return Number.isFinite(x) ? x : null;
};
const book = n(input.book_odds);
const fair = n(input.fair_odds);
if (book == null || fair == null) return {};
return {
priceTriplet: {
book_odds: book,
fair_odds: fair,
model_odds: n(input.model_odds),
ev_pct: n(input.ev_pct),
quarantine_reason: input.quarantine_reason || null,
model_price_locked: input.model_price_locked === true,
},
}; };
} }
@@ -142,4 +181,4 @@ function buildIntelFields(input) {
return out; return out;
} }
module.exports = { mapScanToGradeResult, statLabel, computeEdge, isPhosphorConfirmed, toSignals, buildIntelFields }; module.exports = { mapScanToGradeResult, statLabel, computeEdge, isPhosphorConfirmed, toSignals, buildIntelFields, buildPriceTriplet };
+184
View File
@@ -0,0 +1,184 @@
/* ============================================================
VYNDR — THE PRICE-LAYER LAW (Session 66).
Plain CommonJS so .tsx components import it AND the Jest suite requires it
directly (same pattern as colorContract.js / vyndrTokens.js / playerName.js).
ONE function decides what a price row is allowed to SAY. The triplet renders
whatever this returns and never re-derives a verdict of its own, so there is
exactly one place where "is this value?" is answered.
THE FIVE HONESTY STATES (design: `Vyndr Price Triplet.dc.html`, ACT 01):
VALUE green — ev >= VALUE_EV_THRESHOLD *AND* the book price is
inside the takeable band. Both. Raw positive EV is
NOT value: +11.7% EV at -210 is a juiced price we
will not call a play.
PRICED_OUT blue — real edge, untakeable price. We won't call a juiced
price a value, and we won't pretend the edge wasn't
there. This state exists so green never has to lie
in either direction.
NO_EDGE grey — the honest number is already priced. Stated at full
voice, never hidden. That "no" is the instrument
working.
QUARANTINE amber — the MODEL leg is withheld (graded against inputs we
no longer trust). Book and fair still render: we
never hide the honest fair number because a
different leg is poisoned. RARE — as of Jul 2026 the
only quarantined rows are one frozen historical
cohort, so this is not a routine state.
REFUSAL dim — no fair price we'd defend. Nothing renders. No
fabricated price, no empty gauge.
TRUTH LAW: the triplet never prints a number it can't stand behind. A leg
with no value renders absent, never a zero and never a placeholder.
============================================================ */
/* ---- Backend mirror -----------------------------------------------------
These MUST match src/config/valueEngine.js. The browser can't require the
backend module, so the constants are duplicated here and a test
(priceTriplet.test.js) reads BOTH files and fails if they drift — the same
guard used for playerName.js. If you tune the band, tune it in both. */
const TAKEABLE_ODDS_CEILING = -160; // most-juiced favourite we'll promote
const TAKEABLE_ODDS_MAX = 200; // longest dog we'll promote
const VALUE_EV_THRESHOLD = 2; // a real edge, not rounding
const STATES = Object.freeze({
VALUE: 'VALUE',
PRICED_OUT: 'PRICED_OUT',
NO_EDGE: 'NO_EDGE',
QUARANTINE: 'QUARANTINE',
REFUSAL: 'REFUSAL',
// 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',
});
/** Strict numeric parse — `Number(null) === 0` is the fabrication bug this
* whole layer exists to prevent, so nothing coerces. */
function num(v) {
if (v == null || v === '') return null;
const n = typeof v === 'number' ? v : Number(v);
return Number.isFinite(n) ? n : null;
}
/** isTakeable(american) — inside the promotable band. Mirrors valueEngine. */
function isTakeable(american) {
const a = num(american);
if (a == null) return false;
return a >= TAKEABLE_ODDS_CEILING && a <= TAKEABLE_ODDS_MAX;
}
/** isValue(american, evPct) — takeable AND clears the EV threshold. Both. */
function isValue(american, evPct) {
const ev = num(evPct);
return isTakeable(american) && ev != null && ev >= VALUE_EV_THRESHOLD;
}
/**
* deriveValueState(row) — the single verdict function.
*
* row: { book_odds, fair_odds, model_odds, ev_pct, quarantine_reason,
* refused, model_price_locked }
*
* Order matters and encodes the honesty ordering:
* 1. REFUSAL first — with no fair price there is nothing honest to show.
* 2. QUARANTINE next — the model leg is untrustworthy, so no verdict may be
* computed from it, but book+fair still stand.
* 3. Then the three real verdicts.
* A locked (free-tier) model leg is NOT quarantine and NOT refusal: the data
* exists and is honest, the viewer just isn't entitled to it — so the verdict
* is withheld rather than asserted, and book+fair render normally.
*/
function deriveValueState(row = {}) {
const fair = num(row.fair_odds);
const book = num(row.book_odds);
// 1. Refusal — no fair price we'd defend (or no market to price against).
if (row.refused === true || fair == null || book == null) return STATES.REFUSAL;
// 2. Quarantine — model leg withheld; book + fair still render.
if (row.quarantine_reason) return STATES.QUARANTINE;
// Free-tier lock: entitled data withheld, not absent. No verdict is claimed
// (the verdict IS the model leg), but this is not quarantine — the row is
// honest, it's just gated.
if (row.model_price_locked === true) return STATES.NO_VERDICT_LOCKED;
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;
// 3. The three real verdicts.
if (isValue(book, ev)) return STATES.VALUE;
if (ev >= VALUE_EV_THRESHOLD) return STATES.PRICED_OUT; // edge, untakeable price
return STATES.NO_EDGE;
}
/** valueStateColor(state) — the ONE mapping from verdict to token.
* Green appears here exactly once, on VALUE, and nowhere else. */
function valueStateColor(state) {
switch (state) {
case STATES.VALUE:
return 'var(--g-a)';
case STATES.PRICED_OUT:
return 'var(--priced-out)';
case STATES.QUARANTINE:
return 'var(--amber)';
case STATES.NO_EDGE:
case STATES.REFUSAL:
case STATES.NO_VERDICT_LOCKED:
default:
return 'var(--text-2)';
}
}
function impliedProb(american) {
const a = num(american);
if (a == null) return null;
return a > 0 ? 100 / (a + 100) : -a / (-a + 100);
}
/**
* modelVsFair(modelOdds, fairOdds) — the "+2.9% VS FAIR" figure on the verdict
* line. It is the gap between OUR price and the honest de-vigged price, in
* implied-probability PERCENTAGE POINTS (not a ratio, and NOT book-vs-fair).
*
* Derived from the design file's own two worked examples, both of which this
* reproduces exactly:
* STATE 1 book +125 · fair +110 · model +98 → "+2.9% VS FAIR"
* STATE 3 book +118 · fair +104 · model +112 → "-1.8% VS FAIR"
* Positive = the model prices it SHORTER than fair (we think it's likelier
* than the honest number says). Null unless both legs are real.
*/
function modelVsFair(modelOdds, fairOdds) {
const pm = impliedProb(modelOdds);
const pf = impliedProb(fairOdds);
if (pm == null || pf == null) return null;
return Math.round((pm - pf) * 1000) / 10;
}
/** fmtOddsAmerican(v) — display an American price, or null for honest absence.
* Never returns '0', never returns a placeholder number. */
function fmtOddsAmerican(v) {
const n = num(v);
if (n == null) return null;
const r = Math.round(n);
return r > 0 ? `+${r}` : String(r);
}
module.exports = {
STATES,
TAKEABLE_ODDS_CEILING,
TAKEABLE_ODDS_MAX,
VALUE_EV_THRESHOLD,
isTakeable,
isValue,
deriveValueState,
valueStateColor,
modelVsFair,
impliedProb,
fmtOddsAmerican,
};