Skip to content

Fix uv revert after a vendored dep goes direct (#1374) - #1384

Merged
Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-uv-revert-direct-requires-dist
Oct 11, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-uv-revert-direct-requires-dist

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1374

Summary

In a uv project, vendor a transitive package and then add it directly (uv add six==1.16.0). After that, vendor --revert, remove and rollback wrote a pyproject.toml / uv.lock pair that uv sync --locked rejects. The revert still exited 0, and vendor --check then wrongly said no file references the vendored wheel. Now:

  • The revert puts the root element that uv add created back on the registry, spelled the way pyproject.toml declares it now (the Vendored uv revert writes the pre-vendor specifier back into uv.lock after the user changes the vendored package's version spec, so uv sync --locked fails (vendor --revert / remove / rollback exit 0) #840 respell rule). This covers requires-dist, and also requires-dev for uv add --dev.
  • When that spelling can't be derived (a multi-clause or === declaration, a URL, or no declaration), the revert writes neither file. It drift-keeps both, and the warning says what to change. That matches the issue's fail-closed alternative and the PEP 723 script lane.
  • A final pair gate runs before any write. If the reverted uv.lock would still route through the uuid dir while the reverted pyproject.toml no longer does, nothing is written. A half-reverted uv pair can't be written any more.
  • vendor --check: for a PyPI entry that nothing installs from but some project file still names, the verdict is wiring incomplete: … but uv.lock still resolves through it … with a re-lock-first remedy. It no longer says "no lockfile or config references …".

Root cause

revert_uv (crates/socket-patch-core/src/vendor/pypi_uv.rs) restores only the fragments the ledger recorded. A transitive package has no root requires-dist element when it is vendored, so nothing is recorded for it. The element uv add writes later through the socket-written [tool.uv.sources] entry was therefore never restored. The pair was also written without checking that the reverted lock no longer routes through the wheel. The residual-reference guard in pypi.rs runs only after the write, so it could keep the artifact but couldn't stop the inconsistent write.

Test evidence

Each new test was run red with the fix disabled (the new blocks short-circuited), then green with it:

Issue item Test Without fix With fix
#1374: revert after uv add six==1.16.0 (requires-dist) pypi_uv::tests::revert_after_promoting_a_transitive_to_direct_respells_the_root_entry FAILED ok
#1374: same via uv add --dev (requires-dev) pypi_uv::tests::revert_after_promoting_a_transitive_to_a_dev_group_respells_it FAILED ok
#1374: fail closed, never a half-written pair pypi_uv::tests::revert_keeps_both_files_when_an_unrecorded_reference_survives (multi-clause, ===, undeclared, another name routed through the wheel) FAILED ok
#1374: real uv end to end (vendor transitive, uv add six==1.16.0, vendor --revert, uv lock --check) e2e_vendor_pypi_build::uv_vendor_revert_after_promoting_the_transitive_to_direct (uv 0.11.32) FAILED ("uv.lock must be fully unwired") ok
#1374: vendor --check claims no file references the dir mode_migration_pypi::vendor_check_names_the_file_of_a_half_unwound_uv_pair FAILED ok

remove and rollback call the same revert_uv, so the same fix covers them.

Commands run locally:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test --workspace --all-features: 7,285 passed. 13 failed, all sandbox-only and outside the code this PR touches: 12 write-failure tests whose chmod-made read-only dirs don't block writes because the sandbox runs as root (copy_tree::relax_loop_must_not_traverse_symlinked_root, covgap_commands_vendor ×3, partial_lockfile_write_failure…, redirect_json_mode_write_failures…, vlt::…heal_invalidation_failure_warns, repair_…_unremovable/cleanup_failure ×2, and core copy_tree/vlt_heal/pypi_poetry/pypi_requirements write-failure tests), plus 1 that needs network to the patch API (e2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managed). CI runs them as non-root
  • cargo test -p socket-patch-cli --all-features --test e2e_vendor_pypi_build -- --include-ignored uv_vendor_revert_after with SOCKET_PATCH_UV_E2E_REQUIRED=1: 7 passed (every existing uv relock-then-revert capstone plus the new one)

The npm / PyPI / gem wrappers only dispatch to the binary, so they need no change.

/code-review high

Fixed in b022bef: === and other underivable declarations are no longer restored without a specifier; the keep-both remedies no longer suggest a uv lock that would rebuild the same element; vendor --check uses the probe's clause instead of stripping an English suffix; lock_requirement_declared was folded into declared_lock_specifier; drift_kept is now a plain bool; the doc comment lists the new verdict.

Not fixed:

  • The pair gate uses a substring test on pyproject.toml. A pyproject that still mentions the uuid path in a comment skips the gate. In that case, though, the existing post-write residual-reference guard (pypi_reference_clause, which probes pyproject.toml) keeps the artifact and the ledger entry, so nothing is deleted. A precise TOML-level route check would add a lot of code for a far-fetched input.
  • The root requirement array walk is duplicated with rewrite_root_metadata_entries. Sharing it means refactoring the wire path as well, which is out of scope for this fix.
  • The element is parsed twice (strip path, then with_specifier). This only runs on the rare promoted element, and reusing with_specifier keeps key placement in one place.

🤖 Generated with Claude Code

https://claude.ai/code/session_019iinbVuLMEUcDSE3PWUSho


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
When a package vendored as a transitive uv dependency was later added
directly (`uv add six==1.16.0`), uv wrote a root requires-dist element
that routes through the vendored wheel. `vendor --revert`, `remove`
and `rollback` never restored it, so they wrote a pyproject.toml and
uv.lock that `uv sync --locked` rejects, and revert still exited 0.

The revert now puts any such root requires-dist or requires-dev
element back on the registry, spelled the way pyproject.toml declares
it now. If it can't (no declaration, or a spelling it can't derive),
or uv.lock still routes through the wheel after pyproject.toml stops
naming it, it leaves both files untouched instead of writing a pair
uv rejects.

`vendor --check` no longer claims no file references a vendored PyPI
artifact when one still does: it names the file and suggests
re-locking first.

Fixes #1374

Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the uv revert fix:

- A promoted element whose declaration uv's spelling can't be derived
  from (`===`, a URL, an unreadable spec) now keeps both files instead
  of being restored with no specifier, which `uv sync --locked` would
  also reject.
- The keep-both warnings tell the user what to change; re-running
  `uv lock` alone would rebuild the same element.
- `vendor --check` reports the residual-reference probe's own clause,
  so an unreadable file is reported as such rather than as "no file
  references it".
- Cover the `vendor --check` verdict with a CLI test.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 11, 2026 13:28
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b022bef. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 11, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at b022bef.

  • CI: 36/36 non-skipped checks green on the head; no main-wide failures.
  • Bugbot: reviewed b022bef, no findings.
  • Mergeable: no conflicts with main; no CHANGELOG.md changes.

Slack announcement pending (no Slack send tool in this run; the next run retries).


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. When a vendored uv dep was later promoted to a direct dependency (uv add six), revert_uv left a half-reverted pair: pyproject.toml fixed but uv.lock still routing root requires-dist/requires-dev through the uuid dir. Revert now respells those unrecorded root elements back to the registry using the specifier pyproject.toml declares, fails closed when it can't derive one, and refuses to write a lock that still names the dir once pyproject.toml doesn't. vendor --check now reports wiring incomplete naming the file instead of a wrong "no references" message. Fixes #1374.

Risk: low. New code runs only when there's no drift and only touches root-unit elements named for the package whose path is under .socket/vendor/pypi/<uuid>. Every unclear case (multi-clause or === spec, undeclared name, workspace member, another name routed through the wheel) keeps both files untouched.

Look here

Verified. Read the full diff. cargo test -p socket-patch-core --lib pypi_uv 122 passed, upstream::uv 24 passed, --lib pypi 524 passed (2 failures are chmod-based tests that can't fail when run as root; code untouched by this PR). cargo test -p socket-patch-cli --all-features --test mode_migration_pypi --test in_process_vendor 132 + 55 passed. Short-circuiting the new blocks makes the 3 new revert tests fail, so they exercise the fix. Probed CRLF round-trip (byte-exact), marker elements, extras and dry-run with a multi-clause spec. Did not run the networked uv e2e (body says it passed locally). CHANGELOG untouched. CI 84/84 (36 success, 48 skipped); Cursor Bugbot: no issues.

Changes I made. None.

Open questions (non-blocking).

Auto-merge is armed: approving sends it straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit 077bbd0 Oct 11, 2026
84 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-uv-revert-direct-requires-dist branch October 11, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants