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
+42 -5
View File
@@ -17,8 +17,10 @@ import {
trackUpgradeClicked,
} from '@/lib/analytics';
import PlayerAvatar from '@/components/vyndr/PlayerAvatar';
import NoMarketState, { type PricedLine } from '@/components/vyndr/NoMarketState';
import { indexPricedLines, pricedLinesFor } from '@/lib/pricedLines';
import NoMarketState, { type PricedLine, type PricedStat } from '@/components/vyndr/NoMarketState';
import {
indexPricedLines, pricedLinesFor, pricedStatsForPlayer, pricedCountForPlayer, totalPricedCount,
} from '@/lib/pricedLines';
import { type HeadshotSport } from '@/lib/playerHeadshot';
import { buildBookLink, SUPPORTED_BOOKS, BOOK_LINK_REL } from '@/lib/bookLinks';
@@ -173,6 +175,23 @@ export default function ScanPage() {
? pricedLinesFor(pricedIndex, selectedPlayer, stat) : []),
[pricedIndex, selectedPlayer, stat],
);
// Session (scanner reskin) — S7 Case A + live count, ALL derived from the SAME
// fresh pricedIndex as pricedForSelection, so the count can never disagree
// with the rows. Presentation-only; the index/refresh/onPick path is unchanged.
const pricedStatsForSelection = useMemo<PricedStat[]>(
() => (PRICED_NUDGE_ENABLED && pricedIndex && selectedPlayer
? pricedStatsForPlayer(pricedIndex, selectedPlayer, stat) : []),
[pricedIndex, selectedPlayer, stat],
);
const playerPricedCount = useMemo(
() => (PRICED_NUDGE_ENABLED && pricedIndex && selectedPlayer
? pricedCountForPlayer(pricedIndex, selectedPlayer) : 0),
[pricedIndex, selectedPlayer],
);
const boardPricedCount = useMemo(
() => (PRICED_NUDGE_ENABLED && pricedIndex ? totalPricedCount(pricedIndex) : 0),
[pricedIndex],
);
// One-tap re-scan after a surfaced priced line commits its line/direction.
useEffect(() => {
@@ -681,9 +700,13 @@ export default function ScanPage() {
{/* Session 79 — HELP, not restriction: the board's REAL priced lines
for this exact player+stat. Tap to pre-fill a line that will yield
a real triplet. The scanner still accepts any free-typed line. */}
{/* S7 Case B — the board's REAL priced lines for this player+stat, in
the blue boundary channel (--priced-out): line · BK odds · ◆ fair.
Tap pre-fills the line so the next scan yields a real triplet. Fair
renders NEUTRAL, not amber (blue-boundary law). */}
{pricedForSelection.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 8 }}>
<span className="mono" style={{ fontSize: 9, color: 'var(--text-3)', letterSpacing: '0.1em', alignSelf: 'center' }}>
<span className="mono" style={{ fontSize: 9, color: 'var(--priced-out-dim, #7a93b0)', letterSpacing: '0.1em', alignSelf: 'center' }}>
PRICED TONIGHT:
</span>
{pricedForSelection.map((l, i) => (
@@ -692,9 +715,20 @@ export default function ScanPage() {
type="button"
onClick={() => { setLine(String(l.line)); setDirection(l.direction); }}
className="mono"
style={{ cursor: 'pointer', fontSize: 11, fontWeight: 700, padding: '4px 9px', borderRadius: 999, border: '1px solid var(--border-hi)', background: 'transparent', color: 'var(--text-1)', fontVariantNumeric: 'tabular-nums' }}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6, cursor: 'pointer',
minHeight: 40, fontSize: 11, fontWeight: 700, padding: '0 11px', borderRadius: 999,
border: '1px solid var(--priced-out-border-hi, rgba(106,147,200,0.34))',
background: 'var(--priced-out-tint-2, rgba(106,147,200,0.12))',
color: 'var(--text-0)', fontVariantNumeric: 'tabular-nums',
}}
>
{l.direction === 'under' ? 'U' : 'O'} {l.line}
<span style={{ fontWeight: 800, fontSize: 12 }}>{l.direction === 'under' ? 'u' : 'o'} {l.line}</span>
<span style={{ color: 'var(--text-3)' }}>·</span>
<span style={{ color: 'var(--text-1)' }}>BK {l.book_odds > 0 ? `+${Math.round(l.book_odds)}` : Math.round(l.book_odds)}</span>
{l.fair_odds != null && (
<span style={{ color: 'var(--priced-out-dim, #7a93b0)' }}> {l.fair_odds > 0 ? `+${Math.round(l.fair_odds)}` : Math.round(l.fair_odds)}</span>
)}
</button>
))}
</div>
@@ -882,6 +916,9 @@ export default function ScanPage() {
stat={stat}
line={line}
pricedLines={pricedForSelection}
pricedStats={pricedStatsForSelection}
playerCount={playerPricedCount}
boardCount={boardPricedCount}
onPick={(l) => {
setLine(String(l.line));
setDirection(l.direction);