Skip to content

feat(football): sunset the bundled scroll fallback, floor at 3.2.0 - #349

Merged
ChuckBuilds merged 1 commit into
mainfrom
sunset/football-scroll-fallback
Sep 2, 2026
Merged

feat(football): sunset the bundled scroll fallback, floor at 3.2.0#349
ChuckBuilds merged 1 commit into
mainfrom
sunset/football-scroll-fallback

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

The second B6 sunset, after hockey (#346). Stacked on #346, which is stacked on #333 — retarget down the chain as they land.

Same shape as hockey: scroll_display_legacy.py deleted, the guarded import collapsed to a plain one, manifest floored at 3.2.0. Keeping the try/except with nothing behind it would name the missing scroll_display_legacy rather than the core module actually absent — the single log line a user gets before the scoreboard stops appearing. 709 lines removed.

Football is the one that isn't a pure deletion

Everywhere else the frozen copy and the live path are method-for-method identical. Here _default_game_card_width diverged: the frozen one returns max(128, display_height * 2 + 40), the adopted one measures the score gap with a throwaway GameRenderer and converges — which is what stopped the score being drawn across the logos on tall cards.

Rather than assert that was fine, I built both classes and compared across every supported panel:

panel legacy core delta
64×32 128 128 same
128×32 128 128 same
256×32 128 128 same
64×64 168 176 +8
128×64 168 176 +8
256×64 168 176 +8
128×96 232 240 +8
256×128 296 304 +8

Identical on every 32-tall panel, 8px wider on taller ones, in classic and adaptive layout alike.

And the population that could observe it is exactly the population that will not receive this version. The fallback was only ever reached on a pre-3.2.0 core — everyone on 3.2.0+ has been on the measured path since 2.29.0. Those pre-3.2.0 users keep 2.29.3, because the new floor stops this version reaching them. So no board changes what it draws.

It is recorded in the manifest notes regardless, because "removed dead code" would be false and the next person deserves the real answer.

Tests

test_core_fallback.pytest_core_scroll.py — the same rewrite hockey got. The two files were byte-identical but for the Run: path, so this is that rewrite with one substitution. It asserts the sunset rather than the fallback: top-level unguarded import, no copy present or imported, base class by identity, an old core failing on exactly src.common.sports_scroll, and the manifest floor.

SUNSET_PLUGINS grows to two — which is the point of listing it rather than inferring it. Adding the id is the same act as deleting the file.

Verification

  • Separator-icon constants and every method survive the de-indent byte-for-byte (NFL_SEPARATOR_ICON, NCAA_FB_SEPARATOR_ICON, SCROLL_LEAGUE_KEYS, _SCHEMA_CARD_WIDTH) — the exact thing B5 lost. Review the collapse with git diff -w; most of it is the de-indent out of the old else: block.
  • All 24 safety-harness renders pass — eight panel sizes × three modes (nfl_live, nfl_recent, nfl_upcoming).
  • All four repo gates pass: check_scroll_adoption (+ its 17-case self-test), check_manifest_version_fields, check_module_collisions.
  • Football's suite: 35 passed, 1 skipped, 1 failed.

About that one failure

test_favorite_live_boost.py fails identically on this branch's base and on origin/main, is untouched by this PR, and is already fixed in #348. It is a stale stub missing show_odds — not the spoiler leak its name suggests. Confirmed by stashing this PR's changes and re-running: same failure, same message.

Next

NFL and NCAA are in season, so unlike hockey this one can be soaked against live games rather than a single upcoming fixture. Happy to run that on HDPi on request.

🤖 Generated with Claude Code

https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

scroll_display_legacy.py is deleted and the guarded import of the core's
src.common.sports_scroll collapses to a plain one, with the manifest floored at
3.2.0 to match. Keeping the try/except with nothing behind it would name the
missing scroll_display_legacy rather than the core module actually absent,
which is the single log line a user gets before the scoreboard stops appearing.
709 lines removed.

Football is the one plugin where this is not a pure deletion, and that deserved
measuring rather than asserting. Everywhere else the frozen copy and the live
path are method-for-method identical; here _default_game_card_width diverged.
The frozen one returns max(128, display_height * 2 + 40); the adopted one
measures the score gap with a throwaway GameRenderer and converges, which is
what stopped the score being drawn across the logos on tall cards.

Built both classes and compared across every supported panel:

    panel      legacy    core   delta
    64x32         128     128   same
    128x32        128     128   same
    256x32        128     128   same
    64x64         168     176     +8
    128x64        168     176     +8
    256x64        168     176     +8
    128x96        232     240     +8
    256x128       296     304     +8

Identical on every 32-tall panel, 8px wider on taller ones, in classic and
adaptive layout alike.

That difference only ever reached users on a pre-3.2.0 core, because everyone
on 3.2.0 or newer has been on the measured path since 2.29.0 -- the fallback
was never the modern path's behaviour. And those users keep the version they
have, since the new floor stops this one reaching them. So the population that
could observe the change is exactly the population that will not receive it: no
board changes what it draws. Recorded in the manifest notes anyway, because
"removed dead code" would be false and the next person deserves the real
answer.

test_core_fallback.py -> test_core_scroll.py, the same rewrite hockey got in
#346 (the two files were byte-identical but for the Run: path, so this is that
rewrite with one substitution). It asserts the sunset rather than the fallback:
the import is top-level and unguarded, no copy exists or is imported, the base
is the core class by identity, an old core fails naming exactly
src.common.sports_scroll, and the manifest floors at 3.2.0 or above.

