Skip to content

feat(store): evaluate compatible_versions, not just the floor - #433

Merged
ChuckBuilds merged 3 commits into
mainfrom
feat/gate-reads-compatible-versions
Aug 4, 2026
Merged

feat(store): evaluate compatible_versions, not just the floor#433
ChuckBuilds merged 3 commits into
mainfrom
feat/gate-reads-compatible-versions

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes the gap CodeRabbit surfaced reviewing #427, and recorded in docs/SPORTS_UNIFICATION.md as something that must land before B6.

The gap

compatible_versions is the canonical contract — schema/manifest_schema.json marks it required, and all 42 published manifests carry it. It is also 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. A plugin declaring ["2.0.0 - 2.9.9"] — meaning it stops at 2.x — would be installed on 3.2.0 anyway.

The change

check() now evaluates both, and the more restrictive wins. The array is a set of alternatives (satisfying any one entry suffices), covering 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.

A flaw my own test caught

That last principle 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 (a floor of 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 before parsing, so garbage reads as "no evidence". parse_semver itself is unchanged — the loader depends on its current behaviour and its test only pins non-string inputs.

Verified

  • 815 core unit tests pass, 18 of them new — every range form, the alternatives semantics, both precedence directions, and the untrustworthy-core bypass.
  • Swept the real registry: all 42 published manifests, evaluated at cores 1.0.0, 2.0.0, 3.1.0, 3.2.0 and 4.0.0.
core 3.2.0: 0 of 42 refused
core 3.1.0: 0 of 42 refused
core 2.0.0: 0 of 42 refused
core 1.0.0: 0 of 42 refused
core 4.0.0: 0 of 42 refused

The gate stays inert for shipped plugins — the property that makes it safe to land ahead of B5.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

Summary by CodeRabbit

  • Bug Fixes
    • Improved plugin compatibility checks across supported core versions.
    • Added support for exact versions, comparison operators, caret and tilde ranges, inclusive ranges, and alternative version ranges.
    • Improved handling of invalid or uncertain version declarations.
    • Clarified compatibility behavior for untrusted or unavailable core versions.
    • Ensured declared supported-version ranges are evaluated alongside minimum-version requirements.
    • Added clearer errors when a plugin’s supported version range is incompatible.

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

coderabbitai Bot commented Aug 3, 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: 8 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: e112c41b-ad53-49df-bd1e-c0d5e8249726

📥 Commits

Reviewing files that changed from the base of the PR and between 08b1616 and 06657de.

📒 Files selected for processing (2)
  • src/plugin_system/compatibility.py
  • test/test_plugin_compatibility_gate.py
📝 Walkthrough

Walkthrough

The plugin compatibility gate parses compatible_versions semver ranges, evaluates alternatives, and applies range incompatibility before minimum-version checks. Untrustworthy core versions use ecosystem-floor rules. Tests cover parsing, precedence, and uncertain declarations.

Changes

Plugin compatibility evaluation

Layer / File(s) Summary
Compatible-version range evaluation
src/plugin_system/compatibility.py, test/test_plugin_compatibility_gate.py
The module validates semver tokens and evaluates comparison, exact, tilde, caret, inclusive, prefixed, prerelease, and alternative ranges. Missing or invalid declarations remain uncertain.
Compatibility gate precedence
src/plugin_system/compatibility.py, test/test_plugin_compatibility_gate.py
check evaluates declared ranges before minimum-version floors. Trustworthy cores receive range-specific and floor checks. Untrustworthy or unparseable cores reject floors above TRUSTWORTHY_FLOOR and allow lower or uncertain requirements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginCompatibilityCheck
  participant CompatibleVersions
  participant MinimumVersionFloor
  participant CoreVersion
  PluginCompatibilityCheck->>CoreVersion: parse and assess trust
  PluginCompatibilityCheck->>CompatibleVersions: evaluate declared semver range
  CompatibleVersions-->>PluginCompatibilityCheck: compatible, incompatible, or uncertain
  PluginCompatibilityCheck->>MinimumVersionFloor: evaluate minimum-version constraint
  MinimumVersionFloor-->>PluginCompatibilityCheck: compatibility result
Loading

Possibly related PRs

  • ChuckBuilds/LEDMatrix#431: This PR extends its plugin compatibility logic and tests with compatible_versions range handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: evaluating compatible_versions in addition to the minimum version floor.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gate-reads-compatible-versions

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.

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

🧹 Nitpick comments (2)
test/test_plugin_compatibility_gate.py (2)

260-260: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add return type annotations to the new test methods.

Annotate each new test method with -> None. Annotate the parameterized test inputs at Line 260.

Proposed change
-    def test_range_forms(self, spec, core, expected):
+    def test_range_forms(
+        self, spec: str, core: str, expected: bool
+    ) -> None:
...
-    def test_array_is_alternatives_not_conjunction(self):
+    def test_array_is_alternatives_not_conjunction(self) -> None:
...
-    def test_absent_or_unparseable_is_no_evidence(self):
+    def test_absent_or_unparseable_is_no_evidence(self) -> None:
...
-    def test_upper_bound_blocks_a_core_that_clears_the_floor(self):
+    def test_upper_bound_blocks_a_core_that_clears_the_floor(self) -> None:

Apply the same -> None annotation to the remaining new test methods. As per coding guidelines, "Use type hints for function parameters and return values."

Also applies to: 265-265, 272-272, 285-285, 295-295, 303-303, 308-308

🤖 Prompt for 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.

In `@test/test_plugin_compatibility_gate.py` at line 260, Add -> None return
annotations to the new test methods, including test_range_forms and the methods
at the referenced locations; preserve their existing parameters and test
behavior while applying the repository’s typing style consistently.

Source: Coding guidelines


240-240: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add docstrings to the new test classes.

TestCompatibleVersions and TestMoreRestrictiveWins have no class docstrings.

Proposed change
 class TestCompatibleVersions:
+    """Test compatible-version range evaluation."""
+
 ...
 class TestMoreRestrictiveWins:
+    """Test range and minimum-version precedence."""
+

As per coding guidelines, "Use docstrings for classes and complex functions."

Also applies to: 284-284

🤖 Prompt for 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.

In `@test/test_plugin_compatibility_gate.py` at line 240, Add concise class
docstrings to the new test classes TestCompatibleVersions and
TestMoreRestrictiveWins in test_plugin_compatibility_gate.py, describing the
behavior each test class covers. Do not alter the test logic.

Source: Coding guidelines

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

Nitpick comments:
In `@test/test_plugin_compatibility_gate.py`:
- Line 260: Add -> None return annotations to the new test methods, including
test_range_forms and the methods at the referenced locations; preserve their
existing parameters and test behavior while applying the repository’s typing
style consistently.
- Line 240: Add concise class docstrings to the new test classes
TestCompatibleVersions and TestMoreRestrictiveWins in
test_plugin_compatibility_gate.py, describing the behavior each test class
covers. Do not alter the test logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7651668b-3509-4975-9ddf-32247a53933a

📥 Commits

Reviewing files that changed from the base of the PR and between 183e23e and f31b458.

📒 Files selected for processing (2)
  • src/plugin_system/compatibility.py
  • test/test_plugin_compatibility_gate.py

@codacy-production

codacy-production Bot commented Aug 3, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 32 complexity · 0 duplication

Metric Results
Complexity 32
Duplication 0

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.

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

@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

🧹 Nitpick comments (3)
test/test_plugin_compatibility_gate.py (2)

339-339: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make UNTRUSTWORTHY immutable.

Ruff reports RUF012 because UNTRUSTWORTHY is a mutable class attribute. Use a tuple so test parameters cannot share mutable state.

As per coding guidelines, “Keep tests independent and ensure they do not depend on each other.”

Proposed fix
-    UNTRUSTWORTHY = ["1.0.0", "0.9.0", "1.9.9"]
+    UNTRUSTWORTHY = ("1.0.0", "0.9.0", "1.9.9")
🤖 Prompt for 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.

In `@test/test_plugin_compatibility_gate.py` at line 339, Change the UNTRUSTWORTHY
class attribute from a list to an immutable tuple, preserving the existing
version values and their order so the test behavior remains unchanged.

Sources: Coding guidelines, Linters/SAST tools


84-101: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add type annotations to the new test methods.

Add -> None to each new test method. Add core: str to the parametrized methods.

As per coding guidelines, “Use type hints for function parameters and return values.”

Also applies to: 342-371

🤖 Prompt for 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.

In `@test/test_plugin_compatibility_gate.py` around lines 84 - 101, Add type
annotations to the new compatibility tests: annotate both test methods with a
None return type, and update the parametrized test methods in the referenced
area to declare the core parameter as str. Preserve the existing test behavior
and assertions.

Source: Coding guidelines

src/plugin_system/compatibility.py (1)

187-209: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the low-core exception.

The contract says every unparseable or below-TRUSTWORTHY_FLOOR core resolves as compatible. This branch rejects floors above 2.0.0. State this exception in the docstring.

As per coding guidelines, “Make intentions clear through naming and structure (Explicit over Implicit principle).”

🤖 Prompt for 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.

In `@src/plugin_system/compatibility.py` around lines 187 - 209, Add or update the
docstring for the function containing the TRUSTWORTHY_FLOOR check to explicitly
document the exception: low-core versions (below TRUSTWORTHY_FLOOR or
unparseable) are normally treated as compatible, but this exception does not
apply when the manifest's declared minimum version (evaluated via parse_semver
and declared_min_version) exceeds TRUSTWORTHY_FLOOR. Make the intended behavior
clear by stating that such high-floor declarations are rejected for low-core
systems to prevent incompatible plugin installations.

