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:
Kev
2026-07-11 00:59:17 -04:00
parent 9ae64516e6
commit 5d19660f8e
12 changed files with 325 additions and 26 deletions
+41
View File
@@ -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,