Session 42: Player Intelligence System — archetypes, stat strips, player profile, enhanced cards (2011 tests)
Built from the Claude Design "VYNDR Player Intelligence" bundle (10 sections). - Archetypes: src/services/archetypeService.js — 41 archetypes (15 NBA / 5 WNBA-unique / 15 MLB / 6 soccer), classify -> primary+secondary+blend. Frontend visual map web/src/lib/archetypes.js (colors verified == backend). ArchetypeBadge (full/ghost/tint + glyphs) + ArchetypeBlend (DNA bar). - StatStrip (compact/expanded): player name once, horizontal mono stats, inline GradeBadge props, onPlayerClick -> profile. - Stats API: extended src/routes/stats.js with /player/:name, /leaders, /game/:id (rate-limited). Aggregation in playerIntelService.js (sanitizes name param; grades cache; graceful on cold cache). Next proxies added. - Player Profile /player/[name]: all 9 design sections, graceful empty states. - Enhanced GameCard (MLB pitchers + player-grouped StatStrips) + GradeResultCard (archetype strip + stat context + VYNDR intelligence, optional/self-hiding via gradeAdapter.buildIntelFields). Player-name links wired everywhere. - Settings page replaces the S41 redirect (account/subscription/notifications/ display/responsible-play/danger-zone with DELETE-gated delete). LINKS to the real /settings/security MFA page — does not replace it. + BookChip. - Bonus: Stats Explorer /explore (real /api/stats/leaders leaderboard); added Explore + Settings to Nav MORE. Deferred (need data pipelines, Session 43): Team Hub, Offseason Intel, Slate redesign, Stats Explorer sub-panels. Backend 1940 -> 2011 tests (+71), 157 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,91 @@
|
||||
// Session 42 — player intelligence aggregation. cacheGet is injected so these
|
||||
// run pure (no Redis, no HTTP, no rate limiter).
|
||||
|
||||
const svc = require('../../src/services/playerIntelService');
|
||||
|
||||
const cacheWith = (envelope) => async (key) => (key.startsWith('grades:') ? envelope : null);
|
||||
|
||||
describe('sanitizePlayerName', () => {
|
||||
it('decodes URL encoding and keeps name punctuation', () => {
|
||||
expect(svc.sanitizePlayerName('Luka%20Doncic')).toBe('Luka Doncic');
|
||||
expect(svc.sanitizePlayerName("De'Aaron Fox")).toBe("De'Aaron Fox");
|
||||
expect(svc.sanitizePlayerName('Ronald Acuna Jr.')).toBe('Ronald Acuna Jr.');
|
||||
});
|
||||
|
||||
it('strips injection / control characters', () => {
|
||||
expect(svc.sanitizePlayerName('Luka<script>')).toBe('Lukascript');
|
||||
expect(svc.sanitizePlayerName('a/../../etc/passwd')).toBe('a....etcpasswd');
|
||||
expect(svc.sanitizePlayerName('x'.repeat(200)).length).toBe(60);
|
||||
});
|
||||
|
||||
it('handles malformed percent-encoding without throwing', () => {
|
||||
expect(() => svc.sanitizePlayerName('%E0%A4%A')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlayerIntel', () => {
|
||||
it('returns archetype + intelligence + props, found=true when player has grades', async () => {
|
||||
const envelope = {
|
||||
grades: [
|
||||
{ player: 'Austin Riley', team: 'ATL', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'B+', confidence: 71 },
|
||||
{ player: 'Austin Riley', stat_type: 'home_runs', line: 0.5, direction: 'over', grade: 'C', confidence: 60 },
|
||||
{ player: 'Someone Else', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'A', confidence: 80 },
|
||||
],
|
||||
};
|
||||
const r = await svc.getPlayerIntel('Austin Riley', 'mlb', {
|
||||
cacheGet: cacheWith(envelope),
|
||||
stats: { avg: 0.282, hr: 18, rbi: 54, ops: 0.845, k_rate: 26 },
|
||||
});
|
||||
expect(r.player).toBe('Austin Riley');
|
||||
expect(r.sport).toBe('mlb');
|
||||
expect(r.team).toBe('ATL');
|
||||
expect(r.found).toBe(true);
|
||||
expect(r.archetype.primary).toBeTruthy();
|
||||
expect(r.activeProps).toHaveLength(2); // only Riley's two props
|
||||
expect(r.activeProps[0]).toMatchObject({ stat: 'total_bases', side: 'O', grade: 'B+' });
|
||||
expect(r.propDNA.reliable.length + r.propDNA.volatile.length).toBeGreaterThan(0);
|
||||
expect(r.education.length).toBeGreaterThan(0);
|
||||
expect(r.intel.find((m) => m.label === 'FORM')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('degrades gracefully when the grades cache is cold (found=false, no crash)', async () => {
|
||||
const r = await svc.getPlayerIntel('Nobody Special', 'nba', { cacheGet: async () => null });
|
||||
expect(r.found).toBe(false);
|
||||
expect(r.activeProps).toEqual([]);
|
||||
expect(r.archetype.primary).toBeTruthy(); // fallback archetype
|
||||
expect(Array.isArray(r.intel)).toBe(true);
|
||||
});
|
||||
|
||||
it('survives a throwing cache (returns a valid payload)', async () => {
|
||||
const r = await svc.getPlayerIntel('X', 'nba', { cacheGet: async () => { throw new Error('redis down'); } });
|
||||
expect(r.activeProps).toEqual([]);
|
||||
expect(r.player).toBe('X');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeaders', () => {
|
||||
const envelope = {
|
||||
grades: [
|
||||
{ player: 'A', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'A', confidence: 88 },
|
||||
{ player: 'B', stat_type: 'hits', line: 1.5, direction: 'over', grade: 'B', confidence: 72 },
|
||||
{ player: 'C', stat_type: 'home_runs', line: 0.5, direction: 'over', grade: 'A', confidence: 95 },
|
||||
],
|
||||
};
|
||||
|
||||
it('returns top props by confidence', async () => {
|
||||
const r = await svc.getLeaders('mlb', { cacheGet: cacheWith(envelope), limit: 2 });
|
||||
expect(r).toHaveLength(2);
|
||||
expect(r[0].player).toBe('C'); // highest confidence
|
||||
expect(r[0].confidence).toBe(95);
|
||||
});
|
||||
|
||||
it('filters to a single stat when given', async () => {
|
||||
const r = await svc.getLeaders('mlb', { cacheGet: cacheWith(envelope), stat: 'hits' });
|
||||
expect(r).toHaveLength(2);
|
||||
expect(r.every((x) => x.stat === 'hits')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns [] on a cold cache', async () => {
|
||||
expect(await svc.getLeaders('nba', { cacheGet: async () => null })).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user