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
+195 -206
View File
@@ -1,334 +1,324 @@
/**
* archetypeService — player archetype classification (Session 42).
* archetypeService — VYNDR player archetype classification (Session 42; renamed
* to VYNDR Originals in Session 44).
*
* Pure logic: categorize a player by their prop-behavior pattern from season
* averages + usage. Returns a PRIMARY archetype, an optional SECONDARY, and a
* weighted `blend` (the production-DNA bar in the design's player profile).
*
* The archetype NAMES + colors + glyph keys are the contract shared with the
* frontend `ArchetypeBadge` (web/src/components/vyndr/ArchetypeBadge.tsx) and
* `ArchetypeData` (web/src/lib/archetypes.ts) — ported verbatim from the
* "VYNDR Player Intelligence" design (ArchetypeBadge.dc.html MAP). Keep the
* hex/glyph in sync across all three.
* The archetype NAMES are VYNDR-proprietary (TORCH, BOMBER, ALPHA, …). Each
* carries `legacyName` (the old descriptive label) for reference — never shown.
* colors + glyph keys are the contract shared with the frontend `ArchetypeBadge`
* (web/src/components/vyndr/ArchetypeBadge.tsx) + `archetypes.js`. Keep all three
* in sync.
*
* No API calls, no I/O — feed it a stat object, get a classification. That's
* why it lives in services/ and is unit-testable in isolation.
* No API calls, no I/O — feed it a stat object, get a classification.
*/
// ── Archetype registry ──────────────────────────────────────────────
// color + glyph match the design's badge map. propDNA = which props this
// archetype makes reliable vs volatile. education = the profile's "what does
// this mean" copy.
const ARCHETYPES = {
// ───────── NBA (15) ─────────
'VOLUME SCORER': {
tag: 'VOL SCORER', sport: 'nba', color: '#FF6B4A', glyph: 'triangle',
TORCH: {
legacyName: 'VOLUME SCORER', tag: 'TORCH', sport: 'nba', color: '#FF6B4A', glyph: 'triangle',
description: 'High usage, shot-dependent scorer',
propDNA: { reliable: ['points'], volatile: ['assists', 'threes'] },
education: 'Volume scorers carry a heavy shot diet, so their points props track usage closely. When they get their normal touches, points clear reliably; assists and threes swing with game script.',
education: 'Torches carry a heavy shot diet, so their points props track usage closely. When they get their normal touches, points clear reliably; assists and threes swing with game script.',
},
'FLOOR GENERAL': {
tag: 'FLOOR GEN', sport: 'nba', color: '#4A9EFF', glyph: 'node',
CONDUCTOR: {
legacyName: 'FLOOR GENERAL', tag: 'CONDUCTOR', sport: 'nba', color: '#4A9EFF', glyph: 'node',
description: 'Assist-heavy playmaker',
propDNA: { reliable: ['assists'], volatile: ['points', 'threes'] },
education: 'Floor generals create for others first. Assists are their most stable prop because the offense runs through them; their scoring fluctuates with shot selection and matchup.',
education: 'Conductors create for others first. Assists are their most stable prop because the offense runs through them; their scoring fluctuates with shot selection and matchup.',
},
'TWO-WAY ANCHOR': {
tag: 'TW ANCHOR', sport: 'nba', color: '#A78BFA', glyph: 'shield',
FORTRESS: {
legacyName: 'TWO-WAY ANCHOR', tag: 'FORTRESS', sport: 'nba', color: '#A78BFA', glyph: 'shield',
description: 'Defense, rebounds, blocks',
propDNA: { reliable: ['rebounds', 'blocks'], volatile: ['points', 'assists'] },
education: 'Two-way anchors generate value on the defensive glass and at the rim. Rebounds and blocks are matchup-resilient; their scoring depends on how much offense flows their way.',
education: 'A Fortress generates value on the defensive glass and at the rim. Rebounds and blocks are matchup-resilient; their scoring depends on how much offense flows their way.',
},
'STRETCH BIG': {
tag: 'STRETCH BIG', sport: 'nba', color: '#2DD4BF', glyph: 'target',
ARTILLERY: {
legacyName: 'STRETCH BIG', tag: 'ARTILLERY', sport: 'nba', color: '#2DD4BF', glyph: 'target',
description: 'Floor-spacing shooting big',
propDNA: { reliable: ['threes', 'rebounds'], volatile: ['assists', 'blocks'] },
education: 'Stretch bigs space the floor and crash the glass. Threes and rebounds are their bread and butter; assists and blocks are situational.',
education: 'Artillery bigs space the floor and crash the glass. Threes and rebounds are their bread and butter; assists and blocks are situational.',
},
'USAGE SPONGE': {
tag: 'USG SPONGE', sport: 'nba', color: '#FFB347', glyph: 'uparrow',
SURGE: {
legacyName: 'USAGE SPONGE', tag: 'SURGE', sport: 'nba', color: '#FFB347', glyph: 'uparrow',
description: 'Usage spikes when stars sit',
propDNA: { reliable: ['points'], volatile: ['assists', 'rebounds'] },
education: 'Usage sponges soak up shots when a star sits or is injured. Their points props spike on cascade nights — read the injury report before trusting the baseline.',
education: 'A Surge soaks up shots when a star sits or is injured. Their points props spike on cascade nights — read the injury report before trusting the baseline.',
},
'COMBO GUARD': {
tag: 'COMBO GD', sport: 'nba', color: '#00D4A0', glyph: 'twin',
'DUAL THREAT': {
legacyName: 'COMBO GUARD', tag: 'DUAL THREAT', sport: 'nba', color: '#00D4A0', glyph: 'twin',
description: 'Scoring + playmaking hybrid',
propDNA: { reliable: ['points', 'assists'], volatile: ['rebounds'] },
education: 'Combo guards score and create in equal measure, so points and assists both stay in play. Rebounds are the volatile leg for their size.',
education: 'Dual Threats score and create in equal measure, so points and assists both stay in play. Rebounds are the volatile leg for their size.',
},
'ROLE GLUE': {
tag: 'ROLE GLUE', sport: 'nba', color: '#9499A8', glyph: 'chain',
CONNECTOR: {
legacyName: 'ROLE GLUE', tag: 'CONNECTOR', sport: 'nba', color: '#9499A8', glyph: 'chain',
description: 'Low-usage specialist',
propDNA: { reliable: [], volatile: ['points', 'assists', 'rebounds'] },
education: 'Role glue players do the little things at low usage. Their counting props are thin and matchup-dependent — they reward unders more often than overs.',
education: 'Connectors do the little things at low usage. Their counting props are thin and matchup-dependent — they reward unders more often than overs.',
},
'TRANSITION ENGINE': {
tag: 'TRANS ENG', sport: 'nba', color: '#22D3EE', glyph: 'chevrons',
FASTBREAK: {
legacyName: 'TRANSITION ENGINE', tag: 'FASTBREAK', sport: 'nba', color: '#22D3EE', glyph: 'chevrons',
description: 'Pace-pushing fast-break threat',
propDNA: { reliable: ['points'], volatile: ['assists', 'threes'] },
education: 'Transition engines feast in the open floor. Their points correlate with game pace — target overs in projected up-tempo matchups.',
education: 'Fastbreaks feast in the open floor. Their points correlate with game pace — target overs in projected up-tempo matchups.',
},
'POST SCORER': {
tag: 'POST SCORER', sport: 'nba', color: '#FF5C5C', glyph: 'postup',
'PAINT BOSS': {
legacyName: 'POST SCORER', tag: 'PAINT BOSS', sport: 'nba', color: '#FF5C5C', glyph: 'postup',
description: 'Back-to-basket interior scorer',
propDNA: { reliable: ['points', 'rebounds'], volatile: ['threes', 'assists'] },
education: 'Post scorers operate in the paint. Points and rebounds are reliable against most fronts; perimeter props are noise.',
education: 'A Paint Boss operates in the paint. Points and rebounds are reliable against most fronts; perimeter props are noise.',
},
'DEFENSIVE SPECIALIST': {
tag: 'DEF SPEC', sport: 'nba', color: '#6366F1', glyph: 'shieldCheck',
LOCKDOWN: {
legacyName: 'DEFENSIVE SPECIALIST', tag: 'LOCKDOWN', sport: 'nba', color: '#6366F1', glyph: 'shieldCheck',
description: 'Perimeter stopper, low usage',
propDNA: { reliable: ['steals'], volatile: ['points', 'assists'] },
education: 'Defensive specialists earn minutes with their on-ball defense. Steals and blocks carry their card; offensive props are low-volume and streaky.',
education: 'Lockdowns earn minutes with their on-ball defense. Steals and blocks carry their card; offensive props are low-volume and streaky.',
},
'POINT FORWARD': {
tag: 'PT FWD', sport: 'nba', color: '#38BDF8', glyph: 'half',
SWITCHBOARD: {
legacyName: 'POINT FORWARD', tag: 'SWITCHBOARD', sport: 'nba', color: '#38BDF8', glyph: 'half',
description: 'Oversized primary creator',
propDNA: { reliable: ['points', 'assists', 'rebounds'], volatile: ['threes'] },
education: 'Point forwards run the offense from a wing or big body, so points, assists, and rebounds all stay live. Their three-point output is the swing factor.',
education: 'Switchboards run the offense from a wing or big body, so points, assists, and rebounds all stay live. Their three-point output is the swing factor.',
},
SLASHER: {
tag: 'SLASHER', sport: 'nba', color: '#FB923C', glyph: 'slash',
description: 'Rim-attacking, foul-drawing driver',
ARCHITECT: {
legacyName: 'SLASHER', tag: 'ARCHITECT', sport: 'nba', color: '#FB923C', glyph: 'slash',
description: 'Self-created shot maker, rim-attacking',
propDNA: { reliable: ['points'], volatile: ['threes', 'assists'] },
education: 'Slashers live at the rim and the free-throw line. Points are stable; their three-point props are volatile because they rarely settle for jumpers.',
education: 'Architects manufacture their own shot at the rim and the line. Points are stable; their three-point props are volatile because they rarely settle for jumpers.',
},
'RIM RUNNER': {
tag: 'RIM RUN', sport: 'nba', color: '#F472B6', glyph: 'arc',
PISTON: {
legacyName: 'RIM RUNNER', tag: 'PISTON', sport: 'nba', color: '#F472B6', glyph: 'arc',
description: 'Lob and putback finisher',
propDNA: { reliable: ['rebounds'], volatile: ['points', 'assists'] },
education: 'Rim runners finish lobs and putbacks. Rebounds are reliable; their scoring depends entirely on feeds from creators.',
education: 'Pistons finish lobs and putbacks. Rebounds are reliable; their scoring depends entirely on feeds from creators.',
},
'3-AND-D': {
tag: '3&D', sport: 'nba', color: '#818CF8', glyph: 'crosshair',
SENTINEL: {
legacyName: '3-AND-D', tag: 'SENTINEL', sport: 'nba', color: '#818CF8', glyph: 'crosshair',
description: 'Catch-and-shoot plus defense',
propDNA: { reliable: ['threes'], volatile: ['points', 'assists'] },
education: '3-and-D wings catch and shoot. Threes are their signature prop; total points swing with how many open looks the offense generates.',
education: 'Sentinels catch and shoot. Threes are their signature prop; total points swing with how many open looks the offense generates.',
},
'SIXTH MAN': {
tag: '6TH MAN', sport: 'nba', color: '#FACC15', glyph: 'bolt',
IGNITER: {
legacyName: 'SIXTH MAN', tag: 'IGNITER', sport: 'nba', color: '#FACC15', glyph: 'bolt',
description: 'Bench scoring spark',
propDNA: { reliable: ['points'], volatile: ['rebounds', 'assists'] },
education: 'Sixth men provide instant offense off the bench. Their points props depend on minutes — confirm the rotation before betting overs.',
education: 'Igniters provide instant offense off the bench. Their points props depend on minutes — confirm the rotation before betting overs.',
},
// ───────── WNBA-unique (5) ─────────
'POST FACILITATOR': {
tag: 'POST FAC', sport: 'wnba', color: '#C084FC', glyph: 'node',
DISTRIBUTOR: {
legacyName: 'POST FACILITATOR', tag: 'DISTRIBUTOR', sport: 'wnba', color: '#C084FC', glyph: 'node',
description: 'Playmaking hub from the post',
propDNA: { reliable: ['assists', 'rebounds'], volatile: ['threes'] },
education: 'Post facilitators orchestrate from the elbow and block. Assists and rebounds are reliable; perimeter shooting is the volatile leg.',
education: 'Distributors orchestrate from the elbow and block. Assists and rebounds are reliable; perimeter shooting is the volatile leg.',
},
'TWO-WAY WING': {
tag: 'TW WING', sport: 'wnba', color: '#A78BFA', glyph: 'shieldCheck',
description: 'Two-way perimeter wing',
SHIELD: {
legacyName: 'TWO-WAY WING', tag: 'SHIELD', sport: 'wnba', color: '#A78BFA', glyph: 'shieldCheck',
description: 'Two-way perimeter forward',
propDNA: { reliable: ['points', 'steals'], volatile: ['assists'] },
education: 'Two-way wings contribute on both ends. Points and defensive stats stay live; their playmaking is secondary.',
education: 'Shields contribute on both ends. Points and defensive stats stay live; their playmaking is secondary.',
},
'STRETCH FORWARD': {
tag: 'STRETCH FWD', sport: 'wnba', color: '#2DD4BF', glyph: 'target',
RANGE: {
legacyName: 'STRETCH FORWARD', tag: 'RANGE', sport: 'wnba', color: '#2DD4BF', glyph: 'target',
description: 'Floor-spacing forward',
propDNA: { reliable: ['threes', 'points'], volatile: ['assists', 'blocks'] },
education: 'Stretch forwards space the floor from the four. Threes and points are reliable; interior props are matchup-dependent.',
education: 'Range forwards space the floor from the four. Threes and points are reliable; interior props are matchup-dependent.',
},
'SLASHING GUARD': {
tag: 'SLASH GD', sport: 'wnba', color: '#FB923C', glyph: 'slash',
description: 'Downhill driving guard',
SPARK: {
legacyName: 'SLASHING GUARD', tag: 'SPARK', sport: 'wnba', color: '#FB923C', glyph: 'slash',
description: 'Downhill scoring guard',
propDNA: { reliable: ['points'], volatile: ['threes', 'assists'] },
education: 'Slashing guards attack downhill. Points are stable; three-point props are the volatile leg since they prioritize the rim.',
education: 'Sparks attack downhill. Points are stable; three-point props are the volatile leg since they prioritize the rim.',
},
'INTERIOR ANCHOR': {
tag: 'INT ANCHOR', sport: 'wnba', color: '#6366F1', glyph: 'shield',
description: 'Paint defender and rebounder',
ANCHOR: {
legacyName: 'INTERIOR ANCHOR', tag: 'ANCHOR', sport: 'wnba', color: '#6366F1', glyph: 'shield',
description: 'Dominant paint defender and rebounder',
propDNA: { reliable: ['rebounds', 'blocks'], volatile: ['points', 'assists'] },
education: 'Interior anchors own the paint. Rebounds and blocks are reliable; their scoring depends on post touches.',
education: 'Anchors own the paint. Rebounds and blocks are reliable; their scoring depends on post touches.',
},
// ───────── MLB (15) ─────────
'POWER PULL': {
tag: 'POWER PULL', sport: 'mlb', color: '#FF5C5C', glyph: 'batball',
description: 'HR-dependent, high strikeout power',
BOMBER: {
legacyName: 'POWER SLUGGER', tag: 'BOMBER', sport: 'mlb', color: '#FF6B4A', glyph: 'triangle',
description: 'Middle-of-the-order power producer',
propDNA: { reliable: ['total_bases', 'home_runs'], volatile: ['hits'] },
education: 'Power-pull hitters live and die by the long ball. Total bases and home-run props carry their value; their batting-average-driven props (hits) are volatile from the strikeout risk.',
education: 'Bombers drive the ball over the fence. Total bases and home-run props carry their value; their batting-average-driven props (hits) are the volatile leg from strikeout risk.',
},
CONTACT: {
tag: 'CONTACT', sport: 'mlb', color: '#3DDC84', glyph: 'crosshair',
BRUSH: {
legacyName: 'CONTACT', tag: 'BRUSH', sport: 'mlb', color: '#3DDC84', glyph: 'crosshair',
description: 'High average, low strikeout',
propDNA: { reliable: ['hits'], volatile: ['home_runs', 'total_bases'] },
education: 'Contact hitters rarely strike out, so their hits props are among the most reliable in baseball. Power props (HR, TB) are the volatile leg.',
education: 'Brush hitters rarely strike out, so their hits props are among the most reliable in baseball. Power props (HR, TB) are the volatile leg.',
},
'RUN PRODUCER': {
tag: 'RUN PROD', sport: 'mlb', color: '#4A9EFF', glyph: 'diamond',
DRIVER: {
legacyName: 'RUN PRODUCER', tag: 'DRIVER', sport: 'mlb', color: '#4A9EFF', glyph: 'diamond',
description: 'RBI-dependent, lineup context',
propDNA: { reliable: ['rbi'], volatile: ['hits', 'home_runs'] },
education: 'Run producers hit in the heart of the order. RBI props track lineup context — strong with runners on base; their individual hit props are more variable.',
education: 'Drivers hit in the heart of the order. RBI props track lineup context — strong with runners on base; their individual hit props are more variable.',
},
ACE: {
tag: 'ACE', sport: 'mlb', color: '#A78BFA', glyph: 'star',
ALPHA: {
legacyName: 'ACE', tag: 'ALPHA', sport: 'mlb', color: '#A78BFA', glyph: 'star',
description: 'High K/9, low WHIP, deep games',
propDNA: { reliable: ['strikeouts', 'innings_pitched'], volatile: ['earned_runs'] },
education: 'Aces miss bats and go deep. Strikeout and innings props are their most reliable; earned-run props are noisier because one swing can change a line.',
education: 'Alphas miss bats and go deep. Strikeout and innings props are their most reliable; earned-run props are noisier because one swing can change a line.',
},
'BULLPEN ARM': {
tag: 'BULLPEN', sport: 'mlb', color: '#FFB347', glyph: 'bolt',
description: 'Short outings, high leverage',
WHIFF: {
legacyName: 'STRIKEOUT ARTIST', tag: 'WHIFF', sport: 'mlb', color: '#FFB347', glyph: 'bolt',
description: 'Bat-missing arm, high K with traffic',
propDNA: { reliable: ['strikeouts'], volatile: ['earned_runs', 'innings_pitched'] },
education: 'Bullpen arms throw short, high-leverage outings. Strikeout props can hit in one inning; innings and earned-run props are too small a sample to trust.',
education: 'Whiffs rack up strikeouts but allow more baserunners than an Alpha. The strikeout prop is reliable; earned-run and innings props swing with their traffic.',
},
'SPEED THREAT': {
tag: 'SPEED', sport: 'mlb', color: '#2DD4BF', glyph: 'chevrons',
GHOST: {
legacyName: 'SPEED THREAT', tag: 'GHOST', sport: 'mlb', color: '#2DD4BF', glyph: 'chevrons',
description: 'Stolen bases, speed score',
propDNA: { reliable: ['stolen_bases', 'runs'], volatile: ['home_runs'] },
education: 'Speed threats turn singles into runs. Stolen-base and runs props are their lane; power props rarely clear.',
education: 'Ghosts turn singles into runs. Stolen-base and runs props are their lane; power props rarely clear.',
},
'TWO-WAY PLAYER': {
tag: 'TWO-WAY', sport: 'mlb', color: '#F472B6', glyph: 'half',
HYBRID: {
legacyName: 'TWO-WAY PLAYER', tag: 'HYBRID', sport: 'mlb', color: '#F472B6', glyph: 'half',
description: 'Bats and pitches at elite level',
propDNA: { reliable: ['total_bases', 'strikeouts'], volatile: ['hits'] },
education: 'Two-way players produce on both sides of the ball. Read which role they fill that day — their batting and pitching props live on different lines.',
education: 'Hybrids produce on both sides of the ball. Read which role they fill that day — their batting and pitching props live on different lines.',
},
'UTILITY PLAYER': {
tag: 'UTILITY', sport: 'mlb', color: '#22D3EE', glyph: 'plus',
FLEX: {
legacyName: 'UTILITY PLAYER', tag: 'FLEX', sport: 'mlb', color: '#22D3EE', glyph: 'plus',
description: 'Multi-position lineup flex',
propDNA: { reliable: [], volatile: ['hits', 'total_bases', 'rbi'] },
education: 'Utility players move around the lineup and the diamond. Their props are matchup- and slot-dependent — confirm they are starting before betting.',
education: 'Flex players move around the lineup and the diamond. Their props are matchup- and slot-dependent — confirm they are starting before betting.',
},
'INNINGS EATER': {
tag: 'INN EATER', sport: 'mlb', color: '#818CF8', glyph: 'clock',
description: 'Durable, deep-start workhorse',
WORKHORSE: {
legacyName: 'INNINGS EATER', tag: 'WORKHORSE', sport: 'mlb', color: '#818CF8', glyph: 'clock',
description: 'Durable, deep-start arm',
propDNA: { reliable: ['innings_pitched'], volatile: ['strikeouts', 'earned_runs'] },
education: 'Innings eaters pitch deep without elite stuff. Innings props are reliable; strikeout and earned-run props are more variable since they pitch to contact.',
education: 'Workhorses pitch deep without elite stuff. Innings props are reliable; strikeout and earned-run props are more variable since they pitch to contact.',
},
'POWER SLUGGER': {
tag: 'SLUGGER', sport: 'mlb', color: '#FF6B4A', glyph: 'triangle',
description: 'All-fields power producer',
propDNA: { reliable: ['total_bases', 'rbi'], volatile: ['stolen_bases'] },
education: 'Power sluggers drive the ball to all fields. Total bases and RBI are reliable; speed props are not part of their game.',
},
'TABLE SETTER': {
tag: 'TABLE SET', sport: 'mlb', color: '#38BDF8', glyph: 'diamondLine',
CATALYST: {
legacyName: 'TABLE SETTER', tag: 'CATALYST', sport: 'mlb', color: '#38BDF8', glyph: 'diamondLine',
description: 'On-base leadoff catalyst',
propDNA: { reliable: ['hits', 'runs'], volatile: ['rbi', 'home_runs'] },
education: 'Table setters get on base and score. Hits and runs props are their lane; RBI and power props sit lower in their profile.',
education: 'Catalysts get on base and score. Hits and runs props are their lane; RBI and power props sit lower in their profile.',
},
'GAP HITTER': {
tag: 'GAP', sport: 'mlb', color: '#34D399', glyph: 'uparrow',
description: 'Doubles and extra-base gaps',
MIRROR: {
legacyName: 'GAP HITTER', tag: 'MIRROR', sport: 'mlb', color: '#34D399', glyph: 'uparrow',
description: 'Gap-to-gap line-drive bat',
propDNA: { reliable: ['total_bases', 'hits'], volatile: ['home_runs'] },
education: 'Gap hitters spray doubles. Total bases and hits are reliable; home-run props are the volatile leg of their extra-base profile.',
education: 'Mirror hitters spray doubles to both gaps. Total bases and hits are reliable; home-run props are the volatile leg of their extra-base profile.',
},
CLOSER: {
tag: 'CLOSER', sport: 'mlb', color: '#FB7185', glyph: 'lock',
HAMMER: {
legacyName: 'CLOSER', tag: 'HAMMER', sport: 'mlb', color: '#FB7185', glyph: 'lock',
description: 'Ninth-inning save specialist',
propDNA: { reliable: ['strikeouts'], volatile: ['earned_runs', 'innings_pitched'] },
education: 'Closers throw one high-leverage inning. Strikeout props can hit in a clean save; everything else is a one-inning coin flip.',
education: 'Hammers throw one high-leverage inning. Strikeout props can hit in a clean save; everything else is a one-inning coin flip.',
},
SWINGMAN: {
tag: 'SWINGMAN', sport: 'mlb', color: '#FBBF24', glyph: 'swap',
description: 'Spot starter and long relief',
SINKER: {
legacyName: 'SWINGMAN', tag: 'SINKER', sport: 'mlb', color: '#FBBF24', glyph: 'swap',
description: 'Groundball spot-starter / long relief',
propDNA: { reliable: [], volatile: ['strikeouts', 'innings_pitched', 'earned_runs'] },
education: 'Swingmen bounce between starting and relief. Their workload is unpredictable, so all of their props carry role risk — confirm the assignment.',
education: 'Sinkers bounce between starting and relief and pitch to contact. Their workload is unpredictable, so all of their props carry role risk — confirm the assignment.',
},
'DEFENSIVE WIZARD': {
tag: 'DEF WIZ', sport: 'mlb', color: '#6366F1', glyph: 'shieldCheck',
description: 'Glove-first defensive value',
BRIDGE: {
legacyName: 'BULLPEN ARM', tag: 'BRIDGE', sport: 'mlb', color: '#6366F1', glyph: 'shieldCheck',
description: 'Setup / high-leverage middle relief',
propDNA: { reliable: ['strikeouts'], volatile: ['earned_runs', 'innings_pitched'] },
education: 'Bridges throw short, high-leverage outings. Strikeout props can hit in one inning; innings and earned-run props are too small a sample to trust.',
},
SWITCH: {
legacyName: 'DEFENSIVE WIZARD', tag: 'SWITCH', sport: 'mlb', color: '#FF5C5C', glyph: 'batball',
description: 'Platoon-leveraged, glove-first bat',
propDNA: { reliable: [], volatile: ['hits', 'total_bases', 'rbi'] },
education: 'Defensive wizards earn their spot with the glove. Their offensive props are thin and bottom-of-the-order dependent — lean unders.',
education: 'Switch bats earn their spot with the glove and favorable platoon splits. Their offensive props are thin and matchup-dependent — lean unders outside their platoon edge.',
},
// ───────── Soccer (6) — present in the design map ─────────
POACHER: {
tag: 'POACHER', sport: 'soccer', color: '#FF5C5C', glyph: 'crosshair',
// ───────── Soccer (6) ─────────
FINISHER: {
legacyName: 'POACHER', tag: 'FINISHER', sport: 'soccer', color: '#FF5C5C', glyph: 'crosshair',
description: 'Penalty-box finisher',
propDNA: { reliable: ['shots_on_target', 'goals'], volatile: ['assists'] },
education: 'Poachers finish inside the box. Shots-on-target and goals are their props; they rarely create for others.',
education: 'Finishers score inside the box. Shots-on-target and goals are their props; they rarely create for others.',
},
CREATOR: {
tag: 'CREATOR', sport: 'soccer', color: '#4A9EFF', glyph: 'node',
MAESTRO: {
legacyName: 'CREATOR', tag: 'MAESTRO', sport: 'soccer', color: '#4A9EFF', glyph: 'node',
description: 'Chance-creating playmaker',
propDNA: { reliable: ['assists', 'passes'], volatile: ['goals'] },
education: 'Creators set the table. Assists and passing props are reliable; their goal output is the volatile leg.',
education: 'Maestros set the table. Assists and passing props are reliable; their goal output is the volatile leg.',
},
'TARGET MAN': {
tag: 'TARGET', sport: 'soccer', color: '#FF6B4A', glyph: 'triangle',
TOWER: {
legacyName: 'TARGET MAN', tag: 'TOWER', sport: 'soccer', color: '#FF6B4A', glyph: 'triangle',
description: 'Hold-up aerial striker',
propDNA: { reliable: ['shots', 'shots_on_target'], volatile: ['goals', 'assists'] },
education: 'Target men win aerial duels and hold the ball up. Shot props are reliable; conversion to goals is variable.',
education: 'Towers win aerial duels and hold the ball up. Shot props are reliable; conversion to goals is variable.',
},
'BOX-TO-BOX': {
tag: 'B2B', sport: 'soccer', color: '#00D4A0', glyph: 'chevrons',
MOTOR: {
legacyName: 'BOX-TO-BOX', tag: 'MOTOR', sport: 'soccer', color: '#00D4A0', glyph: 'chevrons',
description: 'All-action central midfielder',
propDNA: { reliable: ['tackles', 'passes'], volatile: ['goals', 'shots'] },
education: 'Box-to-box midfielders cover every blade of grass. Tackles and passing props are reliable; their attacking output swings by role.',
education: 'Motors cover every blade of grass. Tackles and passing props are reliable; their attacking output swings by role.',
},
'WING WIZARD': {
tag: 'WING', sport: 'soccer', color: '#2DD4BF', glyph: 'slash',
BLADE: {
legacyName: 'WING WIZARD', tag: 'BLADE', sport: 'soccer', color: '#2DD4BF', glyph: 'slash',
description: 'Dribbling wide threat',
propDNA: { reliable: ['shots', 'assists'], volatile: ['goals'] },
education: 'Wing wizards beat defenders wide. Shots and assists are their lane; goals come in streaks.',
education: 'Blades beat defenders wide. Shots and assists are their lane; goals come in streaks.',
},
'SWEEPER KEEPER': {
tag: 'SWEEPER', sport: 'soccer', color: '#A78BFA', glyph: 'shield',
WALL: {
legacyName: 'SWEEPER KEEPER', tag: 'WALL', sport: 'soccer', color: '#A78BFA', glyph: 'shield',
description: 'Distributing goalkeeper',
propDNA: { reliable: ['saves', 'passes'], volatile: ['goals_conceded'] },
education: 'Sweeper keepers distribute and defend space. Saves and passing props are reliable; goals-conceded depends on the team in front of them.',
education: 'Walls distribute and defend space. Saves and passing props are reliable; goals-conceded depends on the team in front of them.',
},
};
const num = (v) => (typeof v === 'number' && !Number.isNaN(v) ? v : 0);
/**
* NBA scorers — each returns a 0..1-ish weight from season averages.
* Inputs (all optional): ppg, rpg, apg, bpg, spg, threes (3PM/g), usg (%),
* fg3a (3PA/g), bench (bool), pos ('G'|'F'|'C').
*/
/** NBA scorers — VYNDR Original keys. */
function scoreNBA(s) {
const ppg = num(s.ppg), rpg = num(s.rpg), apg = num(s.apg), bpg = num(s.bpg),
spg = num(s.spg), threes = num(s.threes), usg = num(s.usg), fg3a = num(s.fg3a);
const pos = (s.pos || '').toUpperCase();
const isBig = pos === 'C' || pos === 'F-C' || pos === 'C-F';
return {
'VOLUME SCORER': ppg >= 22 ? (ppg - 16) / 14 + (usg >= 28 ? 0.3 : 0) : 0,
'FLOOR GENERAL': apg >= 6 ? (apg - 3) / 7 : apg >= 4 ? 0.2 : 0,
'TWO-WAY ANCHOR': (bpg >= 1.3 ? bpg / 3 : 0) + (rpg >= 8 ? (rpg - 6) / 8 : 0),
'STRETCH BIG': isBig && threes >= 1.2 ? 0.5 + threes / 6 : 0,
'USAGE SPONGE': s.bench && usg >= 24 ? 0.5 : 0,
'COMBO GUARD': ppg >= 15 && apg >= 3 && apg < 7 ? 0.4 + apg / 20 : 0,
'ROLE GLUE': usg > 0 && usg < 16 && ppg < 10 ? 0.5 : 0,
'TRANSITION ENGINE': ppg >= 16 && spg >= 1.2 ? 0.3 : 0,
'POST SCORER': isBig && ppg >= 16 && threes < 1 ? 0.5 + ppg / 50 : 0,
'DEFENSIVE SPECIALIST': spg >= 1.4 && usg < 18 ? 0.5 + spg / 6 : 0,
'POINT FORWARD': apg >= 5 && (pos === 'F' || pos === 'G-F' || pos === 'F-G') && ppg >= 16 ? 0.6 + apg / 16 : 0,
SLASHER: ppg >= 16 && fg3a < 4 && !isBig ? 0.35 : 0,
'RIM RUNNER': isBig && rpg >= 7 && ppg < 16 && threes < 0.5 ? 0.45 : 0,
'3-AND-D': threes >= 1.6 && usg < 20 && spg >= 0.9 ? 0.5 + threes / 8 : 0,
'SIXTH MAN': s.bench && ppg >= 12 ? 0.4 + ppg / 40 : 0,
TORCH: ppg >= 22 ? (ppg - 16) / 14 + (usg >= 28 ? 0.3 : 0) : 0,
CONDUCTOR: apg >= 6 ? (apg - 3) / 7 : apg >= 4 ? 0.2 : 0,
FORTRESS: (bpg >= 1.3 ? bpg / 3 : 0) + (rpg >= 8 ? (rpg - 6) / 8 : 0),
ARTILLERY: isBig && threes >= 1.2 ? 0.5 + threes / 6 : 0,
SURGE: s.bench && usg >= 24 ? 0.5 : 0,
'DUAL THREAT': ppg >= 15 && apg >= 3 && apg < 7 ? 0.4 + apg / 20 : 0,
CONNECTOR: usg > 0 && usg < 16 && ppg < 10 ? 0.5 : 0,
FASTBREAK: ppg >= 16 && spg >= 1.2 ? 0.3 : 0,
'PAINT BOSS': isBig && ppg >= 16 && threes < 1 ? 0.5 + ppg / 50 : 0,
LOCKDOWN: spg >= 1.4 && usg < 18 ? 0.5 + spg / 6 : 0,
SWITCHBOARD: apg >= 5 && (pos === 'F' || pos === 'G-F' || pos === 'F-G') && ppg >= 16 ? 0.6 + apg / 16 : 0,
ARCHITECT: ppg >= 16 && fg3a < 4 && !isBig ? 0.35 : 0,
PISTON: isBig && rpg >= 7 && ppg < 16 && threes < 0.5 ? 0.45 : 0,
SENTINEL: threes >= 1.6 && usg < 20 && spg >= 0.9 ? 0.5 + threes / 8 : 0,
IGNITER: s.bench && ppg >= 12 ? 0.4 + ppg / 40 : 0,
};
}
function scoreWNBA(s) {
// WNBA reuses NBA archetypes plus 5 unique ones. Start from the NBA scores
// (filtered to WNBA-applicable) and add the unique forwards/guards.
const ppg = num(s.ppg), rpg = num(s.rpg), apg = num(s.apg), bpg = num(s.bpg),
spg = num(s.spg), threes = num(s.threes), usg = num(s.usg);
const pos = (s.pos || '').toUpperCase();
const isBig = pos === 'C' || pos === 'F' || pos === 'F-C';
return {
'VOLUME SCORER': ppg >= 18 ? (ppg - 12) / 12 + (usg >= 26 ? 0.25 : 0) : 0,
'FLOOR GENERAL': apg >= 5 ? (apg - 2) / 6 : 0,
'COMBO GUARD': ppg >= 14 && apg >= 3 && apg < 6 ? 0.4 + apg / 18 : 0,
'POST FACILITATOR': isBig && apg >= 3 && rpg >= 7 ? 0.6 + apg / 12 : 0,
'TWO-WAY WING': !isBig && ppg >= 12 && spg >= 1.2 ? 0.5 + spg / 6 : 0,
'STRETCH FORWARD': (pos === 'F' || isBig) && threes >= 1.2 ? 0.5 + threes / 5 : 0,
'SLASHING GUARD': pos.startsWith('G') && ppg >= 14 && threes < 1.5 ? 0.45 : 0,
'INTERIOR ANCHOR': isBig && (bpg >= 1 || rpg >= 8) ? 0.5 + rpg / 16 : 0,
'DEFENSIVE SPECIALIST': spg >= 1.5 && usg < 18 ? 0.45 : 0,
TORCH: ppg >= 18 ? (ppg - 12) / 12 + (usg >= 26 ? 0.25 : 0) : 0,
CONDUCTOR: apg >= 5 ? (apg - 2) / 6 : 0,
'DUAL THREAT': ppg >= 14 && apg >= 3 && apg < 6 ? 0.4 + apg / 18 : 0,
DISTRIBUTOR: isBig && apg >= 3 && rpg >= 7 ? 0.6 + apg / 12 : 0,
SHIELD: !isBig && ppg >= 12 && spg >= 1.2 ? 0.5 + spg / 6 : 0,
RANGE: (pos === 'F' || isBig) && threes >= 1.2 ? 0.5 + threes / 5 : 0,
SPARK: pos.startsWith('G') && ppg >= 14 && threes < 1.5 ? 0.45 : 0,
ANCHOR: isBig && (bpg >= 1 || rpg >= 8) ? 0.5 + rpg / 16 : 0,
LOCKDOWN: spg >= 1.5 && usg < 18 ? 0.45 : 0,
};
}
/**
* MLB scorers. Hitters and pitchers are disjoint; `isPitcher` (or presence of
* era/k9) routes to the pitcher archetypes.
* MLB scorers. Hitters and pitchers are disjoint.
* Hitter inputs: avg, hr, rbi, sb, ops, runs, k_rate, doubles.
* Pitcher inputs: era, k9, whip, ip_per_start, saves, role ('SP'|'RP'|'CL').
*/
@@ -340,46 +330,47 @@ function scoreMLB(s) {
const era = num(s.era), k9 = num(s.k9), whip = num(s.whip),
ip = num(s.ip_per_start), saves = num(s.saves);
return {
ACE: k9 >= 9.5 && era <= 3.6 && ip >= 5.5 ? 0.6 + k9 / 30 : k9 >= 9 ? 0.3 : 0,
'INNINGS EATER': ip >= 6 && k9 < 9 ? 0.55 + ip / 20 : 0,
CLOSER: role === 'CL' || saves >= 10 ? 0.7 + saves / 60 : 0,
'BULLPEN ARM': role === 'RP' && saves < 10 ? 0.55 : ip > 0 && ip < 3 ? 0.4 : 0,
SWINGMAN: role === 'SP' && ip < 5 && ip > 0 ? 0.4 : 0,
'TWO-WAY PLAYER': s.twoWay ? 0.8 : 0,
ALPHA: k9 >= 9.5 && era <= 3.6 && ip >= 5.5 ? 0.6 + k9 / 30 : k9 >= 9 && era <= 3.6 ? 0.3 : 0,
WHIFF: k9 >= 10.5 && era > 3.6 ? 0.6 + k9 / 30 : k9 >= 9.5 && era > 3.8 ? 0.35 : 0,
WORKHORSE: ip >= 6 && k9 < 9 ? 0.55 + ip / 20 : 0,
HAMMER: role === 'CL' || saves >= 10 ? 0.7 + saves / 60 : 0,
BRIDGE: role === 'RP' && saves < 10 ? 0.55 : ip > 0 && ip < 3 ? 0.4 : 0,
SINKER: role === 'SP' && ip < 5 && ip > 0 ? 0.4 : 0,
HYBRID: s.twoWay ? 0.8 : 0,
};
}
const avg = num(s.avg), hr = num(s.hr), rbi = num(s.rbi), sb = num(s.sb),
ops = num(s.ops), runs = num(s.runs), kRate = num(s.k_rate), doubles = num(s.doubles);
return {
'POWER PULL': hr >= 20 && kRate >= 24 ? 0.6 + hr / 60 : hr >= 18 ? 0.3 : 0,
'POWER SLUGGER': hr >= 20 && ops >= 0.85 && kRate < 24 ? 0.6 + hr / 60 : 0,
CONTACT: avg >= 0.28 && kRate < 16 ? 0.6 + (avg - 0.25) * 2 : avg >= 0.29 ? 0.4 : 0,
'RUN PRODUCER': rbi >= 50 && hr >= 12 ? 0.5 + rbi / 200 : 0,
'SPEED THREAT': sb >= 15 ? 0.6 + sb / 60 : sb >= 10 ? 0.35 : 0,
'TABLE SETTER': runs >= 50 && sb >= 8 && hr < 15 ? 0.5 + runs / 200 : 0,
'GAP HITTER': doubles >= 25 && hr < 20 ? 0.5 + doubles / 80 : 0,
'UTILITY PLAYER': s.utility ? 0.5 : 0,
'DEFENSIVE WIZARD': s.glove && avg < 0.25 ? 0.5 : 0,
'TWO-WAY PLAYER': s.twoWay ? 0.8 : 0,
// BOMBER is the single power archetype — fires for any high-HR bat (incl.
// high-strikeout sluggers like Judge), so power hitters classify as BOMBER.
BOMBER: hr >= 20 ? 0.6 + hr / 60 : hr >= 15 ? 0.3 : 0,
BRUSH: avg >= 0.28 && kRate < 16 ? 0.6 + (avg - 0.25) * 2 : avg >= 0.29 ? 0.4 : 0,
DRIVER: rbi >= 50 && hr >= 12 ? 0.5 + rbi / 200 : 0,
GHOST: sb >= 15 ? 0.6 + sb / 60 : sb >= 10 ? 0.35 : 0,
CATALYST: runs >= 50 && sb >= 8 && hr < 15 ? 0.5 + runs / 200 : 0,
MIRROR: doubles >= 25 && hr < 20 ? 0.5 + doubles / 80 : 0,
FLEX: s.utility ? 0.5 : 0,
SWITCH: s.glove && avg < 0.25 ? 0.5 : 0,
HYBRID: s.twoWay ? 0.8 : 0,
};
}
const SCORERS = { nba: scoreNBA, wnba: scoreWNBA, mlb: scoreMLB };
/** Look up an archetype's static descriptor by name (case-insensitive). */
/** Look up an archetype descriptor by VYNDR name OR legacy name (case-insensitive). */
function getArchetype(name) {
if (!name) return null;
const key = String(name).toUpperCase();
const a = ARCHETYPES[key];
if (!a) return null;
return { name: key, ...a };
if (ARCHETYPES[key]) return { name: key, ...ARCHETYPES[key] };
// Legacy-name fallback so old references still resolve.
const byLegacy = Object.entries(ARCHETYPES).find(([, a]) => a.legacyName === key);
return byLegacy ? { name: byLegacy[0], ...byLegacy[1] } : null;
}
/**
* Classify a player. Returns:
* { sport, primary, secondary|null, blend: [{archetype, weight}] }
* primary/secondary are full descriptors (name, tag, color, glyph, description,
* propDNA, education). blend weights are normalized 0..1 over the top entries.
*/
function classify(sport, stats = {}) {
const sp = String(sport || 'nba').toLowerCase();
@@ -392,8 +383,7 @@ function classify(sport, stats = {}) {
.sort((a, b) => b[1] - a[1]);
if (ranked.length === 0) {
// Fallback so the UI always has something to render.
const fallback = sp === 'mlb' ? 'UTILITY PLAYER' : sp === 'wnba' ? 'TWO-WAY WING' : 'ROLE GLUE';
const fallback = sp === 'mlb' ? 'FLEX' : sp === 'wnba' ? 'SHIELD' : 'CONNECTOR';
return { sport: sp, primary: getArchetype(fallback), secondary: null, blend: [{ archetype: fallback, weight: 1 }] };
}
@@ -402,7 +392,6 @@ function classify(sport, stats = {}) {
const blend = top.map(([name, v]) => ({ archetype: name, weight: +(v / total).toFixed(3) }));
const primary = getArchetype(ranked[0][0]);
// Secondary only if it's a meaningful share (>= 40% of primary's score).
const secondary = ranked.length > 1 && ranked[1][1] >= ranked[0][1] * 0.4
? getArchetype(ranked[1][0])
: null;
+5 -3
View File
@@ -101,11 +101,13 @@ async function getCascadeProjection(sport, player, team, opts = {}) {
// 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; // %
// VYNDR Originals (Session 44): SURGE (usage sponge) benefits most; primary
// creators (TORCH/DUAL THREAT/SWITCHBOARD) next; low-usage glue least.
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;
if (k === 'SURGE' || k.includes('USAGE SPONGE')) return 3;
if (['TORCH', 'DUAL THREAT', 'SWITCHBOARD', 'IGNITER'].includes(k) || k.includes('VOLUME') || k.includes('COMBO') || k.includes('POINT FORWARD')) return 2;
if (k === 'CONNECTOR' || k === 'LOCKDOWN' || k.includes('ROLE GLUE') || k.includes('SPECIALIST')) return 0.5;
return 1;
};
const weights = teammates.map((m) => weightFor(m.archetype));