Session 43: Data pipeline + audit fixes + depth chart foundation (2045 tests)
P0 fixes + wiring real data into the S42 Player Intelligence architecture. - P0 dropdown z-index: the nav's backdrop-filter stacking context let the Ticker/HeartbeatBar paint over the avatar/More dropdowns and eat clicks. nav now position:relative zIndex:2; menus zIndex:100. Avatar Settings -> /settings. - Real MLB stats: mlbStatsAdapter.searchPlayer + getPlayerStats (name->id-> season+gamelog). playerIntelService.resolvePlayerStats normalizes into the archetype classifier; getPlayerIntel returns found:true + real season + archetype classified from real stats. NBA via nbaStatsClient (degrades). - Game cards: slateAdapter.groupPropsByPlayer (playerStrips, name once) + mapPitchers (MLB probables), folded into mapScheduleToGameCards. Legacy GameCard line grid renders BookChip (brand colors) not grey text. - Grade card intel: analyzeViaEngine1.buildIntelFields computes stat-context + form/usage/matchup/rest from the existing feature vector (zero extra I/O); gradeAdapter lights up the card sections. Archetype deferred (needs season line at grade time). - Depth chart foundation: depthChartService (getLineup/getDepthChart/ getCascadeProjection) + /api/stats/lineup|depth|cascade, graceful + injectable. - Mobile: player hero name overflow-wrap + 24px on <=640px (was clipping). Backend 2011 -> 2045 tests (+34), 163 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,10 +129,71 @@ async function getBatterVsPitcher(batterId, pitcherId, group = 'hitting') {
|
||||
return splits.length > 0 ? (splits[0].stat || null) : null;
|
||||
}
|
||||
|
||||
const normName = (n) => String(n == null ? '' : n).toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
/**
|
||||
* Resolve a player name → MLB person record (Session 43). Pulls the season
|
||||
* player list (cached 24h — heavy but rarely changes) and matches by
|
||||
* normalized full name. Returns { id, fullName, team, teamId, position } or
|
||||
* null. Needed because every other adapter method keys on playerId.
|
||||
*/
|
||||
async function searchPlayer(name, season = DEFAULT_SEASON) {
|
||||
const target = normName(name);
|
||||
if (!target) return null;
|
||||
const url = `${BASE}/sports/1/players?season=${season}`;
|
||||
const data = await fetchWithCache(url, `mlbstats:players:${season}`, 24 * 3600);
|
||||
const people = (data && Array.isArray(data.people)) ? data.people : [];
|
||||
const hit = people.find((p) => normName(p.fullName) === target)
|
||||
|| people.find((p) => normName(p.fullName).includes(target) && target.length >= 6);
|
||||
if (!hit) return null;
|
||||
return {
|
||||
id: hit.id,
|
||||
fullName: hit.fullName ?? name,
|
||||
team: hit.currentTeam?.name ?? null,
|
||||
teamId: hit.currentTeam?.id ?? null,
|
||||
position: hit.primaryPosition?.abbreviation ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Name-keyed convenience: resolve the player, then fetch the season stat
|
||||
* object for the right group (pitching for pitchers, hitting otherwise) plus a
|
||||
* recent game log. Returns { found, id, name, team, position, group, season,
|
||||
* last10 } — `season` is the raw MLB stat object, mapped by the caller. Returns
|
||||
* { found: false } on any miss/failure (never throws).
|
||||
*/
|
||||
async function getPlayerStats(name, season = DEFAULT_SEASON) {
|
||||
try {
|
||||
const person = await searchPlayer(name, season);
|
||||
if (!person) return { found: false };
|
||||
const group = person.position === 'P' ? 'pitching' : 'hitting';
|
||||
const [seasonStat, log] = await Promise.all([
|
||||
getSeasonAverages(person.id, season, group),
|
||||
getPlayerGameLog(person.id, season, group),
|
||||
]);
|
||||
if (!seasonStat) return { found: false, id: person.id, name: person.fullName, team: person.team, position: person.position, group };
|
||||
return {
|
||||
found: true,
|
||||
id: person.id,
|
||||
name: person.fullName,
|
||||
team: person.team,
|
||||
position: person.position,
|
||||
group,
|
||||
season: seasonStat,
|
||||
last10: (log || []).slice(-10),
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[mlbStats] getPlayerStats failed:', name, err.message);
|
||||
return { found: false };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getScheduleWithPitchers,
|
||||
getPlayerGameLog,
|
||||
getSeasonAverages,
|
||||
getBatterVsPitcher,
|
||||
__internals: { BASE, TTL, extractSplits, ymd, DEFAULT_SEASON },
|
||||
searchPlayer,
|
||||
getPlayerStats,
|
||||
__internals: { BASE, TTL, extractSplits, ymd, DEFAULT_SEASON, normName },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* depthChartService — lineup / depth-chart / cascade foundation (Session 43).
|
||||
*
|
||||
* Provides the data model + graceful aggregation for:
|
||||
* - getLineup(sport, team) — tonight's projected lineup / starters
|
||||
* - getDepthChart(sport, team) — starters + backups per position
|
||||
* - getCascadeProjection(...) — "when X is OUT, teammate Y gets +delta"
|
||||
*
|
||||
* Sources (all best-effort, injectable for tests, never throws):
|
||||
* - mlbStatsAdapter.getScheduleWithPitchers → probable pitchers (MLB)
|
||||
* - scheduleService.getGameSummary → ESPN injuries / leaders
|
||||
*
|
||||
* This is the FOUNDATION: contracts + real data where it's freely available,
|
||||
* graceful empty defaults elsewhere. The minutes/usage projection model and
|
||||
* full batting orders arrive with the Session-44/45 pipelines.
|
||||
*/
|
||||
|
||||
const norm = (s) => String(s == null ? '' : s).trim().toUpperCase();
|
||||
|
||||
/** Today's UTC date (YYYY-MM-DD). Injectable for deterministic tests. */
|
||||
function todayISO(now) {
|
||||
return (now || new Date()).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tonight's projected lineup for a team. MLB returns the probable starting
|
||||
* pitcher (the one lineup slot the free schedule feed exposes); other sports
|
||||
* fall back to the ESPN summary leaders when a game is found. Always returns an
|
||||
* array (possibly empty).
|
||||
*/
|
||||
async function getLineup(sport, team, opts = {}) {
|
||||
const sp = String(sport || 'nba').toLowerCase();
|
||||
const t = norm(team);
|
||||
if (!t) return [];
|
||||
try {
|
||||
if (sp === 'mlb') {
|
||||
const mlb = opts.mlbAdapter || require('./adapters/mlbStatsAdapter');
|
||||
const date = opts.date || todayISO(opts.now);
|
||||
const games = await mlb.getScheduleWithPitchers(date);
|
||||
const game = (games || []).find((g) => matchesTeam(g.home, t) || matchesTeam(g.away, t));
|
||||
if (!game) return [];
|
||||
const side = matchesTeam(game.home, t) ? game.home : game.away;
|
||||
const out = [];
|
||||
if (side?.probablePitcher?.name) {
|
||||
out.push({ player: side.probablePitcher.name, position: 'SP', battingOrder: null, projectedMinutes: null });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[depthChart] getLineup failed:', sport, team, err.message);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function matchesTeam(side, t) {
|
||||
if (!side || !t) return false;
|
||||
const name = norm(side.team || side.name);
|
||||
if (!name) return false;
|
||||
const lastWord = name.split(' ').pop();
|
||||
return name === t || name.includes(t) || (lastWord.length >= 3 && t.includes(lastWord));
|
||||
}
|
||||
|
||||
/**
|
||||
* Depth chart for a team: positions with starter/backup/thirdString. When no
|
||||
* roster source is wired, returns a valid empty structure (graceful default).
|
||||
* Pass opts.roster ([{player, position, depth}]) to build a real chart.
|
||||
*/
|
||||
async function getDepthChart(sport, team, opts = {}) {
|
||||
const sp = String(sport || 'nba').toLowerCase();
|
||||
const t = norm(team);
|
||||
const base = { sport: sp, team: t, positions: [] };
|
||||
const roster = Array.isArray(opts.roster) ? opts.roster : null;
|
||||
if (!roster) return base;
|
||||
const byPos = {};
|
||||
for (const r of roster) {
|
||||
const pos = norm(r.position) || 'UTIL';
|
||||
(byPos[pos] = byPos[pos] || []).push(r);
|
||||
}
|
||||
base.positions = Object.entries(byPos).map(([position, players]) => {
|
||||
const sorted = players.slice().sort((a, b) => (a.depth || 99) - (b.depth || 99));
|
||||
return {
|
||||
position,
|
||||
starter: sorted[0]?.player || null,
|
||||
backup: sorted[1]?.player || null,
|
||||
thirdString: sorted[2]?.player || null,
|
||||
};
|
||||
});
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cascade projection: what happens to teammates' production when `player` is
|
||||
* OUT. Foundation heuristic — uses the design's archetype cascade weights
|
||||
* (usage sponges benefit most). Returns [] when we can't establish that the
|
||||
* player is actually out or have no teammates to project onto.
|
||||
* opts.teammates: [{player, archetype, baseUsage}] — injected by the caller.
|
||||
*/
|
||||
async function getCascadeProjection(sport, player, team, opts = {}) {
|
||||
const teammates = Array.isArray(opts.teammates) ? opts.teammates : [];
|
||||
if (!player || teammates.length === 0) return [];
|
||||
// Distribute a fixed usage pool across teammates, weighting usage sponges and
|
||||
// high-usage creators heavier (mirrors the design's cascade framing).
|
||||
const POOL = Number.isFinite(opts.usagePool) ? opts.usagePool : 12; // %
|
||||
const weightFor = (a) => {
|
||||
const k = norm(a);
|
||||
if (k.includes('USAGE SPONGE')) return 3;
|
||||
if (k.includes('VOLUME') || k.includes('COMBO') || k.includes('POINT FORWARD')) return 2;
|
||||
if (k.includes('ROLE GLUE') || k.includes('SPECIALIST')) return 0.5;
|
||||
return 1;
|
||||
};
|
||||
const weights = teammates.map((m) => weightFor(m.archetype));
|
||||
const total = weights.reduce((s, w) => s + w, 0) || 1;
|
||||
return teammates.map((m, i) => {
|
||||
const delta = +((POOL * weights[i]) / total).toFixed(1);
|
||||
return {
|
||||
player: m.player,
|
||||
stat: 'usage',
|
||||
delta: `+${delta}%`,
|
||||
reason: `${player} OUT → ${m.player} absorbs touches`,
|
||||
};
|
||||
}).sort((a, b) => parseFloat(b.delta) - parseFloat(a.delta));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLineup,
|
||||
getDepthChart,
|
||||
getCascadeProjection,
|
||||
_internals: { norm, matchesTeam, todayISO },
|
||||
};
|
||||
@@ -265,6 +265,55 @@ function fallbackLegacyResult(rawProp, errors) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Form score (0..100) from recent-vs-baseline averages (Session 43). Hot
|
||||
* (l5 > l20) trends above 70; cold below. undefined when there's no recent avg.
|
||||
*/
|
||||
function computeFormScore(features = {}) {
|
||||
const l5 = features.l5_avg;
|
||||
if (!Number.isFinite(l5)) return undefined;
|
||||
const base = Number.isFinite(features.l20_avg) ? features.l20_avg
|
||||
: Number.isFinite(features.l10_avg) ? features.l10_avg : null;
|
||||
if (base == null || base === 0) return 75;
|
||||
const ratio = l5 / base;
|
||||
return Math.round(Math.max(40, Math.min(99, 70 + (ratio - 1) * 60)));
|
||||
}
|
||||
|
||||
function matchupGradeFromRank(rank) {
|
||||
if (!Number.isFinite(rank)) return undefined;
|
||||
if (rank >= 0.66) return 'A';
|
||||
if (rank >= 0.5) return 'B+';
|
||||
if (rank >= 0.33) return 'B';
|
||||
return 'C';
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligence fields for the grade card (Session 43) — STAT CONTEXT + VYNDR
|
||||
* INTELLIGENCE. Computed ONLY from the already-built feature vector (no extra
|
||||
* I/O), so every field is optional and self-hides on the card when absent.
|
||||
* NOTE: archetype is intentionally NOT set here — the per-prop feature vector
|
||||
* doesn't carry a full multi-stat season line, so classifying it would just
|
||||
* yield the fallback. The archetype strip lights up once the snapshot pipeline
|
||||
* (Session 44) feeds per-player season lines into the grade response.
|
||||
*/
|
||||
function buildIntelFields(features = {}) {
|
||||
const out = {};
|
||||
const round1 = (n) => Math.round(n * 10) / 10;
|
||||
if (Number.isFinite(features.l20_avg)) out.season_avg = round1(features.l20_avg);
|
||||
else if (Number.isFinite(features.season_avg)) out.season_avg = round1(features.season_avg);
|
||||
if (Number.isFinite(features.l10_avg)) out.last10_avg = round1(features.l10_avg);
|
||||
else if (Number.isFinite(features.l5_avg)) out.last10_avg = round1(features.l5_avg);
|
||||
|
||||
const form = computeFormScore(features);
|
||||
if (form != null) out.form = form;
|
||||
if (Number.isFinite(features.usage_rate)) out.usage = `${round1(features.usage_rate)}%`;
|
||||
else if (Number.isFinite(features.minutes_per_game)) out.usage = `${Math.round(features.minutes_per_game)} min`;
|
||||
const matchup = matchupGradeFromRank(features.opp_rank_stat);
|
||||
if (matchup) out.matchup_grade = matchup;
|
||||
if (Number.isFinite(features.rest_days)) out.rest = `${features.rest_days}d rest`;
|
||||
return out;
|
||||
}
|
||||
|
||||
async function analyzeViaEngine1(rawProp = {}) {
|
||||
const featureResult = await computeFeaturesForProp(rawProp);
|
||||
const { features, trap, consistency, prop, meta } = featureResult;
|
||||
@@ -308,6 +357,10 @@ async function analyzeViaEngine1(rawProp = {}) {
|
||||
line: prop.line,
|
||||
});
|
||||
|
||||
// Session 43 — attach grade-card intelligence fields (stat context + VYNDR
|
||||
// intelligence). Optional + self-hiding on the card; zero extra I/O.
|
||||
Object.assign(legacy, buildIntelFields(features));
|
||||
|
||||
return legacy;
|
||||
}
|
||||
|
||||
@@ -319,5 +372,8 @@ module.exports = {
|
||||
fallbackLegacyResult,
|
||||
explainErrors,
|
||||
ERROR_EXPLANATIONS,
|
||||
buildIntelFields,
|
||||
computeFormScore,
|
||||
matchupGradeFromRank,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
|
||||
const { classify } = require('./archetypeService');
|
||||
|
||||
const toNum = (v) => {
|
||||
const n = parseFloat(v);
|
||||
return Number.isNaN(n) ? 0 : n;
|
||||
};
|
||||
const fmt3 = (v) => {
|
||||
const s = String(v == null ? '' : v);
|
||||
return s.startsWith('0.') ? s.slice(1) : s; // ".282" baseball style
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitize a player-name URL param (Montgomery's note). Decode, strip anything
|
||||
* that isn't a letter/number/space or name punctuation (. - '), collapse
|
||||
@@ -39,6 +48,106 @@ async function loadPlayerGrades(sport, name, cacheGetFn) {
|
||||
return grades.filter((g) => normName(g.player_name || g.player) === target);
|
||||
}
|
||||
|
||||
// ── MLB raw-stat normalization (Session 43) ─────────────────────────
|
||||
// Maps the raw statsapi.mlb.com season object into (a) classifier input and
|
||||
// (b) the profile's display rows + last-10 log.
|
||||
|
||||
function mapMlbHitter(s) {
|
||||
const pa = toNum(s.plateAppearances) || toNum(s.atBats);
|
||||
return {
|
||||
avg: toNum(s.avg), hr: toNum(s.homeRuns), rbi: toNum(s.rbi), sb: toNum(s.stolenBases),
|
||||
ops: toNum(s.ops), runs: toNum(s.runs), doubles: toNum(s.doubles),
|
||||
k_rate: pa > 0 ? (toNum(s.strikeOuts) / pa) * 100 : 0,
|
||||
};
|
||||
}
|
||||
function mapMlbPitcher(s) {
|
||||
const ip = toNum(s.inningsPitched);
|
||||
const gs = toNum(s.gamesStarted);
|
||||
return {
|
||||
era: toNum(s.era), whip: toNum(s.whip),
|
||||
k9: toNum(s.strikeoutsPer9Inn) || (ip > 0 ? (toNum(s.strikeOuts) / ip) * 9 : 0),
|
||||
ip_per_start: gs > 0 ? ip / gs : 0,
|
||||
saves: toNum(s.saves),
|
||||
role: gs > 0 ? 'SP' : toNum(s.saves) > 0 ? 'CL' : 'RP',
|
||||
};
|
||||
}
|
||||
|
||||
function mlbSeasonRows(s, group) {
|
||||
if (group === 'pitching') {
|
||||
return [
|
||||
{ k: 'ERA', v: String(s.era ?? '—') },
|
||||
{ k: 'K', v: String(s.strikeOuts ?? '—') },
|
||||
{ k: 'IP', v: String(s.inningsPitched ?? '—') },
|
||||
{ k: 'WHIP', v: String(s.whip ?? '—') },
|
||||
{ k: 'GS', v: String(s.gamesStarted ?? '—') },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ k: 'AVG', v: fmt3(s.avg) || '—' },
|
||||
{ k: 'HR', v: String(s.homeRuns ?? '—') },
|
||||
{ k: 'RBI', v: String(s.rbi ?? '—') },
|
||||
{ k: 'OPS', v: fmt3(s.ops) || '—' },
|
||||
{ k: 'GP', v: String(s.gamesPlayed ?? '—') },
|
||||
];
|
||||
}
|
||||
|
||||
function mlbLast10Rows(log, group) {
|
||||
return (log || []).slice(-10).reverse().map((g) => {
|
||||
const st = g.stat || {};
|
||||
const summary = group === 'pitching'
|
||||
? `${st.inningsPitched ?? '0'} IP · ${st.strikeOuts ?? 0} K`
|
||||
: `${st.hits ?? 0}-${st.atBats ?? 0} · ${st.homeRuns ?? 0} HR`;
|
||||
return { d: String(g.date || '').slice(5), opp: g.opponent ? String(g.opponent).slice(0, 3).toUpperCase() : '', stat: summary };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a player's REAL stats by sport (Session 43). Returns a normalized
|
||||
* bundle or { found:false }. Best-effort + never throws — the profile renders
|
||||
* regardless. Adapters are injectable for tests (opts.mlbAdapter/nbaClient).
|
||||
*/
|
||||
async function resolvePlayerStats(name, sport, opts = {}) {
|
||||
const sp = String(sport || 'nba').toLowerCase();
|
||||
try {
|
||||
if (sp === 'mlb') {
|
||||
const mlb = opts.mlbAdapter || require('./adapters/mlbStatsAdapter');
|
||||
const res = await mlb.getPlayerStats(name);
|
||||
if (!res || !res.found) return { found: false };
|
||||
const classifierInput = res.group === 'pitching' ? mapMlbPitcher(res.season) : mapMlbHitter(res.season);
|
||||
return {
|
||||
found: true,
|
||||
team: res.team || '',
|
||||
classifierInput,
|
||||
season: mlbSeasonRows(res.season, res.group),
|
||||
last10: mlbLast10Rows(res.last10, res.group),
|
||||
splits: [],
|
||||
};
|
||||
}
|
||||
if (sp === 'nba' || sp === 'wnba') {
|
||||
// NBA/WNBA stats come from the Python nba_api service (nbaStatsClient).
|
||||
// It's frequently offline in prod (localhost service) — degrade quietly.
|
||||
const nba = opts.nbaClient || require('./nbaStatsClient');
|
||||
const data = await nba.getSeasonAvg(name).catch(() => null);
|
||||
if (!data || typeof data !== 'object') return { found: false };
|
||||
const ppg = toNum(data.ppg ?? data.points);
|
||||
if (!ppg) return { found: false };
|
||||
const classifierInput = {
|
||||
ppg, rpg: toNum(data.rpg ?? data.rebounds), apg: toNum(data.apg ?? data.assists),
|
||||
bpg: toNum(data.bpg ?? data.blocks), spg: toNum(data.spg ?? data.steals),
|
||||
threes: toNum(data.threes ?? data.fg3m), usg: toNum(data.usg ?? data.usage), pos: data.pos || data.position,
|
||||
};
|
||||
const season = [
|
||||
{ k: 'PPG', v: String(classifierInput.ppg) }, { k: 'RPG', v: String(classifierInput.rpg) },
|
||||
{ k: 'APG', v: String(classifierInput.apg) }, { k: 'BLK', v: String(classifierInput.bpg) },
|
||||
];
|
||||
return { found: true, team: data.team || '', classifierInput, season, last10: [], splits: [] };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[playerIntel] resolvePlayerStats failed:', name, sp, err.message);
|
||||
}
|
||||
return { found: false };
|
||||
}
|
||||
|
||||
/** Derive the VYNDR Intelligence metric row from whatever we have. */
|
||||
function buildIntel(stats, arch, propCount) {
|
||||
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
||||
@@ -69,10 +178,22 @@ async function getPlayerIntel(name, sport, opts = {}) {
|
||||
const cacheGetFn = opts.cacheGet || require('../utils/redis').cacheGet;
|
||||
const clean = sanitizePlayerName(name);
|
||||
const sp = String(sport || 'nba').toLowerCase();
|
||||
const stats = opts.stats || {};
|
||||
|
||||
const archetype = classify(sp, stats);
|
||||
// 1. Real season stats from the sport adapter (Session 43). Injectable for
|
||||
// tests via opts.resolveStats; opts.stats short-circuits to caller-supplied.
|
||||
let resolved;
|
||||
if (opts.stats) {
|
||||
resolved = { found: Object.keys(opts.stats).length > 0, classifierInput: opts.stats, season: opts.season || [], last10: opts.last10 || [], splits: opts.splits || [], team: opts.team || '' };
|
||||
} else {
|
||||
const resolver = opts.resolveStats || resolvePlayerStats;
|
||||
resolved = await resolver(clean, sp, opts);
|
||||
}
|
||||
const realStats = resolved.classifierInput || {};
|
||||
|
||||
// 2. Archetype, now classified from REAL stats when we have them.
|
||||
const archetype = classify(sp, realStats);
|
||||
|
||||
// 3. Tonight's graded props from the slate cache.
|
||||
let props = [];
|
||||
try {
|
||||
props = await loadPlayerGrades(sp, clean, cacheGetFn);
|
||||
@@ -88,22 +209,22 @@ async function getPlayerIntel(name, sport, opts = {}) {
|
||||
confidence: p.confidence != null ? `${p.confidence}%` : null,
|
||||
}));
|
||||
|
||||
const team = (props[0] && (props[0].team || props[0].team_abbr)) || opts.team || '';
|
||||
const team = resolved.team || (props[0] && (props[0].team || props[0].team_abbr)) || opts.team || '';
|
||||
|
||||
return {
|
||||
player: clean,
|
||||
sport: sp,
|
||||
team,
|
||||
found: props.length > 0 || Object.keys(stats).length > 0,
|
||||
found: !!resolved.found || props.length > 0,
|
||||
archetype,
|
||||
propDNA: archetype.primary ? archetype.primary.propDNA : { reliable: [], volatile: [] },
|
||||
education: archetype.primary ? archetype.primary.education : '',
|
||||
season: opts.season || [],
|
||||
last10: opts.last10 || [],
|
||||
splits: opts.splits || [],
|
||||
season: resolved.season || opts.season || [],
|
||||
last10: resolved.last10 || opts.last10 || [],
|
||||
splits: resolved.splits || opts.splits || [],
|
||||
gradeHistory: opts.gradeHistory || [],
|
||||
activeProps,
|
||||
intel: buildIntel(stats, archetype, props.length),
|
||||
intel: buildIntel(realStats, archetype, props.length),
|
||||
injury: opts.injury || null,
|
||||
};
|
||||
}
|
||||
@@ -145,5 +266,6 @@ module.exports = {
|
||||
sanitizePlayerName,
|
||||
getPlayerIntel,
|
||||
getLeaders,
|
||||
_internals: { normName, loadPlayerGrades, buildIntel },
|
||||
resolvePlayerStats,
|
||||
_internals: { normName, loadPlayerGrades, buildIntel, mapMlbHitter, mapMlbPitcher, mlbSeasonRows, mlbLast10Rows },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user