check_scroll_adoption.py gains sunset_violations and SUNSET_PLUGINS, the same
gate #346 adds for hockey. offending_classes is left byte-identical so its
eleven pinned cases keep meaning what they mean. The existing check asks
whether the fallback was INLINED and structurally cannot ask whether it still
EXISTS -- it opens scroll_display.py and nothing else -- so a resurrected file
or a returned guard would both pass it silently. #346 and this PR each name
their own plugin; whichever lands second resolves a one-line conflict in the
set.

Verified: separator-icon constants and every method survive the de-indent
byte-for-byte (NFL_SEPARATOR_ICON, NCAA_FB_SEPARATOR_ICON, SCROLL_LEAGUE_KEYS,
_SCHEMA_CARD_WIDTH); 24 of 24 safety-harness renders pass across eight panel
sizes and three modes; the four repo gates pass; football's suite is 40 passed,
1 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
@ChuckBuilds
ChuckBuilds force-pushed the sunset/football-scroll-fallback branch from 417b360 to f04575e Compare September 1, 2026 15:35
@ChuckBuilds
ChuckBuilds changed the base branch from sunset/hockey-scroll-fallback to main September 1, 2026 15:35
@ChuckBuilds ChuckBuilds closed this Sep 1, 2026
@ChuckBuilds ChuckBuilds reopened this Sep 1, 2026
@ChuckBuilds

Copy link
Copy Markdown
Owner Author

Hardware soak — passed on HDPi, against live games

Deployed 3.1.0 to hdpi (256×64, chain 2), core at v3.2.0-53-gcbc540a6. Unlike hockey — which is out of season and could only be shown a single upcoming fixture — NFL and NCAA are playing, so this exercised real recent, upcoming and live paths.

Loads with no fallback present:

plugin_football_scoreboard - Scroll display manager initialized
src.plugin_system.plugin_manager - Loaded plugin: football-scoreboard
NFLRecentManager - Fetched 32 events 20260818-20260908

No ModuleNotFoundError, no PluginState.ERROR, no football errors. scroll_display_legacy.py is absent on the device.

Real content, rotating, through the scroll path. The panel drew an actual NFL final — Saints 27–24 Cowboys, 8/29, with the odds line (O/U 48.5, −3.5) — then transitioned across further games (MIN @ DEN, NO @ DAL), and NCAA rendered too (TNST vs UGA, 9/5 3:00PM). The controller reports FPS check for nfl_recent - has_enable_scrolling: True, needs_high_fps: True throughout, so it is going through the high-FPS scroll path this PR changed.

Worth noting for the card-width question above: on the 64-tall panel the score sits cleanly between the two logos with the odds legible either side — which is the arrangement the measured _default_game_card_width exists to produce, and the one the frozen 168px version was 8px short of.

On-device test run, against the real core rather than a checkout:

