Skip to content
rlsbl.dep_locks
On this page

Does each lockfile still resolve the manifest beside it? An offline, structural staleness check that never invokes a resolver and never reaches an index.

#rlsbl.dep_locks

#rlsbl.dep_locks

Does each lockfile still resolve the manifest beside it?

The sibling failure class to :mod:rlsbl.dep_floors. That module asks whether a DECLARED floor is behind what the lock resolved; this one asks the question underneath it: does the lock resolve THIS manifest at all, or was a dependency added, removed or re-constrained after the lock was written?

A stale lock is not a cosmetic problem. uv sync/npm ci install what the lock says, so CI and every contributor keep resolving the old dependency set while the manifest advertises a new one; dep-floors compares against a lock that no longer describes the manifest; and the release refreshes the lock at bump time, so the drift surfaces as an unrelated diff in the release commit.

Offline and structural, on purpose ----------------------------------

Nothing here runs a package manager and nothing touches the network. The obvious alternative -- shelling out to uv lock --check -- was rejected:

  • it is a resolver invocation, so it can reach the index (and its exit code

conflates "the lock is stale" with "the index could not be reached"), which would make a check declared local and offline answer differently depending on the network;

  • it writes -- caches, and the lock itself in some modes -- so it could not be

an observe-allowlisted program and the check could not stay pure;

  • it is not available for every ecosystem anyway, so half the check would be

structural regardless.

So each ecosystem is compared structurally, against the parts of the lock that record what the manifest asked for:

rlsbl.dep_locks
targetmanifestlockcompared-----------------------------------------------------------------------------------------------------pypipyproject.tomluv.lockthe project's own package entry: version, metadata.requires-dist, metadata.requires-devnpmpackage.jsonpackage-lock.jsonthe root entry (packages[""]): name, version, and the four dependency mapsgogo.modgo.sumevery required module has a recorded hash

uv and npm both record the requirements they resolved FROM, which is exactly what a staleness comparison needs: a requirement in the manifest that the lock never saw (or one the lock still carries that the manifest dropped) is a stale lock, with no resolution required to see it. Go records no such thing -- the require lines are the resolution -- so the comparison there is the one consistency go itself guarantees: every required module has a go.sum entry.

Which uv.lock is read is :func:rlsbl.uv_workspace.locate_uv_lock's answer: a uv workspace member has no lock of its own, so its manifest is resolved by the workspace root's lock and its entry is found under the member's own path.

Limits, stated rather than hidden ---------------------------------

  • A requirement uv resolves from a SOURCE -- a direct reference

(name @ file:///...) or a [tool.uv.sources] workspace/path/git/url entry -- is compared by PRESENCE only. uv records the source in requires-dist and drops the version specifier entirely, so there is nothing on the lock's side for the declared constraint to be compared against. Adding or removing the source itself is still drift, and is still reported.

  • A lockfileVersion 1 package-lock.json records no root requirement map, so

only presence of each declared dependency is compared, and the outcome says so.

  • A pyproject.toml with a dynamic version has no version to compare.
  • No ecosystem's absent lockfile is an error here: whether a project must

commit a lock is not this check's question. An absent lock is a note.

#DepLockVerdict

Result of comparing every lockfile in one project against its manifest.

#ok

python
def ok(self)

#normalize_version

python
def normalize_version(text)

A PEP 440 version in the canonical spelling uv writes.

uv re-serializes every requirement it locks from its PARSED form, so uv.lock carries the canonical spelling of what pyproject.toml declared -- verified against uv's own output, which rewrote >=1.0.0-alpha1 to >=1.0.0a1, >=1.0.0RC1 to >=1.0.0rc1 and >=01.02.03 to >=1.2.3. Comparing the two texts therefore means canonicalizing the declared side the same way, or a manifest that spelled a version legally but not canonically reads as a lock that predates it.

Anything this does not recognize -- a wildcard (1.0.*), a local directory, an unparseable string -- is returned unchanged rather than guessed at.

#normalize_specifier

python
def normalize_specifier(text)

A PEP 440 / npm specifier reduced to a comparable form.

Whitespace is dropped, each clause's version is canonicalized (see :func:normalize_version) and comma-separated clauses are sorted, so ">=1, <2" and "<2,>=1" are the same constraint -- which they are, and which is exactly the reordering uv performs when it writes the lock.

=== is left alone: PEP 440 defines arbitrary equality as a literal string match, so canonicalizing its operand would change its meaning.

#_normalize_clause

python
def _normalize_clause(clause)

One comparison clause with its version canonicalized.

#_declared_pypi_requirements

python
def _declared_pypi_requirements(data, sourced=frozenset())

(runtime_specs, dev_specs) declared by a parsed pyproject.toml.

runtime_specs is {name: {specifier, ...}} over [project]'s dependencies AND every optional-dependency extra, because uv folds extras into requires-dist with an extra == marker. dev_specs is {group: {name: {specifier, ...}}} over PEP 735 [dependency-groups] plus the legacy [tool.uv].dev-dependencies, which uv records as the dev group.

A direct reference (name @ file:///...) and a requirement redirected by [tool.uv.sources] both contribute their name with the sentinel specifier :data:_URL_SPEC: the lock records the source, not a specifier, so only presence is comparable.

#uv_sources_table

python
def uv_sources_table(data)

The [tool.uv.sources] table of a parsed manifest, or {}.

#source_backed_names

python
def source_backed_names(*tables)

Normalized names whose [tool.uv.sources] entry erases the specifier.

Later tables override earlier ones, which is uv's own precedence: the sources a WORKSPACE ROOT declares apply to every member, unless the member declares its own entry for that name. Reading only the member's table reported every member of a flat uv workspace -- where the sibling sources are declared once at the root -- as a stale lock.

A source may be declared as a table or as a LIST of marker-gated tables; one source-bearing element is enough, because the lock then records the source for that requirement.

#_inherited_sources

python
def _inherited_sources(root)

The [tool.uv.sources] a uv workspace root lends to root, or {}.

#_add_requirement

python
def _add_requirement(bucket, entry, sourced=frozenset())

Record one manifest requirement in bucket ({name: {spec, ...}}).

#_locked_pypi_requirements

python
def _locked_pypi_requirements(entries)

{name: {specifier, ...}} from a lock's list of requires-dist entries.

#_project_lock_entry

python
def _project_lock_entry(lock, relpath)

The lock's package entry for the project at relpath, or None.

uv records a workspace member (and a standalone project) as an editable or virtual source naming its directory relative to the lock, so the entry is found by that path rather than by name -- a name in pyproject.toml that the lock has not caught up with is exactly the drift being looked for.

#_compare_requirement_sets

python
def _compare_requirement_sets(declared, locked, *, where, problems)

Report every name and specifier difference between the two sides.

#parse_go_mod

python
def parse_go_mod(text)

(requires, replaced) from a go.mod's text.

requires is {module: version} over every require line, block form and single-line form alike. replaced is the set of module paths a replace directive redirects to a filesystem path -- those resolve from disk and carry no go.sum entry.

#_record_replace

python
def _record_replace(line, replaced)

Record a a [vX] => target line when its target is a local path.

#_go_sum_keys

python
def _go_sum_keys(path)

{(module, version)} recorded by a go.sum-format file.

#_find_go_work_sum

python
def _find_go_work_sum(root)

A go.work.sum of the go workspace above root, or None.

#evaluate_dep_locks

python
def evaluate_dep_locks(project_root)

Compare every lockfile in project_root against its manifest.

Returns a :class:DepLockVerdict. A project with no readable manifest in any of the three ecosystems comes back with a skip reason.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search