db61876b2f
Completes P0-3 across all three surfaces: - CONSENSUS VS MODEL (MarketBreadth): dedupe by player+market so Ben Williamson's alt-line variants show as ONE consensus row (no per-market cap — a consensus table just shouldn't repeat a player). - LEDGER cards: group by player+market via groupIntoLadders — Alec Bohm's strikeout ladder (U1.6/U1.3/O1.5) is now ONE card with the rungs nested (each its own side/line + tier-colored grade), not three separate cards. - playerGrouping reads player OR player_name (ledger rows use player_name) — regression-tested so the ledger doesn't silently empty. The Alt Line Ladder shape is what /pricing already demos; the record now uses it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.4 KiB
JavaScript
89 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Player/market grouping (P0-3) — the phone audit found the same player listed
|
|
* as separate flat entries everywhere (Bohm's strikeout ladder = 3 ledger cards,
|
|
* a leaderboard flooded with 9 identical stolen_bases rows). Props must present
|
|
* as ONE row per player per market family, with alt lines nested as a ladder,
|
|
* and no single market may flood a ranked list.
|
|
*
|
|
* CommonJS so it's unit-testable by the plain-JS Jest suite AND importable by
|
|
* the .tsx surfaces (allowJs) — same doctrine as vyndrTokens / playerName.
|
|
*/
|
|
|
|
const { nameKey } = require('./playerName');
|
|
|
|
/** A stat's MARKET FAMILY — alt lines of the same market collapse together.
|
|
* (Side/line are stripped; only the stat type identifies the family.) */
|
|
function marketFamily(stat) {
|
|
return String(stat || '').trim().toLowerCase().replace(/\s+/g, '_');
|
|
}
|
|
|
|
function playerMarketKey(player, stat) {
|
|
return `${nameKey(player)}|${marketFamily(stat)}`;
|
|
}
|
|
|
|
/**
|
|
* Dedupe ranked leaderboard rows: ONE row per (player, market family) — keeping
|
|
* the FIRST (best-ranked) occurrence — then a per-market cap so one prop type
|
|
* can't flood the board. Input order is preserved (callers pass pre-ranked).
|
|
*
|
|
* @param {Array<{player?:string, stat?:string}>} rows
|
|
* @param {number} [perMarketCap=4] max rows of one market family on the board
|
|
* @returns {Array} deduped, capped, order-preserving
|
|
*/
|
|
function dedupeLeaders(rows, perMarketCap = 4) {
|
|
const seen = new Set();
|
|
const marketCount = new Map();
|
|
const out = [];
|
|
for (const r of Array.isArray(rows) ? rows : []) {
|
|
const who = r.player || r.player_name;
|
|
if (!r || !who) continue;
|
|
const key = playerMarketKey(who, r.stat);
|
|
if (seen.has(key)) continue; // one row per player+market
|
|
const fam = marketFamily(r.stat);
|
|
const n = marketCount.get(fam) || 0;
|
|
if (perMarketCap > 0 && n >= perMarketCap) continue; // don't let one market flood
|
|
seen.add(key);
|
|
marketCount.set(fam, n + 1);
|
|
out.push(r);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Group flat prop rows into ONE entry per (player, market family) with the alt
|
|
* lines nested as a ladder — the Alt Line Ladder shape (the /pricing Desk demo).
|
|
* `betterOf(a, b)` picks the primary/base line (default: keep the first seen).
|
|
* Order of first appearance is preserved.
|
|
*
|
|
* @param {Array} rows each { player, stat, line, ... }
|
|
* @param {(a,b)=>any} [betterOf] choose the representative row of a ladder
|
|
* @returns {Array<{ player, stat, market, primary, ladder }>}
|
|
*/
|
|
function groupIntoLadders(rows, betterOf) {
|
|
const byKey = new Map();
|
|
const order = [];
|
|
for (const r of Array.isArray(rows) ? rows : []) {
|
|
const who = r.player || r.player_name;
|
|
if (!r || !who) continue;
|
|
const key = playerMarketKey(who, r.stat);
|
|
if (!byKey.has(key)) {
|
|
byKey.set(key, { player: who, stat: r.stat, market: marketFamily(r.stat), primary: r, ladder: [r] });
|
|
order.push(key);
|
|
} else {
|
|
const g = byKey.get(key);
|
|
g.ladder.push(r);
|
|
if (typeof betterOf === 'function') g.primary = betterOf(g.primary, r);
|
|
}
|
|
}
|
|
// Sort each ladder by line ascending for a stable rungs display.
|
|
for (const key of order) {
|
|
const g = byKey.get(key);
|
|
g.ladder.sort((a, b) => (Number(a.line) || 0) - (Number(b.line) || 0));
|
|
}
|
|
return order.map((k) => byKey.get(k));
|
|
}
|
|
|
|
module.exports = { marketFamily, playerMarketKey, dedupeLeaders, groupIntoLadders };
|