Commit Graph
2 Commits
Author SHA1 Message Date
ChuckBuilds 1dea7b0706 feat(sports): share the sports.py surface that is identical in all eight
Nine scoreboards ship their own sports.py -- 41,326 lines. Comparing executable
ASTs across the eight that share a lineage, 48 method bodies are byte-identical
in every one: 1,007 lines carried eight times, so 8,056 lines that must be
edited eight times to fix once.

They are the parts with no sport in them: the selection and rotation engine
(_round_robin_favorites, _favorites_first, _compose_selection,
_check_ranking_coverage, _game_divisions, _normalise_quality), the font/colour/
date subsystem (_scale_headline_fonts, _scorebug_font, _resolve_font_size,
_format_game_date, _font_color), and the switch-mode upcoming card
(_draw_upcoming_center_switch). Nothing here knows what an inning is.

Mixins rather than free functions: every one of these reads host state, so
rewriting 48 bodies into free functions would be a rewrite rather than a move,
and it is the move that keeps the renders identical.

Three of the 48 are deliberately left in the plugins, because a byte-identical
body is not automatically safe to move:

- _get_timezone calls resolve_timezone, imported from a per-plugin module
  (hockey_timezone, soccer_timezone, ...). All eight of those differ -- each
  carries its own _WRITEBACK_FIXED_IN -- so hoisting the caller would silently
  bind every scoreboard to one plugin's copy.
- _extract_game_details and _fetch_data are @abstractmethod stubs. They are the
  sport contract; satisfying them from a mixin would let a plugin instantiate
  without implementing its own sport.

_resolve_font_path went the other way: a module-level function, identical in all
eight, that _scale_headline_fonts needs -- so it is inlined here.

_schema_font_size needed a real change rather than a move. It located the
plugin's config_schema.json with __file__, which here is src/common/, so the
load failed silently, the cache stayed empty and every element fell back to an
unsnapped size -- measured at 81% anti-aliased edges on a panel that should be
pixel-crisp. It now recovers the plugin directory from the instance. Note that
type(self).__module__ alone is not enough: SportsCore is an ABC, so a subclass
built with type(name, bases, ns) -- which the plugins' own tests do -- reports
its module as "abc". _plugin_dir walks the MRO past those synthetic classes to
the first module sitting beside a config_schema.json.

Worth recording: the 176 harness renders did NOT catch that regression. The
plugin's own test_fonts_are_crisp.py did. Renders alone were not a sufficient
gate here.

Not merged with src/common/sports_card.py despite fourteen same-named twins.
Only five are provably equivalent by source comparison; the other nine differ in
ways inspection cannot settle, and a wrong guess silently changes what every
scoreboard draws. That merge needs differential testing and is its own change.
2026-09-02 15:57:30 -04:00
ChuckBuildsandClaude Opus 5 183e23edb3 docs(changelog): record the compatibility gate in 3.2.0
The 3.2.0 section described the unified sports library but none of the
install-path work that landed in #428 and #431 -- which matters more than a
normal changelog omission, because the sunset rule keys on this section to
tell plugin authors what a given floor buys them.

The headline addition: 3.2.0 is the first release that *enforces*
ledmatrix_min_version. Before it the floor was advisory, so a plugin could
declare one and still be delivered to a core that could not run it. That is
the property B6 waits on, and it is now stated where a plugin author will
look for it -- along with the caveat that a core reporting below 2.0.0 is
treated as unknown rather than old and is never blocked.

Also records compatibility.py (and that it does not yet read
compatible_versions), check_release_version.py and its workflow, the
install-preservation fix, the reentrant-lock deadlock fix, and the
web_interface version re-export.

No version bump: 3.2.0 is unreleased, so this describes the release being
cut rather than a new one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
2026-08-03 16:54:53 -04:00