* fix(sports_rankings): skip a malformed poll entry; rank each side by its own id
The two review points deferred on #789 until sports_rankings was the only
copy (the nine scoreboards adopted it in ledmatrix-plugins #648):
- A poll entry whose "current" is None or a string raised TypeError at
`current_rank <= 0`; the broad except returned {} without setting the
cache timestamp, so every team lost its rank and the standings were
re-fetched on every update. The entry is now skipped.
- _ranked_positions() fell back to abbreviations for BOTH sides when only
one side's id was unparseable, so a colliding abbreviation could rank the
wrong school. It now decides id-or-abbreviation per side.
Well-formed ESPN data (ids on both sides, int ranks) ranks as before.
SPORTS_UNIFICATION.md: family 8 marked done and adopted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): a numeric-string rank is still accepted; say which values are skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(common): sports_rankings -- the reconciled rankings family (sports family 8)
New hardware-free module src/common/sports_rankings.py, copied from
ledmatrix-plugins claude/family8-reconcile once the nine scoreboards made
_fetch_team_rankings, _choose_poll (three bodies each),
_load_division_team_ids, _passes_other_filters, _best_rank and
_is_ranked_game (two each) one body each. All of them live on the plugins'
SportsCore, so one mixin, SportsRankingsMixin, carries exactly those six
plus _ranked_positions and _poll_season_ended and the constants they read;
no manager gains a method it lacks today.
- Ranks are kept by ESPN team id as well as abbreviation, and a game with
ids is ranked by id (ESPN lists Ohio State and Ohio State Newark both as
OSU). The badge still reads the abbreviation table.
- A poll whose season's endDate has passed loads nothing, so no badge shows
and the "ranked" filter fails open between seasons (ESPN served April's
college basketball poll on 2026-10-12); read from the payload, no
calendar, no sport names.
- College football's division rosters are asked for the season year
(football's July rule; only that league has rosters, so no seam).
- choose_top_division_poll(): the one poll rule, for the dynamic team
resolvers that expand AP_TOP_n.
- test/test_sports_rankings.py: the plugins' pinned cases (poll choice,
which leagues are asked, payload shapes, hourly cache, the finished-season
rows, the rosters' season year and caching, ranked/best rank/filter over
colliding abbreviations), plus the host contract. Mutation-checked.
- test/test_sports_rankings_parity.py: with LEDMATRIX_PLUGINS, compares each
body and constant with every plugin copy. Passes against
claude/family8-reconcile; fails against the pre-reconcile tree.
- mypy ratchet, src/common/README.md, CHANGELOG (Unreleased, New modules).
- sports_shared and sports_rotation docstrings: the three methods leave the
"stay per-plugin" list.
- docs/SPORTS_UNIFICATION.md: family 8 status and decisions; item 8 restated
to match what measuring found (the standings branch never ran; the season
year only matters to college football).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: cite ledmatrix-plugins #645 for the family 8 reconcile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* ci: re-run code scanning (the Python analysis job never started)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>