Source: Coding guidelines

🤖 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 `@src/plugin_system/compatibility.py`:
- Around line 202-214: Update the compatibility reject paths around
declared_min_version, satisfies_compatible_versions, and sunset handling to use
the strict SemVer token parser for both current and needed versions before any
comparison can return False. Preserve None for malformed declarations, avoid
rejecting valid 3.2.0 values with build metadata or release prefixes, and add
regression tests covering both inputs.
- Around line 202-203: Update the manifest-version handling around
declared_min_version(manifest) and parse_semver(declared) to validate the
requires and versions container types before accessing them. Treat malformed or
unsupported shapes—including non-empty requires lists and mappings in
versions—as having no declared floor, and continue without raising exceptions.

---

Nitpick comments:
In `@src/plugin_system/compatibility.py`:
- Around line 187-209: Add or update the docstring for the function containing
the TRUSTWORTHY_FLOOR check to explicitly document the exception: low-core
versions (below TRUSTWORTHY_FLOOR or unparseable) are normally treated as
compatible, but this exception does not apply when the manifest's declared
minimum version (evaluated via parse_semver and declared_min_version) exceeds
TRUSTWORTHY_FLOOR. Make the intended behavior clear by stating that such
high-floor declarations are rejected for low-core systems to prevent
incompatible plugin installations.

