Published artifacts

Search Sporacle

Use the up and down arrow keys to move through results. Press Enter to open a result and Escape to close search.

Type to search published players, teams, and games.

Releases

1.9.3

All releases, newest first

For you

  • A fourteen point collapse used to rise about five pixels across the whole chart, so a real swing read as flat. The live game card's band is much taller now, states its own 0 to 100% scale, draws the club boundary, not a gap in the card colour, and carries a 50% mark and a labelled time axis from the opening drive on. Its labels rendered at about 5.4 pixels and measure 12, at 1280 pixels of width and at 375.
  • The two clubs in that chart are never the same colour now, and the chart says which is which. On the dark theme the Rams and the 49ers measured 1.37 to 1 against each other, 464 of the 496 possible pairings sat below the 3 to 1 accessibility floor, 394 of 496 on the light theme we had believed was fine, and a rule that moves lightness and leaves each club's hue alone pulls them apart. The only key had been the percentage's colour beneath them, naming whoever is ahead, not a band; the caption says which club is the lower band and which the upper.
  • Two things the live card stated were not true. Its percentage scale ran backwards against the band it labels: a club holding 66% had its boundary drawn where the gutter reads 34%, so a collapsing club drew as a boundary rising toward the 100% label. The boundary sits where the scale says now, a share filling from the end the scale calls 0. Second, once the lead changed the movement sentence measured against a percentage printed nowhere on the card: 54% beside a kickoff forecast of 60%, claiming a move of 14. It names the side it measures on now, as in "up 14 from LV 40% at kickoff", so its two numbers subtract to the number it states.
  • The score reads the same way everywhere on the card, away club first with both team codes; it used to appear twice in opposite orders about 40 pixels apart, reading as a contradiction. The header clock says Now, no longer confused with the times beside each score change, and the kickoff forecast is stated once, not three times, the freed space going to how far the live number has travelled since kickoff.
  • A screen reader is told what the live card says. Everything on it is rewritten about once a minute, the card appears from nowhere when a game starts, and none of it was announced. The one sentence carrying the leading club, its percentage and how far the number has moved is read out politely on each update, once, as a whole; six regions would have interrupted three more times to repeat it.
  • The chart's labels grow with your text size. A reader who has raised their default font size got a doubled score and ticker beside a chart whose labels stayed at twelve pixels; the axis and its gutter answer to the same preference as the rest of the card.
  • Smaller repairs to the same card. A quarter marker landing early in the first half printed its label through the word Kickoff, one run of letters at a narrow phone width; the quarter guide marks are a shade more visible in both themes; the Live badge is a full size touch target, not a 43 by 26 pixel one; and a game whose feed carries only the pre kickoff number leaves no empty gap where the live figure goes.
  • The facts on every game page refresh again: injury designations, the schedule and the team summaries alike. nflverse blanks this week's injury report timestamp instead of dropping the column, a blank value survived every emptiness check before raising an error partway through the build, and the cadence's broad safety net degraded that error to a note, so the published files kept their old contents and looked healthy while nothing advanced, and game pages showed no injuries though a full report had come in. The blank timestamp is read honestly now, and that kind of failure is logged loudly.
  • Rookie rows now carry a number measured for the scoring format you are reading, not one half-PPR number copied into standard and PPR too. Kenyon Sadiq published 7.57 points in all three columns while a veteran like Trey McBride separated to 10.79, 13.89 and 16.98, ranking rookies too high in standard leagues and too low in PPR ones. The rookie cohort behind each projection already scored in all three formats, so standard and PPR read their measured median.
  • Two more places where two clubs' colours touch are pulled apart the same way that card's chart was. A team page's schedule put your club's colour directly against the opponent's with nothing at all between them, and Philadelphia against Houston measured 2.82 to 1 there on the dark theme and 1.48 to 1 on the light one, against a 3 to 1 accessibility floor. A game page's forecast chart draws two curves that are mirror images of each other, so they cross whenever the favourite changes and they sit on top of each other whenever a game prices close to a coin flip; the Chargers against Denver measured 1.15 to 1. Both now use the rule that moves lightness and leaves each club's own hue alone. Four other places where two clubs' colours meet were measured in both themes and left exactly as they were, because a gap, a seam or a number already stands between them.

