Skip to content

Vendored uv revert and remove half-revert a project whose sources use dotted keys under [tool.uv]: uv.lock is restored but the sources.<pkg> line stays, so uv sync --locked fails (vendor --revert exits 0) #544

Description

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

Summary

When a uv project declares its existing sources as dotted keys inside [tool.uv] (sources.localpkg = { path = "./localpkg" } or sources.localpkg.path = "./localpkg"), vendored mode writes the patched package's source the same way: sources.six = { path = ".socket/vendor/pypi/<uuid>/six-….whl" }. But the wiring ledger (.socket/vendor/state.json) records the line as six = { path = "…" }. vendor --revert and remove look for that exact line, don't find it, and treat it as third-party drift (vendor_lock_entry_drifted). They still restore uv.lock byte for byte, but they leave the sources.six line in pyproject.toml and keep the artifact directory.

Impact

  • The project is left inconsistent: pyproject.toml still routes six to the vendored wheel, while uv.lock is back to the registry entry. uv sync --locked fails with "The lockfile at uv.lock needs to be updated" (exit 1 on 0.8.17 / 0.12.22, exit 2 on 0.5.31). Frozen CI breaks after an unwind.
  • A plain uv sync re-locks to the vendored wheel, so the "reverted" project silently stays patched.
  • vendor --revert reports status: success and exits 0. remove reports partialFailure. Re-running either never converges: the same drift is reported every time. No user edit happened, so the "undo the drift and re-run" hint can't be followed.

Repro (Linux, main 61cfb9b, real uv 0.8.17)

mkdir -p localpkg/src/localpkg && touch localpkg/src/localpkg/__init__.py
printf '[project]\nname = "localpkg"\nversion = "0.1.0"\n[build-system]\nrequires = ["hatchling"]\nbuild-backend = "hatchling.build"\n' > localpkg/pyproject.toml
cat > pyproject.toml <<'EOF'
[project]
name = "proj"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["six==1.16.0", "idna==3.7", "localpkg"]

[tool.uv]
sources.localpkg = { path = "./localpkg" }
EOF
uv lock && uv sync && cp pyproject.toml pyproject.orig && cp uv.lock uv.lock.orig
# .socket/manifest.json holds a free six 1.16.0 patch (local mock patch API serving the prebuilt wheel)
socket-patch vendor --json $API          # adds: sources.six = { path = ".socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl" }
uv sync --locked                         # ok, six is patched
socket-patch vendor --revert --json $API # status success, exit 0, events: vendor_lock_entry_drifted, vendor_artifact_kept, vendor_revert_kept
cmp uv.lock uv.lock.orig                 # identical
diff pyproject.orig pyproject.toml       # > sources.six = { path = ".socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl" }
uv sync --locked                         # error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.
uv sync                                  # re-locks to the vendored wheel: six stays patched

The drift message is pyproject.toml fragment for Some("six") changed since vendoring; left untouched, though nothing touched the file. state.json records "new": "six = { path = \".socket/vendor/pypi/<uuid>/six-1.16.0-py2.py3-none-any.whl\" }", and the file holds sources.six = { … }.

socket-patch remove <uuid> gives the same result: partialFailure, the lock is restored, and the sources.six line stays.

Expected vs actual

  • Expected: CLI_CONTRACT / uv-compatibility.md: vendored revert restores the wiring it wrote, byte for byte. The vendor_lock_entry_drifted skip is meant for real third-party edits (pypi_uv.rs revert doc: "revert must never clobber third-party edits"), not for socket-patch's own line. Hosted mode on the same project round-trips byte-identically (checked on this run).
  • Actual: revert misreads its own line as drift and restores only half the project, leaving pyproject and the lock out of sync. It still exits 0.

OS × version

