Wave 5B: Pitcher Arsenal via Baseball Savant (Statcast)
D4 — build the FREE Baseball Savant adapter for pitch-level identity
(mix / velo / usage% / whiff%), the missing layer statsapi doesn't carry.
- savantAdapter.getPitcherArsenal(id|name) — normalizes two public Savant
CSV leaderboards (csv=true, NO parsing dependency): pitch-arsenal-stats
(usage% + whiff% + K%) + pitch-arsenals avg_speed (velo). League-wide,
cached 24h + in-memory mirror, indexed by MLBAM id. Defensive: null on any
unrecognized shape; a missing velo/whiff is ABSENT (null), never 0.
Injectable (fetchImpl/statsCsv/veloCsv/resolveId) → tests hit no network.
Live endpoints VERIFIED (200, exact columns) from the sandbox.
- GET /api/stats/pitcher/:name/arsenal (stats.js) + Next proxy. MLB-only;
an error/miss returns { found:false } so the card self-hides honestly.
- PitcherArsenal.tsx (+ barrel) — the mockup's PITCHER IDENTITY strip:
pitch mix % + velo + whiff%, mono/tabular, ranked by usage, sharpest-whiff
pitch highlighted green. Self-hides (heading included) when arsenal absent.
Mounted on the MLB player profile (a pitcher surface). Context, not a
graded market value.
- Tests: savantAdapter (fake CSV → ranked arsenal; unknown shape/blank cells
→ absent not 0; name→id resolve) + PitcherArsenal source locks (self-hide,
mono/tabular, em-dash-not-zero). +2 suites / +17 tests (3012 → 3029).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-1
@@ -2,7 +2,8 @@ const express = require('express');
|
||||
const { getSupabaseServiceClient } = require('../utils/supabase');
|
||||
const { getStatFilters } = require('../config/statFilters');
|
||||
const { createRateLimit } = require('../middleware/rateLimit');
|
||||
const { getPlayerIntel, getLeaders } = require('../services/playerIntelService');
|
||||
const { getPlayerIntel, getLeaders, sanitizePlayerName } = require('../services/playerIntelService');
|
||||
const { getPitcherArsenal } = require('../services/adapters/savantAdapter');
|
||||
const depthChart = require('../services/depthChartService');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -139,6 +140,26 @@ router.get('/player/:name', intelLimit, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /pitcher/:name/arsenal?sport=mlb — Baseball Savant pitch arsenal (Wave 5B).
|
||||
// Pitch mix % + velo + whiff% (the mockup's PITCHER IDENTITY lens). FREE Statcast
|
||||
// source; the adapter resolves name→MLBAM id + caches 24h. MLB only — any other
|
||||
// sport (or a miss) returns { found:false } and the card self-hides. Never fabricates.
|
||||
router.get('/pitcher/:name/arsenal', intelLimit, async (req, res) => {
|
||||
try {
|
||||
const sport = String(req.query.sport || 'mlb').toLowerCase();
|
||||
if (sport !== 'mlb') {
|
||||
return res.set(MISSION_HEADER).json({ found: false, reason: 'arsenal is MLB-only' });
|
||||
}
|
||||
const name = sanitizePlayerName(req.params.name);
|
||||
const arsenal = await getPitcherArsenal(name);
|
||||
res.set(MISSION_HEADER).json(arsenal || { found: false });
|
||||
} catch (err) {
|
||||
console.error('[stats/pitcher/arsenal]', err.message);
|
||||
// Honesty: an error is an ABSENT arsenal, not a fabricated one. Card self-hides.
|
||||
res.set(MISSION_HEADER).json({ found: false });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /leaders?sport=mlb&stat=hits&limit=10 — tonight's stat leaders (top
|
||||
// graded props by confidence) for the Terminal / Stats Explorer.
|
||||
router.get('/leaders', intelLimit, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user