Commit Graph
9 Commits
Author SHA1 Message Date
Claude 2486bdb249 fix(sports): stop dropping hockey and baseball events on optional feed keys
Both bugs were pinned AS-IS by the B0 characterization suite so this
phase could change them knowingly. Both fixes are adoptions of code the
corresponding plugins already ship, not new inventions.

Hockey: the extractor read competitor["statistics"] unguarded, so a
competitor arriving without that array raised KeyError inside the
generator and the WHOLE event was discarded -- valid scores and status
included. Shot/save counts now default to 0, which is already what the
suite expects for an empty statistics array.

Baseball: for live games the extractor read game_event["status"], the
event TOP-LEVEL status, to get the inning. Real ESPN events duplicate
status there, but MiLB events (synthesized from the MLB Stats API into
an ESPN-like shape) populate only the competition-level one, so the
lookup raised a bare KeyError and dropped the event. It now reads the
competition-level status that _extract_game_details_common has already
validated, so it cannot be missing at that point.

The two characterization tests that pinned the old behaviour are
rewritten to assert the fix rather than deleted, so the suite still
documents the edge case -- and still totals 94.

CHANGELOG records these plus the live-clock change from aaabc61 under
Changed/Fixed, since all three are user-visible. The two new promotion
suites join the CI unit job (449 tests).

Verified: unit job 449 passed, plugin-safety job 60 passed, and the
hockey (16) and baseball (24) plugin harnesses render clean at every
panel size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 16:25:32 +00:00
Claude aaabc614cc feat(sports): promote the nine universal methods into the base classes
Phase B1b of docs/SPORTS_UNIFICATION.md. Every method here is present in
all nine bundled plugin sports.py copies and absent from core, so this is
reuse of code the fleet already agreed on — not new behavior. The
promotions are inert until B5: the plugins' own overrides still run.

SportsCore: cleanup, _get_layout_offset, _load_custom_font_from_element_config
SportsUpcoming: _select_games_for_display
SportsRecent: _get_zero_clock_duration, _clear_zero_clock_tracking,
              _select_recent_games_for_display
SportsLive: _is_game_really_over, _detect_stale_games

Where the copies disagreed, the canonical form was chosen on evidence and
the genuine per-sport differences became seams rather than branches:

- _favorite_key(game, side) -- NRL matches favorites on team id because its
  abbreviations are ambiguous (NEW is both Newcastle Knights and New
  Zealand Warriors). Default is the abbreviation; NRL overrides. Core never
  learns the string nrl.
- FINAL_PERIOD / CLOCK_COUNTS_DOWN -- hockey ends in P3, and soccer/afl/nrl
  clocks count UP, so 0:00 means kickoff, not expiry.
- _config_schema_path() / _font_root() -- plugin-supplied locations, never
  derived from this module's __file__.

BEHAVIOR CHANGE (baseball, ufc): the rejected variant coerced a missing or
non-str clock to the literal 0:00 and then declared the game over at
period >= 4. MLB has no game clock and period is the inning, so live games
were being evicted from the 5th inning onward; UFC likewise. The promoted
variant skips the clock check when the clock is unusable -- it fails safe
(keeps showing the game) instead of failing destructive.

Also fixes a regression from the package move in e591cec: the bodies were
byte-identical but __file__ gained a directory, so _resolve_project_path's
parents[2] silently began resolving to <root>/src instead of the repo root.
Both it and _font_root now derive from a single _INSTALL_ROOT constant, so
a future move needs one line changed rather than two hand-counted depths.
Tests assert the resolved values, not the index.

The font loader takes baseball's body (BDF memo cache + native-strike
retry) under hockey's Optional signature -- the older lineage is the
correct one here, and basketball's positional str default breaks on an
explicit None. It resolves through _font_root rather than the cwd, so it
does not reintroduce the bug just fixed for FontManager, and delegates to
FontManager for the alias table and BDF header parse instead of shipping
second copies. cleanup gained the two new font caches and still leaves
background_service alone -- it is a process-wide singleton.

Verified: 111 new tests (48 core + 59 modes + 4 install-root regression);
characterization + skin suites still exactly 94, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 16:23:41 +00:00
Claude e591ceca48 refactor(sports): convert sports.py into a package (pure move)
Phase B1a of docs/SPORTS_UNIFICATION.md. src/base_classes/sports.py
becomes a package so the upcoming capability modules have a home and
diffs show their blast radius:

  sports/__init__.py   re-exports the public API
  sports/core.py       SportsCore
  sports/modes.py      SportsUpcoming / SportsRecent / SportsLive

No logic change: the 1515 class-body lines are byte-identical to the
original (verified by concatenating the two modules and diffing against
HEAD). Only module docstrings and the redistributed import blocks are
new. MRO and __abstractmethods__ are unchanged, and every existing
import site — including 'from src.base_classes.sports import SportsCore'
in the sport subclasses, the skin tests, and the characterization
suite — resolves through the package __init__.

One test edit was required: the characterization suite monkeypatched
'src.base_classes.sports.get_background_service', which is no longer a
module attribute on a package. Retargeted to
'src.base_classes.sports.core.get_background_service' — the module whose
globals SportsCore.__init__ actually resolves, so the patch is effective
exactly as before. No test logic or assertion changed.

Also adds docs/SPORTS_UNIFICATION.md: the architecture for the whole
B1-B5 sequence — how upgradability (guarded imports, capability probing,
frozen view-model keys, the sunset rule), reusability (promote only what
all nine copies share), and modularity (capabilities as opt-in mixins
rather than config branches, variants as named strategies, sport-unique
code as declared override points) are kept as three separate mechanisms.

Verified: characterization + skin 94 passed; the 10-file unit suite 338
passed; test/plugins 60 passed — all identical to pre-change counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 16:07:46 +00:00
Claude 64e8f87f86 ci: restrict the test workflow's GITHUB_TOKEN to contents:read
CodeQL flagged the new unit-tests job for running with the default
unrestricted token; the pre-existing job had the same exposure. Both
jobs only check out the repo and run pytest, so a workflow-level
contents:read is sufficient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 14:16:51 +00:00
Claude 1a0792225b test: characterization suite for src/base_classes/sports.py ahead of unification
Pins current behavior before the planned merge of the nine drifted
plugin copies back into this ancestor: the _extract_game_details_common
key contract per sport (reusing GUARANTEED_KEYS from the skin tests),
update() flows for upcoming/recent/live against cache-seeded fixtures
under frozen time, rendering smoke per mode class, and guard rails on
the skin-system seam.

Five surprising behaviors are pinned AS-IS and flagged in comments so
the merge changes them knowingly or not at all: is_upcoming also
matching status.type.name; hockey dropping events whose competitors
lack 'statistics'; baseball reading the event-level status for innings;
no past-date filter in upcoming; and favorites-only mode with an empty
favorites list showing nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 14:14:34 +00:00
Claude 5fe9c07f80 feat: ship src/element_style — the per-element style resolver plugins already expect
Three plugins (of-the-day, ledmatrix-music, football-scoreboard) import
src.element_style behind guarded try/except with classic fallbacks, but
the module never existed in core, so the richer per-element styling UI
those code paths implement has been dormant. This lands it:

- ElementStyleResolver.style() resolves per-element font/size/color with
  the key semantic the consumers encode: a config value counts as
  user-forced only when it differs from the schema default (the web UI
  bakes defaults into config.json on save), and untouched configs
  resolve to exactly the caller's classic values — byte-identical
  rendering, proven by of-the-day's committed goldens passing unchanged.
- defaults_from_schema_file parses both declaration forms (the compact
  x-style-elements map and hand-written customization blocks).
- expand_style_elements() expands x-style-elements into full config
  blocks; schema_manager.load_schema() applies it (guarded, no-op for
  schemas without the declaration) so the config form and defaults
  merging see the expanded UI.
- Fonts resolve cwd-independently with (path, size) caching; .bdf loads
  via freetype like FontManager; nothing in the module raises out of
  style().

Verified: 31 new unit tests; of-the-day's previously-skipped 9-test
spec suite now runs and passes; football's resolver tests pass (27);
music's 38 plugin tests pass; schema-manager suites pass (43).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 14:14:18 +00:00
Claude dc659940ab ci: enroll the core unit suites in a dedicated job
The existing workflow ran only the three plugin-harness suites; the
skin-system, font-manager, data-source, extractor, scroll-helper,
adaptive-layout, and loader-compat suites (266 tests) existed but never
ran in CI, so a refactor of src/base_classes or src/common could regress
them silently. Also enrolls the new sports characterization and
element-style suites landing in this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 14:08:18 +00:00
Claude 3f4e915af7 docs: seed CHANGELOG.md with the module-availability release discipline
The plugins monorepo's sunset rule ('delete a bundled fallback copy only
when the manifest floors on the first core release shipping the module')
needs core module additions recorded against version numbers. Seeds the
changelog at 3.1.0 and documents the discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 14:08:18 +00:00
Claude 1baebd2d09 fix(fonts): resolve asset paths against the install root, not the cwd
FontManager built its catalog from cwd-relative paths ('assets/fonts'),
so any process started outside the install root — the plugin safety
harness on CI being the recurring case — found no fonts and silently
degraded every plugin to PIL's default face. Several plugins grew
per-plugin workarounds for exactly this (countdown, text-display,
tide-display in the plugins monorepo).

Catalog population now falls back to the install root derived from this
module's location when the cwd-relative path is missing; behavior when
running from the install root is unchanged. Verified: resolve_font
returns the real FreeType face from a foreign cwd, and the full unit
suites (266 tests) pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
2026-08-01 14:08:17 +00:00