Session 47: Name normalization + grade intel + ticker polish (2149 tests)

- Name normalization completed: NICKNAMES table (Matt↔Matthew, Mike↔Michael...)
  resolved in nameKey, parenthetical team-tag strip "(STL)", verified accent-fold
  (Iván/Ivan, José/Jose). Slate strip now DISPLAYS the normalized de-dotted name
  ("AJ Ewing" not "A.J. Ewing") via buildPlayerStripsFromProps.
- Complete MLB VYNDR INTELLIGENCE: mlbGameLogFeatures derives rest_days (days off
  between latest games; 0=B2B) + ab_per_game (usage). buildIntelFields renders
  usage as "X AB/G", rest as B2B/Xd, matchup from bvp_advantage fallback.
- Ticker SCAN dedup: pushTickerItems keeps one SCAN per sport (sport field or
  text-prefix parse for legacy); MOVE/GRADE preserved; cap 50.
- BOMBER threshold prorated for mid-season (hr>=15 strong / >=10 mod) so June
  sluggers classify BOMBER not FLEX/DRIVER.

Backend 2122 -> 2149 tests (+27), 179 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-19 01:45:36 -04:00
parent c8fc9f577e
commit 78db55d499
13 changed files with 346 additions and 32 deletions
+24 -5
View File
@@ -1,12 +1,28 @@
/* Player-name normalization (Session 46) — frontend copy of
src/utils/playerName.js (the Next bundle can't import from src/). Keep the two
in sync; tests/unit cross-checks they agree. CommonJS so .tsx imports it AND
/* Player-name normalization (Session 46, completed Session 47) — frontend copy
of src/utils/playerName.js (the Next bundle can't import from src/). Keep the
two IDENTICAL; a test cross-checks they agree. CommonJS so .tsx imports it AND
Jest requires it directly. */
const SUFFIXES = new Set(['jr', 'sr', 'ii', 'iii', 'iv', 'v']);
const NICKNAMES = {
matt: 'matthew', mike: 'michael', chris: 'christopher', jake: 'jacob',
josh: 'joshua', nick: 'nicholas', nate: 'nathaniel', dan: 'daniel',
danny: 'daniel', dave: 'david', rob: 'robert', bob: 'robert', joe: 'joseph',
joey: 'joseph', jon: 'jonathan', johnny: 'john', tony: 'anthony',
alex: 'alexander', andy: 'andrew', drew: 'andrew', ben: 'benjamin',
will: 'william', bill: 'william', billy: 'william', willy: 'william',
zach: 'zachary', zack: 'zachary', tom: 'thomas', tommy: 'thomas',
ty: 'tyler', ed: 'edward', eddie: 'edward', teddy: 'theodore',
charlie: 'charles', chuck: 'charles', rick: 'richard', dick: 'richard',
jim: 'james', jimmy: 'james', ray: 'raymond', fred: 'frederick',
kenny: 'kenneth', sam: 'samuel', pat: 'patrick', greg: 'gregory',
steve: 'steven', tim: 'timothy', frank: 'francis',
};
function normalizeName(raw) {
const display = String(raw == null ? '' : raw)
.replace(/\s*\([^)]*\)\s*/g, ' ')
.replace(/\./g, '')
.replace(/\s+/g, ' ')
.trim();
@@ -16,7 +32,10 @@ function normalizeName(raw) {
}
function nameKey(raw) {
return normalizeName(raw).key;
const { key } = normalizeName(raw);
const parts = key.split(/\s+/).filter(Boolean);
if (parts.length >= 2 && NICKNAMES[parts[0]]) parts[0] = NICKNAMES[parts[0]];
return parts.join(' ');
}
module.exports = { normalizeName, nameKey, SUFFIXES };
module.exports = { normalizeName, nameKey, SUFFIXES, NICKNAMES };
+8 -4
View File
@@ -193,8 +193,10 @@ function isRelevantGame(game, now = Date.now()) {
}
// ── Pre-graded snapshot overlay (Session 45) ────────────────────────
// Session 46 — key by the normalized name so "A.J. Ewing"/"AJ Ewing" merge.
const { nameKey } = require('./playerName');
// Session 46/47 — key by the normalized name (nicknames/accents/periods/parens)
// so variants merge; DISPLAY the de-dotted, paren-stripped form.
const { nameKey, normalizeName } = require('./playerName');
const displayName = (raw) => normalizeName(raw).display || String(raw || '');
const gradeKey = (player, stat) => `${nameKey(player)}|${String(stat || '').toLowerCase()}`;
const sideCh = (dir) => (String(dir || 'over').toLowerCase() === 'under' ? 'U' : 'O');
@@ -257,7 +259,7 @@ function buildPlayerStripsFromProps(gameProps, gradeIndex, deltaIndex, now = Dat
const rec = gradeIndex[gradeKey(p.player, p.stat_type || p.stat)];
if (!byPlayer[pk]) {
byPlayer[pk] = {
player: p.player,
player: displayName(p.player),
team: p.team || '',
archetype: rec && rec.archetype ? { primary: rec.archetype } : undefined,
stats: [],
@@ -265,7 +267,9 @@ function buildPlayerStripsFromProps(gameProps, gradeIndex, deltaIndex, now = Dat
};
order.push(pk);
} else {
if (String(p.player).length > String(byPlayer[pk].player).length) byPlayer[pk].player = p.player;
// Display the longest (most complete) de-dotted variant seen.
const cand = displayName(p.player);
if (cand.length > String(byPlayer[pk].player).length) byPlayer[pk].player = cand;
if (!byPlayer[pk].archetype && rec && rec.archetype) byPlayer[pk].archetype = { primary: rec.archetype };
}
if (rec) {