Skip to content

After uv remove of a vendored package, scan --prune, vendor --revert, remove and rollback all keep it as "drifted", so vendor --check stays red and its suggested scan --prune fix loops #1140

Description

[agent] Found by the scheduled uv bug-hunt routine (ledger #310).

Summary

A user vendors six into a uv project and later drops the dependency with uv remove six. That is ordinary dependency churn. uv cleans up after itself: it drops six from dependencies and the [tool.uv.sources] six = { path = ".socket/vendor/…" } line, then relocks, and uv lock --check / uv sync --locked pass. After that, socket-patch can never reclaim the vendored entry:

  • vendor --check exits 1: "dependency removed: no lockfile resolves pkg:pypi/six@1.16.0 any more … run socket-patch scan --mode vendored --prune to revert the vendored entry".
  • scan --mode vendored --prune exits 0 success. With --json it lists six under gc.keptVendoredEntries; the human output says only "No patches available for installed packages." The wheel and the ledger entry stay.
  • vendor --revert exits 0 and keeps the entry ("uv.lock fragment for Some("six") changed since vendoring; left untouched" plus vendor_lock_entry_removed). It advises "undo the drift (restore the vendored lock entries or re-vendor)", which is impossible because the dependency is gone.
  • remove pkg:pypi/six@1.16.0 and rollback exit 1 (partialFailure) with the same drift warnings.
  • vendor --check stays at exit 1 afterwards, so a CI gate on it stays red until someone hand-edits .socket/vendor/state.json.

The Bun lane has the same shape (#1132), and npm has a related human-mode GC gap (#1127). This is the uv code path (revert_uv).

Repro (Linux; uv 0.5.31 and 0.12.23)

mkdir app && cd app
printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "idna==3.7"]\n' > pyproject.toml
uv lock && uv sync
socket-patch scan --mode vendored --yes        # vendors six (exit 0)
uv remove six                                  # uv drops the dep + its [tool.uv.sources] entry; uv lock --check passes
socket-patch vendor --check; echo $?           # 1: "dependency removed … run scan --mode vendored --prune"
socket-patch scan --mode vendored --prune --yes --json | jq .gc.keptVendoredEntries   # ["pkg:pypi/six@1.16.0"], exit 0
socket-patch vendor --revert --yes             # exit 0, "Kept 1 drifted package … undo the drift and re-run"
socket-patch remove pkg:pypi/six@1.16.0 --yes  # exit 1, partialFailure
socket-patch vendor --check; echo $?           # still 1; .socket/vendor/pypi/<uuid>/six-…whl and the ledger entry remain

I served patch data from a local mock of the patch API (six@1.16.0).

Expected vs actual

  • Expected: once uv has removed every reference to the vendored artifact (no .socket/vendor/pypi/<uuid> left in uv.lock or pyproject.toml, and no package entry for it), the revert has nothing left to restore. It should count as converged, so scan --prune (the remedy vendor --check prints) reverts the entry, deletes the wheel and turns vendor --check green. revert_uv already treats a vanished [manifest] overrides fragment as converged when "no surviving reference to this entry's uuid dir" remains. The package and requires-dist records lack that arm.
  • Actual: every unwind drift-keeps, and the advice loops (check → prune → kept → check).

Matrix (Linux; real uv remove / uv lock)

uv socket-patch check message scan --prune vendor --revert remove rollback check after
0.12.23 main 3b4ac84 "dependency removed … run scan --prune" exit 0, kept exit 0, kept exit 1, kept exit 1, kept 1
0.5.31 main 3b4ac84 same – – exit 1, kept – 1
0.12.23 b96a785 (before #1050) "wiring missing … re-run vendor" exit 1, partialFailure exit 0, kept – – 1

So the stuck state predates #1050. What #1050 changed is the remedy: vendor --check now names scan --prune, and scan --prune exits 0 while keeping the entry.

Suspect code

crates/socket-patch-core/src/vendor/pypi_uv.rs:793 (revert_uv):

  • The uv_lock_package / uv_lock_requires_dist arm (line 876) counts only "the recorded pre-vendor original is present" as converged, so a package uv remove deleted is drift.
  • respell_original (line 836) refuses the requires-dist record because pyproject.toml no longer declares six.

Neither arm checks the condition the overrides arm uses: no surviving reference to .socket/vendor/pypi/<uuid> anywhere in the lock or pyproject. The GC then reports kept (commands/vendor.rs run_vendor_gc), and unwired_check_failure keeps sending users to it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions