rlsbl v0.113.0 /rlsbl.dep_floors
On this page

Compares the `>=` floor each ecosystem-internal dependency declares in the manifest against the major.minor the lockfile resolves, for pypi, npm and Go.

#rlsbl.dep_floors

#rlsbl.dep_floors

Dependency floor enforcement for ecosystem-internal dependencies.

The failure class this exists to catch: a release ships work that REQUIRES new behavior from a sibling framework package. The development lock already resolves the new framework version, so the repo's own suite passes -- but the published manifest carries no >= floor (or a stale one), so a consumer installing the artifact resolves an OLDER framework and breaks. Three published releases shipped broken this way before this check existed.

The convention (campaign decision ledger): when a release requires new framework behavior, the manifest carries a >= floor at that version. Floors are not pins; upper bounds stay banned.

What is compared, per ecosystem:

rlsbl.dep_floors
targetdeclared floorlocked version---------------------------------------------------------------------------------------pypipyproject.toml: [project].dependencies, [project].optional-dependencies, and PEP 735 [dependency-groups]uv.locknpmpackage.json dependencies / peerDependencies / optionalDependenciespackage-lock.jsongogo.mod requirego.mod (same file)

Go is automatically satisfied and carries no comparison: a require line IS the declared minimum, and the go toolchain resolves builds by minimal version selection, so the build can never sit ahead of the declared floor. The check degenerates to "a declared minimum exists", which the toolchain guarantees -- so Go is reported as satisfied with a note, not evaluated.

The enforced set of "ecosystem-internal" dependencies comes from the internal_dep_floors config key (a list of package names) plus, in a monorepo, every workspace sibling's package name. No project names are hardcoded here, and nothing on this path touches the network: it reads only committed manifests and lockfiles.

Semantics per enforced dependency, once the lock resolves it:

  • the manifest does not declare it at all -> not this project's floor to

declare (it is transitive); no verdict.

  • the manifest declares it with no readable >= floor -> error.
  • the LOCKED major.minor exceeds the DECLARED floor's major.minor -> error.

Patch drift above the floor is fine; a minor or major boundary is not.

#DepFloorVerdict

Result of evaluating internal dependency floors for one project.

#ok

python
def ok(self)

#version_tuple

python
def version_tuple(text)

Leading (major, minor) of a version string, or None.

Floors are compared at major.minor: a patch bump in the lock never crosses a behavior boundary, so it is not a floor violation.

#pypi_floor

python
def pypi_floor(spec)

Read the lower bound of a PEP 440 specifier set.

Returns ("floor", (major, minor)) when a lower bound is readable, ("none", None) when the constraint pins no floor, or ("skip", None) for constraints that carry no comparable version.

#npm_floor

python
def npm_floor(rng)

Read the lower bound of an npm semver range.

Returns ("floor", (major, minor)), ("none", None) when no lower bound is readable, or ("skip", None) for non-registry ranges (workspace:, file:, git+, a URL) which have no floor to state.

#normalize_pypi_name

python
def normalize_pypi_name(name)

PEP 503 normalization: lowercase, runs of -_. collapse to -.

#normalize_npm_name

python
def normalize_npm_name(name)

#workspace_package_names

python
def workspace_package_names(workspace_root)

Package names of every sibling in a monorepo workspace.

In a monorepo the workspace graph already knows which dependencies are ecosystem-internal, so siblings never need listing in config. Returns an empty set outside a monorepo.

#_split_requirement

python
def _split_requirement(text)

Split a PEP 508 requirement into (normalized_name, kind, constraint).

kind is "spec" for a version specifier or "url" for a direct reference (name @ file:///...), which has no floor to declare. Returns None when the requirement is unparseable.

#pypi_declared

python
def pypi_declared(project_root)

Declared requirements from pyproject.toml, wherever they live.

Three buckets, in the order a floor should be reported from:

  1. [project].dependencies -- runtime, what a consumer resolves.
  2. [project].optional-dependencies.<extra> -- reachable by extra.
  3. [dependency-groups].<group> -- PEP 735, dev-only.

The third bucket is not optional to read. An internal dependency declared ONLY in a dependency group used to be dropped entirely, so the check returned no verdict for it however far behind its floor was -- and a test-infrastructure dependency is exactly the shape that lives there.

Returns {normalized_name: (section_label, kind, constraint)}, or None when there is no readable pyproject.toml.

#pypi_locked

python
def pypi_locked(project_root)

Resolved versions from uv.lock, or None when there is no lock.

#npm_declared

python
def npm_declared(project_root)

Declared consumer-visible dependency ranges from package.json.

devDependencies are excluded: they never reach a consumer's resolver. Returns {normalized_name: (section_label, declared_name, range)}, or None when there is no readable package.json.

#npm_locked

python
def npm_locked(project_root)

Resolved versions from package-lock.json (v1, v2 and v3 shapes).

#_floor_problem

python
def _floor_problem(name, constraint, locked_version, *, kind, floor, where, remedy)

One problem string for a dependency whose floor lags the lock, else None.

#_evaluate_go

python
def _evaluate_go(root, names)

Go floors are structural -- see the module docstring.

#evaluate_dep_floors

python
def evaluate_dep_floors(config, project_root, workspace_names=None)

Evaluate internal dependency floors for one project.

Returns a :class:DepFloorVerdict. Projects that have not adopted the internal_dep_floors config key come back with adopted=False and a skip reason.

Search