Session 44: Make it visible — VYNDR archetype names, grade intel, schedule fix, landing page (2061 tests)

Frontend + wiring only. Wires existing backend into the pages users see.

- VYNDR Original archetype rename (41) across archetypeService.js + lib/
  archetypes.js + ArchetypeBadge, each keeping legacyName (resolves stale data).
  Judge -> BOMBER. Old POWER PULL slot -> WHIFF strikeout-artist pitcher.
- BACKEND_HANDOFF.md: canonical frontend<->backend data contract.
- Grade card intel: scan/page.tsx now forwards the engine's intel fields
  (season_avg/form/usage/matchup_grade/archetype/...) into mapScanToGradeResult
  -> STAT CONTEXT + VYNDR INTELLIGENCE sections populate. The chain already
  preserved them (tierGating + /api/scan spread); the page was dropping them.
- Schedule freshness: slateAdapter.isRelevantGame drops completed games >24h
  old; Slate.filteredGames applies it. (TTL already 60s.)
- Landing: Features.tsx rewritten to user-facing copy (no Point-biserial/Zone
  14/ABS/Phi-coefficient).
- Depth chart Next proxies added (/api/stats/lineup|depth|cascade) - were 404.
- GameCard swap DEFERRED (Kev): legacy on-demand card stays as a bridge until
  the snapshot pipeline populates the grades cache; vyndr/GameCard swaps in then.

Backend 2045 -> 2061 tests (+16), 167 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 20:15:38 -04:00
parent 80683e71b4
commit 7969a4971a
26 changed files with 766 additions and 302 deletions
+28 -14
View File
@@ -10,7 +10,7 @@ describe('archetypeService — registry', () => {
expect(bySport).toEqual({ nba: 15, wnba: 5, mlb: 15, soccer: 6 });
});
it('every archetype has name/tag/color/glyph/description/propDNA/education', () => {
it('every archetype has name/tag/color/glyph/description/propDNA/education/legacyName', () => {
for (const [name, a] of Object.entries(svc.ARCHETYPES)) {
expect(typeof a.tag).toBe('string');
expect(a.color).toMatch(/^#[0-9A-Fa-f]{6}$/);
@@ -21,9 +21,20 @@ describe('archetypeService — registry', () => {
expect(Array.isArray(a.propDNA.volatile)).toBe(true);
expect(a.education.length).toBeGreaterThan(20);
expect(name).toBe(name.toUpperCase());
expect(typeof a.legacyName).toBe('string'); // VYNDR Originals keep the old label
}
});
it('uses VYNDR Original names, not the old descriptive labels', () => {
const keys = Object.keys(svc.ARCHETYPES);
expect(keys).toContain('TORCH');
expect(keys).toContain('BOMBER');
expect(keys).toContain('ALPHA');
expect(keys).not.toContain('VOLUME SCORER');
expect(keys).not.toContain('POWER SLUGGER');
expect(keys).not.toContain('ACE');
});
it('colors are unique WITHIN each sport (reused across sports by design)', () => {
const bySport = {};
for (const a of Object.values(svc.ARCHETYPES)) {
@@ -35,9 +46,12 @@ describe('archetypeService — registry', () => {
}
});
it('getArchetype is case-insensitive and returns null for unknown', () => {
expect(svc.getArchetype('ace').name).toBe('ACE');
expect(svc.getArchetype('Two-Way Anchor').name).toBe('TWO-WAY ANCHOR');
it('getArchetype is case-insensitive, resolves legacy names, returns null for unknown', () => {
expect(svc.getArchetype('alpha').name).toBe('ALPHA');
expect(svc.getArchetype('Fortress').name).toBe('FORTRESS');
// legacy names still resolve to their VYNDR Original
expect(svc.getArchetype('ACE').name).toBe('ALPHA');
expect(svc.getArchetype('POWER SLUGGER').name).toBe('BOMBER');
expect(svc.getArchetype('not a real one')).toBeNull();
});
});
@@ -45,7 +59,7 @@ describe('archetypeService — registry', () => {
describe('archetypeService — NBA classification', () => {
it('classifies a volume scorer', () => {
const r = svc.classifyNBA({ ppg: 30, rpg: 5, apg: 4, usg: 33, threes: 2.5, pos: 'G' });
expect(r.primary.name).toBe('VOLUME SCORER');
expect(r.primary.name).toBe('TORCH');
expect(r.sport).toBe('nba');
});
@@ -58,7 +72,7 @@ describe('archetypeService — NBA classification', () => {
it('classifies a two-way anchor (Wembanyama-type)', () => {
const r = svc.classifyNBA({ ppg: 24, rpg: 11, apg: 3, bpg: 3.2, usg: 31, threes: 1.5, pos: 'C' });
expect(['TWO-WAY ANCHOR', 'POST SCORER', 'STRETCH BIG']).toContain(r.primary.name);
expect(['FORTRESS', 'PAINT BOSS', 'ARTILLERY']).toContain(r.primary.name);
});
it('produces a normalized blend that sums to ~1', () => {
@@ -73,32 +87,32 @@ describe('archetypeService — MLB classification', () => {
it('differentiates an Ace from an Innings Eater', () => {
const ace = svc.classifyMLB({ role: 'SP', era: 2.9, k9: 11.5, whip: 0.98, ip_per_start: 6.2 });
const eater = svc.classifyMLB({ role: 'SP', era: 4.1, k9: 7.0, whip: 1.3, ip_per_start: 6.5 });
expect(ace.primary.name).toBe('ACE');
expect(eater.primary.name).toBe('INNINGS EATER');
expect(ace.primary.name).toBe('ALPHA');
expect(eater.primary.name).toBe('WORKHORSE');
});
it('classifies a closer', () => {
const r = svc.classifyMLB({ role: 'CL', era: 2.2, k9: 12, saves: 24, ip_per_start: 1 });
expect(r.primary.name).toBe('CLOSER');
expect(r.primary.name).toBe('HAMMER');
});
it('classifies a contact hitter and a power-pull hitter differently', () => {
it('classifies a contact hitter (BRUSH) and a power hitter (BOMBER) differently', () => {
const contact = svc.classifyMLB({ avg: 0.315, hr: 8, rbi: 40, k_rate: 12, ops: 0.82 });
const power = svc.classifyMLB({ avg: 0.235, hr: 34, rbi: 88, k_rate: 30, ops: 0.86 });
expect(contact.primary.name).toBe('CONTACT');
expect(power.primary.name).toBe('POWER PULL');
expect(contact.primary.name).toBe('BRUSH');
expect(power.primary.name).toBe('BOMBER');
});
});
describe('archetypeService — WNBA classification', () => {
it('classifies a dominant post / interior anchor (Wilson-type)', () => {
const r = svc.classifyWNBA({ ppg: 27, rpg: 12, apg: 2, bpg: 2.3, usg: 31, pos: 'F' });
expect(['INTERIOR ANCHOR', 'VOLUME SCORER', 'STRETCH FORWARD']).toContain(r.primary.name);
expect(['ANCHOR', 'TORCH', 'RANGE']).toContain(r.primary.name);
});
it('classifies a floor general', () => {
const r = svc.classifyWNBA({ ppg: 16, rpg: 4, apg: 7, usg: 24, pos: 'G' });
expect(r.primary.name).toBe('FLOOR GENERAL');
expect(r.primary.name).toBe('CONDUCTOR');
});
});