Gate 8 of plesty check used to remind you to tag a release. It now downloads the latest wheel of your package from PyPI, statically diffs its public API against your working tree, and refuses to let a breaking change ship under a version that does not admit it. Nothing is imported, nothing is configured — and in CI it fails the pipeline.

1 Metadata2 Hygiene3 API Match4 Data Layer5 Docs6 Deps7 Coverage8 Semantic Versioning9 Licensing10 Audit11 Docs Build

What the gate actually does

Step 1
Find the baseline
The project.name from pyproject.toml is looked up on the PyPI JSON API; the newest bdist_wheel becomes the reference.
Step 2
Unpack it
The wheel is downloaded into a temporary directory and extracted — the published API as shipped, not as remembered.
Step 3
Diff, don't run
griffe loads both trees with allow_inspection=False and reports breaking changes. Your code is never executed.
Step 4
Check the bump
If anything broke, the latest git tag must already be at or beyond the required version.

The reference point is deliberately the published artifact, not the previous commit or the last tag. What consumers installed is what you are compared against, so a break cannot be laundered by a sequence of individually innocent commits.

For module maintainers

Using it

There is nothing to enable. Run the check you already run:

uv run plesty check

Gate 8 reports one of three verdicts:

✓ Compatible
✓ Semantic Versioning (no breaking API changes vs plesty-sdk 0.3.5) — additions only.
✓ Covered by a tag
✓ Semantic Versioning (2 breaking change(s) vs 0.3.5, covered by tag v0.4.0) — you broke it and you said so.
✗ Unadmitted break
Breaking changes with no sufficient tag. Advisory locally, fatal in CI.

What a failure looks like

[Semver] 2 breaking API change(s) vs plesty-thing 0.3.2 (PyPI) without a
sufficient version bump — tag at least v0.4.0 before publishing:
  - plesty/thing/__init__.py:1: move(speed): Parameter was removed
  - plesty/thing/__init__.py:5: home: Public object was removed
  ✓ Semantic Versioning (advisory locally — this fails in CI)

Each line carries the file, the line number, the symbol and the kind of break — enough to decide without re-reading the diff. Long lists are truncated to the first 20 with an ... and N more tail.

Two honest ways out

  1. Keep the promise. If the break was accidental — a parameter renamed for tidiness, a helper that was never meant to be public — restore the old surface. Keeping a deprecated alias beside the new name is a compatible addition, so the gate goes quiet.
  2. Admit the break. Bump the version in pyproject.toml and tag it. The gate reads git describe --tags --abbrev=0, so an untagged version bump does not count — the tag is the witness.
Published on PyPIMinimum version for a breakRule
0.3.20.4.0pre-1.0 — minor bump
0.9.90.10.0pre-1.0 — minor bump
1.2.32.0.0stable — major bump
2.0.13.0.0stable — major bump
Local runs will not stop you — CI will
The plesty init pre-push hook runs uv run plesty check, so gate 8 prints its warning while you push, then lets the push through. The same gate raises inside the pipeline. Treat the local message as the last cheap chance to fix it.
Unpublished, offline, or no wheel
A package with no PyPI release — every freshly scaffolded module — falls back to the previous tag-freshness reminder, and so does a run with no network. If the diff itself cannot complete, the gate says ✓ Semantic Versioning (API diff unavailable: …) and moves on. It degrades; it never blocks work it cannot judge.

For contributors

Building on it

How the surface is computed

The comparison is static. griffe.load(package, search_paths=[root], allow_inspection=False) builds a model of each tree from source alone — no import, no side effects, no hardware, no need for the device to be attached. That is what makes the gate safe to run on a device module whose imports would otherwise reach for a vendor DLL.

Which packages get compared comes from [tool.hatch.build.targets.wheel].packages in pyproject.toml, defaulting to ["plesty"]. If your wheel ships a namespace beyond the default, declaring it there is all the adoption there is.

What counts as breaking

ChangeVerdict
Removing a public function, class, or attributebreaking
Removing or renaming a parameterbreaking
Making an optional parameter required, or reordering positionalsbreaking
Adding a new function, class, or optional parametercompatible
Anything under a private (_-prefixed) namenot part of the surface

The pieces, and where they live

plesty/sdk/semver.py holds the testable parts, deliberately separate from the gate so each can be exercised on its own (16 unit tests accompany them):

FunctionResponsibility
fetch_pypi_baseline(name)Latest published version + wheel URL, or None when unpublished/offline
download_wheel(url, dest)Fetch and extract the baseline into a temp root
find_api_breakages(packages, old, new)griffe diff → one human-readable line per break
required_minimum_version(published)The bump rule, in one place
latest_tag_version(project_dir)The witness tag, parsed from git describe

The gate itself, check_semantic_versioning() in plesty/sdk/commands/check.py, only sequences those calls and decides the verdict. Extending the rule — a different bump policy, an extra exemption — means touching semver.py, not the gate.

How it reaches CI

It already did. plesty-standard-ci gained no new job for this: the existing hub-check job runs

uv run plesty check --standard $PLESTY_STANDARD

and GitLab sets CI on every runner. The gate reads that variable and turns its advisory message into a hard failure. So a module that already includes the standard template inherits authoritative semantic versioning the moment its runner image carries the SDK release containing gate 8 — no template change, no per-project opt-in, nothing to migrate.

Adopting it in your module
Nothing to add. Include plesty-standard-ci as usual, keep project.name matching your PyPI distribution name, and tag your releases. The gate finds everything else by itself.

Why it matters

Every hub module depends on the core libraries, and experiments depend on device modules. Before this, "did that release break me?" was answered by reading a changelog someone wrote from memory. Now the answer is derived from the artifact on PyPI, in the same command that already gates every push — and a version number that says patch can no longer contain a removal.