From 6e361e05ccbca931d53c1d353bc47a821bce6768 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:34:18 -0400 Subject: [PATCH] docs(sports): record B6 as done, and what running it found (#435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(sports): record where B6 stands, and why it is waiting The phase table had B4 as "next" and B5 as "after B4" while both had shipped, and described B6 as blocked on B4's gate — which is now merged and released. A plan that misreports which phase it is in is worse than no plan: the next person reads it and repeats finished work. Corrected, and three things that were only ever decided in conversation are now written down: * **B6 is deliberately held.** 3.2.0 published 2026-08-03; 3.1.0 ran nine months before it. B6's premise is that cores without the module are gone, and there is no release-asset count or install telemetry to show that. Running it now strands users on their current plugin versions. The gate that makes it safe is already built and tested — it is the calendar that is missing, and no amount of further code changes that. * **Stop adopting further shared modules** (data_sources, game_renderer, base_odds_manager) until B6 closes. Each adoption adds a copy to keep in step against a payoff contingent on B6. * **A B5 retrospective**, because "the adoption went fine" is not what happened: four of eight shipped with scroll mode broken on a 3.2.0 core. The bundled fallback did not protect against it — the break was on the modern path — which is an argument for the sunset, not against it. Records the ledger too: net negative on disk until B6 runs. Also replaces the "what's next" list, whose first five items were all done, with what actually remains. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit * docs(sports): stop a wrapped PR reference reading as a heading A line wrapped onto "#433), the newest manifest entry ...", which markdownlint reads as a malformed ATX heading (MD018). Reflowed so the line starts with "(#431, #433)" instead. Not the suggested fix: adding a space after the hash would have turned the PR reference into "# 433". The B5 safety claim raised alongside this was already corrected in ac44b5a. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 * docs(sports): scope the B5 safety claim to the fallback The heading read "B5 — adoption is safe by construction", which this same document disproves two sections later: four of the eight adopted plugins shipped with scroll mode broken on a 3.2.0 core and were repaired in plugins #251. The body was already careful -- it says fallback compatibility is what is guaranteed, and that correctness on a core which *does* ship the module needs object-level and scroll-mode validation. The heading was not, and a heading is what a reader scanning the plan actually takes away. Retitled to name both halves, with a sentence up front saying why the unqualified claim is false and pointing at the retrospective that shows it. The phase intro said "one of them is safe by construction and the other is not"; that now says what it actually means -- one cannot break a user on an old core, the other can. The second review point, MD018 on the ATX heading at line 409, does not reproduce: that line now begins "(#431, #433)" rather than "#433)", so there is no bare-hash heading. `grep -cE '^#+[^ #]'` returns 0 for the whole file. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW * docs(sports): re-check the hold, and close two items that are already done The remaining-work list had two entries that finished without the doc noticing, which is the failure mode this file exists to prevent. - The stale plugin-test tranche is gone. run_plugin_tests.py --all now reports 174 passed, 2 skipped, 0 failed across the whole fleet. Recorded how to re-check it too: these are standalone scripts, not a pytest suite, and one calls sys.exit(1) at import, so pointing pytest at a plugin directory collapses into an INTERNALERROR that looks nothing like the real state. - CLAUDE.md already says eight panel sizes. That leaves the hardware soaks as the only open item needing work rather than calendar time. B6's prerequisite is now built -- core test/test_sports_sunset_matrix.py (#505) -- so the phase table and the regression-test section say so, and the two modelling traps it had to work through are recorded for whoever touches it next: the copy-removed shape must be an unguarded import or the failure names scroll_display_legacy instead of the core module, and only the leaf module may be hidden because a pre-3.2.0 core still ships src/common/. The hold itself is re-checked and unchanged: v3.2.0 is still latest, __version__ is still 3.2.0, no 3.3.0, 23 days rather than the few months the gate asks for. Also worth stating plainly -- the core updates by git pull, not by downloading a release, so release-asset counts would not measure uptake even if we had them. Whatever unblocks this has to come from the store side. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit --- docs/SPORTS_UNIFICATION.md | 190 ++++++++++++++++++++++++++++++------- 1 file changed, 154 insertions(+), 36 deletions(-) diff --git a/docs/SPORTS_UNIFICATION.md b/docs/SPORTS_UNIFICATION.md index 4ef30e94..d783e15b 100644 --- a/docs/SPORTS_UNIFICATION.md +++ b/docs/SPORTS_UNIFICATION.md @@ -204,7 +204,7 @@ legacy compatibility rather than the mechanism. B0–B3 are merged and shipping in core 3.2.0. Everything that remains is **rollout**, and it splits into three phases with very different risk profiles. The original plan folded the last two together; they are separated here because -one of them is safe by construction and the other is not. +one of them cannot break a user on an old core and the other can. | Phase | Scope | Status | Gate | |---|---|---|---| @@ -212,9 +212,9 @@ one of them is safe by construction and the other is not. | **B1** | Promote the nine universal methods; convert `sports.py` → package | ✅ | Characterization suite green; no behavior change intended | | **B2** | `CelebrationMixin` + rotation strategies as opt-in capabilities | ✅ | Non-adopters have zero new code in their MRO; strategies checked against verbatim plugin transcriptions | | **B3** | Upstream the scroll **orchestration** layer as `src/common/sports_scroll.py`, reading `global_config['target_fps']` natively | ✅ | Content building stays per-sport | -| **B4** | Ship 3.2.0 *and* make version reporting trustworthy | ⏳ **next** | Tag, release, and `src.__version__` agree; compatibility gate merged | -| **B5** | Adoption — guarded core imports: three pilots, then the remaining six. **Bundled copies stay.** | after B4 | Per plugin: harness + goldens byte-identical, then a device soak | -| **B6** | Sunset — delete the bundled copies | **blocked** | B4's gate shipped *and* in users' hands (see below) | +| **B4** | Ship 3.2.0 *and* make version reporting trustworthy | ✅ | Released 2026-08-03; tag, release and `src.__version__` agree; compatibility gate merged (#428, #431, #433) | +| **B5** | Adoption — guarded core imports, all eight. **Bundled copies stay.** | ✅ | All eight adopted; harness byte-identical; see the B5 retrospective below — four shipped broken and were repaired in plugins #251 | +| **B6** | Sunset — delete the bundled copies | ✅ | Ran 2026-09-01, all eight. Floors at 3.2.0; the store refuses on all three routes in (#431/#433, #508, #510). See "B6 — what actually happened" | ### B4 — what "ship 3.2.0" actually requires @@ -276,13 +276,21 @@ default is the more restrictive. `compatible_versions`. No manifest still carries it, so there is nothing to migrate there.) -### B5 — adoption is safe by construction +### B5 — the *fallback* is safe by construction; the modern path is not + +The heading matters, because the unqualified version of this claim is false and +this document proves it two sections down: four of the eight adopted plugins +shipped with scroll mode broken on a 3.2.0 core. What is safe by construction is +narrower than "adoption". A plugin adopting core imports keeps its bundled copy and reaches it through the -guarded import (see the Upgradability table above). On a core that ships the -module the plugin uses core code; on one that doesn't it falls back and behaves -exactly as it does today. There is no version of this step that breaks a user, -which is why it does not wait for B6's gate. +guarded import (see the Upgradability table above). On a core that doesn't ship +the module the plugin falls back and behaves exactly as it does today. That +fallback compatibility — and only that — is safe by construction. On a core that +*does* ship the module, correctness is not automatic — object-level and scroll-mode validation +(building both classes and comparing, per the retrospective below) is required +to prove full behavior. There is no version of this step that breaks a user *on +an old core*, which is why it does not wait for B6's gate. The hockey scroll-display pilot is **already validated**: adopted against a core carrying 3.2.0, `scroll_display.py` went from 691 to 289 lines and all 16 harness @@ -311,7 +319,8 @@ been in users' hands long enough that the population running a core without it is small.** The bundled copies cost disk space; deleting them early costs scoreboards, silently. That trade is not close. -Before the first sunset, add a **compatibility regression test**. It has to +Before the first sunset, add a **compatibility regression test**. **Built:** +core `test/test_sports_sunset_matrix.py` (#505). It has to cover four cases, not one — B5's safety claim and B6's failure mode are different propositions and only the second is obvious: @@ -335,35 +344,144 @@ gate rather than trusting the failure to be noticed. The same suite should exercise the install/update gate, since it is the other half of the guarantee. +### B6 — what actually happened + +**Ran 2026-09-01, across all eight scoreboards.** Held from 2026-08-05 to +2026-09-01 on the argument below, which is kept because the reasoning applies to +the next module, not because it is still in force. + +**The hold, and why it lifted.** The stated gate was evidence of 3.2.0 uptake — +"a few months of it being the default download, or store-side install data". +That evidence never arrived and could not: the core updates by +`git pull --rebase`, so release-asset counts cannot measure uptake, and no +store-side telemetry exists. What changed instead is that the *risk* the gate +protected against was closed directly. The store now refuses a plugin whose +floor exceeds the running core on **all three** routes in: + +| route | gated by | +|---|---| +| `install_plugin` — every path that re-downloads, `_reinstall_with_rollback` included | #431, #433 | +| `update_plugin`'s git branch — pulls in place, re-downloads nothing | #508 | +| `install_from_url` — sideloading | #510 | + +With all three closed a pre-3.2.0 user cannot receive a sunset plugin at all; +they keep the version they already run. The population the hold existed to +protect is protected by refusal rather than by a bundled copy — which is what +the copy was standing in for. + +**What shipped.** Eight plugins, ~5,800 lines of frozen fallback deleted. Each: +copy removed, guarded import collapsed to a plain one, floor raised to 3.2.0, +`test_core_fallback.py` rewritten as `test_core_scroll.py` asserting the sunset +rather than the fallback. `scripts/check_scroll_adoption.py` gained +`sunset_violations` and a `SUNSET_PLUGINS` set naming all eight, so a +resurrected copy or a returned guard fails CI. + +Delivered as plugins #346 (hockey, later folded into #351), #349 (football), +#350 (baseball), #351 (the remaining six). + +**Two things found by doing it, both worth carrying forward:** + +- **Only one fallback held orchestration logic the core lacked.** baseball's + `_configure_scroll_helper` reinterpreted `scroll_speed` as pixels-per-*frame* + when `speed × delay` fell outside the 0.1–5.0 window — measured, 10–20× + faster than configured for a speed between 1.0 and 5.0. Standardised onto the + core's behaviour (honour the documented px/sec, clamp) rather than preserved. + Every other difference across the eight was a docstring, an unreachable + `scroll_helper is None` guard, or an equivalent diagnostic. +- **Two tests had been leaning on the guard without anyone knowing.** + `soccer/test_live_screens.py` stubbed `src` in a way that shadowed the core, + so its guarded import fell back and the test had been exercising the *frozen + copy* rather than the shipping class since B5. Before sunsetting anything else + that carries a guarded core import, grep for tests that stub `src`. + +**The floor-raising traps still apply** to any future sunset: four plugins +declare their floor top-level, where editing `versions[0]` is a silent no-op, +and the floor has three live spellings (`min_ledmatrix_version`, +`requires.min_ledmatrix_version`, `versions[].ledmatrix_min_version`, plus +deprecated `ledmatrix_min`). See +`src/plugin_system/compatibility.py:declared_min_version` for the resolution +order any floor-raising tool must reproduce — and note the name is **inverted** +between the top level and `versions[]`. + +**Still not adopted, deliberately:** `data_sources.py`, `game_renderer.py` and +`base_odds_manager.py`. The standing decision held them until B6 closed; it now +has, so they can be reconsidered — with B5's lesson applied, which is to build +the object and diff rendered output rather than trust a static check. + +### B5 retrospective — what the adoption actually cost + +Recorded because it is the evidence behind the two decisions above, and because +"the adoption went fine" is not what happened. + +**Four of the eight shipped with scroll mode broken** on a 3.2.0 core, and were +repaired in plugins-repo #251. The restructure lifted the content methods +verbatim but left the state they read off `self` behind: separator-icon +constants (hockey, basketball, lacrosse) and the game-renderer cache (afl). +hockey/basketball/lacrosse could not construct the scroll display at all; afl +raised inside `prepare_scroll_content`, which the core base *catches*, so its +only symptom was scroll mode silently drawing nothing. + +Three things are worth carrying forward: + +- **The bundled fallback did not protect anyone from this.** The break was on + the modern path, which the fallback never touches. Carrying the second copy + bought nothing against the actual defect while creating the divergence that + produced it. That is an argument *for* B6, not against it. +- **Every gate was green.** The safety harness renders the scoreboard screens, + not scroll mode; `test_core_fallback.py` checked that methods existed and that + their *globals* resolved, and `self.NHL_SEPARATOR_ICON` is an attribute read, + invisible to an AST scan for `Name` loads. The fix was to stop reasoning about + source and **build the object**: construct both classes on both paths, compare + the separator icons they end up with, and assert the adopted class ends up + with every instance attribute the bundled one sets. +- **Test what the change touches, not what is convenient to render.** Scroll + mode had no coverage because the harness could not reach it. A comparison + harness that renders the same games through both paths and diffs the pixels + needs no per-sport knowledge of the right answer, only that adopting core code + did not change it. + +**The ledger.** Before adoption, eight duplicated copies totalled 5,685 lines. +After adoption plus the frozen legacy copies it was 10,610; removing the dead +inline duplication (plugins #252) brought it to roughly 8,620. B6 would take it +to about 3,300 including the shared core module — some 2,400 fewer than before +this project started. **Until B6 runs, the adoption is net negative on disk**, +and its one delivered user-visible gain is that adopted plugins honour the +global `target_fps` instead of hardcoding ~100 FPS. + +### Decision: stop adopting further modules until B6 closes + +`data_sources.py` (9 copies), `game_renderer.py` (8) and `base_odds_manager.py` +are the obvious next candidates. **Do not adopt them yet.** Each adoption adds +carrying cost — a second copy to keep in step — against a payoff that is +contingent on B6, and B6 is gated on an installed base we cannot currently +measure. Consolidate what is already committed; revisit when B6 does. + ## What's next -In order. Each step is independently useful and independently revertible. +Steps 1–5 of the original plan are **done**: 3.2.0 is tagged and published with +a version number CI now asserts (#428), the compatibility gate is in +`install_plugin` and reads `compatible_versions` as well as the floor +(#431, #433), the newest manifest entry is required to use `ledmatrix_min_version` +(plugins #244), and all eight plugins have adopted the scroll orchestration +(plugins #245–#249, repaired in #251, tidied in #252). -1. **Tag and publish v3.2.0.** The code is already on `main` (`21825cbf`). - Nothing else blocks this, and it is what makes `ledmatrix_min_version: - "3.2.0"` refer to something real. -2. **Make the version number honest.** Have the release process assert that the - tag, the GitHub release, and `src.__version__` agree — a check in CI is - cheaper than the confusion of the last two releases. Then revisit the - `< 2.0.0` skip in `_warn_if_incompatible`, which currently silences the - warning for the users who most need it. -3. **Add the compatibility gate** to `StoreManager.install_plugin` and - `.update_plugin`: refuse a plugin whose declared floor exceeds - `src.__version__`, and surface the reason in the store UI rather than only - the log. This is the single change that turns the floor from documentation - into a guarantee, and B6 depends on it. -4. **Migrate the manifests** to `ledmatrix_min_version`, and reconcile them with - `compatible_versions` (see above — that field is the required, canonical one, - and the gate does not read it yet). Currently 28 plugins spell the floor both - ways across their `versions[]` entries, 12 use only the old spelling, and 2 - only the new. Scope the sweep to the nine sports plugins if a 42-plugin - version-bump wave isn't worth it — but the `compatible_versions` half has to - cover every manifest the gate can refuse, or define explicit legacy handling, - before the gate is allowed to block anything. -5. **Run B5 adoption** — hockey, soccer, football, then the remaining six. - Bundled copies stay. Byte-identical harness output per plugin, then a soak. -6. **Only then plan B6**, with the compatibility regression test described above - in CI first. +What actually remains, smallest first: + +1. **Soak the adoptions on hardware.** football and hockey have been run on a + live rig through real games; baseball was watched through one earlier. The + rest are proven by harness, unit tests and pixel comparison. Out-of-season + sports cannot be soaked until their season starts. When you do, **check the + rig's `*_display_mode` first** — a board in `switch` mode will happily load a + sunset plugin and tell you nothing about the scroll code the sunset changed. +2. **Cut 3.3.0.** Not required by B6 — its floors are 3.2.0, which is released — + but `calendar` 1.2.3 floors at 3.3.0 for the device-authorization endpoints + that landed after 3.2.0, so it is un-installable until the release exists. +3. **Reconsider the held modules** (`data_sources.py`, `game_renderer.py`, + `base_odds_manager.py`) now that the sunset has closed. `game_renderer.py` is + the largest single duplication left: ~11,500 lines across eight plugins, with + ~36,500 more in the eight `sports.py`. Note that core already ships + `src/base_classes/sports/` (~143KB, promoted in B1/B2) that **no plugin + imports** — check whether it has drifted before treating it as the target. ## How to keep this project healthy