Session 54: Audit cleanup — name edges + polish (2255 tests)

P1 name edge cases (BOTH playerName.js copies, kept identical):
- normalizeName strips hyphens (display+key): "Jung-hoo Lee" === "Jung Hoo Lee".
- nameKey strips single-letter MIDDLE tokens: "Josh H Smith" === "Josh Smith"
  (keeps first+last; real middle names + collapsed initials untouched).
- richie -> richard added to NICKNAMES.

P2 polish:
- Team Hub names normalized at the source (teamService.getTeamHub) so
  "J.C. Escarra" renders as "JC Escarra" like the dashboard.
- snapshotService dedup keeps the highest-confidence GRADE but the richest
  DISPLAY (accented "José" over "Jose") so prop rows match the pitcher line.
- correlationWarning names the game: "2 legs from the same game (NYY @ BOS)".

Backend 2246 -> 2255 tests (+9), 194 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 15:45:07 -04:00
parent b012da13f8
commit 8629021774
11 changed files with 137 additions and 12 deletions
+17 -2
View File
@@ -213,15 +213,30 @@ async function runSnapshot(sport, opts = {}) {
// merged names. PropLine sends "Matt"/"Matthew", "A.J."/"AJ", "(STL)" tags as
// separate players; collapse to ONE grade per normalized player + stat (keep
// the highest-confidence; rawGraded is already confidence-desc).
// Session 54 — also keep the RICHEST display per player (prefer the accented
// variant: "José" over "Jose", then the longer string) so the prop rows match
// the accented pitcher line. The GRADE picked is still the highest-confidence.
const hasAccent = (s) => [...String(s)].some((c) => c.charCodeAt(0) > 127);
const richerDisplay = (a, b) => {
if (!b) return a;
if (hasAccent(a) !== hasAccent(b)) return hasAccent(a) ? a : b;
return a.length >= b.length ? a : b;
};
const dedup = new Map();
const bestDisplay = new Map();
for (const g of rawGraded) {
const disp = normalizeName(g.player || g.player_name).display || g.player || g.player_name || '';
const k = `${nameKey(disp)}|${String(g.stat_type || g.stat || '').toLowerCase()}`;
const pk = nameKey(disp);
bestDisplay.set(pk, richerDisplay(disp, bestDisplay.get(pk)));
const k = `${pk}|${String(g.stat_type || g.stat || '').toLowerCase()}`;
const cur = { ...g, player: disp, player_name: disp };
const prev = dedup.get(k);
if (!prev || (Number(g.confidence) || 0) > (Number(prev.confidence) || 0)) dedup.set(k, cur);
}
const graded = [...dedup.values()];
const graded = [...dedup.values()].map((g) => {
const disp = bestDisplay.get(nameKey(g.player)) || g.player;
return { ...g, player: disp, player_name: disp };
});
// Archetype per unique player (pure math once we have stats). Best-effort —
// a missing stat line → no badge (not a fallback archetype).