ChangeLog
Unreleased
Fixed
ESPN Site v2 rankings parse (mirrors sdv-py #732)
espn_{cfb,mbb,wbb,mch,wch}_rankings({ parsed: true })returned[]on the live payload: therankingsshort was registered toparse_items, which reads a Core v2itemslist, but the Site v2 payload keeps its polls in a top-levelrankingslist. A newparse_rankings(a port of sdv-py's) gives one row per ranked or vote-receiving team per poll: on the 2026-10-08 captures, cfb 208 rows over 5 polls, mbb 77, wbb 75, mch 35, wch 20.poll_idandteam_idare decimal strings;weekis the poll's week within itsseason_type(occurrence.value);rankisnullon the vote-receiving rows. The college baseball and softball wrappers route through it too, but ESPN answers their rankings URL with a 404.
4.0.0 (2026-10-07)
The first release after 3.0.0: 3.1.0 was never published, and its entries are folded in here.
BREAKING
Each entry below changes a public contract of 3.0.0. Every pre-v4 and 3.0.0 name stays callable as a deprecated alias (first entry); the rest change return values, errors or TypeScript types.
Public names are sportsdataverse-py's; every pre-v4 and 3.0.0 name is a deprecated alias
#69, #101
Every generated wrapper now carries sdv-py's public name, so the same endpoint
has the same name in Python and JavaScript. tools/codegen/generate.mjs ports
sdv-py's emit-time rename layer (tools/codegen/generate.py at the vendor pin):
- ESPN (all 29 leagues):
athlete→player,event→game(plurals too) as whole_-separated words (espnNbaAthleteGamelog→espnNbaPlayerGamelog,espnNflEvents→espnNflGames;athlete_eventlog→player_eventlog),event_competitor*→game_team*,event_competition→game_competition. sdv-py's curated CFB renames apply (espnCfbSeasonFutures→espnCfbFutures, …, from the now-vendoredespn_rename_map.yaml). Where sdv-py hand-writes aplayer_stats, the web-v3 stats endpoint isplayer_stats_v3(espnNbaAthleteStats→espnNbaPlayerStatsV3; cfb, mbb, mlb, nba, nfl, nhl, wbb, wnba), elsewhere plainplayer_stats. - Native APIs use sdv-py's
name_pattern/qualifier: NHL api-webnhlApiWeb*→nhl*(nhlWebPbp/nhlWebSchedulewhere sdv-py's ownnhl_pbp/nhl_scheduletake the plain name); NFL.comnflApi*→nfl*. - CBS takes sdv-py's 16 short names (
cbsBoxscore→cbsGameBoxscore,cbsClientConfiguration→cbsClientConfig, …). The 16 CBSFLAT_WRAPPERS[].shortvalues change with them (boxscore→game_boxscore); each def keeps its pre-v4 short aslegacyShort, and the docs playground (share links?e=flat:cbs:boxscore,RunCell) and/api/run({ api: 'cbs', endpoint: 'boxscore' }) still accept it. Code that matchesFLAT_WRAPPERSbyshortshould also matchlegacyShort. - 3.0.0's file-stem names for five native families take the v4 name of the same
endpoint:
mlbApi*→mlb*(mlbApiSchedule→mlbSchedule),cbsNapi*→cbs*(cbsNapiBoxscore→cbsGameBoxscore),foxBifrost*→foxApi*(foxBifrostScoreboard→foxApiScoreboard),yahooShangrila*→yahoo*, andsdv.recruiting.sports247*→recruiting*(itself deprecated, see Deprecated below). - Families already named like sdv-py (Statcast, NHL edge / stats-rest / records, nba_stats, wnba_stats, Torvik) are unchanged.
Nothing is removed. Every public name of the published 3.0.0 (the
sportsdataverse@3.0.0 tarball on npm) is still callable. All 1,766 renamed pre-v4
names (3,532 counting both snake_case and camelCase) are kept as deprecated aliases:
each forwards to the new wrapper (its .name is the old name) and emits one
DeprecationWarning per name per process, with code: 'SDV_DEPRECATED_NAME' so it
can be filtered. The full mapping is the new Deprecated names (v4)
reference page. tools/codegen/pre_v4_names.json freezes the 3.0.0 tarball's names
(7,579 namespace members, plus the exports of both entry points) and the later
pre-v4 development names. test/naming.test.js asserts every one still resolves,
that each renamed 3.0.0 name forwards to its v4 wrapper and warns once, and that
the v4 names equal sdv-py's generated names at the pin. The raw-JSON default and { parsed: true }
are unchanged. Wrapper defs gain publicShort (ESPN) / publicName (flat) and
LeagueConfig gains publicShorts; makeLeagueModule / makeFlatModule build
the v4 names and register the same aliases.
Also BREAKING for parsed: true: Baseball Savant CSV rows (parse_mlb_statcast_leaderboard, parse_mlb_statcast_search, and the hand-written mlb_statcast_search / _search_minors / _search_wbc with { parsed: true }) are now typed the way sdv-py's pandas reader types them: numeric columns are numbers (were strings), an integer column with a value beyond Number.MAX_SAFE_INTEGER is BigInt (one warning per column per process), inf / -inf are Infinity / -Infinity, True/False columns booleans, NA cells ("", NA, NaN, ...) null. The MLBAM id columns (batter, pitcher, on_1b..on_3b, fielder_2..fielder_9, game_pk) are pinned to Int64 in the CSV rows and in parse_mlb_statcast_gamefeed (whose game_pk was the feed's string "745444"), as sdv-py's _pin_id_columns does, and come back as decimal strings ("745444", the id rule in the next entry), so the two join. Found by the parser-parity harness below.
Integer id columns are decimal strings, everywhere
#91
- One type per id column, in every row, call and season, on every surface. A column whose name marks an id that holds integers now comes back as exact decimal strings, whatever width it was stored with (INT32, INT64, or a DOUBLE / JSON number holding integer ids, e.g. pandas' Float64 of an id column with nulls) and whatever the magnitude. Before, it was a
number(or aBigIntpast 2^53, with a warning), so its type changed with the season and the source: ESPN college play ids are 12-13 digits through 2013 and 18 digits from 2014-15, and one release storesgame_idas INT32 where another stores it as INT64. Nowload_nhl_game_infoandload_nhl_schedulesjoin ongame_id(1,398 of 1,398 games in 2025),load_cfb_scheduleandload_cfb_team_infoonvenue_id(662 of 662), loader ids join parser ids, and every result is JSON-safe. A string id is exact past 2^53 and works with===,SetandMapkeys. - Which names are ids: the last dotted segment of the column name is
id; ends_idor_ids; is a numbered id (athlete_id_1,sack_player_id2,team_id_247); isid_playorid_drive; ends_id_started/_id_ended(drive_play_id_started); ends stats.nba.com's underscore-lessteamid,personid,playeridormatchupid(hometeam_teamid,gameleaders_homeleaders_personid); or is camelCase…Id/…Ids(playerId,homeTeamId,eventId). Dotted names use their last segment (start.team.id,participants.0.athlete.id). Listed names count too: the MLBAM id columnsbatter,pitcher,on_1b..on_3b,fielder_2..fielder_9,game_pk, and the stats.nba.com video playlist'shid/vid(home / visitor team ids). Words such asvalid,paid,void,idle,widthandevent_idxnever match, and neither do the non-id columnsteam_id_source("espn"),*_sportec_id_overwrite(a flag),player_id_list(a joined list) and Savant'sn_pk(a pickoff count). Integer*_keyjoin keys (247Sports / On3 / recruitingplayer_key,institution_key, ...) are not id-named and stay numbers, with the same type on every surface. One predicate (isIdColumn,src/core/int64.ts) decides on every surface. Soload_nba_pbpathlete_id_1joinsload_nba_player_boxscoreathlete_id,load_nhl_scoringplayerIdjoinsload_nhl_skater_boxscoresplayer_id,load_cfb_pbphomeTeamIdjoinsload_cfb_team_infoteam_id, andload_nfl_pbpdrive_play_id_startedjoins itsplay_id. - Release loaders (
load*; rows andformat: "columns", one season or many): every id column, e.g.loadCfbPbpid"401628579101849903"(2024) and"332830097002"(2013),game_id"401628579";loadNflPbp/loadNflPbpParticipationplay_id(a DOUBLE in the release)"40", never"40.0"; INT32 ids such asloadNflFtnChartingftn_game_id;id_int64columns (pinned to Int64 like sdv-py's_cast_ids_int64), e.g.loadCfbRatingsteam_id"2306". Code-like id columns are strings in the loaders now too, as they already were in the parsers:type_id,status_id,period_id,season_id, ... (r.type_id === 58becomesr.type_id === "58", orNumber(r.type_id) === 58). - Producers: the box producers'
game_id,team_id,athlete_id,opponent_team_id(helper_<lg>_player_box/helper_<lg>_team_box), and the pbp producers' playid,game_id,homeTeamId,awayTeamId, the result'sgameIdand thetimeoutsid lists (espn_<lg>_pbp/helper_<lg>_pbp), fornba,wnba,mbb,wbb, so producer rows join the release rows. - Parsers (
parsed: trueand theparse_*exports): every id column of integers. That coversnormalize(MLB Stats API, NHL api-web / EDGE / stats-rest / records, ESPN, CBS, Fox, Yahoo, ...),parse_nba_stats_result_sets(player_id,team_id, ...;game_idalready was a string), Baseball Savant (game_pk,batter,pitcher,on_1b..on_3b,fielder_2..fielder_9,id; an empty cell staysnull), the PFF API, NFL Pro,parse_cdn_rankings(poll_id), 247Sports and the on3 / soccer frames. - HockeyTech analytics:
<lg>_game_shifts,<lg>_player_toi,<lg>_pbp,<lg>_game_corsiand the league-parameterisedhockeytech_shift_stints,hockeytech_player_toi,hockeytech_enriched_pbp,hockeytech_game_corsireturn their id columns (game_id,player_id,goalie_id, ...) as decimal strings, likeparse_hockeytech_pbp. The pure frame functions indist/analytics/hockeytech.jsstay sdv-py-faithful. - Unchanged: any other integer column is a
numberwhen every value is a safe integer, else aBigInt. Its warning is now emitted once per (loader or parser, column) per process, with its own codeSDV_INT64(a loader's used to beSDV_RELEASE, on every call). - Not an integer: an id column whose values are not exact integers (a fraction, a DOUBLE past 2^53, a boolean) is never turned into strings, since that would be a wrong id: a loader leaves it as read with one
SDV_INT64warning per (loader, column) per process, and a parser leaves it as read. A missing DOUBLE (NaN) in an id column isnull. Across seasons, an id column that is a string in one season and a DOUBLE in another is unified by the same rule (a DOUBLE NaN isnull, never"NaN"; a fraction is left as read with the warning, never"1.5"). A fraction in one season does not change the other seasons, so such a column mixes strings and numbers (["40", "71", 1.5, 2]) where sdv-py'sdiagonal_relaxedmakes one String column; no real release has one, and the warning names it. One real column stays a number for this reason:load_cfb_pbp_rid_play, whose 18-digit ESPN play ids the release stores as DOUBLE, already rounded past 2^53. A JSON number past 2^53 was already rounded byJSON.parse; the pbp producers throw aTypeErrorthere (as before). - Migrating: compare and join ids as strings. For a number back,
Number(row.game_id)(safe ids only); for a 64-bit id keep the string or useBigInt(row.id); for a column array,cols.id.map(Number)orcols.id.map(BigInt). To match your own numeric id:String(myId) === row.team_id. This covers the widened names too:athlete_id_1,playerId,homeTeamId,id_play,start.team.id(e.g.Number(r.playerId)). - Also: the loaders run the integer policy only on the id columns and the columns some season stores as INT64 (CFB pbp 2024: 153 INT64 columns of 506), about 2x faster after the decode; the
maxCellsdefault is documented as a measured heuristic (long-string-only selections cost more per cell).
A failed fetch never comes back as empty data; 400 / 422 raise InvalidParameterError (sdv-py #696 / #700)
#103
- BREAKING: an empty or non-JSON 2xx body raises
AssetFetchError. Before, the shared getter returned any 2xx body as data for the non-ESPN families on it (mlb,nhl_api_web,nhl_edge,nhl_stats_rest,nhl_records,cbs,yahoo,yahoo_scores,fox,nfl_api,odds_api,asa,recruiting), so an empty 200 or an HTML challenge page came back as""or the page text, andparsed: trueturned it into[]. Nowrequest()raises for every family: on an empty 2xx (barttorvik's block page, pro.nfl.com's rejected params, a throttled stats host) and, on a JSON request, on a body that is not JSON.torvik/bart_wbbandmlb_statcaststill return CSV and HTML as text, as sdv-py does, but a body they label JSON must decode, and an empty 200 raises there too (it returned"", then[]). - BREAKING: 400 / 422 raise
InvalidParameterErrorfor every family (it wasAssetFetchErroreverywhere butpff_api), and are never retried. The message names host, path and status with a redacted excerpt of at most 200 characters of the body, never the query string. The PFF message now quotes PFF's error body instead of reformatting it. - 204 / 205 return
{}(""for a text request) on every path. ESPN raisedAssetFetchErrorand the shared getter returned"".nba_stats/wnba_statsstill read a blank reply as throttling (AssetFetchError), as sdv-py does. classifyError(registerFamilyDefaults) now also sees an empty or non-JSON 2xx, sonfl_prokeeps its empty-200InvalidParameterError; 400 / 422 no longer reach the hook.- Checked against sdv-py:
tools/oracle/error_vocabulary_oracle.pyrecords sdv-py's own_getoutcome (sdv-py 938df90, #700) for 25 wire answers across the default, torvik and statcast getters. Served from a local server through the real axios transport, sdv-js gives the same outcome for all 25. A status-by-family matrix covers 19 families (17 JSON ones plus torvik and statcast), on real captured bodies where the repo has one. - Migrating: code that read
[],{}or""as "no rows" after a failed fetch now gets an error. CatchAssetFetchError(the answer is unknown: retry later) apart fromNoDataError(nothing is there), and catchInvalidParameterErrorfor a 400 / 422 (fix the arguments). A 204 / 205 is still a success with nothing in it.
stats.nba.com / stats.wnba.com default season: the latest season with data (sdv-py #693)
#99
- BREAKING: an unset season is now the latest season that has rows, not the previous season. sdv-py #693 replaced
season_or_previouswithseason_latest_with_data, and JS follows (season_or_previousis retired). The season is resolved per call, then dated for the request's league (LeagueID, else the host's), endpoint andSeasonType, in local time as sdv-py'sdate.today(). The NBA rolls over in November (2025-26until October 2026,2026-27from November). The G League (league_id: "20") rolls over in January, the Summer League ("15") in August (July 2026's is2026-27) and the WNBA in June. The draft combine (SeasonYear) rolls over in June, whichever league asks.drafthistorysends a year: July for the NBA, May for the WNBA.commonplayoffseriesand aSeasonTypeofPlayoffsorPlayInroll over in the league's playoff month: May for the NBA and the G League, October for the WNBA.All Starrolls over in March for the NBA and August for the WNBA; the G League has no All-Star rows and keeps its regular rule. A fixed month table cannot follow a lockout or a pandemic calendar, so passseasonfor those years. - 160 season arguments carry the default (91
nba_stats, 69wnba_stats), as the vendored YAML says. They include 55 that had no default (30 NBA, 25 WNBA). The API answered those without a season with every season summed:leaguedashteamstatsreturned each franchise since 1996-97.nba_stats_playerdashptshotsnow defaultsteam_idto1610612747, the default player's team, because the API answersTeamID0 with an HTTP 500 once a season is sent. season: ""is sent asSeason=, the API's own no-season answer, as in sdv-py. Before, the default replaced it.- Checked against sdv-py's own
_latest_seasonon every day of 1999, 2008 and 2026, for 4 leagues, 4 endpoint classes and 5 season types: 87,680 cases, 0 diffs (tools/parity/season_oracle.py). sdv-py's every-day tests are ported too. - Vendor sync to sdv-py
28ee34b. It also brings #696, whose generated docstrings no longer listrequests.exceptions.RequestException(no JS surface change), and #698 / #699, which are sdv-py test changes only.
TypeScript — Typed returns: generated row types for parsed: true
#100
- Row types for every verified endpoint. Codegen writes a row interface for each returns schema the parser-parity harness verified on a real sdv-py capture: 438 endpoints in 13 families (MLB Stats API, Statcast, the four NHL APIs, stats.nba.com, stats.wnba.com, ASA, MLS, NWSL, On3, 247Sports). They are exported from the package root (
MlbTeamRosterRow,NbaStatsBoxscoredefensivev2Tables, ...), andsdv.nhl.nhlRecordsAllTimeRecordVsFranchise({ parsed: true })resolves toNhlRecordsAllTimeRecordVsFranchiseRow[]. Every other wrapper's parsed rows areRow(Record<string, unknown>). - The column rule: an id column (the runtime's own
isIdColumn) isstring, orstring | numberwhen the schema stores a double (such an id past 2^53 is no exact integer and stays a number, asload_cfb_pbp_rid_playdoes); other numbers arenumber,characterisstring,logicalisboolean. A column that is null in every capture isunknown. Every column is optional and nullable, because the MLB, NHL api-web and 247 parsers leave out a column the payload lacks. Dates are the payload's ISO strings (string). Release loader rows get their own row types from sdv-py's loader schemas (the next entry). - Wrapper signatures: every wrapper is a
WrapperorSectionedWrapper. Withoutparsed: trueit resolves tounknown(the raw payload; it wasany). A stats.nba.com / stats.wnba.com endpoint resolves to its tables or, for a one-set payload, that table, as a union;sectionwith a table name the schema types resolves to that table's rows. The summary dispatchers resolve toParsedTables. - The default export is typed
Sdv: one interface per namespace (NbaWrappers,NhlWrappers, ...) lists every generated wrapper, and each pre-v4 alias carries@deprecatedand its v4 wrapper's type. Legacy service methods, loaders and the hand-written members (Statcast search, HockeyTech analytics, cricket WP, the basketball producers, odds math) are typed too. A namespace or member that does not exist is now a type error. - Checked:
test/types/agreement.test.jsparses the generated TypeScript and checks every value the parsers return on the committed captures against it (27,441 typed columns, 468,325 cells, 0 mismatches), including the key set of every row and table object and each typedsection.test/types/namespaces.test.jscompiles type tests againstdist/*.d.tsunderstrictand holds every namespace's typed members equal to the runtime object's. - Migrating (TypeScript only; no runtime change): narrow or cast a raw payload (
(await w(args)) as MyShape), and read typed rows asstring | null | undefinedetc. The API report (etc/sportsdataverse.api.md) now lists every wrapper. The parser modules' row type is one exportedParserRow.
TypeScript — Typed wrapper params and loader rows; strict mode
#102
- Every wrapper takes its endpoint's own params type. Codegen writes one per endpoint from the endpoint YAML, exported from the package root:
EspnTeamRosterParams(one per ESPN endpoint, shared by every league; asoccer/cricketwrapper also takesleague),MlbScheduleParams,PffApiPlayerDefenseSummaryParams, and so on. A path param is required unless the YAML makes it optional (required: false, a default,default_from, or thenow_varianttoggle). A query param is optional unless the YAML marks it required. This is sdv-py's rule.int,strandint|strparams takenumber | string, because both go on the URL as text and ids come back from parsed rows as decimal strings.boolparams take aboolean, andint|List[int]params take one value or a list. Every param is accepted under its snake_case name or its camelCase alias (team_idorteamId), and an optional one may benullfor unset. Flat families add their per-call controls:headerseverywhere, PFF'sapi_key/strict, NFL Pro'stoken/email/password/paginate/max_pages, and KenPom'semail/password. The params argument is optional exactly when every param is. - Release loaders return typed rows. Each of the 323 loaders has a row type (
LoadNflPbpRow,LoadCfbRatingsRow, ...) built from sdv-py's loader schemas (schemas/loader_schemas.yaml, now vendored at the pin). An id column of integers isstring, and a DOUBLE id isstring | number. Other INT64 columns arenumber | bigint, other numbersnumber,Stringisstring,Booleanisboolean,Date/DatetimeisDate,List(T)is an array, and aNullcolumn isunknown. Every column is optional and nullable. A column the schema does not list reads asunknown, since releases add columns over the seasons.columns: ["game_id", "epa"]narrows the result toPick<LoadNflPbpRow, "game_id" | "epa">[], andformat: "columns"returnsReleaseColumns<Row>, an array per column. A deprecated loader has its replacement's type. - Parser rows are
Record<string, unknown>.ParserRowand the return type of everysportsdataverse/parsersfunction (ParserRow[], previouslyRecord<string, any>[]) no longer useany.ParserFn/FlatParserFntakeraw: unknown. - The package compiles under
strict. The roottsconfig.jsonisstrict: trueacross all ofsrc/, the legacyget*service methods included. Their ids are typednumber | stringand their{ year, month, day }argumentsnumber | string | null, and T18a'sas anycasts are gone.tsconfig.strict.jsonnow equals the build. - Checked:
test/types/loader-agreement.test.jsloads the committed real release fixtures (8 loads, 1,462 typed columns, 35,422 cells) and checks every value against its generated row type: 0 mismatches. The parser agreement test still reports 0 mismatches, andtest/types/surface.check.tslocks the params and loader overloads understrict. - Migrating (TypeScript only; no runtime change): a param the endpoint does not have, a missing required path param, or a non-boolean
boolparam is now a type error. Narrow parser row values fromunknown. A loader's rows are its row type rather thanRecord<string, unknown>.
Error vocabulary, pluggable transport + auth
#63
Every wrapper (ESPN and flat-API) now fetches through one runtime core
(src/core/request.ts): auth provider → transport → retry → classification.
- BREAKING: wrapper failures raise
NoDataError/AssetFetchErrorinstead of raw axios errors.NoDataError= the fetch worked and there is nothing there (HTTP 404, or an ESPN 200 body{ code: 404 }, which used to be returned as data).AssetFetchError= the fetch failed (403, 429, 5xx, network, retries exhausted; carriesstatus,url,cause). They are siblings underSdvError;NoESPNDataErroraliasesNoDataError;SeasonNotFoundErrorandTransportUnavailableErrorare new too. - BREAKING: the Statcast, BartTorvik and HockeyTech getters no longer turn a
failed HTTP fetch into
{}/""— they throw like every other wrapper, so a failed fetch can't be mistaken for an empty table. (HockeyTech's unknown-league and unparseable-200 cases, which still returned{}here, throw as of the "HockeyTech hardening" entry below.) - Retries: network errors and the family's retry statuses (default
DEFAULT_RETRY_STATUSES= 403 / 408 / 429 / 500 / 502 / 503 / 504, as sdv-py) are retried with exponential backoff + jitter (0.5s doubling, capped at 4s), honouringRetry-After(capped at 120s). The default budget is 3 retries, with at most 4 spent on statuses. 403 is retried because ESPN Core v2 answers 403 under load. Auth-gated families narrow the set withregisterFamilyDefaults(family, { retryStatuses });nfl_apinever retries a 403. A 401 refreshes credentials once. configure({ transport, auth, retries, timeoutMs, userAgent })+getConfig()/resetConfig(). Transports and auth are per family (site_v2,core_v2,mlb,nfl_api, …) with an optionaldefaulttransport.- Transports:
axiosTransport(default) andcreateImpersonatingTransport()— browser TLS fingerprinting via the new optional peer dependencyimpit(npm install impit). - Auth helpers:
bearerAuth,headerAuth,queryAuth,tokenAuth(minted + cached tokens),sessionAuth(login cookies / headers). NFL.com auth is now atokenAuthregistered fornfl_api(sameNFL_ACCESS_TOKEN/NFL_CLIENT_KEY/NFL_CLIENT_SECRETbehaviour). - Fix: a flat wrapper's
headersargument is now forwarded for every family whose getter takes headers. It was dropped for non-authfamilies such as 247Sports and Yahoo. HockeyTech's getter builds its own headers and still takes params only. - New guide: Transport, auth & errors. CI adds a
strict: truetype-check of the runtime core (npm run typecheck:strict).
HockeyTech season years and season resolution (sdv-py parity)
#93
Same rules as sdv-py #694, checked cell for cell against sdv-py on the 20 leagues' seasons feeds (season year and label of all 1,102 rows, the default season, and 3,520 season lookups).
- BREAKING:
season_yris the season's end year in every name form. The first rule that matches wins: aYYYY-YYYY,YYYY-YYorYYYY/YYspan (spaces allowed) gives its end year ("2025-2026" and "2025/26" are 2026; they were 2120 and 2025); aYY-ZZspan of consecutive years gives 20ZZ ("26-27 Regular Season" is 2027); otherwise the first 4-digit token, which is a year when it falls in 1950 to this year + 2 and a compact span when its halves are consecutive ("CCHL 2425 Special Events" is 2025, was 2425). Anything else is null. A preseason or exhibition named with the one year it starts in belongs to the next season ("2026 Pre-season", starting 2026-08, is 2027); PWHL's "2024 Preseason", which began 2023-11-01, stays 2024. Every league but AHL, CHL, MHL, PWHL, SJHL and USHL has rows that change. - BREAKING: exhibitions get their own
game_type_label, "exhibition" (was "regular"), andhockeytech_resolve_season_idtakesgameType: "exhibition". A "Preseason Exhibition" stays a preseason. - BREAKING:
most_recent_hockeytech_seasonis the newest regular season that is not a one-off event, falling back to the newest row of any kind when the feed lists no such season. A preseason the feed lists before its regular season is no longer the default: ECHL in the 2026 preseason window gives 2026, not 2027. - BREAKING:
hockeytech_resolve_season_idskips one-off events and ranks what is left. For a regular season or playoffs, all-star games, showcases, prospect games, combines, special events, exhibitions and play-ins are dropped (AHL 2026 is 90, not the 91 All-Star Challenge). The remaining rows rank by: no other league's code in the name ("CCHL 2009/2010" in the OJHL feed ranks last), the name says its game type, the name spans two years, then feed order. Single-year tournaments ("2025 Mowat Cup", "2025 Cottage Cup") therefore lose to the regular season but still resolve where they are all a league has (CHL's Memorial Cups). Divisions listed side by side with neither marker keep feed order (BCHL 2024: "2023-24 BC Regular Season" but "2024 AB Playoffs"); passseasonIdfor the others. - The PWHL fallback table (used when the seasons feed fails or lacks the season) now has ids 1 to 11, as in sdv-py.
- 50 return-column descriptions in the
hockeytech_scorebar,_schedule,_stats,_player_search,_leadersand_team_rostertables, and the seasons table'sseason_yrandgame_type_label, now use sdv-py's text (sdv-py #690 and #694), with sdv-py function names replaced by the JS ones.
HockeyTech hardening (error vocabulary, User-Agent, returns descriptions)
#86
- BREAKING: a failed HockeyTech fetch is no longer an empty result. The shared getter behind the 16
hockeytech_*wrappers and the season helpers used to turn any HTTP-200 body it could not use into{}(soparsed: truegave[]). It now throwsAssetFetchErrorfor an empty or unparseable body and for HockeyTech's in-body error sentinels ({"SiteKit"|"GC": {"Undefined": "Undefined Tab <view>"}},{"error": "InvalidView error: …"}). The one reply that still reads as empty is the recognised plain-textFeed type access denied.(a league key without access to that feed, e.g. MJHL's game summary):{}raw,[]parsed, as in sdv-py. HTTP 404 staysNoDataError. A missing or unknownleaguenow throws instead of returning{}. - BREAKING:
most_recent_hockeytech_season/hockeytech_season_idthrow on a failed fetch instead of returning 2026 /[]. When the feed answers with no seasons,hockeytech_season_idreturns[]andmost_recent_hockeytech_seasonthrowsNoDataError(sdv-py returns a hard-coded 2026, already stale: PWHL's newest season is 2026-27).hockeytech_resolve_season_idkeeps the PWHL fallback table for a failed fetch, but rethrows a non-SdvError. - The
<lg>_pbp/<lg>_game_corsianalytics now also reject aGCerror sentinel on the game summary (they used to take it as blank game metadata). - HockeyTech requests send the configured User-Agent (
configure({ userAgent }), defaultMozilla/5.0 (compatible; sportsdataverse-js/4.x)) instead of a hard-coded one carrying a+https://token, which also overrodeconfigure. HockeyTech and Baseball Savant both answered 200 to the default UA (live check, 2026-10-05). parse_hockeytech_scorebarnow sharesparse_hockeytech_schedule's implementation (sameSiteKit.Scorebarpayload); both names stay.- The
hockeytech_scorebar,_stats,_player_search,_player_game_log,_playoff_bracketand_transactionsreturns tables replace 250 placeholder descriptions ("HockeyTechxfield.") with real ones: sdv-py's text where the column exists there and the captured data agrees with it, otherwise text written from the captured payloads.hockeytech_schedule(the sameSiteKit.Scorebarframe) now carries the scorebar descriptions, replacing several that were wrong (date,game_letter,quick_score).
HockeyTech schedules read the season-scoped schedule view
sdv-py "fix(hockeytech): schedules read the season-scoped modulekit/schedule view"; spec in sdv-internal-refs #49, sdv-swagger #34
- BREAKING:
hockeytech_schedule/hockeytechSchedulereadsmodulekit/schedule&season_id=<id>, which the feed scopes to that one season. It used to readmodulekit/scorebarwith a 10,000-day window andlimit=10000; scorebar ignores theseason_idit is sent, returns games oldest-first and stops at the limit, so AHL season 90 (2025-26) came back as 10,000 games from 1995 to 2012 and none from 2025-26. Live on 2026-10-08 the schedule view returned only the requested season in all 20 leagues (AHL 2025-26: 1,152 games, OHL 682, PWHL 120). With noseason_idthe feed returns the season in progress, which can be a preseason; a season's playoffs and preseason are separate season ids. - BREAKING: the date-window params are gone from
hockeytech_schedule:number_of_days_back,number_of_days_ahead,limitandleague_id. Usehockeytech_scorebar/hockeytechScorebar(unchanged) for games in a date window.hockeytech_scheduletakesleague,season_idand a new optionalteam_id(one team's games). - BREAKING:
parsed: truerows carry the schedule view's columns, not the scorebar's. The main renames:idstays andgame_idis added (equal);home_id→home_team(the HockeyTech team id) with the name inhome_team_name(washome_long_name);home_code/home_city/home_nickname→home_team_code/home_team_city/home_team_nickname;home_goals→home_goal_count; everyvisitor_*→visiting_*(visitor_id→visiting_team,visitor_goals→visiting_goal_count, …);game_statusis now the label ("Final","Final OT","Final OT2","Final SO","Postponed", or the start time) where it was the numeric code, which moves tostatus(1 not started, 2 in progress, 3 unofficial final, 4 final);game_status_string/_longare gone. Same name, new meaning:dateis the display date ("Sep. 19"; the YYYY-MM-DD date isdate_played) andscheduled_timeis the display time ("7:00 pm EST"; HH:MM:SS isschedule_time).league_idis gone.date_time_playedis local time with a misleadingZsuffix, not UTC: usegame_date_iso8601for the real offset. The returns table lists all 78 columns seen on the 20 leagues' real replies; the 15 leagues without abroadcastersobject ship flat*_video_url/*_webcast_urlcolumns instead. This supersedes the #86 note above that schedule and scorebar share oneSiteKit.Scorebarframe:parse_hockeytech_scorebarkeeps returning the scorebar rows unchanged.
HockeyTech derived coordinates are rotations of the feet frame
sdv-internal-refs #50 / #51 (hockeytech/CANVAS.md, "The derived columns, defined"); the same fix as fastRhockey #75, and the one going into sdv-py's add_coord_transforms
- BREAKING:
x_coord_fixed/y_coord_fixed,x_coord_right/y_coord_rightandx_coord_vertical/y_coord_verticalchange values inadd_coord_transforms,enrich_pbpand the<lg>_pbp/hockeytech_enriched_pbpanalytics. They are now rotations of the rink-feet framex = x_coord / 3 - 100(−100..100),y = 42.5 - y_coord * 85 / 300(−42.5..42.5):*_fixed=(-x, -y)for every event, so the home team shoots right (the feed's home team attacks raw x = 0 in every period, 320 PWHL games);*_right=(-x, -y)for home events and(x, y)for visitor events, so every team shoots right;*_vertical=(-y_coord_right, x_coord_right), so every team shoots up. The old home flip100 + (100 - x),42.5 - (y - 42.5)was a 0–200 × 0–85 mirror applied to centre-origin feet: on PWHL game 42 the home team's shots and goals hadx_coord_right191.7–289.7 ft andy_coord_right49.0–125.2 ft, off the rink; they now run −8.3–89.7 ft, like the visitor's −3.7–91.0.x_coord_fixedwasx / 3andy_coord_fixedapplied the y transform twice (74.8–96.4 ft); neither had a meaning. - BREAKING: the right and vertical columns are
nullwhen an event's side is unknown: a null or emptyteam_idorhome_team_id, or nohome_team_idcolumn. Faceoffs carry no team, so theirs are null, as is every event of a game enriched without its game summary. They used to take the visitor's (unflipped) values. x_coord,y_coord,*_original,*_neutral,shot_distanceandshot_angleare unchanged. Theload_pwhl_pbprelease dataset is built by the R / Python producers, so its derived columns change when it is rebuilt with the fix.- Oracle: the HockeyTech analytics oracle moves to sdv-py ac69bb7, the merge of sdv-py #728, which makes the same fix there (
HOCKEYTECH_PINintools/sdv_py_pin.py; the other ports stay atPORT_PIN). Regenerated there, it differs from the old oracle only in the six derived columns, and the cell-by-cell parity test now compares them too.
HockeyTech empty-net goals measure to the net their team attacks
sdv-internal-refs #52 (hockeytech/CANVAS.md, "Shot geometry"); the same rule as fastRhockey and sdv-py's add_shot_distance_angle
- BREAKING:
shot_distance/shot_anglechange for empty-net goals scored from the shooter's own half inadd_shot_distance_angle,enrich_pbpand the<lg>_pbp/hockeytech_enriched_pbpanalytics, andscoring_chance(distance ≤ 25 ft) follows. A goal whoseempty_netis"1"(the goal-level fieldparse_pbptakes fromproperties.isEmptyNet) is measured to the net its team attacks,dx = |attack_x - x|, withattack_x = -goal_x(−89 ft) for the home team and+goal_xfor the visitor, as the feed lays out the rink; it used to measure to the nearer net,dx = goal_x - |x|. On 320 PWHL games, 27 such goals move from a median 59 ft to 126 ft. On the attacking half the two formulas agree. - Every other event keeps the nearer net: per-event validation found that own-half events with a goalie in net carry mirrored coordinates. The goal-twin shot rows carry no
empty_net, and a row with a null or emptyteam_id/home_team_id(or no such column, e.g. a game enriched without its summary) also keeps the nearer net.
Fox is vendored from sdv-py's fox_api
#85
- BREAKING — Fox:
sdv.foxis now vendored from sdv-py'sfox_api.- The canonical names are
fox_api_*/foxApi*. Every pre-v4fox_*name is a deprecated alias. scorechipno longer sendsapi-version, which made it answer 400.- The 5 routes sdv-py dropped as dead (
fs_feed,fs_images,fs_layouts,fs_videos,explore_favorite) keep their old names, are deprecated, and warn once (DeprecationWarning, codeSDV_DEPRECATED_ENDPOINT, the code every deprecated endpoint now carries,recruiting_*included). parsed: trueoutput is unchanged.
- The canonical names are
Yahoo stats graph: no locale params, no league default
#64
sdv.yahoo.*(stats graph): host is nowhttps://graphite-secure.sports.yahoo.com/v1/query/shangrila(same final URLs). The wrappers no longer declarelang/region/tz(Yahoo applies the same locale defaults server-side) and the stats queries no longer defaultleague=ncaaf— passleagueexplicitly. BREAKING for callers that passed a locale or relied on theleaguedefault.
Legacy get* methods: typed errors, retries, timeout
#90
- BREAKING — errors: a failed fetch throws a typed error instead of a raw axios error. HTTP 404 is
NoDataError. A 403 / 429 / 5xx that persists after retries, or a network error, isAssetFetchError, whosecausecarries no request config. These cases differ beyond the error class:- The ESPN methods that return the body as-is (
getScoreboard,getStandings,getTeamList,getTeamInfo,getTeamPlayers,getConferences) now throwNoDataErrorfor ESPN's HTTP 200{ code: 404 }body, which they used to return. They throwAssetFetchErrorfor a 2xx body that is not JSON, which they used to return as a string. - On such a body,
nba.getSummary/mlb.getSummaryused to return a 12-key object ofundefineds andmbb.getSummary/wbb.getSummarya 7-key one. They now throwNoDataError/AssetFetchError. The other 12 summary-reading methods (getSummaryon cfb / nfl / nhl / wnba,getPickson 6 leagues, NHLgetPlayByPlay/getBoxScore) used to throw aTypeErrorthere. They now throwNoDataError/AssetFetchErroras well. sdv.ncaa.getInfo/getBoxScore/getPlayByPlay/getScoreboard(casablanca) used to return a 2xx body that is not JSON as a string. They now throwAssetFetchError.- A 403 / 408 / 429 / 5xx is now retried (at most 4 times) before it fails. The 247Sports and stats.ncaa.org families never retry a 403.
- Requests now time out after 30 s by default (
configure({ timeoutMs })); they had no timeout. They also send the sportsdataverse User-Agent instead of axios's. The deprecated 247Sports scrapers still send their own browser User-Agent.
- The ESPN methods that return the body as-is (
Added
section on the stats.nba.com / stats.wnba.com wrappers
#98
nba_stats_*/wnba_stats_*:{ parsed: true, section: "<result set>" }returns that one result set, as sdv-py'sresult_setdoes; an unknown name returns[](sdv-py: a zero-row frame, no error). Withoutsectionthe result is unchanged. Each result set of the committed real captures equals sdv-py's frame. The playground passessectionto flat multi-table parsers and offers a section picker; the parity oracles keep sdv-py's NaN / inf as a marker the tests decode.
NFL Pro email/password login
#80
- NFL Pro (
nfl_pro) can now log in for you, ported from sdv-py'snflpro_runtime. The token resolves in sdv-py's order:headers.Authorization>token>NFLPRO_TOKEN> a headless-browser id.nfl.com login withemail/passwordon the call, elseNFLPRO_EMAIL/NFLPRO_PW(the same env var names as sdv-py) >NflProAuthErrorwith instructions.NFLPRO_TOKENwins overemail/passwordon the call, as in sdv-py. - The login drives id.nfl.com as a state machine (e-mail, an optional passkey offer, password, in whatever order the site shows them). It keeps only a token whose JWT carries an active
NFL_PLUS_*plan, because an anonymous token looks the same. Tokens are cached per account until 120 s before they expire, keyed by an HMAC of e-mail + password under a random per-process key (a wrong password is never answered from the cache; expired entries are dropped). Concurrent calls for one account share one login; the whole login has one 3-minute deadline, after which the browser is closed and every waiter rejects. A401on a logged-in token drops it and logs in again once (a supplied token is never re-minted). The e-mail, password and token never reach an error, itscause, or a warning; note thatDEBUG=pw:apimakes Playwright itself logfill()values to stderr. - The password is submitted once: if id.nfl.com still shows the password field afterwards, the login stops with "did not accept the password" instead of retrying (sdv-py resubmits, which with a wrong
NFLPRO_PWis repeated failed attempts against a paid account). playwrightis a new optional peer dependency, imported only when a login is actually needed:npm i playwright && npx playwright install chromium. Without it, a login throwsTransportUnavailableErrornaming that command.- New exports
nflProBrowserLogin(email, password, { playwright })(the login itself; the module is injectable) andnflProClearTokenCache(), plus thePlaywrightLiketype.nflProToken(new in this release) is nowasyncand takes{ token, email, password }. Live test:SDV_NFLPRO_LIVE=1(orSDV_NFL_PRO_LIVE=1) withNFLPRO_TOKENorNFLPRO_EMAIL/NFLPRO_PW.
ESPN basketball PBP producers (NBA / WNBA / MBB / WBB)
#82
sdv.<lg>.espn_<lg>_pbp(game_id, { raw })plushelper_<lg>_pbp(game_id, pbp_txt)and its stageshelper_<lg>_pickcenter,helper_<lg>_game_data,helper_<lg>_pbp_features(+ camelCase, e.g.sdv.nba.espnNbaPbp,sdv.mbb.helperMbbPbp) fornba,wnba,mbb,wbb, under sdv-py's names.espn_<lg>_pbpfetches the summary throughespn_<lg>_summary, keeps sdv-py's incoming keys (absent ones defaulted as py does) and returns py's cleaned game dict:playsrows with py's column names and order (dotted, e.g.end.half_seconds_remaining), thetimeoutsmap (team id ->{"1": play ids, "2": play ids}), and the summary keys passed through;raw: truereturns the trimmed payload. The helpers are pure. Ported from sdv-py (pin 719de79) with its per-league facts: NBA quarters with the 720 / 1440 / 2160 / 2880 ladder; WNBA and WBB use the format's period count (else the season year: WNBA halves before 2006, WBB before 2016) for the 600-second quarter or 1200-second half ladders; MBB plays halves with its own columns (half= period,start.period_seconds_remaining, ...) and an Int32 M:SS clock -- a decimal clock such as"23.4"throws, as in py, but ESPN's college feeds use whole-second M:SS clocks, so that only happens on a non-MBB payload; OT is 300 seconds everywhere. Seconds-remaining columns round to float32 at every step like polars.helper_<lg>_pickcenter/helper_<lg>_game_datareturn a value taken from the pickcenter as a one-element array (py: a numpy array), e.g.gameSpread: [-8.5]; a league default stays a scalar. The playidcolumn and thetimeoutsid lists are exact decimal strings in every era (see "integer id columns are decimal strings, everywhere" above). Lag / lead / row numbers never cross agame_id. Not ported, deliberately:<lg>_pbp_disk(it reads a local JSON path; in JS,JSON.parsethe file and callhelper_<lg>_pbp, andfswould break browser builds). Parity: every league is compared cell by cell with sdv-py's ownespn_<lg>_pbpon 19 real captures (5 new live-captured games: WNBA 2003 halves, WBB 2015 halves + OT, WBB 4OT, MBB 2OT, NBA OT with two pickcenter providers) and 25 derived payloads, including the halves cutoffs pinned on both sides (tools/parity/espn_basketball_pbp_oracle.py).
ESPN basketball box producers (NBA / WNBA / MBB / WBB)
#81
sdv.<lg>.helper_<lg>_player_box(summary)andhelper_<lg>_team_box(summary)(+ camelCase, e.g.sdv.nba.helperNbaPlayerBox) fornba,wnba,mbb,wbb: one game's ESPN summary payload in, the rows the hoopR / wehoop box-score releases publish out. Pure (no network): fetch withespn_<lg>_summary({ event_id })and pass the result in. Ported from sdv-py (pin 719de79) with its per-league facts: NBA/WNBA carryplus_minus(a string such as"+16"), MBB/WBB do not; MBB/WBB skip a game whose second team ships no athletes, NBA/WNBA publish the first team's rows; MBB ordersactivelast. Same columns, order and null handling as sdv-py; Int32 columns are numbers, except the id columns (game_id,team_id,athlete_id,opponent_team_id), which are decimal strings like the release loaders return;game_date_timeandgame_dateare JSDates, exactly as the release loaders decode the published parquet (the instant, and the New York calendar date at UTC midnight), so producer rows and loaded rows join on the same values. A payload sdv-py skips returns[]. Parity: every helper is compared cell by cell with sdv-py's output on 14 real captures (full games in all four leagues, plus archival, one-sided, scheduled and stat-less payloads) and 24 derived gate payloads (tools/parity/espn_basketball_box_oracle.py).
Release dataset loaders (323 load* functions)
#67
- New: one loader per entry of sdv-py's
releases.yaml(vendored verbatim), generated onto its league namespace as camelCase + snake alias, e.g.await sdv.cfb.loadCfbPbp({ seasons: 2024, columns: ['game_id', 'text', 'EPA', 'home_wp_before'] })/sdv.cfb.load_cfb_pbp(...): cfb 71, nba 41, mbb 34, wbb 34, wnba 34, mlb 32, nfl 29, nhl 27, pwhl 21 (newsdv.pwhlnamespace). They read the published SportsDataverse / nflverse parquet assets — play-by-play with EPA/WP, schedules, rosters, box scores, ratings, player value — and resolve to an array of plain row objects. Options:seasons(one or a list; integers or 4-digit year strings, anything else raisesSdvError),columns(read only those),format("rows"default, or"columns"→{ [column]: values[] }, ~4x lighter),maxCells,timeoutMs(default 5 min). - Size guard: before decoding each season, a loader adds its rows × columns (from the parquet footer) to a running total and checks it against
maxCells(seasons are downloaded and decoded one at a time, so at most one compressed download is held) — by default V8 heap limit / 100 for rows (≈45M cells on Node's default 4 GB heap) and / 30 forformat: "columns"— and throws a catchableSdvErrornamingcolumns,format: "columns"and--max-old-space-sizeinstead of crashing the process. A full row-object read of CFB play-by-play 2024 (163,567 × 506) needs ~5.4 GB of heap;format: "columns"loads it in ~1.7 GB, and the generated play-by-play examples passcolumns. - Seasons below a loader's floor raise
SeasonNotFoundErrorbefore any download; a season with no asset (HTTP 404) is skipped with a warning; any other failure raisesAssetFetchError. Multi-season results union columns, null-fill gaps and cast a column whose type changed between seasons to the common type (sdv-py'sdiagonal_relaxed: an integer id that became a string → strings,"123"never"123.0").{season + 1}assets (nba_stats) take the START year, as in sdv-py. The threeload_nba_stats_*_v3loaders are deprecated aliases (one-timeDeprecationWarning). - Id columns (
id,*_id,*_ids,*_pk) of integers are exact decimal strings whatever their stored width — ESPN play ids (idin CFB / MBB play-by-play, 18 digits) are beyond 2^53 — andid_int64columns are pinned to Int64 first, as sdv-py's_cast_ids_int64does (canonical integer strings convert; anything else is left untouched). Other INT64 columns are plainnumberwhen every value is a safe integer, elseBigIntwith one warning per column per process (see "integer id columns are decimal strings, everywhere" above). - Downloads go through the new keyless
releasestransport family (default retry statuses, 403 included). Parquet is decoded by the new runtime dependencieshyparquet+hyparquet-compressors(the assets are ZSTD from Polars and SNAPPY from arrow-cpp). Node only; loaders are not in the docs playground. Each league's reference gains a Dataset loaders page.
Odds market math (sdv.odds)
#74
sdv.oddsmarket math, ported from sdv-pywexp.market(pin 719de79):prob_from_american,prob_from_decimal,devig_multiplicative,devig_shin,spread_to_prob,logit_blend,moneyline_pair_prob(snake_case + camelCase). Same formulas, edge cases and error types as Python; parity-tested against a committed sdv-py oracle over real The Odds API h2h rows.
247Sports via sdv.sports247 (supersedes recruiting)
#68
- New namespace
sdv.sports247with 47 wrappers vendored from sdv-py at the pin.sports247(12) covers the 247Sports Recruit Database onipa.247sports.com: recruits, transfers, coaches, institution rankings, the ranking feeds, target predictions, positions, sport years and tag autocomplete.sports247_site_pages(35) covers the247sports.com/*.jsonpage models: player, recruitment, season, coach, institution, league and event. Parsersparse_sports247_result_setandparse_sports247_site_pageare faithful ports of sdv-py's. sdv-py's returns tables are attached forsports247only. The site-page tables are left off because sdv-py's schemas at the pin type many numeric columns ascharacter, and a test checks the column types of every attached table against the real captures. sports247mints the free 247Sports guest JWT for you (GET https://247sports.com/sets aJWTcookie, valid about 12 h). The registeredtokenAuthcaches it, re-mints it a minute before itsexpand once after a401, andsports247ClearTokenCache()drops it. If the mint fails, the request goes out without a token, as in sdv-py, and one warning is emitted per process. Public routes still answer; routes that need the token fail withAssetFetchError. Neither 247 family retries a403.- Both hosts block plain HTTP clients at the TLS layer, so both families default to the browser-impersonating transport, which needs the optional
impitdependency (npm install impit). Without it, calls reject withTransportUnavailableError. The families are kept off the docs playground and its proxy allowlist. - The 13 RDB routes that need a logged-in 247Sports session are not wrapped, as in sdv-py.
Cricket in-play win probability / WPA (sdv.cricket)
#71
sdv.cricket.cricket_win_probability/cricket_match_state/cricket_expected_runs/cricket_wpa(+ camelCase): faithful port of sdv-py's cricket in-play win probability (resource surface + isotonic calibration, bundled asdist/models/data/cricket_wp_tables.json). Pure/offline; parity-tested to 1e-12 against sdv-py outputs on real ESPN + Cricsheet states.
Subscription families: PFF Developer API, KenPom, NFL Pro
#70
Vendored from sportsdataverse-py (719de79). Each needs the caller's own paid credentials, has its own runtime, never retries a 403, and is left out of the docs playground and its proxy allowlist. See the "Subscription families" section of the Transport & auth guide.
- PFF Developer API —
sdv.nfl.pffApi*(68 wrappers,api.pff.com). Bearer key fromheaders.Authorization>api_key>SDV_PFF_API_KEY>PFF_API_KEY.400/422throw the newInvalidParameterErrorwith PFF's message;404→NoDataError;401/403/429/5xxand non-object200bodies →AssetFetchError. Columns withheld by entitlement (restricted) warn, or throw withstrict: true/SDV_PFF_STRICT=1./v1query keys stay snake_case as the spec. Parsersparse_pff_report/parse_pff_player_detail/parse_pff_v2_tableare ports of sdv-py's and match its output on PFF's published examples. - KenPom —
sdv.mbb.kenpom*(30 wrappers,kenpom.com). Password login (email/password, orKENPOM_EMAIL/KENPOM_PW, hoopR'sKP_USER/KP_PW), session reused for 30 minutes; a rejected login throws. The default transport for this family is the browser-impersonating one (kenpom.com's Cloudflare check answers403to Node's TLS fingerprint), so it needs the optionalimpit.{ parsed: true }returns every table on the page keyed by HTML id; the parser ports sdv-py'sparse_kenpom_pageand thepandas.read_htmlheader rules it relies on, and matches sdv-py on the committed fixture and on eight live pages. The parser is node-only (cheerio), so it is registered by the runtime rather than in the browser parser bundle (NODE_ONLY_PARSERS). - NFL Pro —
sdv.nfl.nflPro*(16 wrappers,pro.nfl.com). User-bound bearer fromheaders.Authorization>token>NFLPRO_TOKEN, checked for an activeNFL_PLUS_*plan and expiry first (NflProAuthError). Pages onoffsetuntil the envelope'stotal; a capped result is flagged_truncated. An empty200(how the API rejects a parameter) →InvalidParameterError. With no token it logs in withemail/password(see "NFL Pro email/password login"). - Core:
registerFamilyDefaults(family, { classifyError })lets a family map a final failed response to its own error (404 staysNoDataError). New exports:InvalidParameterError,NflProAuthError,nflProToken,resolvePffApiKey,hasKenpomLogin,kenpomLogin,kenpomClearSessionCache,parse_kenpom_page,NODE_ONLY_PARSERS, and theFlatParserFn/ParsedTablestypes (a flat parser may return a dict of tables, as for KenPom pages and PFF's multi-table bodies). - Live tests:
SDV_PFF_LIVE=1,SDV_KENPOM_LIVE=1,SDV_NFL_PRO_LIVE=1, each also skipped without its credentials. section(theMULTI_TABLE_SECTIONSmechanism, extended):parse_pff_v2_table'rows'(default) /'teamTotals',parse_pff_player_detail'weeks'(default) /'career', and for the dict-defaultparse_pff_report/parse_kenpom_page, one table by name (a PFF report or dict key, a KenPom table id). An unknown name throws, listing the valid ones.MULTI_TABLE_SECTIONSentries may now havedefault: null(sdv-py's dict of tables) andsections: null(payload-named tables, described bydynamic). The reference docs and JSDoc showsectionfor every multi-table wrapper.- Public TS type change:
PARSERS/parserForare now typedFlatParserFn((raw, section?) => rows | ParsedTables), since a flat parser may return a dict of tables. Code that calledparserFor(...)(raw).map(...)must narrow the result first. sportsdataverse/parsersexportsparse_pff_report/parse_pff_player_detail/parse_pff_v2_table/parse_pff_matrix/parse_nfl_pro_statsandparse_kenpom_page. The playground bundle is now built fromsrc/parsers/browser.ts, which excludes the node-only KenPom parser.- KenPom: explicit-credential sessions are keyed by an HMAC of e-mail + password under a random per-process key (never the plaintext or an unsalted hash), cached only after a successful login, and capped at 8, so a corrected password now works. Concurrent calls for one account share a single login, and a request in flight keeps its session even if a ninth account evicts it from the cache. A page that comes back logged out refreshes the session it used once, then throws
AssetFetchError; it is never returned as data. A401refreshes the session the request actually used (AuthContext.requestis passed torefresh). - NFL Pro: boolean query params are sent as sdv-py's
requestssends them,"True"/"False"(unverified live).
HockeyTech analytics: shifts, time on ice, on-ice tracking, Corsi/Fenwick
#73
Port of sdv-py's hockeytech/_analytics.py (+ parse_shifts / parse_pbp) at the existing pin (719de79), in src/analytics/hockeytech.ts (pure) and hockeytech_family.ts (fetch).
- Every HockeyTech league gets
<lg>_game_shifts(gameId),<lg>_player_toi(gameId),<lg>_game_corsi(gameId)and py's public<lg>_pbp(gameId)(fully enriched play-by-play) onsdv.hockeytech(camelCase aliases too, e.g.sdv.hockeytech.pwhlGameCorsi(42)), plus league-parameterisedhockeytech_shift_stints,hockeytech_enriched_pbp,hockeytech_player_toi,hockeytech_game_corsitaking{ league, game_id }.hockeytech_game_shiftsstays the raw-feed flat wrapper. - Pure building blocks in
dist/analytics/hockeytech.js:parse_shifts,parse_pbp,enrich_pbp(game meta, coordinate transforms, clock, power-play back-fill, shot geometry, on-ice),build_on_ice,add_strength_state,corsi_fenwick,corsi_fenwick_on_ice,player_toi,game_corsi_rows, and the rest of py's_analytics. - A 200 body with no recognisable pbp / shift / game-summary structure (unparseable, error sentinel) throws
AssetFetchError; only a present-but-empty structure gives[]. HockeyTech's plain-textFeed type access denied.reply (e.g. MJHLgc/gamesummary) is recognised as "no access" and treated as empty, as in sdv-py. NewhockeytechGetTextreturns the raw feed text;hockeytechGetis unchanged. - Corsi/Fenwick are proxies: the feed has no missed-shot event, so attempts = shot + blocked_shot + goal (
corsi_includes_missed = false). - Checked cell by cell against sdv-py's own output (
test/fixtures/hockeytech/analytics/oracle.json, generated bytools/oracle/hockeytech_analytics_oracle.py) on the real PWHL game 42 / OHL game 27225 captures plus hand-built edge frames. Deliberate differences:enrich_pbpnever fetches (pass the payloads), group-by output order is first-seen, and an empty pbp returns[]where py raises.
Discovery + name lookup (sdv-py discover.py / find.py)
#72
listFunctions/functionCount(index of every wrapper per namespace,search,parsersOnly,wrappersOnly) andfindTeam/findAthlete/findEvent/clearTeamCache(ESPN name -> id via teams, rosters, scoreboard). Built over the live registries, so generated/renamed wrappers appear automatically. py snake_case aliases exported too.
Keyless sdv-py families: On3, ASA, MLS, NWSL, women's T-Rank, ESPN FPI
#66
Vendored from sdv-py at the existing pin (719de79); no key or login for any of them.
sdv.on3.*(78 endpoints, On3 Recruit Databaseapi.on3.com/public/rdb),sdv.asa.*(15, American Soccer Analysis,league_slug= mls/nwsl/uslc/usl1/mlsnp),sdv.mls.mls_*(12, the three mlssoccer.com hosts) andsdv.nwsl.nwsl_*(9, StatsPerform SDP; sdv-py's names, v4). MLS and NWSL send the siteRefererplus a browser User-Agent; NWSL composite ids (nwsl::Football_Season::<hex>) go on the wire with the::unencoded. The four deprecated On3_next/datascrape shims are not ported.sdv.torvik.bart_wbb_ratings(women's T-Rank,barttorvik.com/ncaaw).espn_<league>_fpion every league: ESPN's resolved Football Power Index table (site.web.api.espn.com/apis/fitt/v3, a fifth ESPN host family), parsed byparse_fpi.- Parsers are ports of sdv-py's, checked cell by cell against sdv-py's own output on real captures. Multi-table parsers (ASA goals-added, MLS standings and match, NWSL lineups) return one sub-frame under
parsed: true— the one sdv-py's returns schema documents (ASAsummary, MLSentries/match_information, NWSLplayers) — andsection: "<name>"selects any other (unknown name throws, listing the valid ones; values are in the generated reference). Every sub-frame at once:parse_asa_goals_added_tables,parse_mls_standings_tables,parse_mls_match_tables,parse_nwsl_lineups_tablesfromsportsdataverse/parsers. - A 2xx non-JSON body (HTML bot block) from these JSON hosts is an
AssetFetchError, not an empty result; a genuinely empty[]/{}is data. - Playground proxy: the new hosts (and MLS's per-endpoint
sportapi/dapihosts) are allowlisted; the proxy supplies the MLS / NWSLReferer.
stats.nba.com / stats.wnba.com (nba_stats 128, wnba_stats 111)
#65
- Vendored from sdv-py@719de79 onto
sdv.nba.nba_stats_<slug>(e.g.nba_stats_leaguedashplayerstats,nba_stats_playercareerstats) andsdv.wnba.wnba_stats_<slug>;league_idselects"00"NBA,"20"G-League,"15"Summer League (WNBA"10").{ parsed: true }runs the portedparse_nba_stats_result_sets(single set -> rows, several ->{ setName: rows }, empty/malformed ->[]). - Needs a TLS-impersonating transport. stats.nba.com fingerprint-blocks plain clients (the request hangs instead of failing) and hangs on datacenter/cloud IPs. Both families default to
createImpersonatingTransport({ browser: "chrome" })(optional peerimpit:npm install impit) with the sdv-py stats headers, and 403 is never retried. Withoutimpit, calls reject withTransportUnavailableErrorexplaining the install and the datacenter-IP caveat. A timeout, blank body or bare{}is a failure (AssetFetchError), never "no data". - Not on the docs playground proxy. Live tests use their own gate
SDV_NBA_STATS_LIVE=1(never set in CI).
HockeyTech: 15 more leagues, season-id helpers, PWHL views
#60
- Registry now covers all 20 sdv-py leagues: added
echl,sphl,chl,ushl,bchl,ajhl,sjhl,ojhl,cchl,gojhl,mhl,nojhl,vijhl,kijhl,mjhl(keys/ids copied from sdv-py;SDV_<LEAGUE>_API_KEYoverrides). Registry entries also carrypbpStyleandotPeriodLength. Caveats:ushlPBP has no coordinates;mjhl's public key has no gamecenter access (pbp/game_summary come back empty). - New wrappers (every league):
hockeytech_scorebar,_player_game_log,_player_search,_stats,_transactions,_playoff_bracket. py'sstreaks,game_info,player_boxandplayer_infoare not ported (upstream view absent / non-functional in sdv-py). - Season helpers on
sdv.hockeytech:hockeytech_season_id(league),most_recent_hockeytech_season(league),hockeytech_resolve_season_id(league, {season|seasonId, gameType})(camelCase too). hockeytech_seasonsparsed rows gainseason_yrandgame_type_label;hockeytech_schedulenow defaults to the full-history window (numberofdaysback/ahead/limit= 10000, as sdv-py).- Season ids:
hockeytech_schedule/hockeytech_playoff_brackettake a rawseason_id(py accepts an end-yearseason=). Resolve first withhockeytech_resolve_season_id(league, { season }); brackets needgameType: 'playoffs'. The views that sdv-py sendsleague_idon (schedule/scorebar, standings, transactions, brackets) now send the league's registryleagueId(overridable vialeague_id).
HockeyTech and BartTorvik families
#49
3.1.0 was never published to npm; its changes ship in 4.0.0.
Two standalone provider namespaces join the flat-API surface. Both expose
the usual dual-case names (snake_case + camelCase), accept { parsed: true } for
tidy rows, and ship fully column-described returns tables.
- HockeyTech / LeagueStat (
sdv.hockeytech.*, 10 endpoints) — a league-parameterized women's + junior hockey family covering the PWHL plus the CHL juniors (AHL / OHL / WHL / QMJHL); pick the league with aleagueslug (e.g.sdv.hockeytech.hockeytech_schedule({ league: 'pwhl' })). Endpoints:seasons,schedule,teams,team_roster,player_stats,game_shifts,standings,leaders,pbp,game_summary. Hosts:lscluster.hockeytech.com(andcluster.leaguestat.comfor QMJHL). Thesrc/core/hockeytech_runtime.tsgetter unwraps the JSONP envelope (angular.callbacks._N({…})) beforeJSON.parse, switches thegcfeed to itstab=form (notview=), applies the PWHL play-by-play key override, and injects each league'sclient_code/key/site_idfrom a per-league registry (overridable viaSDV_<LEAGUE>_API_KEY). Ported fromfastRhockey'shockeytech_*andsportsdataverse-py'ssportsdataverse/hockeytech/. - BartTorvik / T-Rank (
sdv.torvik.*, 5 endpoints) — men's college-basketball advanced analytics (ratings,team_factors,game_stats,player_stats,game_schedule) frombarttorvik.com. Thesrc/core/torvik_runtime.tsgetter sends a browser-like User-Agent (barttorvik rejects default programmatic UAs) and returns the raw response body so each parser can branch on format — CSV via papaparse, orJSON.parseof the headerless positional column arrays (31 / 55 / 67 fields, ported verbatim fromhoopR'storvik_*.R).
A flat family that needs non-JSON bodies or custom request shaping registers its
own getter runtime in GETTER_OVERRIDES (src/leagues/_make_flat.ts) — the same
pattern as the existing Statcast getter — so the shared no-auth getter stays
JSON-only.
Playground caveat. QMJHL's secondary host (
cluster.leaguestat.com) works from the library, but not from the in-browser playground: the/api/runproxy allowlist derives a single host per family, so QMJHL calls there hit the primary host. Use the library directly for QMJHL.
Returns-table descriptions, utilities catalogue, examples and tutorials
- Returns-table column descriptions. Every returns table (ESPN, native families, loaders)
resolves its blank
descriptioncells the way sdv-py's generator does: the schema's own text, then sdv-py's hand-curatedmanual_column_descriptions.yaml(by schema key, then_global), then the column descriptions mined from the SDV R packages (r_column_descriptions.yaml: the league's own-sport packages only — see "Column descriptions are sport-strict" below). Both files are vendored verbatim at the sdv-py pin (vendor.yamlcopy:, in the LOCK).npm run codegenprintsdescriptions <family>: <filled>/<total>and writesdocs/src/generated/description_coverage.json; overall fill went from 26% to 95.8% (stats.nba.com / stats.wnba.com / MLB / the four NHL families from 0%). The generated row interfaces carry the descriptions as doc comments. (#107) - Loader returns tables + row types in the docs. Every
load*block documents its columns (col_name | type | description, the type being its generated row type's) and names itsLoad<Name>Row; ESPN and native blocks name their verified row interface, or say Rows are untypedRow[](not parity-verified yet). ESPN endpoints with no fixed table list the leagues that expose them and link the parser's section of the parsed-returns page. (#107) - Utilities catalogue.
tools/codegen/utilities.yamllabels every hand-written non-data export (parsers, analytics, odds, models, producers, discovery, transforms, HTTP core, errors, config); codegen rendersdocs/docs/utilities/,src/generated/utilities.tsand a "Utilities" sidebar group.listFunctions(…, { detail: true })returns{ name, kind: 'data' | 'utility', category }rows (FunctionEntry);UTILITY_CATEGORIES/UtilityCategoryare exported. (#107) - Typed TSDoc on every generated wrapper: every
@param params.xcarries its TypeScript type and a description (the YAML description where sdv-py wrote one, else what the param is),@returns Promise<…>naming the row interface, and@seelinking the docs page. The TypeDoc API pages now cover the hand-written modules (parsers, analytics, odds, models, producers, HTTP core) besides the four basketball ESPN samples. (#107) - Breaking-change callouts.
tools/codegen/breaking.yamlrecords every 4.0.0 breaking change by surface; each affected generated page opens with a:::danger Breaking in 4.0.0admonition linking the changelog, andreference/deprecations.mdcarries the "Breaking changes by version" table. (#107) - "How this library is built" (
docs/docs/architecture/): per surface (ESPN vendored, native vendored, native JS-owned, loaders, hand-written) — source of truth, generator step, output, CI gate, how to change it, and a codegen-rewritten status block (counts + the sdv-py pin). Every generated docs page ends with a visible provenance footer linking its page (the hidden MDX comment is gone). (#107) - Capture-derived returns schemas for fox / cbs / yahoo / yahoo_scores.
tools/codegen/regen-capture-schemas.mjs(npm run schemas:captures,--checkgate) rewrites the JS-owned returns schemas of the four families from the registered parsers' rows on the committed real captures (the parity manifest's cbs / fox / yahoo entries; yahoo_scores reads the editorial captures), so the tables describe the 4.0.0 per-entity row builders (fox 13 endpoints, cbs 7, yahooseason_stats_football_passing_ncaaf, yahoo_scoresboxscore33 columns /scoreboard104). The 18 fox and 24 yahoo schemas written for the pre-port output with no capture are markedunverified(the docs render the note, not the table).test/parsers/capture-schema-agreement.test.jsholds every one to its parser: the column set is the rows' key union, every value is of its column's type, ids are decimal strings, and a capture-less endpoint publishes no table. (#107) - Column descriptions are sport-strict. The cross-sport
_mergedunion of the R-package descriptions is never read: a league resolves through its own sport's packages only (hoopR + wehoop for basketball, cfbfastR for cfb, the NFL packages for nfl, baseballr for baseball, fastRhockey for hockey); an unmapped namespace and the shared parsed-returns page get sdv-py's manual text only; the NFL aggregate families (nfl_api / nfl_pro / pff_api) get no play-level nflfastR text. Overall fill is 90.4% (honest; it read 95.8% with the leak), floor 0.90, with a denylist of sport-specific phrases asserted over the generated docs. (#107) - Breaking-change register: the Baseball Savant
parsed: truetyping change is on record (19 entries); a family's vendored / JS-owned label in the docs footer comes from vendor.mjs's# VENDOREDheader (kenpom is vendored; the yahoo page names both its families). (#107) - Runnable examples.
examples/holds 16 ESM scripts (NN_<topic>.mjs), one per surface (ESPN scoreboard / pbp / standings / rankings, Torvik, Statcast + MLB Stats, NHL api-web + EDGE, HockeyTech Corsi, soccer, The Odds API + odds math, release loaders, discovery, and three sdvplot-js integrations). They run offline by default:examples/_offline.mjsinstalls aconfigure({ transport })that servestest/fixtures/**by URL (an unrouted URL throws);SDV_LIVE=1hits the real hosts.node --import ./_resolve.mjs <script>resolvessportsdataversefrom the repo'sdist/(no install) and@sportsdataverse/*from a sibling sdvplot-js checkout (SDVPLOT_JS_DIR); a script that needs an unbuilt sdvplot-js printsskipped:and exits 0.test/examples.test.jsruns every script. (#108) - Tutorials.
docs/docs/tutorials/grows to 17 pages (+ an index with a sources column), one per example script: the script's source and its frozen output are injected by the output injector's newscriptfamily (inject:source/inject:examplemarkers; artifacts copied todocs/static/examples/) and drift-gated bynpm run docs:examples:check. The ESPN basketball shot-coordinate frame used by the shot-chart tutorial was fitted on four captures, one of them committed and held to it bytest/examples-shot-frame.test.js(coordinate_x= feet across, hoop at 25;coordinate_y= feet from the hoop, which fits at y ≈ 0–1; both teams on one basket; free throws carry a sentinel). New fixtures with provenance READMEs: WNBA 2025 / NFL 2024 / LaLiga 2024 standings, NFL 2024 week-1 scoreboard, one verbatim Odds API historical snapshot. (#108) - llms.txt.
docusaurus-plugin-llmsemitsllms.txt,llms-full.txtand a.mdcopy beside every docs page (TypeDoc'sdocs/api/**excluded from all three;test/docs-llms.test.jspins the config). (#108) - Live-editable code blocks.
@docusaurus/theme-live-codeblockwith a swizzledReactLiveScopeexposing the browser parser bundle,fetchViaProxy(the/api/runrequest body),resolve+endpoints,<Table/>and React; new guideguides/live-blocks.md. (#108) - Open in StackBlitz. An
OpenInStackBlitzbutton under every tutorial's script and in the quickstart opens the snippet as a Node project on StackBlitz (@stackblitz/sdk, loaded on click). The source mapdocs/src/generated/examples-source.jsonis written bytools/docs/examples-source.mjsas part ofnpm run docs:examples. (#108)
Changed
Version 4.0.0
#105
package.jsonis4.0.0. The default User-Agent isMozilla/5.0 (compatible; sportsdataverse-js/4.x)(was3.x;configure({ userAgent })still overrides it), andSECURITY.mdnames the 4.x line as the one receiving fixes.
sdv-py frames_by returns schemas
#96
- Codegen: sdv-py adds
frames_by(a request parameter name) to akind: framesreturns schema when the function returns ONE table, whose columns are the frame that parameter's value names (the pff_api/v2reports:position_reportandteam_reportbyreport,team_leadersbygroup,team_statsbycategory).vendoraccepts the key only onkind: frames, only as a non-empty string, and only when every endpoint that attaches the schema takes that parameter; anything else still fails closed. The reference docs render one column table per value ("Whengroupispassing") under a single-table heading instead of "an object of tables", and the wrapper's@returnssays the columns depend on the parameter. The parity harness counts every frame's columns but leaves these endpoints unverified (a capture records no request parameter to pick the frame by). The returns-table renderer moved totools/codegen/returns-tables.mjs, so the tests run the real sdv-py schema through it. Unblocks the vendor sync (#88, #95).
Tooling, playground and docs cleanup
#92
- Exports:
DEPRECATED_NAME_CODEandDEPRECATED_ENDPOINT_CODEare exported from the package entry, so callers can filter the v4 rename and dead-routeDeprecationWarnings byw.codewithout copying the strings. - Playground: five Examples presets named pre-v4 family stems (
mlb_api,cbs_napi,fox_bifrost,yahoo_shangrila,yahoo_editorial) and selected nothing; they now usemlb,cbs,fox,yahooandyahoo_scores, and a test checks that every preset and guide cell resolves. The parser bundle is rebuilt (the HockeyTech scorebar parser was stale), and a test now compares it byte for byte with a freshnpm run bundle:parsers. from-openapi: a--hostwithout itshttps://scheme now fails with a message that names the fix, instead of a bareInvalid URL.- Oracle generators: every sdv-py oracle generator (
tools/parity,tools/oracle) runs one shared guard,tools/sdv_py_pin.py: the checkout must be at the pin, clean, and the onesportsdataverseis imported from. The cricket and odds generators had no pin check before. The cricket and HockeyTech generators now produce the same bytes on every run; their committed fixtures changed in key and row order only. The six keyless-parser oracles (On3, ASA, MLS, NWSL, FPI, women's T-Rank) now have a generator too,tools/parity/keyless_oracle.py, which reproduces them byte for byte. - Tests: a test scans every page Docusaurus compiles for braces and tags that MDX would parse (it reports file:line), so a broken page fails locally and not only on Vercel. Warn-once warnings share one registry that tests can reset, and tests that trigger a deprecation on purpose capture the warning instead of printing it.
Vendor tooling + CI hardening
#89
fetchWithRetry: each attempt gets its ownAbortSignal.timeout(30 s), and the body is read inside the attempt, so a hung socket or a mid-body reset is retried (error names the URL) and cannot stall a job. Worst case per URL: 3 x 30 s plus 1.5 s of backoff.raw.githubusercontent.comfile fetches retry exactly like the API ones.- BOM: upstream text is decoded by one function for a fresh fetch and a committed copy, so a leading BOM can never make the two diverge. The bytes on disk (and in LOCK) stay verbatim.
- Overlays: an overlay addition whose
pathduplicates a vendored endpoint (or an earlier addition) now throws. On a pin bump,vendorwarns when a whole-key overlay patch replaces a key whose upstream value also changed in that bump (the change is masked). vendor.yaml:py_reservedgainsespn_nba_pbp,espn_wnba_pbp,espn_mbb_pbp,espn_wbb_pbp(hand-written in sdv-py); a test checks no generated ESPN wrapper takes one.- Workflows:
timeout-minuteson every CI, vendor-sync and live-smoke job. The vendor and live steps have their own step timeouts, so a hang reaches the issue step. Live smoke serialises runs and files a separatelive-tests:build-failureissue when install or build fails (only a failing live step is "drift"). The vendor-sync failure issue names the resolved sdv-py sha. - vendor-sync to CI: the sync PR is opened with
GITHUB_TOKEN, which starts nopull_requestrun.ci.ymlgainsworkflow_dispatch, and a separatedispatch-cijob (fresh VM, no checkout or npm, the only place withactions: write) runsgh workflow run ci.yml --ref chore/vendor-syncafter the PR is created or updated. The workflow-wide permissions are now empty; thesyncjob holds contents / pull-requests / issues write only, so its token cannot dispatch other workflows (for example a publish). - Masked overlay patches: the check compares the raw upstream value (taken before the schema policy runs) and the warning shows old -> new; it lands in the sync PR body and the step summary. The duplicate-path check keys on host + path + params.
- Tests: the token-scoping check allows a token or secret expression only in a step
env(any other place, includingtoJSON(secrets),secrets[...],secrets: inheritand container credentials, fails, proven on mutated workflow clones); the failed-fetch test runs on a temp copy (SDV_VENDOR_ROOT), never the real codegen dir; the pipefail semantics test no longer spawns a barebashon Windows (WSL); the pruned-file check asserts the file is gone instead of reading a spawned process status; the skipped-family pre-bump message is covered. - Docs:
@param params.parsedon the 130kind: framesflat endpoints now says it returns an object of tables keyed by result set (matching@returns).FLAT_FAMILY_HOSTSin the generator is built once and frozen.
Vendor pin 81eb7e7060: ESPN CDN, Fox, stats/On3 returns tables, on_missing
#85
-
Pin: the vendored sdv-py ref moves from
719de79to81eb7e7060. Two JS overlays the upstream absorbed are gone:- The MLB
pbptimecodepatch (sdv-py #679). - The torvik
game_stats/player_stats/game_scheduleadditions. They are vendored now (sdv-py #678). Names are unchanged, and JS keeps its hoopR-ported parsers and its own returns tables.
- The MLB
-
Added — ESPN CDN (
cdn.espn.com/core): 5 endpoints become 41 wrappers on 14 leagues:espn_<lg>_cdn_playbyplay,_cdn_boxscore,_cdn_schedule,_cdn_scoreboard, andespn_cfb_cdn_rankings.- The generator honours sdv-py's per-endpoint
include_prefixes(a probed league allowlist) and the family'sfixed_params(xhr=1, which a caller param can override). - Ported parsers:
parse_cdn_gameruns the summary dispatcher and takessection.parse_cdn_scoreboardandparse_cdn_schedulereturn scoreboard rows.parse_cdn_rankingsreturns a stringteam_id.
- The generator honours sdv-py's per-endpoint
-
Fixed — legacy
getScheduledates:sdv.{nba,wnba,nhl,mlb,mbb,wbb,cfb,nfl}.getSchedule({ year, month, day })sentdates=. The CDN ignores that key and answered with today's page for every date.- The methods now route through
espn_<lg>_cdn_schedule, which sendsdate=. The signature and the return shape (content.schedule) are unchanged, and a failed fetch throwsAssetFetchError. - The other legacy CDN methods (
getPlayByPlay,getBoxScoreon nba / wnba / mbb / wbb / mlb / cfb / nfl,sdv.cfb.getRankings,sdv.nfl.getWeeklySchedule) also route through the vendoredespn_<lg>_cdn_*wrappers instead of raw axios overhttp://: https, the same signatures and return shapes, and the core error vocabulary.
- The methods now route through
-
Fixed — ESPN JSON families: a 2xx response whose body is not a JSON object / array (the CDN's HTTP 202 HTML bot challenge, an error page, an empty body) now throws
AssetFetchErrorinstead of passing through as data. Before, a parsed CDN wrapper returned[](a failed fetch that looked like no data) and the legacygetSchedulethrew aTypeError.- Football pages ignore dates, so
cfb/nfltake an optionalweek. A date without aweekwarns once (SDV_CDN_FOOTBALL_DATE).
- Football pages ignore dates, so
-
BREAKING — Fox:
sdv.foxis now vendored from sdv-py'sfox_api; see Fox is vendored from sdv-py'sfox_apiunder BREAKING above. -
Codegen: an endpoint-level
fixed_paramson a flat family is honoured (sent first, overridable), as sdv-py's spec allows; a family-level one on a flat YAML fails codegen (sdv-py merges it only for ESPN families).kind: framesendpoints document theirparsed: truereturn as an object of tables keyed by result set. -
Loaders —
on_missing: raise: the 25 seasonal NFL loaders that sdv-py hand-writes (pbp, rosters, the usage/tendency tables, …) now throwNoDataErrorfor a season with no published asset instead of skipping it. A failed fetch is stillAssetFetchError. -
Returns tables: sdv-py now derives these from parser output, so they document their columns again:
family tables documented 247Sports site pages 35 nba_stats126 of 128 wnba_stats110 of 111 On3 23 of 78 (48 are unverifiedupstream)- A
kind: framesschema renders one table per frame. Anunverifiedschema prints its reason. The vendor fails closed on a schema shape it does not know. - Kept off, because the parity harness disproves them on real captures:
nba_stats.leaguedashptstats: the table documents the default measure type, and the 7 other tracking measure types return other columns.- 7 On3 tables: sdv-py stringifies a bool column that contains a null, while JS keeps booleans.
- A
-
Parity harness: 259 more sdv-py captures (124 NBA, 109 WNBA, 26 On3), with
kind: frameschecked frame by frame. Verified endpoints go from 179 to 438.
Package checks: attw + publint on the packed tarball, API Extractor reports
#83
- New CI gates (Node 20 and 22).
npm run pack:checkpacks the package withnpm packand runs@arethetypeswrong/cliandpublint --stricton that tarball.npm run api:checkfails when the public API indist/*.d.tsno longer matches the committed API Extractor reports,etc/sportsdataverse.api.md(package root) andetc/sportsdataverse-parsers.api.md(sportsdataverse/parsers). After an intended API change, runnpm run api:reportand commit the updated report, so the change shows up in the PR diff. Generated wrappers are not listed one by one in the report (the default export is typedRecord<string, Record<string, any>>); they are reviewed throughsrc/generated/**and the codegen drift gate. - Fixed:
sportsdataverse/parsersnow resolves its types under TypeScriptmoduleResolution: "node"/"node10"(newtypesVersionsentry; the package root already resolved). Theexportsmap is unchanged. package.jsonnow says"sideEffects": true. Importing the package registers per-family transport defaults and the KenPom parser, so a bundler must not drop those imports. Behavior is unchanged (a missing field already meanttrue).- The package stays ESM-only. attw's
cjs-resolves-to-esmrule is ignored on purpose: CommonJS callers useawait import('sportsdataverse'), orrequire()on Node 20.19+ / 22.12+. - New devDependencies only:
@arethetypeswrong/cli,publint,@microsoft/api-extractor.npm auditis clean.
Vendor LOCK online integrity check + vendor-sync hardening
#78
- New:
npm run vendor:check:online(CI and the weekly vendor-sync) checksvendor/upstream/LOCKagainst sdv-py'sgit/trees/<ref>at the pinned ref: every blob sha must match, and LOCK's path set must equal exactly what the vendor fetches (computed by the same path-selection helpernpm run vendoruses), so editing a copy together with its LOCK line, or dropping a LOCK line and hand-editing the vendored copy, both fail. A network failure is a failure to verify, never a pass; 5xx/network errors are retried (3 attempts), 403/404 are not. - vendor-sync:
GITHUB_TOKENis exposed to the vendor step only (codegen runs in its own step without it); a ref/vendor/codegen failure opens or updates a "vendor-sync failed" issue naming the failing step (the run still goes red) instead of only failing. - Pin bumps:
npm run vendor(notvendor:check) now removes a py schema copy that the new pin renamed or dropped (byte-identical to the old upstream, not referenced by any endpoint; JS-authored files are never touched). It derives the outgoing outputs per family, and names any family whose prune was skipped. - The vendor tests no longer flake against their timeout: endpoint-YAML parsing (the slow path, repeated per temp-tree copy) is memoized.
CI
#59, 5447b6b1
- New weekly
live-smoke.ymlruns theSDV_LIVE=1suite and opens/updates onelive-tests:driftissue on failure;npm-publish.ymlnow runsnpm testbefore publishing. - Every GitHub Action still declaring
using: node20is on its Node 24 major (actions/checkout@v5,setup-node@v5,upload-artifact@v7,github-script@v9,cache@v5, …); SHA-pinned steps are repinned to the new major's SHA, not loosened to a tag.
Written, documented generated source; bounded TypeDoc
#51, #53, #54
- Every ESPN league is emitted as written, TSDoc-documented source (
src/generated/espn/<league>.ts+ a barrel) instead of runtime-materialised wrappers, so TypeDoc and IDEs see every function; the flat-API families likewise (src/generated/flat/<family>.tsover a sharedcallFlatcore).makeLeagueModule/makeFlatModulebecome fallbacks. The reference docs take sdv-py's per-function layout (sport → league category → one page per family, one block per function). TypeDoc over the whole package OOMs, so the site builds a bounded sample fromdocs/typedoc.tsconfig.json. A.gitattributesLF rule makes the drift gates deterministic on Windows.
Vendor sync to sdv-py afafaedae4
#104
- The vendored sdv-py ref moves to
afafaedae4, the pin 4.0.0 ships with (the weeklyvendor-sync.ymlPR).
Vendor sync to sdv-py 89c638a6: NFL Pro / On3 / Fox column descriptions
#110
- The vendored sdv-py ref moves from
afafaedae4to89c638a6(sdv-py #700 to #715). It brings sdv-py #714'smanual_column_descriptions.yaml(the NFL Pro, On3 andfox_api_<short>blocks plus five single columns), so the generated returns tables now describe those families. Description fill 89.9% to 93.3% overall (57,520/63,987 to 59,707/64,000 cells):nfl_pro0/1,036 to 1,006/1,036 (97.1%),on3152/1,024 to 1,024/1,024,fox18/193 to 193/193;asa,nba_stats,wnba_statsandpff_apigain cells too.r_column_descriptions.yamlis unchanged.test/codegen-descriptions.test.jsraises the overall fill floor from 0.89 to 0.93 (the measured 93.3%, rounded down); the wrong-sport denylist still passes on the sport-scoped text. - ASA (sdv-py #715's codegen run): new
asa_players_xpasswrapper (/{league_slug}/players/xpass, with its returns schema) and aleague_slugparameter description on every ASA endpoint. The API report (etc/sportsdataverse.api.md),test/fixtures/py/parity_coverage.json(917 py tables, 617 documented, 438 verified) and the documented wrapper count (1,060 flat-API wrappers across 27 families) follow.
Parser-parity harness: returns tables verified on real captures
#75
- New
test/parsers/parity.test.jsruns the vendored parsers on sdv-py's real committed captures (216, listed intest/fixtures/py/manifest.yaml; provenance intest/fixtures/py/README.md) and checks each endpoint's py returns table against what the JS parser returns: every documented column present, each value's JS type matching the documented type, every value's type matching sdv-py's own dtype, and cells equal to sdv-py's own output on the same bytes (oracle regenerated bytools/parity/py_oracle.pyat the vendor pin; join keys incl.*_pkand MLBAM ids compare strictly). 179 of the 239 endpoints carrying a py returns table are verified, including all of MLB Stats, NHL api-web / EDGE / Records / Stats REST and the 247Sports RDB. test/fixtures/py/parity_coverage.json(drift-checked) splits all 825 vendored py returns tables per family into documented / no_schema / incompatible / overridden / overlay_schema / undeclared (asserted to sum), and lists per verified endpoint the schema columns its captures exercise; 117 columns on 25 verified endpoints are null in every capture (unexercised, type unchecked) and must be typedunknown. Generated row types key on this file.- Returns tables that fail on real captures are dropped (
tools/codegen/vendor.yaml, evidence inline; the parsers still match sdv-py cell for cell).nba_stats,wnba_statsandon3wholesale (schema_compatible: false): sdv-py builds the stats.nba.com tables from its endpoint catalog rather than the parser (fg3m/leagueidvs the parsedfg3_m/league_id, multi-result-set payloads) and the On3 ones from other captures; every table the harness could check failed (11/13 NBA, 8/8 WNBA, 3/3 On3). They return when sdv-py regenerates those schemas from parser output (a later pin bump, gated by the harness). Per endpoint, via the newschema_incompatiblekey: NWSLstandings, Statcastgamefeed/leaderboard_catcher_stance. npm run vendor/vendor:checkdelete / flag a py schema copy the vendor wrote and no longer attaches by its exact path (byte-identical to the upstream copy; this removed the copies of the three flipped families and a stalenative/bart_wbb/ratings.yaml); a JS-authored schema is never touched, wherever it lives. A hand-added file in a vendored directory is no longer flagged.
Provider method naming: vendor codenames dropped (every 3.0.0 name still works)
#52
The provider methods no longer carry internal vendor API codenames. The namespaces
(sdv.fox / sdv.cbs / sdv.yahoo / sdv.mlb / sdv.recruiting) are unchanged.
The method prefixes foxBifrost*, cbsNapi*, yahooShangrila*, mlbApi* and
sports247* (on sdv.recruiting) take their v4 names, listed in the 4.0.0 naming
entry above. Each 3.0.0 name is kept as a deprecated alias of the same endpoint, so
this is not a removal. yahooEditorial* keeps its 3.0.0 name.
The codegen api stems, returns-schema paths, parser names, playground endpoint ids
and reference docs follow the new names. Upstream URL paths that contain the vendor
codename (Fox's /bifrost/v1/…, Yahoo's /v1/query/shangrila/…) are the real
endpoints and are unchanged. odds_api, mlb_statcast, the four nhl_* families,
hockeytech and torvik are real product names and were left as they are.
Shared endpoint YAML is vendored from sportsdataverse-py
#64
The shared codegen families (espn_site_v2, espn_core_v2, espn_web_v3,
leagues, mlb_statcast, nfl_api, the four nhl_*, mlb, torvik, cbs,
yahoo) and every returns schema they reference are now derived from a pinned
sportsdataverse-py commit by tools/codegen/vendor.mjs (npm run vendor), with
JS-only endpoints in tools/codegen/overlay/. npm run vendor:check (CI) fails
on a hand-edit; a weekly vendor-sync.yml PR bumps the pin. The first re-vendor
(sdv-py 719de79edb) converges the 127 shared endpoint definitions that had
drifted since the 2026-06 fork:
- New wrappers: ESPN Core v2
season_week_powerindex(every league) and NCAArecruiting_years/recruiting_athletes/recruiting_rankings;sdv.nfl.nflApi{GameDetailsBySlug,GameDetailsV2,LiveTeamStatistics,LivePlayerStatistics};sdv.yahoo.yahooEditorial{Boxscore,Scoreboard}(on the editorial host — flat endpoints may now carry their ownhost). Their py-only parsers fall back to the family's generic parser in JS. - New query defaults: ESPN
team_roster/transactions/season_group_childrensendlimit=500;awards/season_awards/positions/tournamentslimit=200;teams_core/season_teams/season_recruitslimit=1000+page=1;venueslimit=1000;season_coacheslimit=500. ESPN otherwise pages these silently (CFB rosters were cut at 100).nflApiRostersgainsteam_id. sdv.yahoo.*(stats graph): new host, no locale params, noleaguedefault — BREAKING; see Yahoo stats graph: no locale params, noleaguedefault under BREAKING above.sdv.cbs.*: hosthttps://api.cbssports.com/napi(what sdv-py ships).- Param transforms:
sdv.nhl.nhlEdge*/nhlApiWeb*accept a 4-digit season and send the 8-digit form (season: 2025→20242025, sdv-py'sformat_nhl_season; an unrecognized season throws).nflApi*include_*flags and ESPNathletes_indexactiveare sent as"true"/"false". The codegen fails on a transform the runtime doesn't implement. - 75 returns-schema files arrive from sdv-py (NHL Records, NHL EDGE, NHL Stats REST, NHL api-web, MLB Stats, nfl_api). A py schema is attached only where vendor.yaml declares the JS parser equivalent to sdv-py's (fail-closed); CBS, Yahoo, torvik, ESPN and the endpoints on a different or fallback parser keep JS's own schema or show none.
Docs-site overhaul
#48, #50
A docs-site overhaul that makes the guides literate and the navigation data-driven:
- Sport-grouped reference sidebar. ESPN league reference docs are now nested
under a collapsible category named for their sport (plus a Providers group),
emitted by codegen into
docs/src/generated/reference-sidebar.jsand consumed bydocs/sidebars.js. It's drift-guarded (npm run codegen:check) and regroups automatically fromleagues.yaml— the reference pages stay flat (no URL change). - Nav/footer + sidebar Playground link. Docs / News / Tutorials / Playground appear in both the navbar and the footer, and a 🛝 Playground link sits near the top of the docs sidebar.
- Data-driven homepage.
docs/src/pages/index.jsmaps over the generatedendpoints.json(leagues by sport + provider families), so adding a sport/provider updates the home page onnpm run codegen— no bespoke edit. - Embeddable live RunCell guides.
<RunCell>is a compact single-endpoint live runner droppable inline in any.mdxguide: editable params → resolved URL → Run (via the/api/runproxy) → raw JSON or a parsed table. It handles every flat family, thesummarysection selector, enum dropdowns, and non-JSON Statcast CSV, and is SSR-safe. - Build-time output injector.
tools/docs/inject-outputs.mjs(+ the manifesttools/docs/examples.mjs) freezes real parsed tables into guides between<!-- inject:example:ID -->markers — deterministic (fixture-driven, no network).npm run docs:exampleswrites them;npm run docs:examples:checkis a CI drift gate. - Site search + analytics (#57):
@easyops-cn/docusaurus-search-localgives the site full-text search (it had none) and Plausible's per-site script replaces the legacy tag. - Data status link (#58): the README and the docs navbar link sportsdataverse.org/status.
- Rspack / SWC build (#76): the site builds with
@docusaurus/faster— the webpack client compile of the generated reference tree had reached Vercel's 8 GB ceiling. Only theremoveLegacyPostBuildHeadAttributev4 flag is enabled (fullfuture.v4breaks the guides' HTML comments). - Cheat sheet (2873d7d8): the README and the assistant instruction files link the printable sportsdataverse (Node.js) cheat sheet at sportsdataverse.org/cheatsheets.
Dependencies, coverage gate, docs search
- Dependencies: mocha 11 -> 12 and the root
overridesblock (js-yaml, serialize-javascript, diff — forced only for mocha 11) is gone; the resolved tree is at or above every forced floor. In-range bumps: papaparse 5.7.0 (the playground parser bundle is rebuilt for its BOM stripping), tabletojson 4.1.15, esbuild 0.28.2, typedoc 0.28.20, yaml 2.9.1, @microsoft/api-extractor 7.59.4, @types/node 22.20.5.npm audit --omit=devis clean; the 4 dev-only moderate findings aresprintf-jsunder api-extractor with no upstream fix.docs/: lockfile refreshed (compression, proxy-addr) andtinypooloverridden to^2.2.0under thefasterbuild — 44 advisories (16 critical) down to 41 (0 critical). (#106) - Coverage:
npm testruns mocha underc8(.c8rc.json:dist/**minusdist/generated/**, which is contract-tested by the codegen suites;text-summary+lcovintocoverage/). The measured baseline is the threshold — lines 94% / functions 95% / branches 84% (measured 94.82 / 95.91 / 84.58 on 4,374 tests) — and--check-coveragefails the run below it; CI uploadscoverage/lcov.infoas an artifact.npm run coverage:generatedre-reports the same run overdist/generated/**alone. (#106) - Docs search: the local search index no longer indexes the TypeDoc tree (
docs/api/**), −13% onsearch-index.json; the playground copy says what it covers (every native family, the Parsed toggle, share links) and links the tutorials and the live-blocks guide. (#108)
Deprecated
sdv.recruiting (api.247sports.com)
#68
- All 25
recruiting_*methods are@deprecated, becauseapi.247sports.comanswers HTTP 500. Each emits oneDeprecationWarningon its first call. Twelve name theirsports247_*replacement. The other 13 have none, since they need a logged-in 247Sports session. The methods still exist. Endpoint YAML can now mark any flat wrapperdeprecated:.
Other deprecations
#59
- The six stats.ncaa.org scrapers in
sdv.ncaa(getSports,getSeasons,getDivisions,getSportDivisionData,getPlayerData,getTeamData) are marked@deprecatedand emit a one-timeDeprecationWarning(the host 403s plain clients).ncaa.getScoreboardonly serves historical seasons.
Removed
- The unused
tsxdevDependency, anddecode-html: the one call site (ncaa.service.ts's<select>scraper) reads cheerio's already-decoded.text(), which is identical todecode($(el).html())on every entity the old helper handled (an offline test pins&amp;/'decoding;SECURITY.mdno longer lists it or the rootoverrides). (#106) - RunKit: the embedded RunKit notebooks (pinned to
sportsdataverse@^3) and theRunKitcomponent are gone; the quickstart, NHL and providers guides use the StackBlitz button beside their frozen output. (#108)
Fixed
Redaction of quoted credential values (sdv-py #692)
#103
redactSecrets, and so every error message andcause, follows sdv-py's pair grammar. A quoted value runs to its closing quote, sopassword="a b"no longer leaksb, and the,;)]}closing an unquoted value stays outside the redaction ((password=x), nextkeeps), next). sdv-py's test table and its ten linear-time shapes are ported.
ESPN basketball pbp producers (mirrors sdv-py #688)
#97
- Pickcenter spreads (
espn_<lg>_pbp,helper_<lg>_pbp,helper_<lg>_pickcenter; NBA, WNBA, MBB, WBB): a pickcenter with a single provider is now read; it used to fall back to the default (2.5, home favored,gameSpreadAvailablefalse), as on every game ESPN lists with DraftKings alone. The spread and the home favorite now come from the same provider row, the first with a spread instr(provider.id)order. Before, a record-only teamrankings row that sorts first lent its favorite flag to consensus' spread and flipped the home line's sign: sdv-py counts about 127 MBB games plus NBA 401430219, where MIA's home line is now +4.5, not -4.5. A spread of 0 takes that row's own favorite flag (home when unset), and a pickcenter with no spread keeps the defaults instead of throwing.helper_<lg>_pickcenterreturns plain numbers and booleans, not one-element arrays. Fields that change:gameSpread,homeFavorite,homeTeamSpread,overUnderandgameSpreadAvailable, in the plays and in the game dict. - Team timeouts:
homeTimeoutCalled/awayTimeoutCalledand thetimeoutsmap count every team-timeout type (RegularTimeOut, ShortTimeOut, Full / Short / No / Reset Timeout), not only NCAA ShortTimeOut, so the NBA and modern WNBA maps are no longer empty. Each timeout goes to the play's ownteam.id; a play without one falls back to the team's name as a whole word, so "Memphis" no longer matches PHI. Official and TV timeouts stay out. - MBB overtime and clocks:
end.period_seconds_remainingresets to 300 on the first play of every overtime, asend.game_seconds_remainingdoes (in 2OT and later it took the next play's value). A bare-seconds clock ("23.4") reads as 0:23 instead of throwing. - Oracle: the basketball pbp oracle moves to sdv-py 3135873 (
BASKETBALL_PBP_PINintools/sdv_py_pin.py; the other ports stay atPORT_PIN) and gains three live captures (NBA 401430219, NBA 260312029, WNBA 400927398) plus sdv-py's real pickcenter arrays. Regenerated at the new pin, it differs from the old oracle only in the cells #688 changes, and the JS output matches it cell for cell.
Legacy get* methods fetch through the request layer
#90
- The last 77 raw axios calls in the hand-written
sdv.<league>.get*methods (src/services) now go through the core request layer. That gives them https, retries with backoff,configure()transports, and theNoDataError/AssetFetchErrorvocabulary. Signatures, URLs (barhttp://→https://), queries and return shapes are unchanged. For each ESPN endpoint shape, the old and new code returned the same object live.- ESPN site API, family
site_v2:getSummary,getPicks,getScoreboard,getConferences,getTeamList,getTeamInfo,getTeamPlayerson the 8 leagues, NHLgetPlayByPlay/getBoxScore(read off the summary), andsdv.tennis.getScoreboard.getStandings(site.web.api.espn.com) uses familyweb_v3. - 247Sports HTML scrapers (deprecated
getPlayerRankings,getSchoolRankings,getSchoolCommitson cfb / mbb): familysports247_html. Overhttp://, 247sports.com answered HTTP 406. Over https the pages load and parse again. As on the other 247 families, a 403 is the edge's block and is not retried. sdv.ncaa: ncaa.com and data.ncaa.com (getRedirectUrl,getInfo,getBoxScore,getPlayByPlay,getScoreboard) use familyncaa_com. A 2xx casablanca response that is not JSON throwsAssetFetchError.getRedirectUrlthrowsAssetFetchErrorwhen the final URL carries no game id (e.g. a custom transport that does not report redirects); it used to returnNaN. The deprecated stats.ncaa.org scrapers use familystats_ncaaover https and keep theirDeprecationWarning. Their Akamai 403 is a block, so it is not retried.
- ESPN site API, family
- Fixed —
sdv.nba.getSummary/sdv.mlb.getSummary: 7 of their 12 fields (teams,id,plays,competitions,season,seasonSeries,standings) read agamepackageJSONkey, which only the CDN game pages have. A site v2 summary does not, so these fields were alwaysundefined. They now read the summary'sheader(teams,id,competitions,season) and top level (plays,seasonseries,standings), assdv.wnba.getSummaryalready did.idstays the header's string. MLB summaries have noleaderssection, somlbleaders(ingetSummaryandgetPicks) is stillundefined. - BREAKING — errors: a failed fetch throws a typed error instead of a raw axios error; see Legacy
get*methods: typed errors, retries, timeout under BREAKING above.
Transport / auth runtime
#87
- Date query values. A
Datepassed as a query parameter is sent as ISO-8601 UTC again (2025-02-01T00:00:00.000Z, what axios sent before the transport layer); it had becomeDate#toString()("Sat Feb 01 2025 …"). An invalidDatethrows anSdvErrornaming the parameter before anything is sent. Both built-in transports serialise the same way, and axios'config.urlstays query-free (an app interceptor that logs it never sees a query-param key). A date-only API (YYYY-MM-DD, ESPN'sYYYYMMDD) wants a string. - Credential redaction.
safeCause(everySdvErrorcause) also redacts credential-looking text, so an error your own transport throws can't carry it:Authorization/Proxy-Authorization/X-Api-Key/X-Auth-Token/X-Access-Token/Api-Key/Ocp-Apim-Subscription-KeyandCookie/Set-Cookievalues,Bearer <token>/Basic <base64>, JWT-shaped strings (also glued to a word or%3D), and the values ofpassword/token/accessToken/refreshToken/api_key/client_secret/ … pairs (=,:, URL-encoded, or"password": "…"; a barekey=only when the value looks like a credential). Ordinary text is untouched; the error name is redacted too. Every redaction pattern is now linear-time: the URL patterns were quadratic on long scheme-character runs (100 KB took seconds; now < 10 ms). - Retries / timeout per family.
registerFamilyDefaultstakesretriesandtimeoutMs; a value you pass toconfigurestill wins.pff_apinow retries 4 times (sdv-py's budget; was 3).nfl_prokeeps sdv-py's 45 s timeout but no longer overridesconfigure({ timeoutMs }). Login and token-mint requests (KenPom, 247Sports,nfl_api) use the resolved timeout too.pff_apino longer retries a408(sdv-py retries{429, 5xx}; the read budget is shared). nfl_api: the built-in token mint retries a network error or a408/429/5xxitself (never401/403); since the transport layer one network blip at/identity/v3/tokenfailed the call at once. When the retries run out,nflTokenGenthrows anAssetFetchError(sanitized cause) instead of the transport's raw error.tokenAuth: a401on a request that carried your own credential no longer mints a token that would never be sent.sports247: a403re-mints the guest JWT once and retries, as sdv-py does (not when you sent your ownAuthorization); after a failed mint, calls go out tokenless for a minute instead of re-trying the site root on every call;sports247ClearTokenCache()also resets the once-per-process warning; the User-Agent is now the one impit'schrome142profile sends (was Chrome/124 over a Chrome 142 TLS fingerprint).torvik/bart_wbb: no hard-coded User-Agent any more (it carried a+https://token, which ESPN's site API answers with 403). They send the configureduserAgent, soconfigure({ userAgent })applies; a callerUser-Agentheader still wins. barttorvik.com answered 200 to the default UA (one live request, 2026-10-05).nfl_prologin: the error name is scrubbed like the message; the browser-close cap no longer lets the process exit before the login settles.AuthProviderdocuments its failure contract on the interface. Test isolation: an@internal_unregisterFamilyDefaultsseam.
Parser / analytics minors (sdv-py parity)
#84
- Statcast
/gf: an empty-string id cell (batter: "") is nownull, like sdv-py'sto_numeric(""); it kept the column text before (whitespace still leaves the column as read, as in py). findTeam/findAthlete: the team-list cache is keyed by namespace identity (an injected namespace no longer poisons or reads another's), caches the in-flight promise (concurrent first calls share one fetch) and evicts it on rejection.- Odds math:
sumis CPython 3.12+ compensated (Neumaier), sodevig_*match py bit for bit; the Shin-fallback warning fires once per distinct message per process (py default filter); a NaN function value anywhere in the Shin solver is aValueError, as in scipy. - Cricket
norm_cdf: relative-accurate in the far-left tail (was ~1.8e-11 relative off from the1 - erfbranch; now ~4e-16 vs a 60-digit reference). - nba_stats: documented/tested that a legit-empty
resultSetsis data (raw keeps the headers) and parses to[]/an empty entry.
Other fixes
#77, #59, #56, #62
- NHL api-web / EDGE / records wrappers now honor sdv-py's
now_variant: omitting the toggle arg (season,date, ...) requests the endpoint'snow_variantroute (/now,/current, or the collection path) instead of a malformed dated URL (now_variant/now_togglecarried through codegen,resolveFlat, and the playground resolver). sdv.cbs.*: host is nowhttps://api.cbssports.com/napi(every endpoint 404'd without the/napibase).tools/codegen/from-openapi.mjsno longer drops the spec base path when--hostis a bare origin.getPicks(cfb, mbb, mlb, nba, nfl, nhl):pickcenterwas populated fromwinprobability; it now returns the realpickcenter.sdv.wnba.getTeamList()no longer throws when called with no argument.- ESPN site API rejected the default User-Agent (HTTP 403 on every site_v2 wrapper: scoreboard, summary, teams, rosters…); the default UA no longer carries the
+https://…token.
Fox / CBS / Yahoo parsers on real captures
- Fox Bifrost parsers never reached the rows their endpoints are about on real captures (one row per date section with the games JSON-stringified; whole table objects for standings;
details[]hijacking league/header, conferences and trending; layout cells for rosters; result buckets for search). They now port sdv-py's_fox_layoutrow builders and agree with sdv-py cell by cell on 13 captures;search_popularmaps toparse_fox_searchlike py. (#106) - CBS NAPI parsers: team/standings came out as one 151-column row (py: one row per
season_yearxseason_type), endpoint/registry as one 64-column row (py: one row perkey), the scoring plays / drives feeds skipped py's pinned columns and integer casts, and{plays: []}yielded a bogus row.parse_cbs_list/parse_cbs_standingsnow portcbs_napi_parsers(10 captures, cell-by-cell parity; booleans stay native as forparse_on3_rdb). (#106) - Yahoo parsers:
parse_yahoo_statsreturned the statTypes dictionary instead of the leaders, and the editorial boxscore / scoreboard parsers joined the second id level into column names. They now portyahoo_shangrila_parsers(entity_id+sub_id, one column per stat type; the frame each endpoint is about — pinned in the parity harness) and agree with sdv-py on 4 captures. (#106)
Security
Dependency patches and credential redaction
#79, #70
- Runtime dependencies patched:
axios^1.17.0→^1.20.0(22 advisories, 8 high — header injection, prototype-pollution gadgets, ReDoS, HTTP/2 DoS, fetch-adapter redirect SSRF) andundici(pulled in bycheerio) 7.27.2 → 7.30.0 in the lockfile (21 advisories, 6 high). Both stay within their major version; no API change.npm audit --omit=devis clean. Theundicibump is lockfile-only: a consumer's install resolves cheerio's^7.19.0itself. sdv-js never sends requests throughundici; it only callscheerio.loadon HTML thataxiosfetched, and the KenPom parser importscheerio/slim. - Dev-only dependencies patched (never shipped —
filesisdist/only):js-yaml4.3.2 (override floor raised from^4.2.0),brace-expansion2.1.7 / 5.0.12,markdown-it14.3.2,linkify-it5.0.2, all throughmocha/typedoc. Rootnpm auditis clean. - Docs-site dependencies (
docs/package-lock.json, build-time only, never shipped) refreshed: every@docusaurus/*package 3.10.1 → 3.10.2 on one version (including@docusaurus/fasterand@docusaurus/types, which the lockfile refresh would otherwise have left on 3.10.1), plus in-range patches that close 53 of 54 Dependabot advisories (brace-expansion,fast-uri,js-yaml,joi,svgo,postcss,nanoid,image-size,browserslist,shell-quote,http-cache-semantics,webpack-dev-server,qs, …). The one left,braces(GHSA-vfj7-8cjw-p6xm), has no fixed release and is build-time only; seeSECURITY.md. - Credentials no longer reach
err.cause. A raw axios error carries its request config — theAuthorizationheader, cookies, and a POSTed login form, password included — and it was attached as-is toAssetFetchError(network failures, auth failures), soutil.inspect(err)or a logged error could expose them. EverySdvErrornow stores itscausethroughsafeCause(name, message and stack with URL query strings anduser:password@redacted, pluscode/errno/syscall— nothing else).axiosTransportand the impersonating (impit) transport reject with the same sanitized errors. This applies to every family.
Tests
- Real captures replace synthetic payloads in the CBS NAPI, Fox Bifrost, Yahoo shangrila + editorial, api.nfl.com, MLB Stats and NHL (api-web / EDGE / Stats REST / Records) parser suites. The CBS, Fox, Yahoo and NFL.com bodies are sdv-py's captures copied byte for byte into
test/fixtures/{cbs,fox,yahoo,nfl_api}/(provenance READMEs); the MLB / NHL suites run on the captures already vendored undertest/fixtures/py/. The assertions are concrete (row counts, named cells, snake_case keys, decimal-string id columns). Synthetic payloads survive only as labelled malformed-/empty-payload edge cases and in the three suites with no public capture anywhere (The Odds API, the 247 RDB, the token-gated api.nfl.com routes), listed under "Known synthetic suites" intest/fixtures/README.md. (#106) - Offline behaviour tests:
test/espn_shapes.test.jsdrives 40 real ESPN captures through their generated wrappers with{ parsed: true }behind a stubbed transport (rows, snake_case keys, string ids, the documented URL);tennis.getScoreboardgets its first offline test on a live-captured ATP scoreboard (test/fixtures/legacy/). (#106) test/examples.test.js(every example script offline),test/docs-llms.test.js(llms.txt config),test/docs-examples.test.js(thescriptinjector family wiring). (#108)
Docs
- CHANGELOG: the three 4.0.0 breaking changes that sat outside the BREAKING heading (Fox vendor
pin, Yahoo
leaguedefault, legacyget*errors) are under it; draft lines removed; sdv-js PR numbers added; 3.0.0 dated; 2.0.0 → 1.0.x backfilled from the git history and npm publish dates. (#108) - README rewritten for 4.x (sources table, typed quick start, dataset loaders, utilities,
errors & config, examples, how the library is built);
.github/copilot-instructions.mdandCONTRIBUTING.mdbrought up to date (loaders, error vocabulary, coverage gate, examples and tutorials workflow, utilities catalogue,breaking.yaml); the phantomtools/codegen/templates/references are gone. (#108) - Tutorials 17-19 with sdvplot-js (
examples/94,96,97): a game's shot chart against the 2023-24 league (the shot kit'scellsVsLeague/shotCells, PNG via resvg), the final 2024 NFL standings as a publication table (sdvtablesgridTables+htmlToPNG), and a player's form from his ESPN game log (Plot in Node,meanLines,headshots). New real fixtures with provenance and re-derivation tests (test/tutorial-fixtures.test.js): the 2023-24espn_nba_shotsrelease counted per ESPN spot (tools/snapshots/nba-league-shots.mjs) and LeBron James's 2023-24 game log.examples/_resolve.mjsresolves@observablehq/plotandjsdomfrom the sdvplot-js checkout.
v3.0.0 (2026-06-17)
A major release that turns sportsdataverse into a cross-league ESPN client
plus a native (non-ESPN) live-API client with a tidy parser layer.
ESPN cross-league surface
- 116 endpoint wrappers generated for 29 leagues (31 namespaces) from a
single YAML source of truth — every league exposes the same
espn_<league>_<short>methods (e.g.sdv.nba.espn_nba_scoreboard(),sdv.soccer.espn_soccer_scoreboard({ league: 'eng.1' })). Soccer, cricket, and the UFL join the existing NBA/NFL/NHL/MLB/WNBA/MBB/WBB/CFB set.
Native API integration (255 flat wrappers across 7 families)
Beyond ESPN, the package now wraps the major leagues' own live APIs, merged onto the matching league namespace:
- MLB Stats API (
statsapi.mlb.com) —sdv.mlb.mlb*(e.g.mlbSchedule). - Baseball Savant / Statcast (
baseballsavant.mlb.com) —sdv.mlb.mlbStatcast*, including date-chunked Statcast search; heterogeneous CSV/JSON/HTML responses are handled by a content-type-aware getter. - NHL — api-web game feed + EDGE player tracking (
api-web.nhle.com), Stats REST (api.nhle.com/stats/rest), and Records (records.nhl.com):sdv.nhl.nhlApiWeb*/nhlEdge*/nhlStatsRest*/nhlRecords*. - NFL.com "Shield" API (
api.nfl.com) —sdv.nfl.nflApi*, with automatic anonymousWEB_DESKTOPbearer-token minting (cached + auto-renewed; no credentials required).
Provider families (5 cross-sport, standalone namespaces)
Beyond ESPN + the league-native APIs, v3.0.0 adds five independent provider
families, each generated from a canonical OpenAPI spec (the sdv-swagger
collection) via a new reusable OpenAPI 3.x → endpoint-YAML transform
(tools/codegen/from-openapi.mjs, with path / query / $ref param resolution +
auth detection). Each cross-sport family gets its own standalone sdv.<ns>.*
namespace and its own generated reference page; pass { parsed: true } for tidy
rows. The transform makes adding the next provider largely mechanical.
- The Odds API (
sdv.odds.*, 10 endpoints) — betting odds / scores / events across sports.apiKeyis a plain query param you supply. Ported fromoddsapiR; odds / event / history parsers unroll events → bookmakers → markets → outcomes to one row per outcome. - 247Sports (
sdv.recruiting.*, 25 endpoints) — recruiting rankings / commits / profiles (caller-supplied JWT viaheaders). Supersedes the legacygetPlayerRankings-style service methods (kept for back-compat). - CBS Sports (
sdv.cbs.*, 82 endpoints) — the public NAPI (scores / standings / teams / odds across sports), keyless. - Fox Sports (
sdv.fox.*, 38 endpoints) — the Bifrost API ({sport}path param across 11 sports); a publicapikey+api-versionquery pair, defaulted. - Yahoo Sports (
sdv.yahoo.*, 107 endpoints) — the editorial scoreboard / boxscore feed + the shangrila stats-graph API; keyless (needs browser-yOrigin/Refererheaders).
In total: 517 flat-API wrappers across 13 families (the 7 native + 5 provider
- Statcast).
tidy.js parser layer
- Every native wrapper returns the raw response by default; pass
{ parsed: true }to run the payload through a registered parser that flattens it to a tidy array of row objects via a shared in-housenormalizehelper (ajson_normalizeequivalent). Strictly additive — omittingparsedis the unchanged raw-response behavior, matchingsportsdataverse-py'sreturn_parsed=True. - The
@tidyjs/tidytoolkit is re-exported (import { tidy } from 'sportsdataverse') so the parsed tidy arrays compose directly with grammar-of-data-manipulation verbs (groupBy,summarize, …). - ESPN parsed dispatch. The cross-league ESPN wrappers now accept the same
{ parsed: true }flag — a faithful port ofsportsdataverse-py's_common_espn_parsers(22 parsers: scoreboard / standings / rosters / leaders / athlete deep-dives / the 21-sub-framesummarydispatcher, plus two generics for the Core v2 list + single-resource long tail). All 116 ESPN endpoints route through these parsers;summaryadditionally honours asectionarg ({ parsed: true, section: 'boxscore_team' }). Omittingparsedis the unchanged raw-Dictbehavior.
Dual-case naming
- Every generated wrapper (ESPN and native) is exposed under BOTH its snake_case
name (
mlb_teams, py/R parity) and its camelCase canonical name (mlbTeams, idiomatic JS) — the same function under either name.
Docs + playground
- Generated reference pages now include a Native API —
<family>section per league (host, path, params, parser, auth gate). - A shared ESPN parsed returns reference page documents the
col_name | type | descriptioncolumns each of the 22 ESPN parsers produces (the 116 endpoints share them, so they're documented once by parser rather than repeated per endpoint), with every league page linking to it. - The live playground groups native
endpoints under their league by family and runs them through a flat-aware
serverless proxy — host-allowlisted (derived from the generated metadata), with
server-side token minting for NFL.com and content-type passthrough for Statcast
CSV/HTML so credentials never reach the browser and non-JSON bodies display raw.
It now also has a Raw/Parsed toggle (tidy rows render as a sortable table —
with a section selector for the 21-sub-frame
summarydispatcher), deep-linkable URLs (every call is shareable), and an Examples menu of curated presets across ESPN, the native APIs, and all 5 providers. The provider families each appear under their standalone namespace. - Getting-started guides (
/docs/guides/) — per-area recipe pages (quickstart, NBA/WNBA/college-basketball/NFL/MLB/NHL/CFB/soccer, providers) with runnable raw-vs-parsed snippets, "Open in playground" deep-links, and embedded RunKit live notebooks.
Tooling / housekeeping
- Migrated to TypeScript. Authored in TypeScript; ships
.d.tsdeclarations alongside the ESM build. - ESM-only, Node ≥ 20.18.1.
cheerio1.2 (viaundici7) sets the floor. - Codegen + drift gate.
npm run codegenregenerates the wrapper tables (ESPN + flat) and docs fromtools/codegen/endpoints/*.yaml;npm run codegen:checkfails CI if the committed output is stale. - Legacy methods preserved. Every pre-3.0 method (
sdv.nba.getPlayByPlay(id), etc.) still works — the generated wrappers are merged alongside them.
v2.0.0 (2024-02-14)
- Converted to ESM (fixes the
tabletojsonimport error). - Dependency versions updated.
- Functions broken by upstream API 404s removed so the suite passes: mbb
getRankings, wbbgetRankings, ncaagetTeamStats/getScoringSummary. - README and Markdown docs regenerated.
v1.2.5 (2023-07-31)
- NFL
getWeeklySchedule(week, year, seasonType)added by @unmonk (#20). - The
tnnstest file renamed totennis(#19).
v1.2.4 (2023-06-20)
- MLB service (
sdv.mlb) added by @unmonk (#17), with non-breakingnpm auditfixes. - jsdoc / jsdoc2md bumped, GitHub workflows fixed, optional mochawesome HTML test report.
v1.2.3 (2022-03-16)
.npmignoreadded; repository links moved frommastertomain.
v1.2.2 (2022-03-05)
- Standings hotfixes (1.2.1 was not published).
v1.2.0 (2022-03-03)
- Standings functions provide league-wide, conference and division splits for each
applicable sport from ESPN (#15, #16); the
groupsparameters are documented. - Dependencies updated per
npm audit; the Docusaurus docs site (docs2/, started 2021-12) takes its Dependabot bumps (#7–#13); repository location updated.
v1.1.4 (2021-05-25)
- Committed editor settings and
node_modulesremoved;.gitignoreadded (#6).
v1.1.3 (2021-05-24)
- Dependency/patch release:
table-to-json^2.0.6 (no changelog recorded at the time).
v1.1.2 (2021-05-11)
- Dependency/patch release:
underscoredev-dependency patch (no changelog recorded at the time).
v1.1.1 (2021-03-14)
- Module export fix and README update after the 1.1.0 restructure.
v1.1.0 (2021-03-14)
Breaking changes:
- Submodules simplified/removed — all functions are now
{sport-league}.getXXX(e.g.cfb.getTeamList()instead ofcfbTeams.getTeamList()). - Support for statistics from stats.ncaa.com added.
- Documentation website created.
v1.0.x (2021-02-19 → 2021-03-12)
Twenty-four patch releases, 1.0.0 to 1.0.23, while the package took shape:
- 1.0.0–1.0.1 (2021-02-19): initial release — ESPN scrapers for CFB, MBB, WBB, NBA, WNBA, NFL and NCAA, with the npm publish workflow.
- 1.0.2–1.0.5 (2021-02-20): NCAA scoreboard; scoreboard service fixes; README.
- 1.0.6–1.0.12 (2021-02-21 → 02-24): scoreboard fixes —
seasontype=2default, thegroups(notgroup) param, zero-padded month / day in dates (#2, #3). - 1.0.13–1.0.14 (2021-03-02): NHL support;
groups/seasontypeadjusted for the pro leagues; NCAA README examples (ncaaGames). - 1.0.15–1.0.21 (2021-03-11):
gameUrlparameter on the NCAA game functions, so a redirected game id is resolved before the fetch (#5); schedule / scoreboard date parsing; first docs pages. - 1.0.22–1.0.23 (2021-03-12): NCAA team / player statistics service
(
ncaaStatistics) and its exports; more docs.