Session 48: Name normalization at every layer + usage field (2156 tests)

Trace-first: the normalizer functions were correct (S47) but raw names still
flowed through paths that skipped them. Fixed each leaking path.

- 2a (source chokepoint): snapshotService.runSnapshot normalizes each grade's
  player to the de-dotted display AND dedupes to one grade per nameKey|stat
  (highest confidence) before writing grades:{sport} + snapshot:latest. Every
  consumer (GameCard, Explore, leaders, profile) now gets clean merged names.
- 2b: buildPlayerStripsFromProps dedupes a player's props by stat (graded >
  awaiting) → one row per stat (kills "Ks 5.5 AND Ks 3.5" variant dupes).
- 2c: scan tonightsPlayers grid groups by nameKey, displays normalized name.
- 3: profile VYNDR INTELLIGENCE "+0%"/"—" was buildIntel's defaults (separate
  from the grade card's buildIntelFields, which already works). resolvePlayerStats
  now attaches real usage (AB/G) + rest (B2B/Xd); buildIntel renders them; REST
  default is now "—".

Backend 2149 -> 2156 tests (+7), 181 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 02:42:13 -04:00
parent 78db55d499
commit 91b03c4044
10 changed files with 259 additions and 15 deletions
+18 -3
View File
@@ -25,7 +25,7 @@ const DELTA_NOISE = 0.5; // ignore movements smaller than this
const DELTA_MOVE = 1.0; // ticker MOVE threshold
const STATS_CONCURRENCY = 5;
const { nameKey } = require('../utils/playerName');
const { nameKey, normalizeName } = require('../utils/playerName');
// Session 46 — group/dedupe by the normalized name key so "A.J. Ewing" and
// "AJ Ewing" (or "Jazz Chisholm" / "Jazz Chisholm Jr.") collapse to one player.
const norm = (s) => nameKey(s);
@@ -200,8 +200,23 @@ async function runSnapshot(sport, opts = {}) {
now: deps.now,
cacheSet: async (_k, v) => { envelope = v; },
});
const graded = (envelope && Array.isArray(envelope.grades)) ? envelope.grades : [];
if (graded.length === 0) return { sport: sp, status: 'skipped', reason: 'no grades', gradeCount: 0 };
const rawGraded = (envelope && Array.isArray(envelope.grades)) ? envelope.grades : [];
if (rawGraded.length === 0) return { sport: sp, status: 'skipped', reason: 'no grades', gradeCount: 0 };
// Session 48 — normalize player display names + dedupe variant grades at the
// SOURCE so every consumer (GameCard, Explore, leaders, profile) gets clean,
// 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).
const dedup = 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 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()];
// Archetype per unique player (pure math once we have stats). Best-effort —
// a missing stat line → no badge (not a fallback archetype).