Scanner reskin: amber → blue boundary channel + build never-built S6/S7 states

Semantic COLOR fix, not cosmetic. The S6/S7 build predated the current Scanner
States spec: it used AMBER (the quarantine / model-suppressed channel) for the
"no market / line not priced" case, telling users "model suppressed" when the
truth is "the board never priced this." Corrected to the HANDOFF Session-3
blue-boundary law: BLUE (--priced-out #8FB2DE) = no-market boundary; amber stays
QUARANTINE; red stays REFUSAL.

Phase 1 (reskin): NoMarketState → dashed BLUE void box + blue header/copy; the
S7 rows → spec format (o 27.5 · BK −114 · ◆ −105 · OPEN READ ▸) at 44px,
390-legible. Input-area surfacer pills reskinned to the blue channel too.

Phase 2 (never-built states, only those Phase 0 confirmed against live data):
- GREEN CTA with LIVE player count ("PLAYER · N PRICED PROPS ▸"), degrading
  honestly to the board path ("N PROPS LIVE · TONIGHT'S BOARD ▸") at 0 — count
  from the SAME fresh index as the rows (pricedCountForPlayer), can't disagree.
- CASE A none-priced DEFAULT: "WE PRICE THESE FOR [player]" — the player's other
  priced stats (pricedStatsForPlayer, filter by nameKey).
- Typed-line-mismatch blue fact line ("o X ISN'T PRICED · NEAREST ↓").

FLAGGED / not built (no shells): Case C off-slate quiet-stop needs schedule/
roster membership the pricedLines index doesn't carry (out of the presentation
fence). Spec CONTRADICTION: Case B says "fair previews amber," but the law
reserves amber for quarantine — fair renders NEUTRAL ◆ (blue-dim), not amber, to
avoid blurring the channel.

