Releases
2.0.8
For you
- An archived game edition now looks like the rest of the game pages: its win likelihoods and projected score are rows of one list instead of boxed panels, its confidence badge draws the Sporacle mark instead of a diamond character that changed shape from one device to the next, and a neutral site game reads "vs" instead of "at". Every published number is unchanged.
- The footer on a phone now names the year beside "Updated", as the desktop footer always has.
- When a final's quarter by quarter scores cannot be shown, the linescore now says so beside its dashes instead of leaving eight dashes unexplained: it says they could not be read right now if the feed failed, and that they were not recorded only if the feed was read and does not settle them.
- A player page no longer dates an injury from nflverse to the time our run read it. Such a designation is dated by the official report's own release time when the report carries one, and otherwise the page says nothing about when it was reported, which is every nflverse designation until the release time is switched on. A designation from the faster source keeps its own report time.
- You can ask the MCP for a past week's rankings, player lookups and game cards. It answers from the last run published for that week, the newest of the runs the site's archive lists for it, and says plainly when a week was never published.
- The MCP's rankings can be narrowed to one team or to both sides of one game, and a new get_players tool looks up to 50 players in one call.
- A compact option trims each MCP ranking row to its numbers and their sources, so a full FLEX page is a fraction of the size it was.
- The MCP refuses a week later than the published one, refuses a game id from another week, and says "unknown team" for a team code no player carries.
- The Sporacle MCP now accepts LAR for the Rams and WSH for Washington, along with the other alternate team codes, wherever you ask for a game card.
- The MCP's rankings keep each player's rank when you ask for only some fields, so the rank column no longer reads absent.
- A game's live trajectory in the MCP drops back to back readings that did not change, so halftime no longer lists the same state a dozen times.
- The live disclaimer now arrives once, in the MCP's server instructions, instead of at the top of every live summary. The full result still carries it.
- The written explanations on player and game pages no longer turn a score range into two separate numbers: a range like 10 to 14 stays a range. An explanation that uses a word the site never prints is written again, up to two more times, with the word named; only if it still slips is it replaced by the plain template sentence, which says only what the model was handed. Club and player names that happen to contain such a word are never counted, and the writer is told which plain words to use instead. The prompt no longer asks the writer for causes the briefing does not contain. Every number on the pages is unchanged.
- The data and methodology page is gone, and its address now opens the About page, whose "How the numbers are made" section names the engine, the schedule and every input. The footer and menu links that pointed there open that section, and a game with no forecast yet links its publishing rules on the About page. The season's calls stay downloadable as a CSV from the track record.
- The method page no longer makes claims about Brier scores and the market's closing forecast that nothing we publish measures. Its list of how a call is scored now includes the margin range check the track record runs, and "9 in 10" reads as how often the band is built to hold, which is a target.
- Screen readers no longer read "right arrow" inside link text: the arrows beside links such as "Whole conference", "Open this week's games" and "Track record" are drawn but hidden from assistive technology.
- The track record's "How this page works" panel explains near coin flips in a sentence that parses: if the side we leaned toward wins it is a Hit, and if not, a Miss.
- The small explanation popovers lost their drop shadow and now separate from the page with a thin border, like every other panel on the site.
- On the track record, the "Home team, always" row no longer prints a coin flip's Brier score as its own. It shows the Brier score of the home win rate benchmark the track record publishes, and a dash when that benchmark is not published.
- On the rankings toolbar, "Start or sit" stays on one row at every desktop width; the toolbar wraps instead.
- The teams page no longer shows two order buttons, by rating and by playoff chances, that could not be pressed; they come back when those orders are built.
- Sportsbook words are gone from the rest of the site's copy: the page not found screen says "Chance this page exists", the About page's opening sentence is rewritten, team pages say playoff chances, the track record's benchmark note names the market's closing favourite, and your logged calls freeze at kickoff. The method page's sentence about the closing forecast now says everything the market knows is priced into it. The same word check that already covered the changelog now covers the site's documents, the glossary, the page not found screen, several panels and text the browser draws.
- A few stock phrases now appear once per page instead of everywhere: the page not found kicker reads "404 · Page not found", the team page's injury panel is headed "Injuries", and Start or sit says the overlap is what matters.
- The DST page now says what the glossary says: we built a defence ranking and tested it, it missed the bar we set before the test, so we do not publish it, and kickers are not modelled yet.
- Small consistency fixes: the phone menu's search box uses the same prompt as the search dialog, the menu and the footer send "Data sources" to the same place, an overtime final in the row of games reads "Final · OT" in the same case as "Final", and the glossary spells favoured the way the rest of the site does.
- Two explanations now say only what we can back: a team's luck tile says positive means more wins than expected, without predicting it will even out, and the live explanations name the real timings, a check of the league feed once a minute during games and a new live reading every 30 to 45 seconds.
- The privacy page now lists everything the site keeps in your browser: five local storage keys, including the calls you log on the track record, which it had left out, plus the one schedule flag.
- Each game page tells search engines a little more about the game: its own address, the stadium when the schedule names one, and whether it was postponed.
- Search shows "Loading published search data." in the dialog while it loads, instead of an empty box, and when it cannot load it says so and offers Try again, which loads afresh. Closing and reopening search after a failure also tries again; it used to keep the failure until the page was reloaded.
- Share previews say plainer things: the home page's says our win likelihoods are scored in public for calibration rather than calling them calibrated, and each rankings preview is one sentence about what the table shows. The page not found screen no longer mentions the old games index, and a note about stylesheet bytes leaves the For you section of the 2.0.4 notes.
- The share images for the site's main pages are regenerated with that copy and given new file names, so a link shared again shows the new image instead of one a platform kept.
- The status page's corrections log now lists the track record's own dated corrections, each with its date and what changed, instead of a blank table. The record does not say which run made a correction, so the Run column stays blank rather than naming the current run. The first is the 2026-10-10 change to how player projections are graded.
- On the track record, the comparison with the market's closing favourite and with always backing the home team now leaves a tie out of both sides, ours and theirs, and says how many games it covers and how many ties it left out, on the phone panel's heading as well as the desktop's. Our own season record still counts a tie as a miss, so the comparison can show a different game count than the season tiles; it does so as soon as this ships, because a tie is already in the record.
- The note beneath that comparison now says the market's closing number is the hardest benchmark there is and that we report where we stand against it, which matches the method page; it no longer says beating it is the only claim worth making.
- The live win likelihood on a game page now reads a game the way the model learned it, so at some moments the number you see there moves. When there is no down in play, such as an extra point or the pause the live scoreboard reports between plays, the model is no longer told it is 1st down. In overtime it is told how much of the overtime clock is left instead of being told the game has ended. To size this we replayed every 2025 play with a team on the ball and a field position, turned into the game state the live number reads. These are replayed plays, not readings the site served, and they cannot say how often the live scoreboard actually reports a pause between plays. 4,036 of those 43,060 states read differently: half of them by more than 3.6 percentage points, 466 by more than 10, the largest by 53 in overtime. The kickoff the live number prices between a score and the next snap was already read this way and does not change, and neither does any published pre game call or the number at the opening kickoff.
- The game page's injury list and the rankings status note now say "Game day inactives not included." The game page's injury list also says when the injury report is from.
- Injury reports posted on Wednesday to Saturday afternoons now reach the numbers at 7:45 AM ET the next morning (6:45 AM ET from November 1). For Wednesday to Friday reports that is 3 hours 45 minutes sooner than before; for Saturday's it is 1 hour 35 minutes sooner, because Sunday already had a 9:20 AM ET run (8:20 AM ET from November 1).
- On a phone, the week chips, tabs, segment buttons and small links answer a tap across a 44 pixel square where there is room, though they look the same size as before. Where controls sit closer together than that, a tap near the side of one never selects the one beside it.
- No text on the site is smaller than 11 pixels any more, apart from the headers and cells of the dense data tables.
- With text at 11 pixels, two charts needed room made for it. In a window 1024 pixels wide, the clock times along the bottom of a finished game's win likelihood chart, such as 15:00 and 10:05, ran into each other; now, short of the full desktop width, the times that would touch are left out, as narrower screens already did. On a player page in a window just wider than a phone, the "ceiling" label in How sure ran into the projection's number; there it now sits one row lower, and at wider windows it stays where it was.
- Panel headings, tile labels and section labels read in sentence case instead of capitals, on the team pages, the teams index and the share images too. The last-run labels on the track record, the team label on player pages and the heading above the start or sit tool read in sentence case too. Only table column headers and live status tags stay in capitals.
- Buttons, tabs and segment controls have 8 pixel corners instead of fully rounded ones; status pills stay round.
- The start or sit range band and our bar in the forecaster comparison are no longer painted in the site's accent colour, which is kept for things you can press.
- In light mode, raised buttons stand out from their outline again, quieter labels read lighter than body text, links darken on hover, and the warning outlines are dark enough to see on white.
Under the hood
- The cadence's weekly scoreboard read now stands down while the pulse's shared backoff row holds, as the Workers already do. It reads that row once per run and sends nothing if the row cannot be read; the schedule still publishes from nflverse alone.
- The shared stylesheet drops about 2 KB of rules no page uses any more (the old card-table pattern, a toggle button, and four other unused classes), which keeps the rankings page inside its size budget, with nothing on screen changing.
- The injury source decision is written into the data sources doc: nflverse is the only injury source, game day inactives are not reflected because no source we found has terms that allow it, and the Sunday run depends on nflverse like every other run. Comments that cited the local-only notes now cite the numbered rules in the repository guide where one fits, and the unused bucket listing script is gone.
- The live disclaimer text has one home, `artifacts/contract/live_disclaimer.txt`, and tests in the live Worker, the pipeline and the MCP each assert their copy equals it. Two stale comments in the live Worker and the MCP readers are gone.
- The research and rating modules read the injury table through the same step as the rest of the model, so a table with only 2025 and later seasons no longer fails on its missing date column. The ruled out set is judged once per run and the injury release time is fetched once per ingest, not once per season. The cadence and the ingest share one git sha helper, and an absent sha is an empty string in both. The experiment harnesses and the standings table no longer name another service.
- When a game kicks off is now worked out in one place in the pipeline, `features/schedule_time.py`, instead of five copies. Every published kickoff, every freeze and every model input stays exactly as it was. Production code no longer imports from the research module whose own notes say nothing in it is deployed.
- The weekly runner reads the date in Eastern from the real time zone instead of a fixed four hour offset, and works out the week for a season with no hand written start date from that season's first kickoff, so the 2027 season needs no new start date. Late on a winter Monday night, between 11 PM and midnight Eastern, a run now counts as the current week rather than the next one, and the check on whether to read the faster injury source uses that same week, so the two cannot disagree. In the offseason months a run stops without downloading the data first.
- Every upload to the artifact store now goes through one shared step. A run's files land first, the run's own manifest after them, and the pointer the site follows moves only once every file is confirmed in place with the right contents; a failed upload stops with an error naming what landed, and the site keeps the previous run. The daily depth chart archive upload, which had no checks at all, now gets the same ones: it refuses test data and refuses to upload from a replay. The keys and contents of everything uploaded are unchanged.
- The published history of every forecast point per game can no longer be wiped by a failed read. Before, when reading that history back from the artifact store failed for any reason, the run treated it as missing, started a new history from nothing and uploaded it in place of the published one. Now only a file that genuinely does not exist counts as missing; any other failure keeps the published history exactly as it was for that run, logs an error, and the rest of the facts still update. A run whose facts upload fails still triggers the site rebuild for the run it already published, then fails loudly.
- The live poller, the injury poller and the Phase 0 measurement now reach the scores and injury source through one shared module that sends one identifying name, checks each caller is registered for the host it calls, and never retries a refusal. The injury poller pauses on a refusal, as before. The Phase 0 measurement is counted in the declared request budget for the first time, but keeps polling through a pause on purpose, so its measurement of what the source actually answers stays unbroken. The live poller never pauses, by design. The two pipeline callers keep their own module for now.
- Each place a run marks itself degraded now names its reason, so the injury report check's `stale` and a publish check's `stale` read apart. The manifest can carry each reason as a short sentence, the site prints them in the footer, right below when the numbers were last updated, and the MCP prints them beside the degraded sources. The manifest write stays off until the release that teaches the site to read it reaches the public site, so nothing changes on the site yet.
- A rookie's ranking row can name the history he does not have yet (targets, carries, pass attempts, snap share, and yards per target, carry and pass attempt), and his player page says his projection and range are what earlier rookies at his position scored per game. The write stays off for the same reason, and no number changes.
- The run id pattern is written once for the site, in `site/src/lib/run-id.mjs`, which the artifact reader, the archive address check, the edition clock and `fetch-artifacts.mjs` all import, and `artifacts/fixtures/run_ids.json` holds it and the pipeline's `RUN_ID_RE` to one pattern and one answer per case, tested on both sides.
- The site names its durations once: `site/src/lib/durations.ts` holds `MINUTE_MS`, `HOUR_MS` and `DAY_MS`, the live window's bounds are `PREGAME_WINDOW_MS` and `MAX_GAME_LENGTH_MS`, and the seven hand written day constants use `DAY_MS`. The stalled feed's three minutes is recorded as the site's own number; the live Worker keeps no such constant.
- A research helper now pairs each player's real weekly stats and game with the model's backtest output, ready for the planned model checks. It changes nothing the site shows or the weekly run does.
- A research only reader now shows the depth chart as it stood before each past kickoff, for testing how backups inherit a starter's work. Nothing it reads reaches a projection.
- A research only replay now rebuilds, for a past week, the list of players the weekly update would have projected, backups who never took a snap included, using only what was known before kickoff. Nothing it builds reaches a projection.
- Added offline research tooling for the isotonic recalibration check: it fits on earlier seasons only, accepts only four named columns, and reads only Engine 1's own graded output, never market data. Nothing in the live pipeline uses it, so no published number changes.
- The track record builder can now publish each graded game call complete: the side we leaned toward, our whole percent in it as the card printed it, who won, hit or miss by the one tie rule, the call's own Brier score, its confidence tier, whether the call was published only after kickoff, and the margin range published with it. The site's track record, its game page calibration sentence and the assistant tools render that grade instead of grading the call again, and grade it themselves, exactly as before, when a record carries none. The write is off until a release carrying those readers reaches main, so every record published now renders as it did. A shared table of cases in artifacts/fixtures/graded_calls.json holds the builder, the site and the assistant tools to the same answers, ties and late calls included.
- A dated note beside one graded call can now be written into the track record, and the builder refuses one that does not start with its date. None is written yet.
- The assistant tools' track record summary now says the record's dated corrections, and any dated note beside a listed call.
- A research tool now builds the game model's projected total and margin for each past season from a model trained only on earlier seasons, and attaches them to the player model's rows as candidate inputs for a planned test. The leak check that confirms the player model reads nothing from the game model still passes, and it can now also prove such a frame was built from earlier seasons only. Nothing in the published projections changes. Club codes are matched through the same aliases the player features use, and the check reports how many player rows found no game and whether each was a bye or an unrecognised club code.
- Team codes now have one home. The pipeline exports its 32 club codes and the other codes some feeds use for them to a committed contract file, and the pipeline, the site, the MCP server and the live layer are each checked against it, so a club cannot be spelled one way in one place and another way somewhere else without a check failing.
- The pipeline now keeps one player registry: each player's id and his full name, from the roster first and the id crosswalk second. A name it cannot match to exactly one player is never guessed at; it goes to a review file kept on the machine that ran the pipeline and published nowhere. A new local `sporacle identity` command lists every published short name such as "C.Watson" and the full name it would become. The rankings and the track record still print the names they did; switching them to the registry's full names is a separate change, made after that list has been reviewed.
- The live model's training rows and the rows it is served now come from one builder, pipeline/src/sporacle/engines/live_features.py, with the play by play and the live scoreboard as two adapters into it and a test that writes the same game state both ways and requires the same row (issue #257). Serving now leaves a missing down and a missing field position empty, as the training rows always did, keeps its old fill only for timeouts and the distance to go when a down is in play, which the training rows never leave empty, and works out the lead per minute in the same single precision arithmetic training uses. That last part on its own moves 26 of the 38,625 replayed regular time states with a down in play, by at most 4.6 percentage points, because serving had been a hair off training there. The training rows are exactly what they were, checked value by value and type by type across the whole local play by play, so the model refit from them each time the poller starts is the same model. Live readings now carry live-0.2.0 instead of live-0.1.0, taken from the model itself; readings already stored keep live-0.1.0. A game whose clock the scoreboard leaves out now gets no live number instead of being read as the end of the period.
- The cadence's 09:30 UTC refresh, Wednesday to Sunday, moves to 11:45 UTC, 28 minutes after the 11:17 UTC ingest, so nflverse's overnight injury release (about 06:00 UTC) is priced at 11:45 rather than 15:30 (Sunday's 13:20 UTC inactives run already priced it at 13:20, so Sunday gains 1 hour 35 minutes). The Friday 23:00 UTC injury run is removed: it read the same data as the 21:30 refresh, because the Friday game status report reaches nflverse only on Saturday morning. Cadence runs go from 38 to 37 a week; the ingest schedule does not change.
- The injury report contract gains an optional `release_updated_at`, the time nflverse last rebuilt its injury release, and the site reads it: a known time shows as the report's time, a null shows "time unknown", and a report without the field shows nothing. The pipeline does not write it yet; that waits, behind a flag, for the release that puts the reader on main.
- The brand rules now say the accent is never a data colour and nothing uses a gradient, set an 11 pixel type floor with a dense table exception, give every phone control a 44 pixel hit area, and keep capitals for table headers. Panels and status pills each come from one shared style, and unused styles and aliases are gone.
- Each run's manifest now lists, by id, the players the faster injury source rules out who have no roster entry and so are left out of the injury report. The write waited for Release 2.0.7, which taught the site to read that field.
- A release pull request from dev into main no longer reruns the pipeline and site tests just because it started a few seconds before the dev run on the same commit finished. When the pull request's tree is its head commit's tree and that commit's dev run is still going, Gate 1 waits up to two and a half minutes for it, then skips both jobs only if that run passed. Anything else, a slower run, a red run or a failed lookup, still runs everything. The release checks, including the dated changelog heading into main, run either way.
- Gate 1's site job now times out at 30 minutes instead of 35, since recent runs took 14 to 20.
- The brand colour check ignores comments, so an issue number cited in a code comment no longer reads as a colour and fails it. A colour in a stylesheet, a style string or an attribute is still checked.
- Three pipeline test files share one schedule helper and six site test files share one board page helper, instead of each keeping its own copy. No test or assertion changed.