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
+1 -1
View File
File diff suppressed because one or more lines are too long
+13 -6
View File
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import ProcessingGrade from '@/components/vyndr/ProcessingGrade';
import type { GradeResultData } from '@/components/vyndr/GradeResultCard';
import { mapScanToGradeResult } from '@/lib/gradeAdapter';
import { normalizeName, nameKey } from '@/lib/playerName';
import { markReadComplete } from '@/lib/reads';
import { useAuth } from '@/contexts/AuthContext';
import { useParlay } from '@/contexts/ParlayContext';
@@ -175,15 +176,21 @@ export default function ScanPage() {
})
.then((data: { props?: Array<{ player?: string; stat_type?: string }> }) => {
if (cancelled) return;
const byPlayer = new Map<string, Set<string>>();
// Session 48 — group by the normalized name key so variants
// ("A.J."/"AJ", "Matt"/"Matthew", "Jazz Chisholm"/"Jr.") show as ONE
// tile; display the normalized (longest) name.
const byPlayer = new Map<string, { name: string; stats: Set<string> }>();
for (const p of data.props || []) {
if (!p.player || !p.stat_type) continue;
const set = byPlayer.get(p.player) || new Set<string>();
set.add(p.stat_type);
byPlayer.set(p.player, set);
const key = nameKey(p.player);
const disp = normalizeName(p.player).display || p.player;
const entry = byPlayer.get(key) || { name: disp, stats: new Set<string>() };
if (disp.length > entry.name.length) entry.name = disp;
entry.stats.add(p.stat_type);
byPlayer.set(key, entry);
}
const list = Array.from(byPlayer.entries())
.map(([name, stats]) => ({ name, stats: Array.from(stats) }))
const list = Array.from(byPlayer.values())
.map(({ name, stats }) => ({ name, stats: Array.from(stats) }))
.sort((a, b) => a.name.localeCompare(b.name));
setTonightsPlayers(list);
})
+12 -1
View File
@@ -291,7 +291,18 @@ function buildPlayerStripsFromProps(gameProps, gradeIndex, deltaIndex, now = Dat
});
}
}
return order.map((key) => byPlayer[key]);
// Session 48 — one prop row per stat (variant dupes like "Ks 5.5" + "Ks 3.5"
// from "Matt"/"Matthew" collapse). Prefer the graded prop over an awaiting one.
return order.map((key) => {
const e = byPlayer[key];
const byStat = new Map();
for (const pr of e.props) {
const sk = String(pr.stat).toLowerCase();
const ex = byStat.get(sk);
if (!ex || (pr.grade && !ex.grade)) byStat.set(sk, pr);
}
return { ...e, props: [...byStat.values()] };
});
}
// ── MLB probable pitchers (Session 46) ──────────────────────────────