In `@test/test_plugin_compatibility_gate.py`:
- Line 339: Change the UNTRUSTWORTHY class attribute from a list to an immutable
tuple, preserving the existing version values and their order so the test
behavior remains unchanged.
- Around line 84-101: Add type annotations to the new compatibility tests:
annotate both test methods with a None return type, and update the parametrized
test methods in the referenced area to declare the core parameter as str.
Preserve the existing test behavior and assertions.
🪄 Autofix

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: 8989f9aa-1212-4ba6-8f0c-d28e44d8cc49

📥 Commits

Reviewing files that changed from the base of the PR and between f31b458 and 08b1616.

📒 Files selected for processing (2)
  • src/plugin_system/compatibility.py
  • test/test_plugin_compatibility_gate.py

Comment thread src/plugin_system/compatibility.py
Comment thread src/plugin_system/compatibility.py
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
@ChuckBuilds
ChuckBuilds merged commit 53af53b into main Aug 4, 2026
17 checks passed
ChuckBuilds added a commit that referenced this pull request Aug 11, 2026
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
ChuckBuilds added a commit that referenced this pull request Aug 21, 2026
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
ChuckBuilds added a commit that referenced this pull request Sep 1, 2026
Sideloading a plugin from a URL validated required manifest fields, warned on
version-field and schema problems, and then installed whatever it had found --
never once comparing the core version. With this, all three routes in refuse on
the same rule: install_plugin (#431/#433), the git-pull update path (the commit
before this one), and now sideloading.

