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:
@@ -32,7 +32,16 @@ function indexPricedLines(grades) {
|
||||
// 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)) {
|
||||
rows.push({ line, direction: side, book_odds: Number(book) });
|
||||
// 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);
|
||||
}
|
||||
@@ -51,4 +60,67 @@ function pricedLinesFor(index, player, stat) {
|
||||
return [...rows].sort((a, b) => a.line - b.line);
|
||||
}
|
||||
|
||||
module.exports = { indexPricedLines, pricedLinesFor };
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user