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:
Kev
2026-06-18 15:38:06 -04:00
parent 8bc79f3c38
commit 80683e71b4
19 changed files with 940 additions and 18 deletions
+129
View File
@@ -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 },
};