From df8f69af30e85e58e6dca127197e7ff569107980 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Wed, 5 Aug 2026 14:14:00 -0400 Subject: [PATCH 1/5] docs(sports): record where B6 stands, and why it is waiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/SPORTS_UNIFICATION.md | 140 +++++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 30 deletions(-) diff --git a/docs/SPORTS_UNIFICATION.md b/docs/SPORTS_UNIFICATION.md index 4e731278..74cecefd 100644 --- a/docs/SPORTS_UNIFICATION.md +++ b/docs/SPORTS_UNIFICATION.md @@ -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 | **blocked, deliberately** | 3.2.0 *in users' hands*. Released 2026-08-03; there is no adoption data yet. See "B6 — the decision as of 2026-08-05" | ### B4 — what "ship 3.2.0" actually requires @@ -321,35 +321,115 @@ 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 — the decision as of 2026-08-05 + +**Do not run B6 yet. Do not abandon it either.** The blocker is no longer +technical; it is calendar time, and it is the one thing here that cannot be +worked around by writing more code. + +**Why not yet.** 3.2.0 was published **2026-08-03**. Its predecessor 3.1.0 ran +for nine months. B6's entire safety argument is "cores without +`src.common.sports_scroll` are gone", and two days after release that is not +close to true. There are no release assets to count and no install telemetry, so +we cannot demonstrate otherwise — and that absence of evidence *is* the answer. +Executing B6 now would strand essentially the whole user base on their current +plugin versions. + +**What is already done and waiting.** The hard part is built and tested. The +install gate refuses a plugin whose floor exceeds the core's version, and +refuses one whose floor is above 2.0.0 when the core reports an untrustworthy +version — so a v3.1.0-release user (who reports `1.0.0`) keeps a working plugin +instead of receiving one that cannot load. Every adopted plugin has a +`test_core_fallback.py` covering both paths. + +**What would unblock it.** Evidence of 3.2.0 uptake — a few months of it being +the default download, or store-side install data if that is ever added. Revisit +then, not on a schedule. + +**When it happens, remember:** 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. + +### 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. - -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. +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). + +What actually remains, smallest first: + +1. **Nothing on the critical path.** B6 is the only remaining phase and it is + waiting on calendar time, not on work. Resist the urge to fill the gap by + adopting more modules — see the decision above. +2. **The stale plugin-test tranche** — 5 failures across baseball, hockey and + basketball, all pre-existing API drift in the plugins' own older tests + (`plugin.initialized`, `CacheManager(config_manager=...)`, a bare + `cache_manager` import, `MockLogger.setLevel`, `BasketballPluginManager`). + None are scroll-related. They make the suite noisy, which is how a real + failure gets ignored. +3. **Soak the remaining adoptions on hardware.** Only baseball has been watched + through a live game, and hockey has been loaded on devpi. The other six are + proven by harness, unit tests and pixel comparison — not by a live match. + Out-of-season sports cannot be soaked until their season starts. +4. **`CLAUDE.md` in the plugins repo says four panel sizes; the harness renders + eight.** A one-line doc fix, and the discrepancy has already produced one + false review finding. +5. **Then, when the evidence supports it, B6** — with the four-case + compatibility regression test above in CI first. ## How to keep this project healthy From ac44b5a6f558c8dea27070bf5015c1eb3861960b Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:59:54 +0000 Subject: [PATCH 2/5] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- docs/SPORTS_UNIFICATION.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/SPORTS_UNIFICATION.md b/docs/SPORTS_UNIFICATION.md index 74cecefd..7130cdb3 100644 --- a/docs/SPORTS_UNIFICATION.md +++ b/docs/SPORTS_UNIFICATION.md @@ -265,10 +265,13 @@ migrate there.) ### B5 — adoption is safe by construction 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 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 From ed2b81a4fb50c0514aaa76788ed3ba59bf295cd8 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Tue, 11 Aug 2026 12:37:18 -0400 Subject: [PATCH 3/5] 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_UNIFICATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SPORTS_UNIFICATION.md b/docs/SPORTS_UNIFICATION.md index 7130cdb3..c88a635b 100644 --- a/docs/SPORTS_UNIFICATION.md +++ b/docs/SPORTS_UNIFICATION.md @@ -408,8 +408,8 @@ measure. Consolidate what is already committed; revisit when B6 does. 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` +`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). From cc6a75712c42d8e726b9939a86060be43d9b190c Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 21 Aug 2026 16:20:12 -0400 Subject: [PATCH 4/5] docs(sports): scope the B5 safety claim to the fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_UNIFICATION.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/SPORTS_UNIFICATION.md b/docs/SPORTS_UNIFICATION.md index c88a635b..143e2f74 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 | |---|---|---|---| @@ -262,13 +262,18 @@ 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 doesn't ship the module the plugin falls back and behaves exactly as it does today. That -fallback compatibility is safe by construction. On a core that *does* ship the -module, correctness is not automatic — object-level and scroll-mode validation +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. From c2ace903535b1d0589465678ffc39d86b05b973f Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Wed, 26 Aug 2026 10:33:31 -0400 Subject: [PATCH 5/5] 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 --- docs/SPORTS_UNIFICATION.md | 47 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/SPORTS_UNIFICATION.md b/docs/SPORTS_UNIFICATION.md index 143e2f74..14b9c199 100644 --- a/docs/SPORTS_UNIFICATION.md +++ b/docs/SPORTS_UNIFICATION.md @@ -214,7 +214,7 @@ one of them cannot break a user on an old core and the other can. | **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 | ✅ | 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 | **blocked, deliberately** | 3.2.0 *in users' hands*. Released 2026-08-03; there is no adoption data yet. See "B6 — the decision as of 2026-08-05" | +| **B6** | Sunset — delete the bundled copies | **blocked, deliberately** | 3.2.0 *in users' hands*. Released 2026-08-03; still no adoption data at the 2026-08-26 re-check. The prerequisite regression test is built (#505); only the evidence is missing. See "B6 — the decision as of 2026-08-05" | ### B4 — what "ship 3.2.0" actually requires @@ -305,7 +305,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: @@ -354,6 +355,15 @@ instead of receiving one that cannot load. Every adopted plugin has a the default download, or store-side install data if that is ever added. Revisit then, not on a schedule. +**Re-checked 2026-08-26 — still held.** v3.2.0 remains the latest release and +`src.__version__` is still `3.2.0`; no 3.3.0 has been cut. That is 23 days, not +the few months above, and no store-side install data has appeared. Note also +that the core updates by `git pull --rebase` rather than by downloading a +release, so release-asset counts would not measure uptake even if we had them — +whatever eventually unblocks this has to come from the store side. The +prerequisite test is now built (#505), so when the evidence does arrive the +remaining work is the sunset itself. + **When it happens, remember:** 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`, @@ -423,21 +433,30 @@ What actually remains, smallest first: 1. **Nothing on the critical path.** B6 is the only remaining phase and it is waiting on calendar time, not on work. Resist the urge to fill the gap by adopting more modules — see the decision above. -2. **The stale plugin-test tranche** — 5 failures across baseball, hockey and - basketball, all pre-existing API drift in the plugins' own older tests - (`plugin.initialized`, `CacheManager(config_manager=...)`, a bare - `cache_manager` import, `MockLogger.setLevel`, `BasketballPluginManager`). - None are scroll-related. They make the suite noisy, which is how a real - failure gets ignored. +2. ~~**The stale plugin-test tranche**~~ — **done** (verified 2026-08-26). + The 5 failures across baseball, hockey and basketball are gone; + `scripts/run_plugin_tests.py --all` reports 174 passed, 2 skipped, 0 failed + across the whole fleet. Re-check with that runner rather than by pointing + pytest at a plugin directory: these are standalone scripts, not a pytest + suite, and one of them calls `sys.exit(1)` at import, which collapses a + pytest run into an INTERNALERROR that looks nothing like the real state. 3. **Soak the remaining adoptions on hardware.** Only baseball has been watched through a live game, and hockey has been loaded on devpi. The other six are proven by harness, unit tests and pixel comparison — not by a live match. - Out-of-season sports cannot be soaked until their season starts. -4. **`CLAUDE.md` in the plugins repo says four panel sizes; the harness renders - eight.** A one-line doc fix, and the discrepancy has already produced one - false review finding. -5. **Then, when the evidence supports it, B6** — with the four-case - compatibility regression test above in CI first. + Out-of-season sports cannot be soaked until their season starts. **This is + now the only open item that needs work rather than calendar time.** +4. ~~**`CLAUDE.md` says four panel sizes**~~ — **done.** It reads "Default + matrix sizes are eight … design for the classic four first". +5. **Then, when the evidence supports it, B6.** Its prerequisite — the + four-case compatibility regression test — is built: core + `test/test_sports_sunset_matrix.py` (#505). It drives all four cells through + the real `PluginManager.load_plugin` and asserts the sunset cell as + `PluginState.ERROR` plus an error naming the missing module, not as a raise. + Two modelling traps it had to work through, worth knowing before trusting + any successor: the copy-removed shape must be an *unguarded* import, or the + failure names the missing `scroll_display_legacy` instead of the core + module; and only the leaf module may be hidden, since a pre-3.2.0 core still + ships `src/common/`. ## How to keep this project healthy