Wave 1: kill three trust bugs (billing renewal + namesake collision + Desk copy)
FIX 1 — Honest billing renewal render. VYNDR tiers are monthly, so a `subscription_end` far in the future (the manually-seeded "RENEWS 6/9/2036" founder row) is a comped/lifetime/seed value, not a renewal. New web/src/lib/billingDisplay.js `classifyRenewal()` → date | none | lapsed | unknown (strict Date.parse guard, MONTHLY_RENEWAL_MAX_DAYS=60). Profile page renders the classified label for both the "Renews" stat and the cancel-scheduled "Access ends" line — no raw far-future date. No DB row mutated. FIX 2 — MLB namesake collision (James Wood → "Chicago Cubs"). searchPlayer now collects ALL exact-nameKey matches instead of first-`.find`; a ≥2 collision resolves ONLY via a confident teamHint (the prop's game participants, matched against the cached /teams list with ESPN↔statsapi abbr reconciliation), else refuses (null) — never guesses. The hint threads getPlayerStats → resolvePlayerStats → snapshotService (built from each prop's home/away team). Join invariant: a single-exact player whose team isn't in the hinted game has its team DROPPED (null), so streaks/rosterlogs never tag a foreign team. Full teamHint recovery shipped (not just the refuse fallback). FIX 3 — DeskShowcase headline "A $1M terminal." → deadpan value-showing copy "Every grade, every alt line, live." Prices ($44.99 / $34.99) unchanged. Tests: billingDisplay.test.js (7), mlbNamesakeResolve.test.js (12, disambiguation + join invariant + pure helpers), ds5PricingStates updated to assert the new headline and no "$1M". Full suite green (237 suites / 2863 tests); web `next build` exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -298,6 +298,18 @@ async function runSnapshot(sport, opts = {}) {
|
||||
// (statsapi/ESPN), never guessed. Feeds the ledger team/opponent columns
|
||||
// and the slate join guard (a prop only attaches to its own game).
|
||||
const teamByPlayer = {};
|
||||
// Wave 1 (trust bug) — the prop's game participants become the resolve's
|
||||
// teamHint: it disambiguates namesake collisions (two "James Wood") and, when
|
||||
// the resolved player's real team isn't in the prop's game, the resolver drops
|
||||
// the team rather than tag a foreign one (the streaks/rosterlogs JOIN
|
||||
// INVARIANT, mirroring the S59 slate guard). Keyed by the normalized name.
|
||||
const teamHintByPlayer = {};
|
||||
for (const p of props || []) {
|
||||
const k = norm(p.player);
|
||||
if (!k || teamHintByPlayer[k]) continue;
|
||||
const hint = [p.home_team, p.away_team].filter(Boolean);
|
||||
if (hint.length) teamHintByPlayer[k] = hint;
|
||||
}
|
||||
// Session 60 (night2/B) — THE STREAKS PRODUCER. The aggregator (streaks +
|
||||
// hot lists) starved because its data producers were all external and
|
||||
// unarmed (tank01-prefetch via n8n, the offline Python grading flow).
|
||||
@@ -308,7 +320,8 @@ async function runSnapshot(sport, opts = {}) {
|
||||
const logEntries = [];
|
||||
await mapLimit(players, STATS_CONCURRENCY, async (player) => {
|
||||
try {
|
||||
const stats = await deps.resolveStats(player, sp);
|
||||
const teamHint = teamHintByPlayer[norm(player)] || null;
|
||||
const stats = await deps.resolveStats(player, sp, teamHint ? { teamHint } : {});
|
||||
if (stats && stats.found) {
|
||||
const c = deps.classify(sp, stats.classifierInput || {});
|
||||
archByPlayer[player] = c.primary ? c.primary.name : null;
|
||||
|
||||
Reference in New Issue
Block a user