The argument for gating it is not the same as for the other two, and worth
stating. An automatic store update is something that happens TO a user; a
sideload is something they chose. But the floor is not advice about intent --
it is a statement that the plugin cannot run on this core. Letting it through
produces exactly the silent PluginState.ERROR at load that the store gate
exists to prevent, and someone who pasted a URL is no better placed to diagnose
that than someone who pressed Update.

Checked after the download, like the others, because the manifest is the only
place the floor is written. Placed before the move into plugins/, so the
existing `finally` removes the temp tree and nothing half-installed survives a
refusal. The reason string is returned to the caller, so the web UI shows the
same actionable message the store path already gives.

TestSideloadGate pins the three cases the other routes pin: refuses a floor
above this core and leaves nothing behind, still allows a compatible plugin
(the guard against a gate that refuses everything and passes the first test),
and does not block a 2.0.0 floor on a core reporting an untrustworthy version
-- the v3.1.0-reports-1.0.0 population, who must not be locked out.

TestInstallGate's docstring said this route was still ungated. It no longer is,
so it now points at TestSideloadGate rather than describing a gap.

Verified the tests bite: removing the check fails the refusal case.
Full suite 3723 passed, 6 skipped.


Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
This PR was opened to say B6 was deliberately held. It has since run, so the
document said the opposite of the truth -- which is the exact failure it was
written to fix: "a plan that misreports which phase it is in is worse than no
plan".

Merges current main first (53 commits), which brings in #508's corrections to
this same file, then replaces the hold with what happened.

**Why the hold lifted is worth recording, because the stated gate was never
met.** It asked for evidence of 3.2.0 uptake, and that evidence could not
arrive: the core updates by `git pull --rebase`, so release-asset counts cannot
measure it, and no store-side telemetry exists. What changed 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:
install_plugin (#431/#433), update_plugin's git branch (#508), and
install_from_url (#510). A pre-3.2.0 user cannot receive a sunset plugin at
all, so they keep the version they run. Refusal replaced the bundled copy,
which is what the copy stood in for.

Records what shipped (eight plugins, ~5,800 lines, plugins #346/#349/#350/#351)
and the two findings worth carrying to the next module: baseball's fallback was
the only one holding orchestration logic the core lacked, and two tests had
been leaning on the guard -- soccer's stubbed `src` in a way that shadowed the
core, so it had been exercising the frozen copy rather than the shipping class
since B5.

The remaining-work list is replaced too. Its first item was "nothing on the
critical path, B6 is waiting on calendar time", which is no longer true. What
remains: hardware soaks (with a note to check the rig's display_mode first, or
a board in switch mode tells you nothing about the scroll code), cutting 3.3.0
-- not required by B6, whose floors are 3.2.0, but calendar 1.2.3 floors at
3.3.0 and is un-installable until it exists -- and reconsidering the modules
held back during the sunset.

Keeps the pre-B6 sections as history. The reasoning still applies to the next
module; it is just no longer in force for this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
…511)

#510 shows as merged, but into fix/gate-git-pull-updates -- #508's branch --
rather than main. #508 reached main first, so the sideload gate was left behind
on a branch. Same failure as plugins #350/#351, which merged into each other's
bases; worth knowing the pattern, because GitHub reports these as MERGED and
`gh pr list` shows nothing outstanding.

main today has two of the three routes gated: install_plugin (#431/#433) and
update_plugin's git branch (#508). install_from_url validates required manifest
fields and then installs whatever it found, never comparing the core version.

Cherry-picked unchanged from the orphaned branch -- it applies to main with no
conflict. TestSideloadGate pins the three cases the other routes pin: refuses a
floor above this core leaving nothing behind, still allows a compatible plugin
(the guard against a gate that refuses everything), and does not block a 2.0.0
floor on a core reporting an untrustworthy version.

Full suite 3725 passed, 6 skipped.


Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ChuckBuilds added a commit that referenced this pull request Sep 2, 2026
* 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

* fix: apply CodeRabbit auto-fixes

Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

* 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

* 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

* 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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
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