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 record): 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:
| target | declared floor | locked version | ------ | ------------------------------------------------------------- | -------------------- | pypi | pyproject.toml: [project].dependencies, [project].optional-dependencies, and PEP 735 [dependency-groups] | uv.lock | npm | package.json dependencies / peerDependencies / optionalDependencies | package-lock.json | go | go.mod require | go.mod (same file) |
Which uv.lock is read is :func:rlsbl.uv_workspace.locate_uv_lock's answer, not "the one beside the project root": a uv workspace member has no lock of its own, and one lock at the workspace root resolves every member.
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
def ok(self)#version_tuple
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
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
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
def normalize_pypi_name(name)PEP 503 normalization: lowercase, runs of -_. collapse to -.
#normalize_npm_name
def normalize_npm_name(name)#workspace_package_names
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
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
def pypi_declared(project_root)Declared requirements from pyproject.toml, wherever they live.
Three buckets, in the order a floor should be reported from:
[project].dependencies-- runtime, what a consumer resolves.[project].optional-dependencies.<extra>-- reachable by extra.[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_at
def pypi_locked_at(lock_path)Resolved versions from the uv.lock at lock_path.
Returns None when the file is absent or does not parse. WHICH file that is is not decided here: a uv workspace member has no lock of its own, so the location comes from :func:rlsbl.uv_workspace.locate_uv_lock.
#npm_declared
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
def npm_locked(project_root)Resolved versions from package-lock.json (v1, v2 and v3 shapes).
#_floor_problem
def _floor_problem(name, constraint, locked_version, *, kind, floor, where, remedy)One problem string for a dependency whose floor lags the lock, else None.
#_pass_note
def _pass_note(ecosystem, name, constraint, version)One fact line for a dependency whose declared floor covers the lock.
The go path always stated its outcome, while pypi and npm said nothing on success -- so a passing run showed only the go line and read as if the other ecosystems had never been evaluated, in exactly the place someone looks to confirm that they were. A pass now names what it compared.
#_nothing_policed_note
def _nothing_policed_note(ecosystem, manifest)The outcome when a manifest declares none of the policed dependencies.
#_evaluate_go
def _evaluate_go(root, names)Go floors are structural -- see the module docstring.
#evaluate_dep_floors
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.