Skip to content

docs(sports): split adoption from sunset, and say what makes the sunset safe - #427

Merged
ChuckBuilds merged 2 commits into
mainfrom
docs/rollout-plan-sunset-gate
Aug 3, 2026
Merged

docs(sports): split adoption from sunset, and say what makes the sunset safe#427
ChuckBuilds merged 2 commits into
mainfrom
docs/rollout-plan-sunset-gate

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Documentation only. No code changes, no version bumps.

Why

The phase table folded two steps with very different risk profiles into one B5:

  • adopting core imports — safe by construction, because the guarded import keeps the bundled copy as a fallback. On a core that ships the module the plugin uses core code; on one that doesn't it behaves exactly as it does today.
  • deleting the bundled copies — removes that fallback, turning the guarded import into a hard dependency.

They are now B5 and B6, gated separately.

What I found while checking whether the sunset was safe

The declared ledmatrix_min_version floor protects nobody today:

  • PluginLoader._warn_if_incompatible is advisory — it logs a warning and continues.
  • It doesn't even warn for the at-risk population: it skips entirely when the parsed core version is below 2.0.0, and the v3.1.0 release 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.
  • Neither StoreManager.install_plugin nor .update_plugin compares the core version at all. update_plugin compares the plugin's manifest version against the registry's latest_version and nothing else — a routine store update happily delivers a plugin that floors above the user's core.

Verified against a v3.1.0 worktree: src/common/sports_scroll.py, src/element_style.py and the src/base_classes/sports/ package are all absent there, and the import fails with exc.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.ERROR with one journal line, and warns nobody. The user just loses that scoreboard.

What changes in the plan

  • B4 grows. Cutting the tag is the small part; it now also covers making the tag, release, and src.__version__ agree, revisiting the < 2.0.0 skip, migrating manifests to ledmatrix_min_version, and adding the install/update compatibility gate.
  • B5 (adoption) explicitly does not wait on that gate — it is safe either way.
  • B6 (sunset) waits for the gate to have shipped and reached users, and for a compatibility regression test in CI: load each adopted plugin with its copy removed against a pinned old-core worktree, assert it fails loudly; against current core, assert it works.

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

  • Documentation
    • Updated the sports unification rollout plan with staged B0–B6 milestones.
    • Clarified version requirements, compatibility checks, adoption fallback behavior, migration steps, and validation gates.
    • Added guidance for handling version-reporting issues and failures when loading older cores.
    • Updated project-health and next-step guidance to reflect the revised sequencing and enforcement requirements.

…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
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 72ffb157-eb2e-4260-81db-ffb3766b3228

📥 Commits

Reviewing files that changed from the base of the PR and between 62c9cbb and 9972111.

📒 Files selected for processing (1)
  • docs/SPORTS_UNIFICATION.md
📝 Walkthrough

Walkthrough

The 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.

Changes

Sports unification rollout

Layer / File(s) Summary
Rollout gates and version requirements
docs/SPORTS_UNIFICATION.md
The sunset rule now requires a manifest version floor and the B6 compatibility gate. The rollout plan documents B4 version verification and required compatibility checks.
Guarded adoption behavior
docs/SPORTS_UNIFICATION.md
B5 now defines core implementation selection with bundled fallbacks and records hockey pilot validation criteria.
Sunset sequencing and maintenance rules
docs/SPORTS_UNIFICATION.md
B6 documents old-core load failures, regression tests, and delayed fallback deletion. The next steps and maintenance rules now cover enforced checks and synchronized version documentation.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the documentation change that separates adoption from sunset and defines the safety requirements.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/rollout-plan-sunset-gate

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.

@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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 21825cb and 62c9cbb.

📒 Files selected for processing (1)
  • docs/SPORTS_UNIFICATION.md

Comment thread docs/SPORTS_UNIFICATION.md Outdated
Comment thread 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
@ChuckBuilds
ChuckBuilds merged commit 963ab82 into main Aug 3, 2026
17 checks passed
ChuckBuilds added a commit that referenced this pull request Aug 4, 2026
* 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>
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.

1 participant