docs(sports): split adoption from sunset, and say what makes the sunset safe - #427
Conversation
…et safe
The phase table folded two steps with very different risk profiles into one
B5: adopting core imports (safe by construction -- the guarded import keeps
the bundled fallback) and deleting the bundled copies (removes the fallback,
so the import becomes a hard dependency). They are now B5 and B6.
Reading the enforcement path showed the declared floor protects nobody today:
- PluginLoader._warn_if_incompatible is advisory, and skips entirely when the
parsed core version is below 2.0.0.
- The v3.1.0 release ships __version__ = "1.0.0" -- the tag was cut
2026-05-31 and the string was not bumped until 2026-07-12 -- so the skip
matches exactly the users most likely to be behind.
- Neither StoreManager.install_plugin nor .update_plugin compares the core
version, so a store update delivers a plugin that floors above the core.
Verified against a v3.1.0 worktree: sports_scroll, element_style and the
sports package are absent there, and the import fails with
exc.name == 'src.common.sports_scroll' -- a guard set of {"src"} does not
match it.
B4 therefore grows to include making the version number trustworthy and
adding the install/update gate; B6 waits on that gate having shipped and
reached users. Also adds an ordered "What's next" and the durable lessons
this migration paid for.
Documentation only; no code changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
|
Warning Review limit reached
Next review available in: 13 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe documentation replaces the former B0–B5 plan with a B0–B6 rollout. It adds compatibility enforcement, guarded adoption with fallbacks, delayed fallback deletion, regression testing, and synchronized maintenance rules. ChangesSports unification rollout
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Up to standards ✅🟢 Issues
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/SPORTS_UNIFICATION.md`:
- Around line 295-299: Update docs/SPORTS_UNIFICATION.md lines 295-299 so
StoreManager.install_plugin and update_plugin validate src.__version__ against
at least one compatible_versions semver range and surface incompatibility in the
store UI. Update lines 219-240 to migrate deprecated ledmatrix_version by
populating compatible_versions, rather than only renaming
versions[].ledmatrix_min. Update lines 300-303 to require all manifests to be
migrated before enabling the global gate, or define explicit handling for legacy
manifests.
- Around line 277-281: Expand the compatibility regression test described in
SPORTS_UNIFICATION.md to load every adopted plugin on pinned old core both with
and without its bundled copy, asserting the bundled-copy path succeeds and the
missing-copy path yields PluginState.ERROR with the exact missing module path
rather than an uncaught exception. Also test current-core loading in both
configurations and exercise the install/update gate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e763d545-7165-44c4-915b-69b09bbb5591
📒 Files selected for processing (1)
docs/SPORTS_UNIFICATION.md
Both CodeRabbit findings on #427 hold up against the code; one sub-point was already moot. 1. The compatibility regression test was described as one case (bundled copy removed on an old core) when it needs four. The case that actually matters is the one that was missing: an adopted plugin loading *with* its bundled copy on an old core, which is the entire basis for claiming B5 is safe to run ahead of the gate. Now a 2x2 table. The assertion was also wrong. "Fails loudly and specifically" is aspirational -- PluginManager.load_plugin catches ModuleNotFoundError, so nothing propagates and it fails into PluginState.ERROR with one log line. A test expecting a raise would pass for the wrong reason. Specified as PluginState.ERROR plus the exact missing module path, which is also what the rest of this document already says the failure looks like. 2. `compatible_versions` -- not `ledmatrix_min_version` -- is the canonical contract: schema/manifest_schema.json requires it, all 42 published manifests carry it, and it holds semver ranges ([">=2.0.0"] in 41, [">=1.0.0"] in 7-segment-clock). The gate as merged reads only the floor. Harmless today: no manifest uses an upper bound, and the two fields agree everywhere except 7-segment-clock. But the schema's range syntax permits upper bounds, so a plugin declaring ["2.0.0 - 2.9.9"] would be installed on 3.2.0 regardless. Recorded as a named gap the gate must close before B6, and the migration step now has to reconcile both fields across every manifest the gate can refuse. Skipped, with reason: the finding also asked to migrate the deprecated top-level `ledmatrix_version`. No manifest carries it -- verified across all 42 -- so there is nothing to migrate. Documentation only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
* feat(store): evaluate compatible_versions, not just the floor Closes the gap CodeRabbit surfaced on #427. `compatible_versions` is the canonical compatibility contract -- schema/manifest_schema.json marks it required, all 42 published manifests carry it -- and it is the only field that can express an *upper* bound. `ledmatrix_min_version` is a floor and cannot say "not compatible with 4.x". The gate read only the floor, so a plugin declaring ["2.0.0 - 2.9.9"] would be installed on 3.2.0 regardless of having said it stops at 2.x. check() now evaluates both and the more restrictive wins. The array is a set of alternatives (satisfying any one entry suffices), supporting every form the schema permits: >=, <=, >, <, ~, ^, a bare exact version, and an inclusive "A - B" range, with prerelease/build suffixes tolerated. Refusal still requires evidence. Anything unparseable, absent, or below TRUSTWORTHY_FLOOR resolves to compatible. That last point needed a new strict parser. parse_semver is deliberately lenient -- it strips non-digits and yields (0, 0, 0) for a string with no numbers at all. Harmless for a floor (0.0.0 never blocks) but wrong for a range, where the same leniency turned an unreadable spec into a *refusal*: a manifest whose only entry was garbage got compared against 0.0.0 and refused. Range specs are now shape-checked first, so garbage reads as "no evidence". parse_semver itself is unchanged, since the loader depends on its behaviour. Verified: 815 core unit tests pass, 18 of them new. Swept the real registry -- all 42 published manifests, at cores 1.0.0 / 2.0.0 / 3.1.0 / 3.2.0 / 4.0.0 -- and nothing is refused at any of them. The gate stays inert for shipped plugins, which is the property that makes it safe to land ahead of B5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 * feat(store): protect the one population the sunset would break The B6 sunset deletes each plugin's guarded-import fallback, so a plugin that floors at 3.2.0 must never reach a core that lacks the 3.2.0 modules. The gate could not stop that for the population most at risk. A device installed from the v3.1.0 release reports __version__ = "1.0.0". The gate treated anything below TRUSTWORTHY_FLOOR as "unknown, do not block" -- correct while every manifest floors at 2.0.0, because blocking would have emptied the plugin store for those users. But after the sunset it hands them a 3.2.0-floored plugin with no fallback, which fails to load with one log line. Nothing else in the system protects them: they cannot be told apart from a genuine 1.0.0 install. On an untrustworthy core the gate now refuses a floor ABOVE 2.0.0 and still allows anything at or below it. A floor above the ecosystem baseline says the plugin needs modules that arrived after 2.0.0, and a core reporting below that -- whether it is the v3.1.0 release or something genuinely ancient -- will not have them. Refusing leaves the user on the version they already run instead of one that cannot load. Measured against all 42 published manifests: today (every manifest floors at 2.0.0) core 1.0.0 / 2.0.0 / 3.1.0 / 3.2.0 / unparseable -> 0 of 42 refused after B6 (same manifests floored at 3.2.0) core 1.0.0 -> 38 refused, core 3.1.0 -> 38 refused, core 3.2.0 -> 0 So nobody loses the store today, and the sunset cannot reach a core that cannot run it. Two older tests asserted the previous "allow everything" behaviour; they now express the new rule with a 2.0.0 floor, which is what their no-lockout intent was actually about. The 38-of-42 in that measurement surfaced a separate B6 trap, recorded here because it will bite whoever raises the floors: four plugins (flights, leaderboard, music, stocks) declare the floor as a TOP-LEVEL `min_ledmatrix_version`, a third spelling, which declared_min_version checks before the versions[] array. For those, editing versions[0] is a silent no-op and the floor stays at 2.0.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 * fix(store): address review — malformed manifests, and suffixed versions Both CodeRabbit findings on #433 verified against the code and fixed. 1. Malformed manifest sections raised instead of degrading. `requires` as a list hit AttributeError ('list' object has no attribute 'get') and `versions` as a mapping hit KeyError: 0. Both reproduced. This got worse with the sunset rule in the previous commit: that branch resolves the floor for *every* manifest on an untrustworthy core, where the old code returned early. One hand-edited or third-party file with the wrong shape would have taken down the whole install path rather than just itself. Container types are now validated and an unrecognised shape reads as "no declared floor". 2. Prerelease and build metadata leaked into the version numbers. The digit scrape parsed "3.2.0+build42" as (3, 2, 42) and "3.2.0-rc1" as (3, 2, 1) -- a release candidate ranking above its own release. Both fed reject decisions, and the consequence was demonstrable: a plugin pinned to exactly "3.2.0" refused a core running 3.2.0+build42, which is that same version. The suggested remedy -- use the strict token parser -- would not have fixed it. _parse_strict validates the shape but delegates the numbers to parse_semver, so it returned the same (3, 2, 42). The bug is in the scrape, so suffixes are now dropped before it. Prereleases compare equal to their release rather than below it; full prerelease ordering is more than any caller needs and equal is far closer to right than what it did before. parse_semver is shared with PluginLoader, so its suite was re-run: unchanged, and it only ever gets more correct here. Verified: 839 core unit tests pass, 21 of them new -- six malformed shapes, five suffixed forms, and the two demonstrated regressions. The real-registry sweep is unchanged at 0 of 42 refused across cores 1.0.0, 3.1.0, 3.2.0, 3.2.0+build42 and an unparseable string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documentation only. No code changes, no version bumps.
Why
The phase table folded two steps with very different risk profiles into one B5:
They are now B5 and B6, gated separately.
What I found while checking whether the sunset was safe
The declared
ledmatrix_min_versionfloor protects nobody today:PluginLoader._warn_if_incompatibleis advisory — it logs a warning and continues.2.0.0, and thev3.1.0release ships__version__ = "1.0.0". The tag was cut 2026-05-31; the string wasn't bumped to"3.1.0"until 2026-07-12 (7f7f0d64), six weeks later. So the skip matches exactly the users most likely to be behind.StoreManager.install_pluginnor.update_plugincompares the core version at all.update_plugincompares the plugin's manifest version against the registry'slatest_versionand nothing else — a routine store update happily delivers a plugin that floors above the user's core.Verified against a
v3.1.0worktree:src/common/sports_scroll.py,src/element_style.pyand thesrc/base_classes/sports/package are all absent there, and the import fails withexc.name == 'src.common.sports_scroll'— a guard set of{"src"}does not match it.Put together: a plugin that deletes its copy today reaches an un-updated user through a normal store update, fails to load, gets marked
PluginState.ERRORwith one journal line, and warns nobody. The user just loses that scoreboard.What changes in the plan
src.__version__agree, revisiting the< 2.0.0skip, migrating manifests toledmatrix_min_version, and adding the install/update compatibility gate.Also adds an ordered What's next and a short How to keep this project healthy section with the lessons this migration paid for.
Companion PR
The sunset rule is stated in both repos. The matching change is ChuckBuilds/ledmatrix-plugins#242 — the two must land together, and the docs now say so.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Summary by CodeRabbit