Session E (night2): Phase 4 — scan + parlay polish
4.1 ROOT CAUSE of 'Ohtani returns nothing': no backend
/api/players/search existed (MLB 404'd; NBA/WNBA hit the offline
Python service). New Express route + mlbStatsAdapter.matchPlayers —
canonical nameKey fuzzy match (exact > last-name prefix > folded
substring). LIVE-VERIFIED vs the real 1,299-player list: Ohtani /
Aaron Judge / Sánchez / sanchez / Chisholm Jr all resolve; accented
and unaccented return identical results. Non-MLB matches the
platform's cached names (rosterlogs + grades), cache-only.
4.2 Reveal choreography per §7: analyzing steps → DECLASSIFIED stamp →
90ms-staggered context panels (entrance floors visible per the
Phase-0 rule); prefers-reduced-motion skips straight to the card.
4.3 PRIOR READS chips on scan results — the model's public ledger
history for the player (deferred-render, outcomes + pending, never
invented). /api/ledger/model gains ?player= on entries.
4.4 Parlay Lab: humanized stat labels via the ONE shared formatter
(lib/gradeAdapter.statLabel); 1-leg provisional grade ('Leg grade:
B — add a leg for the combined read'); discoverable entry — Nav
'Parlay Lab' item opens the drawer via window.__openParlay, and the
open drawer now renders an honest empty state at 0 legs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,45 @@ const { nameKey } = require('../../utils/playerName');
|
||||
* requires a UNIQUE same-last-name + same-first-initial candidate; anything
|
||||
* ambiguous returns null (a missing profile beats another player's log).
|
||||
*/
|
||||
/**
|
||||
* Session 60 (night2/E, audit fix 4.1) — fuzzy MULTI-match against the
|
||||
* canonical list. Pure: rank = exact nameKey > last-name prefix > folded
|
||||
* substring. Case/diacritic-insensitive via nameKey's folding, so
|
||||
* "ohtani", "Sánchez", "sanchez", "Chisholm Jr" all resolve.
|
||||
*/
|
||||
function matchPlayers(people, query, limit = 12) {
|
||||
const qKey = nameKey(query);
|
||||
if (!qKey) return [];
|
||||
const qLast = qKey.split(' ').pop();
|
||||
const scored = [];
|
||||
for (const p of people || []) {
|
||||
const k = nameKey(p.fullName);
|
||||
if (!k) continue;
|
||||
let score = null;
|
||||
if (k === qKey) score = 0;
|
||||
else if (k.split(' ').some((w) => w.startsWith(qLast)) && qLast.length >= 3) score = 1;
|
||||
else if (k.includes(qKey)) score = 2;
|
||||
if (score == null) continue;
|
||||
scored.push({ score, p });
|
||||
}
|
||||
scored.sort((a, b) => a.score - b.score);
|
||||
return scored.slice(0, limit).map(({ p }) => ({
|
||||
id: p.id,
|
||||
fullName: p.fullName,
|
||||
team: p.currentTeam?.name ?? null,
|
||||
position: p.primaryPosition?.abbreviation ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Multi-result fuzzy search (scan search box). Cached list, free API. */
|
||||
async function searchPlayers(query, opts = {}) {
|
||||
const season = opts.season || DEFAULT_SEASON;
|
||||
const url = `${BASE}/sports/1/players?season=${season}`;
|
||||
const data = await fetchWithCache(url, `mlbstats:players:${season}`, 24 * 3600);
|
||||
const people = (data && Array.isArray(data.people)) ? data.people : [];
|
||||
return matchPlayers(people, query, opts.limit || 12);
|
||||
}
|
||||
|
||||
async function searchPlayer(name, season = DEFAULT_SEASON) {
|
||||
const targetKey = nameKey(name);
|
||||
if (!targetKey) return null;
|
||||
@@ -248,6 +287,8 @@ module.exports = {
|
||||
getSeasonAverages,
|
||||
getBatterVsPitcher,
|
||||
searchPlayer,
|
||||
searchPlayers,
|
||||
matchPlayers,
|
||||
getPlayerStats,
|
||||
getTeams,
|
||||
resolveTeam,
|
||||
|
||||
Reference in New Issue
Block a user