Under the hood

  • The live card's readout carries role=status, aria-live=polite and aria-atomic=true, the card's only live region. Before, no element in it matched [aria-live],[role=status],[role=alert],[role=log], against a script that rewrites score, clock, ribbon aria-label, large percentage, movement sentence and whole ticker on each reading and reveals the card from hidden mid-session. WCAG 2.2 SC 4.1.3. games-pages.test.ts pins the attributes and the count of one.
  • The ribbon's viewBox is written from its own measured box, so one viewBox unit is one CSS pixel and an axis label declared at 12 renders at 12; a ResizeObserver redraws it when the box changes; a fixed 640 by 88 scaled to 317px, a factor of 0.495, had shrunk every label to 5.45px and tied band height to card width, so a 14 point swing drew 5.2px. Band height is a CSS decision independent of width now, and the same swing draws 24.6px at desktop and 20.4px at 375px. The y axis is deliberately NOT zoomed, because a zoom would stop the two regions being shares of one whole. Axis furniture is measured in label sizes, read from the svg's computed font size at draw time: LABEL_PX = 12 was the one dimension the pixel unit system left hard coded, so at a 200 percent text preference the score went 22px to 44px, the ticker 13px to 26px and the stated scale 12px to 24px while every <text> stayed at 12px, 0.46 of the prose beside it. .glv-ribbon declares 0.75rem, and the gutter, the axis strip, the baseline and the collision guard are multiples of what that resolves to, so a default root draws the same pixels as before.
  • The quarter label collision guard is derived, not guessed. It was a flat 38 against a Kickoff string measuring 39.6px, leaving a 9.85px window of tick positions, about 3.6 percent of the elapsed time axis, that drew both labels on top of each other: 6.0px of measured overlap at a 375px viewport, a single run reading KickoffQ2 at a 240px card; it is the end label's rendered advance plus half a quarter label now, 48 at a default root, and a dropped label still draws its tick. The tick loses its 0.75 opacity, the reason it missed WCAG 1.4.11's 3:1 graphical floor in both themes: 2.97:1 dark, black against the away band, 2.93:1 light, white against the home band, against 3.55:1 and 4.22:1 at full opacity. Its 2 3 dash pattern and 1px width separate it from the 1.5px dashed midline and the 4px boundary casing.
  • The Explain trigger can take the --touch-target token through an absolutely positioned ::after, the mechanism WeekStrip and GameMatchup already use, and the Live card opts in. Opt in, not default: a 44px box centred on a 26px label reaches 9px above and below it, and GameMatchup's rows sit on an 8px gap, where the later row's invisible box would cover the earlier row's label and take its taps.
  • The readout is revealed inside the same hasModelReadings gate its two children already sat behind, so a prior-only feed shows no empty paragraph in a grid gap; the ResizeObserver and the one minute window recheck are retained and released where the state machine reaches final and the socket already closes. The honest absence around the readout is untouched: no model reading still means no Now clause in the aria-label, an empty numeral rather than a restated pregame number, and no probability clause on a ticker row.
  • separateAdjacentBands in site/src/lib/team-colors.ts is the deterministic rule for two club colours drawn touching: the lighter mixes 6% toward white and the darker 6% toward black, a step at a time, kept only while the result clears 3 to 1 against the card surface. GameLiveCard.astro stamps it per game and per theme as custom properties in a style element, because the answer depends on BOTH clubs and cannot be a per club token. The cost to a club's colour is CIEDE2000 15.7 dark and 10.7 light, the mean across the 496 pairings of the larger of each pairing's two shifts, or 13.0 and 9.3 per colour that moved. team-colors.test.ts pins all 496 pairings clearing 3:1 on both surfaces, unchanged, and colour identity at the measured worst case: CIEDE2000 36 not 40 against a worst of 35.28 (Tampa Bay's #D50A0A to #F2B3B3 against Washington, dark), plus a new floor of 30 percent of published chroma, worst measured 31 percent.
  • The card's score text has one formatter now, scoreLine in live-ticker.ts, shared by the header and every ticker row, so the two orders cannot drift apart again. lockedDrift states how far the live figure has moved from the kickoff call instead of restating that percentage, and the ribbon geometry hands back the 50% position instead of leaving the drawing and its label to derive it. live-card.test.ts drives all five fixes through a real DOM.
  • facts/injuries.py no longer requires nflverse's date_modified column to publish a designation, and _parsed_timestamp catches that column present but blank (pandas NaT) as it already caught it absent. The column only fed the latest report dedup and the practice day labels, so its absence degrades those two alone, loudly, with a warning naming the missing column, not by silently emptying the players list. The previous check recognised only None and a float NaN, so a blank timestamp reached a strftime call that raised, and the cadence's safety net turned that into a quiet, unrefreshed facts channel. A blank timestamp still omits that player's practice day instead of inventing one, and the build logs an ERROR naming the failed facts refresh and why, where the failure happens, not as a note afterward.
  • The live poller's post state branch no longer falls through to the prior fallback, and checks it has both scores before asserting an outcome. New England at Seattle finished 13 to 10 for Seattle while the live payload kept serving Seattle's pre game number; a new Source.FINAL label reports 1.0, 0.0, or 0.5 with the tie named, straight from the final score. It had trusted home_score and away_score blindly, and GameState filled a score the feed did not send with a silent 0, so a finished game missing one would have reported a confident final tie at 0 to 0; GameState keeps a missing score as an honest absence now, the branch and the live model's feature row both refuse an unreadable score, and a D1 write for such a reading is skipped, not persisted as an invented 0 to 0. A genuine 0 to 0 final, both scores reported zero, still serves the named tie. The terminal value reaches the live payload and the MCP server's snapshot tool, and no further. Nothing a reader sees changes yet: the game page's ticker, ribbon and fact rows each filter to model sourced readings, deliberately, so a finished game still shows its locked pre kickoff call. Presenting it is backlogged as Final state game page presentation, and this release does not do it.
  • Every production manifest's git_sha now reflects the commit the container was built from, not an empty string, and its Engine2.version is read from the engine's declaration, not a hardcoded literal, so a version bump cannot drift the two apart. The image ships with no .git directory, so the deployed sha is baked into a file at build time and the manifest reads it when the git command is unavailable.
  • Track record scoring stamps each call with the game's own week, not the run's. A full-season card set published by a week 1 run was stamped week 1 onto every call, so week_range filtering filtered on the run instead of the game.
  • The private MCP server's get_live_snapshot tool now scopes to the one game a caller asks for. Passing a game id used to return every game on the board plus a trajectory key bolted on; it returns only that game's row and its D1 history, and an id the live board does not carry gets a clear error. It also accepts the nflverse game id every other tool keys by, resolved through the live snapshot's nflverse_game_id column, alongside the numeric id it already took.
  • The same server's get_live_snapshot and get_freshness no longer wait on D1 forever. A stalled live_readings read once hung a get_live_snapshot call for four minutes; the trajectory and latest reading queries race a five second clock and answer honestly, trajectory: null with a note that the store did not answer, instead of returning an empty history reading as a game with no movement. Both notes state the bound they raced, derived from the injected timeout, not written as a literal, so a caller passing a different bound is not told the wrong number.
  • get_freshness no longer reports a timed out D1 read as an empty table, and names the evidence behind each surface's age, not just the number. Bounding the latest reading query gave a null reading a second cause, and the d1 surface reported both as age_seconds absent, which reads as "the poller has never written a reading" and sends an operator after the wrong fault; a timed out read reports age_seconds unknown with a note naming the bound it exceeded, a genuinely empty table still reports absent, and the field name and units are unchanged. An acceptance test once saw the live surface hours old while get_live_snapshot's body read fresh, neither response showing where the age came from, so every surface carries body_generated_at and uploaded_at beside age_seconds, null rather than guessed when it has none.
  • get_rankings now pages past its 100 row cap instead of truncating there, and accepts an optional fields list. FLEX alone clears 100 rows on week one with no way to reach row 101; offset and limit page through the published order, next_offset appears while more rows remain so a short page is not mistaken for the end of the board, and a negative offset or an out of range limit is rejected, not quietly clamped. fields trims a row to the columns a caller needs instead of full projected_stats every call, but player_id, name and position always survive: FLEX mixes RB, WR and TE rows into one published order, so a trimmed page that dropped position left a caller unable to place a row. A field absent from a row stays absent, and a call with neither argument returns what it returned before.
  • The readiness probe reads nflverse's NameError for a season it has not built yet as an answer rather than a failure to get one: the ingest of 2026-09-11 wrote all eight tables and still exited 2 and mailed the owner. The match is that one undefined name, so every other crash still reports NOT CHECKED. What the crash does NOT establish is what the source published, because a transient failure reading a season that does exist reaches the same handler and the library destroys the original error, so the two cannot be told apart; what the check reports says only that a read did not produce the season, and names that limit. An absence the source states itself, through a 404, keeps the stronger reading, as does one the schedule rules out because no game has been played.
  • The scheduled ingest's dead man ping now fires right after the master upload, not behind the readiness probe. The first scheduled run on 2026-09-10 ingested cleanly and uploaded every table, but the probe returned PARTIAL because one source check could not be answered (a season's play by play file nflverse had not yet published), which failed the step the ping sat behind and would have left healthchecks.io alarming that a live job was dead. The probe still fails the run on the same exit code either way; it now also posts an annotation saying whether that failure blocks or is merely an incomplete check.
  • separateAdjacentBands has three callers now and still one implementation. TeamSchedule.astro stamps a separated pair per schedule row and GameDrift.astro stamps one per game, both as custom properties in a style element per theme, which is the delivery GameLiveCard.astro already established and it is the only one available: the answer is a function of BOTH clubs, so it can never be a per club token in teams.css. Six surfaces that draw two clubs' colours were measured against the generated teams.css in both themes and four were already separated, so they are untouched: the game list card's bar keeps a 1 unit gap, the projection bar a 0.7 unit seam in the card colour, the margin range a 2 unit window plus its tied marker, and the matchup rows set two readings between the two bars. The drift chart's end chips also stay on the published chip colour, because the 4.5 to 1 guarantee for the abbreviation printed on them is measured against that colour and nothing else.
  • Every test that builds the real site now gets its own Astro project root, so no two builds share site/.astro. Astro derives the content store from the project root and nothing else, and two builds writing its content-assets.mjs through the same write-then-rename pair left the second renaming a temp file the first had already consumed. node:test runs a file's top level before hooks concurrently whatever the concurrency flag says, so rankings-pages.test.ts raced itself: four of eight cold-store runs of that file alone failed every test in it, and ten of ten pass now.