Compare commits

...
5 Commits
Author SHA1 Message Date
ChuckBuildsandClaude Opus 5 c2ace90353 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
2026-08-26 10:33:31 -04:00
ChuckBuildsandClaude Opus 5 cc6a75712c 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
2026-08-21 16:20:12 -04:00
ChuckBuildsandClaude Opus 5 ed2b81a4fb 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
2026-08-11 12:37:18 -04:00
coderabbitai[bot]andCodeRabbit ac44b5a6f5 fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-08-05 20:59:54 +00:00
ChuckBuildsandClaude Opus 5 df8f69af30 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
2026-08-05 14:14:00 -04:00
+143 -36
View File
@@ -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 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. **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 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 | | 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 | | **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 | | **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 | | **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 | | **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: three pilots, then the remaining six. **Bundled copies stay.** | after B4 | Per plugin: harness + goldens byte-identical, then a device soak | | **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** | B4's gate shipped *and* in users' hands (see below) | | **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 ### B4 — what "ship 3.2.0" actually requires
@@ -262,13 +262,21 @@ default is the more restrictive.
`compatible_versions`. No manifest still carries it, so there is nothing to `compatible_versions`. No manifest still carries it, so there is nothing to
migrate there.) 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 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 guarded import (see the Upgradability table above). On a core that doesn't ship
module the plugin uses core code; on one that doesn't it falls back and behaves the module the plugin falls back and behaves exactly as it does today. That
exactly as it does today. There is no version of this step that breaks a user, fallback compatibility — and only that — is safe by construction. On a core that
which is why it does not wait for B6's gate. *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 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 carrying 3.2.0, `scroll_display.py` went from 691 to 289 lines and all 16 harness
@@ -297,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 is small.** The bundled copies cost disk space; deleting them early costs
scoreboards, silently. That trade is not close. 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 cover four cases, not one — B5's safety claim and B6's failure mode are
different propositions and only the second is obvious: different propositions and only the second is obvious:
@@ -321,35 +330,133 @@ gate rather than trusting the failure to be noticed.
The same suite should exercise the install/update gate, since it is the other The same suite should exercise the install/update gate, since it is the other
half of the guarantee. 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.
**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`,
`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 ## 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`). What actually remains, smallest first:
Nothing else blocks this, and it is what makes `ledmatrix_min_version:
"3.2.0"` refer to something real. 1. **Nothing on the critical path.** B6 is the only remaining phase and it is
2. **Make the version number honest.** Have the release process assert that the waiting on calendar time, not on work. Resist the urge to fill the gap by
tag, the GitHub release, and `src.__version__` agree — a check in CI is adopting more modules — see the decision above.
cheaper than the confusion of the last two releases. Then revisit the 2. ~~**The stale plugin-test tranche**~~ — **done** (verified 2026-08-26).
`< 2.0.0` skip in `_warn_if_incompatible`, which currently silences the The 5 failures across baseball, hockey and basketball are gone;
warning for the users who most need it. `scripts/run_plugin_tests.py --all` reports 174 passed, 2 skipped, 0 failed
3. **Add the compatibility gate** to `StoreManager.install_plugin` and across the whole fleet. Re-check with that runner rather than by pointing
`.update_plugin`: refuse a plugin whose declared floor exceeds pytest at a plugin directory: these are standalone scripts, not a pytest
`src.__version__`, and surface the reason in the store UI rather than only suite, and one of them calls `sys.exit(1)` at import, which collapses a
the log. This is the single change that turns the floor from documentation pytest run into an INTERNALERROR that looks nothing like the real state.
into a guarantee, and B6 depends on it. 3. **Soak the remaining adoptions on hardware.** Only baseball has been watched
4. **Migrate the manifests** to `ledmatrix_min_version`, and reconcile them with through a live game, and hockey has been loaded on devpi. The other six are
`compatible_versions` (see above — that field is the required, canonical one, proven by harness, unit tests and pixel comparison — not by a live match.
and the gate does not read it yet). Currently 28 plugins spell the floor both Out-of-season sports cannot be soaked until their season starts. **This is
ways across their `versions[]` entries, 12 use only the old spelling, and 2 now the only open item that needs work rather than calendar time.**
only the new. Scope the sweep to the nine sports plugins if a 42-plugin 4. ~~**`CLAUDE.md` says four panel sizes**~~ — **done.** It reads "Default
version-bump wave isn't worth it — but the `compatible_versions` half has to matrix sizes are eight … design for the classic four first".
cover every manifest the gate can refuse, or define explicit legacy handling, 5. **Then, when the evidence supports it, B6.** Its prerequisite — the
before the gate is allowed to block anything. four-case compatibility regression test — is built: core
5. **Run B5 adoption** — hockey, soccer, football, then the remaining six. `test/test_sports_sunset_matrix.py` (#505). It drives all four cells through
Bundled copies stay. Byte-identical harness output per plugin, then a soak. the real `PluginManager.load_plugin` and asserts the sunset cell as
6. **Only then plan B6**, with the compatibility regression test described above `PluginState.ERROR` plus an error naming the missing module, not as a raise.
in CI first. 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 ## How to keep this project healthy