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:
@@ -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 },
|
||||
};
|
||||
Reference in New Issue
Block a user