core scroll import tests (post-sunset)
PASS test_the_core_import_is_unguarded
PASS test_the_bundled_copy_is_gone
PASS test_the_classes_are_defined_at_module_level
PASS test_the_base_is_the_core_class
PASS test_the_manifest_floors_at_the_release_that_ships_the_module
PASS test_content_methods_can_resolve_what_they_use
PASS test_scroll_display_constructs
PASS test_an_old_core_fails_naming_the_module
All tests passed.

A CI note worth checking on other stacked PRs

The first CI run on this PR reported failure, and gh pr checks showed only Codacy and a CLEAN mergeable state — both misleading. The run had executed against 417b360, the pre-rebase commit from when this was stacked on #346; the force-push to f04575e never triggered a new run, and the stale result stayed attached.

Closing and reopening the PR re-triggered it. Both jobs now pass on the actual head — confirmed by checking headSha rather than trusting the summary:

Plugin Structure  success
Plugin Safety     success
ran on f04575e -> success

If you force-push a rebased branch here, it is worth verifying gh run view <id> --json headSha matches the branch head before believing a green — or a red.

Caveats

  • One rig. devpi has been unreachable throughout.
  • HDPi still has football 3.1.0 installed from this test, replacing the 2.29.4 it had.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 09716ab3-4f85-482e-95dc-f984327e5fff


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ChuckBuilds

Copy link
Copy Markdown
Owner Author

Deeper soak — and a gap in the first one worth recording

Re-tested on HDPi after ~2.5 hours of continuous running. Zero football errors in that window. The only two errors on the box are nhl_ background-fetch callback failures, recurring hourly (12:36, 13:36) — the joiner bug fixed in core #509, unrelated to this PR and present before it.

The first soak did not actually exercise scroll mode

Worth being explicit, because I reported that soak as passing. HDPi's football config has live/recent/upcoming_display_mode all set to switch, not scroll. So what was on the panel was the full-screen scorebug — while scroll_display.py is the only file this PR changes.

Scroll display manager initialized proves the manager is constructed (which test_scroll_display_constructs already covers), and FPS check … needs_high_fps: True reports what the mode supports, not what is rendering. Neither shows the scroll renderer running. That is precisely the trap the B5 retrospective calls out — the safety harness renders scoreboard screens, not scroll mode, and four plugins shipped broken behind exactly that gap.

Redone with scroll actually on

Switched NFL's three modes to scroll, restarted, and confirmed on the panel:

plugin_football_scoreboard - Scroll display manager initialized
plugin_football_scoreboard - High-FPS scrolling enabled for football scoreboard
src.plugin_system.plugin_manager - Loaded plugin: football-scoreboard

Captured successive frames and checked they differ by hash rather than eyeballing one: two identical frames then a distinct third, advancing through real content — an Ohio State upcoming card (9/5 12:30PM) and the Saints 27–24 Cowboys final with its odds line. NCAA scrolled too (ncaa_fb_recent). No ModuleNotFoundError, no PluginState.ERROR, no football errors at any point.

So the scroll path — the one thing this PR touches — now has hardware evidence behind it, not just construction evidence.

Config restored to switch afterwards and the service restarted clean; the rig is back exactly as it was, with a backup left at /tmp/config.json.presoak.

Suggestion for the remaining six

Check *_display_mode before soaking each one. A rig in switch mode will happily load a sunset plugin and tell you nothing about the code the sunset changed.