Free-tier gate VERIFIED before rendering FAIR: fair_odds is the de-vigged MARKET
price (valueState: "never hide the honest fair number"), NOT the gated
model_odds — no paid leak. Carried through indexPricedLines (additive; keying/
refresh/onPick/stale-tap all unchanged — the proven S7 data path is untouched).
Scan A byte-identical; PRICED_NUDGE_ENABLED still the kill switch; reversible.
Build exit 0; priced + parity suites green (67).

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-22 21:52:41 -04:00
parent 83e9da3663
commit 125919f86a
4 changed files with 350 additions and 77 deletions
+171 -68
View File
@@ -1,121 +1,224 @@
'use client';
/**
* NoMarketState (Session 79) — the honest empty state for a scanned prop the
* board never priced.
* NoMarketState (S6 · SCAN EMPTY STATE) — the honest marketless read card.
*
* A manual scan is an arbitrary player + stat + free-typed line, so most scans
* land on a prop no book line was captured for. The join correctly returns no
* market and the read card's price triplet correctly self-hides — but a blank
* space reads as "the product is empty." This says, truthfully, what happened
* and points the user at what IS priced.
* land on a prop the board never priced. The join correctly returns no market
* and the read card's price triplet correctly self-hides — but a blank space
* reads as "the product is empty." This states, truthfully, what happened and
* offers the player's REAL priced stats as the way through.
*
* TRUTH LAW: NO market numbers, no placeholder odds, no "coming soon." The only
* numbers shown are REAL priced lines from the current snapshot (passed in),
* each a one-tap route to a genuine triplet. If there are none, it degrades to
* copy + a path to the live board. Everything renders from tokens.
* COLOR LAW (HANDOFF Session 3 — blue boundary channel): "no market / line not
* priced / priced out" is the BLUE channel (--priced-out #8FB2DE), NOT amber.
* Amber is reserved for QUARANTINE (model leg suppressed); red for REFUSAL
* (a model decision). The pre-spec build used amber here — telling users "model
* suppressed" when the truth is "the board never priced this." This is the
* semantic correction, not a cosmetic one.
*
* FRAMING: this is HELP ("here's what we price tonight"), not RESTRICTION — the
* scanner still accepts any prop; this only appears once a scan came back
* marketless.
* TRUTH LAW: NO market numbers are invented. The only numbers shown are REAL
* priced lines from the current snapshot (book + de-vigged fair, both free-tier
* visible; MODEL is never rendered here), each a one-tap route to a genuine
* triplet. Counts come from the SAME fresh index as the rows, so they can never
* disagree. Nothing is suggested, interpolated, or rounded to a nearest price.
*
* FLAG (spec vs law): the S7 spec calls fair previews AMBER; the blue-boundary
* law reserves amber for quarantine. To avoid blurring the channel the fair
* number renders in a restrained neutral `◆` treatment, not amber.
*/
export interface PricedLine {
line: number;
direction: 'over' | 'under';
book_odds: number;
fair_odds?: number | null;
}
export interface PricedStat {
stat: string;
lines: PricedLine[];
}
const fmtOdds = (o: number) => (o > 0 ? `+${Math.round(o)}` : String(Math.round(o)));
const statLabel = (s: string) => String(s || '').replace(/_/g, ' ').toUpperCase();
// Blue boundary-channel tokens (globals.css --priced-out family), with the
// documented-intentional hex fallbacks.
const BLUE = 'var(--priced-out, #8fb2de)';
const BLUE_DIM = 'var(--priced-out-dim, #7a93b0)';
const BLUE_TINT = 'var(--priced-out-tint-2, rgba(106,147,200,0.12))';
const BLUE_BORDER = 'var(--priced-out-border-hi, rgba(106,147,200,0.34))';
/** The S7 spec row: `{o|u} {line} · BK {odds} · ◆ {fair} · OPEN READ ▸`, a 44px
* tap straight into the real triplet. Legible at 390px — the label is compact
* (BK/◆) so it never overflows. */
function PricedRow({ l, onPick }: { l: PricedLine; onPick?: (l: PricedLine) => void }) {
return (
<button
type="button"
onClick={() => onPick && onPick(l)}
className="mono"
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
width: '100%',
minHeight: 44,
padding: '0 12px',
borderRadius: 10,
border: `1px solid ${BLUE_BORDER}`,
background: BLUE_TINT,
color: 'var(--text-0)',
cursor: 'pointer',
fontVariantNumeric: 'tabular-nums',
textAlign: 'left',
}}
>
<span style={{ fontWeight: 800, fontSize: 13 }}>
{l.direction === 'under' ? 'u' : 'o'} {l.line}
</span>
<span style={{ color: 'var(--text-3)' }}>·</span>
<span style={{ fontSize: 12, color: 'var(--text-1)' }}>
BK <span style={{ fontWeight: 700 }}>{fmtOdds(l.book_odds)}</span>
</span>
{l.fair_odds != null && (
<>
<span style={{ color: 'var(--text-3)' }}>·</span>
{/* fair preview — NEUTRAL, not amber (blue-boundary law); ◆ carries it */}
<span style={{ fontSize: 12, color: BLUE_DIM }}>
<span style={{ fontWeight: 700 }}>{fmtOdds(l.fair_odds)}</span>
</span>
</>
)}
<span className="mono" style={{ marginLeft: 'auto', fontSize: 9.5, fontWeight: 800, letterSpacing: '0.08em', color: BLUE }}>
OPEN READ
</span>
</button>
);
}
export default function NoMarketState({
player,
stat,
line,
pricedLines = [],
pricedStats = [],
playerCount = 0,
boardCount = 0,
onPick,
}: {
player: string;
stat: string;
line: number | string;
/** REAL priced lines for this exact player+stat from the current snapshot. */
/** REAL priced lines for this EXACT player+stat (typed-line mismatch path). */
pricedLines?: PricedLine[];
/** Tap a real priced line → re-scan it (the market is re-validated server-side). */
/** The player's OTHER priced stats (Case A "we price these for [player]"). */
pricedStats?: PricedStat[];
/** Priced-prop count for THIS player (green CTA). 0 → degrade to board path. */
playerCount?: number;
/** Board-wide priced-prop count ("N PROPS LIVE"). */
boardCount?: number;
/** Tap a real priced line → re-scan it (re-validated server-side). */
onPick?: (l: PricedLine) => void;
}) {
const statLabel = String(stat || '').replace(/_/g, ' ');
const sorted = [...pricedLines].sort((a, b) => a.line - b.line);
const hasExact = sorted.length > 0; // this stat IS priced (typed mismatch)
const hasOtherStats = pricedStats.length > 0; // Case A
return (
<div
className="no-market-state"
style={{
border: '1px solid var(--border)',
border: `1px dashed ${BLUE_BORDER}`, // DASHED blue void box (refusal-frame shape, blue not red)
borderRadius: 12,
padding: '16px 18px',
background: 'var(--bg-deep)',
background: BLUE_TINT,
marginTop: 12,
}}
>
<div
className="mono"
style={{ fontSize: 10.5, fontWeight: 800, letterSpacing: '0.12em', color: 'var(--amber)', marginBottom: 6 }}
style={{ fontSize: 10.5, fontWeight: 800, letterSpacing: '0.12em', color: BLUE, marginBottom: 6 }}
>
NO LIVE MARKET FOR THIS LINE
</div>
<p style={{ fontSize: 13, color: 'var(--text-1)', margin: '0 0 12px', lineHeight: 1.5 }}>
The board hasn&rsquo;t priced <span className="mono" style={{ color: 'var(--text-0)' }}>{player}</span>{' '}
{statLabel} at <span className="mono" style={{ color: 'var(--text-0)' }}>{String(line)}</span>. The grade
above stands; there&rsquo;s just no book number to price it against.
<p style={{ fontSize: 13, color: 'var(--text-1)', margin: '0 0 8px', lineHeight: 1.5 }}>
No book prices <span className="mono" style={{ color: 'var(--text-0)' }}>{player}</span>{' '}
{String(stat || '').replace(/_/g, ' ')} at{' '}
<span className="mono" style={{ color: 'var(--text-0)' }}>{String(line)}</span> right now so
there&rsquo;s no vig to strip and no fair number to show. The grade above stands; we won&rsquo;t
invent a market that isn&rsquo;t there.
</p>
<div className="mono" style={{ fontSize: 8.5, fontWeight: 700, letterSpacing: '0.14em', color: 'var(--text-3)', marginBottom: 12 }}>
NO BOOK · NO FAIR · NO MODEL · NOTHING FABRICATED
</div>
{pricedLines.length > 0 ? (
{/* ── PATH FORWARD ──────────────────────────────────────────────────── */}
{hasExact ? (
// Typed-line mismatch: this exact stat IS priced, just not at the typed
// line. Show the real priced lines — never rewrite the user's intent.
<>
<div
className="mono"
style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.12em', color: 'var(--text-3)', marginBottom: 8 }}
>
PRICED TONIGHT · TAP TO READ THE TRIPLET
<div className="mono" style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.12em', color: BLUE_DIM, marginBottom: 8 }}>
<span style={{ color: 'var(--text-2)' }}>o {String(line)} ISN&rsquo;T PRICED</span> · NEAREST PRICED LINE
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{pricedLines.map((l, i) => (
<button
key={`${l.direction}-${l.line}-${i}`}
type="button"
onClick={() => onPick && onPick(l)}
className="mono"
style={{
cursor: 'pointer',
fontSize: 12,
fontWeight: 700,
padding: '7px 12px',
borderRadius: 999,
border: '1px solid var(--fair-border)',
background: 'var(--fair-tint)',
color: 'var(--text-0)',
fontVariantNumeric: 'tabular-nums',
}}
>
{l.direction === 'under' ? 'U' : 'O'} {l.line}{' '}
<span style={{ color: 'var(--text-2)' }}>· {fmtOdds(l.book_odds)}</span>
</button>
))}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{sorted.map((l, i) => <PricedRow key={`${l.direction}-${l.line}-${i}`} l={l} onPick={onPick} />)}
</div>
</>
) : (
<a
href="/dashboard"
className="mono"
style={{
display: 'inline-block',
fontSize: 11,
fontWeight: 800,
letterSpacing: '0.08em',
color: 'var(--g-a)',
textDecoration: 'none',
}}
>
SEE TONIGHT&rsquo;S PRICED BOARD
</a>
)}
) : hasOtherStats ? (
// CASE A — none priced for THIS stat, but the player has other priced
// stats. The DEFAULT, common case: quiet, structured, the way through.
<>
<div className="mono" style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.12em', color: BLUE_DIM, marginBottom: 8 }}>
WE PRICE THESE FOR {String(player).toUpperCase()}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{pricedStats.map((ps) => {
const l = ps.lines[0];
return (
<button
key={ps.stat}
type="button"
onClick={() => onPick && l && onPick(l)}
className="mono"
style={{
display: 'inline-flex', alignItems: 'center', gap: 6, minHeight: 40, padding: '0 12px',
borderRadius: 999, border: `1px solid ${BLUE_BORDER}`, background: 'transparent',
color: 'var(--text-0)', cursor: 'pointer', fontVariantNumeric: 'tabular-nums',
}}
>
<span style={{ fontSize: 9.5, fontWeight: 700, letterSpacing: '0.06em', color: BLUE_DIM }}>{statLabel(ps.stat)}</span>
{l && <span style={{ fontSize: 12, fontWeight: 800 }}>{l.direction === 'under' ? 'u' : 'o'} {l.line}</span>}
</button>
);
})}
</div>
</>
) : null}
{/* ── LIVE COUNT CTA — player-specific when >0, else honest board path ── */}
<div style={{ marginTop: hasExact || hasOtherStats ? 14 : 0 }}>
{playerCount > 0 ? (
<a href="/dashboard" className="mono" style={ctaStyle(BLUE)}>
{String(player).toUpperCase()} · {playerCount} PRICED PROP{playerCount === 1 ? '' : 'S'}
</a>
) : (
<a href="/dashboard" className="mono" style={ctaStyle(BLUE)}>
{boardCount > 0 ? `${boardCount} PROPS LIVE · ` : ''}TONIGHT&rsquo;S BOARD
</a>
)}
</div>
</div>
);
}
function ctaStyle(color: string): React.CSSProperties {
return {
display: 'inline-block',
fontSize: 11,
fontWeight: 800,
letterSpacing: '0.08em',
color,
textDecoration: 'none',
};
}