Spelling of existing sources uv 0.5.31 uv 0.8.17 uv 0.12.22
[tool.uv] + sources.localpkg = { path = … } – ❌ –
[tool.uv] + sources.localpkg.path = … ❌ ❌ (reproduced 3×) ❌
[tool.uv.sources] + localpkg = { … } (control) – ✅ byte-identical –
hosted scan → rollback, both dotted spellings (control) – ✅ byte-identical –

The failure is a CLI-side text match, so it doesn't depend on the OS; uv only has to reject the inconsistent result. Not bisected.

Related but distinct: #524 (sub-table spelling leaves an empty header; its "dotted key comes back byte-identical" note covered hosted only) and #474 (real drift from uv add --script). Also, the inline spelling [tool.uv] + sources = { … } is refused up front with pypi_uv_lock_parse_failed: … is not a standard table, so it never reaches this path.

Suspect code

  • crates/socket-patch-core/src/vendor/pypi_uv.rs:602-610: toml_edit inserts the key into a dotted sources table, so it prints sources.six = …, but the record hard-codes format!("{canon_name} = {{ path = … }}").
  • crates/socket-patch-core/src/vendor/pypi_uv.rs:884 (remove_exact_line(&pyproject_text, new) in revert_uv): the exact-line match fails, and the "already converged" probe sees the uuid needle, so the line is reported as drift. Meanwhile the uv.lock records are reverted anyway, which causes the half-revert. One possible fix: record the line toml_edit actually emitted (or match it through the TOML document by key path). And when any pyproject record drifts, don't revert the lock alone.

Activity

  1. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] One more spelling with the same failure (uv 0.8.17, main 61cfb9b): [tool] + uv.sources.localpkg = { path = "./localpkg" }. Vendor writes uv.sources.six = { path = … }, and vendor --revert reports vendor_lock_entry_drifted (status success) and leaves the line, so uv sync --locked fails afterwards. Root-level tool.uv.sources.x is very likely the same, since any dotted parent prints a prefixed key.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (uv / PyPI family). Confirmed on main 61cfb9b: crates/socket-patch-core/src/vendor/pypi_uv.rs inserts the key through toml_edit (ensure_table(&["tool","uv","sources"])), which keeps whatever dotted spelling the project already uses. The wiring record, though, hard-codes format!("{canon_name} = { path = … }"), so revert never finds its own line. This is a different cause from #524 (empty header left behind) and #474 (real third-party drift). Not a duplicate, and no open or merged PR covers it.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Shares root cause with #524: the uv [tool.uv.sources] wiring assumes a standalone, explicit [tool.uv.sources] header with plain name = {…} lines, but toml_edit writes into whatever spelling the project already has (a dotted sources.<pkg> key under [tool.uv], or a header-less parent implied by [tool.uv.sources.<pkg>] sub-tables). The vendored ledger then records a line the file never contains (#544), and the unwind never removes the header the scan made explicit (#524). Will be fixed together.


    Generated by Claude Code

  4. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #524; shared root cause: uv sources wiring ignores the project's existing sources-table spelling). Branch: agent/fix-uv-sources-table-spelling. Claim-ID: 2026-10-02T09:20:46Z-d304db


    Generated by Claude Code

  5. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #545


    Generated by Claude Code

  6. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Confirmed the root-level spelling on main 61cfb9b (uv 0.8.17, real uv sync --locked). Two variants: tool.uv.sources.localpkg = { path = "localpkg" } above [project], and tool.uv.sources.localpkg.path = "localpkg". Vendor writes tool.uv.sources.six = { path = … }. vendor --revert exits 0 with vendor_lock_entry_drifted / vendor_revert_kept, the line stays, and uv sync --locked fails.

    I also tested draft PR #545 (head bec2311), built locally, on both root-level variants. In both, revert restores pyproject.toml and uv.lock byte for byte, and uv sync --locked reinstalls upstream six. Vendored repair (wheel deleted → rebuilt with the identical sha256) passes on the dotted [tool.uv] sources.x spelling on main. A PEP 723 script with # [tool.uv] + # sources.localpkg = … already round-trips byte-identically on main.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions