Files
vyndr/web/src/lib/pricedLines.js
T
builtbykev 125919f86a 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
2026-07-22 21:52:41 -04:00

127 lines
5.1 KiB
JavaScript

/* ============================================================
pricedLines (Session 79) — the REAL priced lines from the current snapshot,
indexed for the scanner to surface. Plain CommonJS so it's unit-testable and
importable by the scan page (allowJs).
NON-FABRICATING BY CONSTRUCTION: it only ever returns lines that are actually
in the snapshot with a real book price. Nothing is suggested, interpolated,
or rounded to a nearest. A player+stat the board didn't price returns [].
============================================================ */
const { nameKey } = require('./playerName');
/**
* indexPricedLines(grades) — snapshot grades → Map(playerKey|stat → [rows]).
* A row is kept ONLY if it carries a real book price — a graded prop with no
* market is not a priceable line to surface.
*/
function indexPricedLines(grades) {
const idx = new Map();
for (const g of Array.isArray(grades) ? grades : []) {
if (!g) continue;
const book = g.book_odds;
if (book == null || book === '') continue; // no market → not surfaceable
const player = g.player_name || g.player;
const stat = String(g.stat_type || g.stat || '').toLowerCase();
if (!player || !stat) continue;
const line = Number(g.line);
if (!Number.isFinite(line)) continue;
const key = `${nameKey(player)}|${stat}`;
const side = String(g.direction || g.side || 'over').toLowerCase() === 'under' ? 'under' : 'over';
const rows = idx.get(key) || [];
// Dedupe on line+side — one snapshot shouldn't carry the same line twice,
// but be defensive so the chip list never repeats.
if (!rows.some((r) => r.line === line && r.direction === side)) {
// fair_odds is the de-vigged MARKET price (free-tier visible — NOT the
// gated model_odds). Carried through so the S7 spec row can render
// `◆ FAIR`; strict null (Number(null) === 0 would fabricate a price).
const fair = g.fair_odds;
rows.push({
line,
direction: side,
book_odds: Number(book),
fair_odds: fair == null || fair === '' ? null : Number(fair),
});
}
idx.set(key, rows);
}
return idx;
}
/**
* pricedLinesFor(index, player, stat) — the priced lines for an EXACT
* player+stat. A different stat priced for the same player returns nothing for
* the picked stat; an off-slate player returns nothing. Sorted by line.
*/
function pricedLinesFor(index, player, stat) {
if (!index || !player || !stat) return [];
const key = `${nameKey(player)}|${String(stat).toLowerCase()}`;
const rows = index.get(key) || [];
return [...rows].sort((a, b) => a.line - b.line);
}
/**
* pricedStatsForPlayer(index, player) — the player's priced stats ACROSS the
* board (S7 Case A "WE PRICE THESE FOR [player]"). Reads the SAME fresh index
* the per-selection chips use — filters keys by the player's nameKey prefix so
* the count and the chips can never disagree with the rows above them. Returns
* [{ stat, lines }] sorted by stat; `excludeStat` drops the already-selected
* stat (Case A is "this stat has none, but these others do").
*/
function pricedStatsForPlayer(index, player, excludeStat) {
if (!index || !player) return [];
const prefix = `${nameKey(player)}|`;
const skip = excludeStat ? String(excludeStat).toLowerCase() : null;
const out = [];
for (const [key, rows] of index) {
if (!key.startsWith(prefix)) continue;
const stat = key.slice(prefix.length);
if (skip && stat === skip) continue;
if (Array.isArray(rows) && rows.length) {
out.push({ stat, lines: [...rows].sort((a, b) => a.line - b.line) });
}
}
return out.sort((a, b) => a.stat.localeCompare(b.stat));
}
/**
* pricedCountForPlayer(index, player) — how many priced props (distinct stats)
* the board carries for this player tonight. The number behind the green CTA
* "PLAYER · N PRICED PROPS ▸"; 0 → the caller must degrade to the board path,
* never render "0 PRICED PROPS".
*/
function pricedCountForPlayer(index, player) {
return pricedStatsForPlayer(index, player).length;
}
/**
* totalPricedCount(index) — board-wide priced-prop count ("N PROPS LIVE"). Each
* index key is one player+stat prop. Real exhaust or 0 — never a placeholder.
*/
function totalPricedCount(index) {
return index && typeof index.size === 'number' ? index.size : 0;
}
/**
* nearestPricedLine(index, player, stat, typedLine) — for the typed-line
* mismatch fact line: the priced line closest to what the user typed, so the
* strip can say "o 30.5 ISN'T PRICED · NEAREST ↓ o 27.5" WITHOUT rewriting the
* user's intent. Null when nothing is priced for that exact player+stat.
*/
function nearestPricedLine(index, player, stat, typedLine) {
const rows = pricedLinesFor(index, player, stat);
if (!rows.length) return null;
const t = Number(typedLine);
if (!Number.isFinite(t)) return rows[0];
return rows.reduce((best, r) => (Math.abs(r.line - t) < Math.abs(best.line - t) ? r : best), rows[0]);
}
module.exports = {
indexPricedLines,
pricedLinesFor,
pricedStatsForPlayer,
pricedCountForPlayer,
totalPricedCount,
nearestPricedLine,
};