@ChuckBuilds
ChuckBuilds merged commit 2c95b6e into main Sep 2, 2026
4 checks passed
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
…e core (#350)

The third B6 sunset, after hockey (#346) and football (#349). Same shape:
scroll_display_legacy.py deleted, the guarded import of the core's
src.common.sports_scroll collapsed to a plain one, manifest floored at 3.2.0.
Keeping the try/except with nothing behind it would name the missing
scroll_display_legacy rather than the core module actually absent, which is the
single log line a user gets before the scoreboard stops appearing. 764 lines
removed.

Baseball is the one plugin whose frozen copy carried orchestration logic the
core does not have, and that is the point of doing it deliberately rather than
by deletion. Its _configure_scroll_helper had an extra branch: when
scroll_speed * scroll_delay fell outside the 0.1-5.0 pixels-per-frame window
but scroll_speed alone sat inside it, it reinterpreted scroll_speed as
pixels-per-FRAME rather than the pixels-per-second the setting is documented
as. Verified by running both implementations:

    scroll_speed  delay   legacy   core
    50.0          0.01     0.5     0.5    (the default -- agree)
    1.0           0.01     1.0     0.1    10x
    2.0           0.01     2.0     0.1    20x
    0.5           0.01     0.5     0.1     5x

Checked across all eight lineages: baseball's was the only copy with it.

The core's behaviour is the one to keep. The branch silently ignored the unit
the setting is defined in and ran an order of magnitude faster than asked; the
core honours the configured pixels-per-second and clamps to the same window,
which is what every other scoreboard already does. Re-measured after the
collapse: baseball now matches the core exactly at every point in the range
above.

In practice this reaches nobody. The branch only ever ran on a pre-3.2.0 core;
everyone on 3.2.0 or newer has been on the core path since 1.22.0, and the new
floor stops this version reaching the rest. Recorded in the manifest anyway,
because a silently retired behaviour is worse than a documented one.

test_core_fallback.py -> test_core_scroll.py, the same rewrite hockey and
football got. SUNSET_PLUGINS grows to three.

Verified: all three separator-icon constants and every method survive the
de-indent byte-for-byte (MLB_SEPARATOR_ICON, MILB_SEPARATOR_ICON,
NCAA_BASEBALL_SEPARATOR_ICON, SCROLL_LEAGUE_KEYS, _SCHEMA_CARD_WIDTH); 24 of 24
safety-harness renders pass across eight panel sizes; the four repo gates pass;
baseball's suite is 26 passed, 0 skipped, 0 failed.


Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

Co-authored-by: Claude <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
…#351)

* feat(sports): sunset the bundled scroll fallback in the last five

afl, basketball, lacrosse, nrl and soccer. Same shape as hockey (#346),
football (#349) and baseball (#350): scroll_display_legacy.py deleted, the
guarded import of the core's src.common.sports_scroll collapsed to a plain one,
manifests floored at 3.2.0. 3,622 lines of frozen copy removed.

Checked before deleting, not after. Every orchestration method in all five
frozen copies was diffed against the core's, looking for logic the core lacks:

- `if not self.scroll_helper` guards -- unreachable in core, which imports
  ScrollHelper unguarded and always constructs one. Legacy needed them because
  its own import was guarded and it sets self.scroll_helper = None.
- `get_dynamic_duration`'s `return 60` fallback -- same unreachable guard.
- `_scroll_start_time` -- legacy reads it, core does not, but only to compute
  an average-FPS debug line that core produces from _fps_sample_start instead.
- `get_current_leagues` returning `.copy()` vs `list()` -- identical.
- `_log_scroll_progress` throttling -- core has it.
- `clear()` -- core resets strictly more state.

None is a behaviour the core is missing. Baseball's px/frame heuristic was the
only real one across all eight, and it was handled in #350.

Two tests were relying on the guard, and both are worth naming because the
sunset is what exposed them:

- lacrosse/test_lacrosse_plugin.py stubs the host `src` modules so the plugin
  imports without a core, and the list did not include src.common.sports_scroll
  -- the guard used to swallow that. Stubbed now, with real classes rather than
  None, since ScrollDisplay subclasses one at module level.
- soccer/test_live_screens.py installs a stub `src` package to fake
  src.logo_downloader, which SHADOWED the core. So its guarded import had been
  falling back, and the test has been exercising the frozen copy rather than
  the class that ships -- since B5. The stub now carries a __path__ into the
  real core so only logo_downloader is faked. Driving the real class then
  surfaced a missing display_width on its hand-built object, which the legacy
  path never read.

Verified: every method and class constant survives the de-indent byte-for-byte
in all five, separator icons included; 112 safety-harness renders pass (24 each
for afl, basketball, nrl and soccer, 16 for lacrosse); five repo gates pass;
fleet is 225 passed, 2 skipped, 0 failed.

SUNSET_PLUGINS names seven. Hockey is the eighth and its sunset (#346) merged
into #333 rather than main, so it arrives with that branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

* feat(hockey): sunset the bundled scroll fallback, completing B6

Hockey joins this PR so it is not stranded. Its sunset previously lived only in
#346, which merged into #333's branch rather than main -- and #333 is
superseded by #353, which does not carry the sunset. Closing #333 without this
would leave hockey the one scoreboard of eight still shipping a fallback.

Same change as the other seven: scroll_display_legacy.py deleted (703 lines),
the guarded import collapsed to a plain one, floor raised to 3.2.0.
test_core_fallback.py -> test_core_scroll.py, identical to afl's but for the
Run: path.

Bumped to 1.22.0 rather than 1.21.0 so it clears every version hockey currently
holds anywhere: 1.20.3 on main, 1.21.0 on #353's branch. A floor is only
meaningful on a version that can actually supersede what users have.

SUNSET_PLUGINS now names all eight, which is the point of listing it rather
than inferring it -- the set is a statement that the sunset holds, and it is
now true of the whole fleet.

Also brings docs/plugin-development/08-shared-sports-code.md up to date. Its
sunset rule still read "Until condition 3 holds, keep the guarded
try-core/except-local import", which was correct in August and is now the
opposite of what the fleet does. Condition 3 holds for src.common.sports_scroll:
the store refuses on all three routes in -- install_plugin (core #431/#433),
the git-pull branch of update_plugin (#508) and install_from_url (#510). The
rule now says to keep the guard for modules that have NOT been through a
sunset, to drop it along with the copy for those that have, and to raise the
floor in the same commit as the deletion. The instruction to keep it in step
with the core doc "in the same PR" is corrected too: they are in different
repositories, so that was never possible.

Verified: all four separator-icon constants and every method survive the
de-indent byte-for-byte; hockey 19 passed, 0 failed; 16 safety-harness renders
pass; five repo gates pass, with check_scroll_adoption now reporting 8 sunset
plugins free of a fallback; fleet 225 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

---------

Co-authored-by: Claude <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
* feat(football): sunset the bundled scroll fallback, floor at 3.2.0

scroll_display_legacy.py is deleted and the guarded import of the core's
src.common.sports_scroll collapses to a plain one, with the manifest floored at
3.2.0 to match. Keeping the try/except with nothing behind it would name the
missing scroll_display_legacy rather than the core module actually absent,
which is the single log line a user gets before the scoreboard stops appearing.
709 lines removed.

Football is the one plugin where this is not a pure deletion, and that deserved
measuring rather than asserting. Everywhere else the frozen copy and the live
path are method-for-method identical; here _default_game_card_width diverged.
The frozen one returns max(128, display_height * 2 + 40); the adopted one
measures the score gap with a throwaway GameRenderer and converges, which is
what stopped the score being drawn across the logos on tall cards.

Built both classes and compared across every supported panel:

    panel      legacy    core   delta
    64x32         128     128   same
    128x32        128     128   same
    256x32        128     128   same
    64x64         168     176     +8
    128x64        168     176     +8
    256x64        168     176     +8
    128x96        232     240     +8
    256x128       296     304     +8

Identical on every 32-tall panel, 8px wider on taller ones, in classic and
adaptive layout alike.

That difference only ever reached users on a pre-3.2.0 core, because everyone
on 3.2.0 or newer has been on the measured path since 2.29.0 -- the fallback
was never the modern path's behaviour. And those users keep the version they
have, since the new floor stops this one reaching them. So the population that
could observe the change is exactly the population that will not receive it: no
board changes what it draws. Recorded in the manifest notes anyway, because
"removed dead code" would be false and the next person deserves the real
answer.

test_core_fallback.py -> test_core_scroll.py, the same rewrite hockey got in
#346 (the two files were byte-identical but for the Run: path, so this is that
rewrite with one substitution). It asserts the sunset rather than the fallback:
the import is top-level and unguarded, no copy exists or is imported, the base
is the core class by identity, an old core fails naming exactly
src.common.sports_scroll, and the manifest floors at 3.2.0 or above.

check_scroll_adoption.py gains sunset_violations and SUNSET_PLUGINS, the same
gate #346 adds for hockey. offending_classes is left byte-identical so its
eleven pinned cases keep meaning what they mean. The existing check asks
whether the fallback was INLINED and structurally cannot ask whether it still
EXISTS -- it opens scroll_display.py and nothing else -- so a resurrected file
or a returned guard would both pass it silently. #346 and this PR each name
their own plugin; whichever lands second resolves a one-line conflict in the
set.

Verified: separator-icon constants and every method survive the de-indent
byte-for-byte (NFL_SEPARATOR_ICON, NCAA_FB_SEPARATOR_ICON, SCROLL_LEAGUE_KEYS,
_SCHEMA_CARD_WIDTH); 24 of 24 safety-harness renders pass across eight panel
sizes and three modes; the four repo gates pass; football's suite is 40 passed,
1 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

* feat(baseball): sunset the bundled scroll fallback, standardise on the core

The third B6 sunset, after hockey (#346) and football (#349). Same shape:
scroll_display_legacy.py deleted, the guarded import of the core's
src.common.sports_scroll collapsed to a plain one, manifest floored at 3.2.0.
Keeping the try/except with nothing behind it would name the missing
scroll_display_legacy rather than the core module actually absent, which is the
single log line a user gets before the scoreboard stops appearing. 764 lines
removed.

Baseball is the one plugin whose frozen copy carried orchestration logic the
core does not have, and that is the point of doing it deliberately rather than
by deletion. Its _configure_scroll_helper had an extra branch: when
scroll_speed * scroll_delay fell outside the 0.1-5.0 pixels-per-frame window
but scroll_speed alone sat inside it, it reinterpreted scroll_speed as
pixels-per-FRAME rather than the pixels-per-second the setting is documented
as. Verified by running both implementations:

    scroll_speed  delay   legacy   core
    50.0          0.01     0.5     0.5    (the default -- agree)
    1.0           0.01     1.0     0.1    10x
    2.0           0.01     2.0     0.1    20x
    0.5           0.01     0.5     0.1     5x

Checked across all eight lineages: baseball's was the only copy with it.

The core's behaviour is the one to keep. The branch silently ignored the unit
the setting is defined in and ran an order of magnitude faster than asked; the
core honours the configured pixels-per-second and clamps to the same window,
which is what every other scoreboard already does. Re-measured after the
collapse: baseball now matches the core exactly at every point in the range
above.

In practice this reaches nobody. The branch only ever ran on a pre-3.2.0 core;
everyone on 3.2.0 or newer has been on the core path since 1.22.0, and the new
floor stops this version reaching the rest. Recorded in the manifest anyway,
because a silently retired behaviour is worse than a documented one.

test_core_fallback.py -> test_core_scroll.py, the same rewrite hockey and
football got. SUNSET_PLUGINS grows to three.

Verified: all three separator-icon constants and every method survive the
de-indent byte-for-byte (MLB_SEPARATOR_ICON, MILB_SEPARATOR_ICON,
NCAA_BASEBALL_SEPARATOR_ICON, SCROLL_LEAGUE_KEYS, _SCHEMA_CARD_WIDTH); 24 of 24
safety-harness renders pass across eight panel sizes; the four repo gates pass;
baseball's suite is 26 passed, 0 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

* feat(sports): sunset the bundled scroll fallback in the remaining six (#351)

* feat(sports): sunset the bundled scroll fallback in the last five

afl, basketball, lacrosse, nrl and soccer. Same shape as hockey (#346),
football (#349) and baseball (#350): scroll_display_legacy.py deleted, the
guarded import of the core's src.common.sports_scroll collapsed to a plain one,
manifests floored at 3.2.0. 3,622 lines of frozen copy removed.

Checked before deleting, not after. Every orchestration method in all five
frozen copies was diffed against the core's, looking for logic the core lacks:

- `if not self.scroll_helper` guards -- unreachable in core, which imports
  ScrollHelper unguarded and always constructs one. Legacy needed them because
  its own import was guarded and it sets self.scroll_helper = None.
- `get_dynamic_duration`'s `return 60` fallback -- same unreachable guard.
- `_scroll_start_time` -- legacy reads it, core does not, but only to compute
  an average-FPS debug line that core produces from _fps_sample_start instead.
- `get_current_leagues` returning `.copy()` vs `list()` -- identical.
- `_log_scroll_progress` throttling -- core has it.
- `clear()` -- core resets strictly more state.

None is a behaviour the core is missing. Baseball's px/frame heuristic was the
only real one across all eight, and it was handled in #350.

Two tests were relying on the guard, and both are worth naming because the
sunset is what exposed them:

- lacrosse/test_lacrosse_plugin.py stubs the host `src` modules so the plugin
  imports without a core, and the list did not include src.common.sports_scroll
  -- the guard used to swallow that. Stubbed now, with real classes rather than
  None, since ScrollDisplay subclasses one at module level.
- soccer/test_live_screens.py installs a stub `src` package to fake
  src.logo_downloader, which SHADOWED the core. So its guarded import had been
  falling back, and the test has been exercising the frozen copy rather than
  the class that ships -- since B5. The stub now carries a __path__ into the
  real core so only logo_downloader is faked. Driving the real class then
  surfaced a missing display_width on its hand-built object, which the legacy
  path never read.

Verified: every method and class constant survives the de-indent byte-for-byte
in all five, separator icons included; 112 safety-harness renders pass (24 each
for afl, basketball, nrl and soccer, 16 for lacrosse); five repo gates pass;
fleet is 225 passed, 2 skipped, 0 failed.

SUNSET_PLUGINS names seven. Hockey is the eighth and its sunset (#346) merged
into #333 rather than main, so it arrives with that branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

* feat(hockey): sunset the bundled scroll fallback, completing B6

Hockey joins this PR so it is not stranded. Its sunset previously lived only in
#346, which merged into #333's branch rather than main -- and #333 is
superseded by #353, which does not carry the sunset. Closing #333 without this
would leave hockey the one scoreboard of eight still shipping a fallback.

Same change as the other seven: scroll_display_legacy.py deleted (703 lines),
the guarded import collapsed to a plain one, floor raised to 3.2.0.
test_core_fallback.py -> test_core_scroll.py, identical to afl's but for the
Run: path.

Bumped to 1.22.0 rather than 1.21.0 so it clears every version hockey currently
holds anywhere: 1.20.3 on main, 1.21.0 on #353's branch. A floor is only
meaningful on a version that can actually supersede what users have.

SUNSET_PLUGINS now names all eight, which is the point of listing it rather
than inferring it -- the set is a statement that the sunset holds, and it is
now true of the whole fleet.

Also brings docs/plugin-development/08-shared-sports-code.md up to date. Its
sunset rule still read "Until condition 3 holds, keep the guarded
try-core/except-local import", which was correct in August and is now the
opposite of what the fleet does. Condition 3 holds for src.common.sports_scroll:
the store refuses on all three routes in -- install_plugin (core #431/#433),
the git-pull branch of update_plugin (#508) and install_from_url (#510). The
rule now says to keep the guard for modules that have NOT been through a
sunset, to drop it along with the copy for those that have, and to raise the
floor in the same commit as the deletion. The instruction to keep it in step
with the core doc "in the same PR" is corrected too: they are in different
repositories, so that was never possible.

Verified: all four separator-icon constants and every method survive the
de-indent byte-for-byte; hockey 19 passed, 0 failed; 16 safety-harness renders
pass; five repo gates pass, with check_scroll_adoption now reporting 8 sunset
plugins free of a fallback; fleet 225 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

---------

Co-authored-by: Claude <noreply@anthropic.com>

* fix(tests): drop a dead import, and name the missing core where it is missed

Two review findings, both valid.

**`import types` was unused** in every copy of test_core_scroll.py. Inherited
rather than introduced: the original test_core_fallback.py never used it
either, and football's copy already on main carries it too. Removed from all
eight, football's included, so the eight stay byte-identical bar the `Run:`
path -- that identity is the property that makes them one rewrite replicated
rather than eight files to keep in step. It was the only F401 in the set.

**test_live_screens.py stubbed `src` even when it could not find a core.** The
stub only receives a `__path__` when discovery succeeds; without one it shadows
the real package, and scroll_display's now-unguarded import fails with
"'src' is not a package" -- naming `src` rather than the core module, which is
exactly the misleading symptom the comment three lines above warns about. It
now says so at the discovery point instead.

Skips rather than fails, exit 2 per run_plugin_tests.py's convention: no core
on the path is a "cannot run here", not a broken plugin. Verified both ways --
with a core the file passes as before, without one it exits 2 and the message
names the real cause and the fix.

Fleet 246 passed, 2 skipped, 0 failed; four gates pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants