Fix vendored rescans failing on unwired ledger entries (#541) - #543
Mikola Lysenko (mikolalysenko) wants to merge 8 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A vendored scan re-added every vendor-ledger entry to discovery, even after the dependency was upgraded or uninstalled. The vendor step then tried to re-vendor a package the lockfile no longer has, so every rescan failed with exit 1, including the --prune run that reverts the stale entry (#541). Ledger entries are now filtered through the same lockfile in-use check the prune GC uses. Entries the lock no longer wires are skipped with a vendor_ledger_entry_unwired warning that points at --prune. When the crawl finds nothing, a --prune run still does the lock-based vendored half of GC, so the last removed dependency can be reverted. Assisted-by: Claude Code:claude-opus-5-5
Covers #541 with real vlt: after the vendored dependency is bumped or uninstalled, a plain vendored rescan exits 0 with a warning, --prune reverts the entry and exits 0, and the next run is clean. The harness's mock patch API now answers only for the purls a batch request asks about, as the production API does, so a bumped release is not offered the old release's patch. Assisted-by: Claude Code:claude-opus-5-5
Adds the npm package-lock regression test for #541 and documents the new vendor_ledger_entry_unwired warning and the empty-crawl vendored GC in CLI_CONTRACT.md. Assisted-by: Claude Code:claude-opus-5-5
On vlt 0.0.0-30 to 0.0.0-32 a dependency removed from package.json stays in the lock: vlt uninstall keeps its file: spec declared, and vlt install keeps the removed edge and node. socket-patch correctly keeps the vendored entry there, so the uninstall leg now skips those releases with a documented compatibility rule. The leg keeps a bystander dependency and re-locks by hand if vlt leaves the spec declared. A new mock npm test covers --prune reconciling an unwired entry when the crawl finds no packages. Assisted-by: Claude Code:claude-opus-5-5
|
[agent] On 27991ff, I ran every supported release from 0.0.0-19 through rc.13 locally, and only 0.0.0-30, 0.0.0-31 and 0.0.0-32 behave this way. 5d6bfb6 adds a Generated by Claude Code |
|
[agent] The PDM
Each failing job has exactly one FAIL out of 36–46 cells, and a different cell fails on every run. The 2.10.4 cell that failed on 27991ff passes on 5d6bfb6, and the two jobs that fail on 5d6bfb6 passed on 27991ff. Each failing cell ran 20–34s against the live patch API, while its sibling cells took about 5s. PR #530 hit the same pattern ( The code path rules this PR out as well. The new filter only drops entries that the lockfile in-use probe proves unwired, and that probe exists only for npm and cargo; it answers "keep" for every PyPI entry. Hosted runs have no vendor ledger at all. I'll re-run the failed jobs once when this run finishes. Generated by Claude Code |
|
BugBot review Generated by Claude Code |
An npm entry whose lock entries were re-resolved since vendoring is drift-kept by scan --prune, so the vendor_ledger_entry_unwired warning came back on the next rescan while still suggesting only --prune. The warning now also names the way out of that case: restore the vendored dependency and run vendor --revert. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The unwired-entry warning suggested restoring the dependency and running vendor --revert. That does not clear a drift-kept npm entry: reinstalling writes a fresh registry entry that still counts as drift, and vendor --revert reverts every ledger entry anyway. The warning now gives the remedy the drift-keep contract supports: restore the lockfile from before vendoring, run scan --prune again, then re-run the package manager's install. The purls move into parentheses so they no longer read as command arguments. The npm test follows those steps and checks that the warning is gone afterwards. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The previous wording told users to restore the pre-vendor lockfile and prune again. That lock has no .socket/vendor paths, so the prune would revert every vendored entry, not just the stale ones. The warning now names only scan --prune. A prune that drift-keeps an entry already explains it on its own GC: kept line, so the warning points there instead of suggesting a lock edit whose reach it cannot bound. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] On 11934b9, Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 11934b9. Configure here.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #541
Summary
After a vendored dependency was upgraded or uninstalled, every
scan --mode vendoredexited 1 (partial_failure/vendor_lock_entry_not_found) and told the user to runvlt install, which they already had. The documented reconcile,scan --prune, failed too: it exited 1 in the same run where it reverted the stale entry. Scheduled vendored scans stayed red after any routine upgrade.With this change, a vendored rescan skips such an entry, prints a
vendor_ledger_entry_unwiredwarning that points atscan --prune, and exits 0.--prunereverts the entry and exits 0. That holds even when the removed dependency was the project's last one.Root cause
vendored_ledger_supplement(scan/discovery.rs) puts back into discovery every vendor-ledger entry that has no crawled counterpart. This exists for the fresh-clone case, where the committed artifact is the dependency. The function never checks whether the lock still wires the artifact. Once the dependency has left the lock, the vendor step tries to re-vendor a package the lock no longer has, and it does this before the prune GC runs. The bug is in shared code: npm package-lock projects hit it the same way.Fix
vendor::dispatch_in_use_one). An entry is supplemented only when the probe says it's in use or can't decide (None, fail-safe: no probe for the ecosystem, or no readable lock). Entries proven unwired come back inLedgerSupplement::unwired.getuses the same supplement and gets the same behavior.vendor_ledger_entry_unwiredwarning, naming the purls and pointing atscan --prune. Any non-hosted--prunerun reverts them in its GC with no warning.--prunenow still runs the vendored half of the GC (gc::run_vendor_only_gc, under the apply lock) whenever unwired entries exist. That half decides from the lockfile and manifest, not the crawl. The manifest prune is still skipped.scan --prune, which reverts just the entries proven unwired. For a drift-kept entry it points at the prune's ownGC: keptline. Both lock-edit remedies I tried were unsafe: one doesn't converge, and the other would unwire every vendored entry. Reclaiming such entries is the npm prune follow-up noted on Vendored vlt scan exits 1 after the patched dependency is upgraded or uninstalled, and evenscan --pruneexits 1 while it reverts the stale entry #541.Tests (red on main, green here)
--prunereverts + exit 0, next run cleane2e_vendor_vlt_build::vlt_pinned_matrix_vendored_dependency_bumped_rescanrescan: exit 1)vlt uninstall, same assertionse2e_vendor_vlt_build::vlt_pinned_matrix_vendored_dependency_uninstalled_rescanrescan: exit 1)scan_vendor_e2e::scan_vendor_skips_ledger_entry_the_lock_no_longer_wires--pruneruns the vendored GC on an empty crawl, a drift-kept entry's repeat warning points atGC: keptscan_vendor_e2e::scan_vendor_prune_reconciles_unwired_entry_on_an_empty_crawldiscovery::tests::ledger_supplement_skips_entries_the_lock_no_longer_wires,..._keeps_wired_and_undecidable_entriesTest-harness notes:
vlt uninstallkeeps afile:spec declared, andvlt installkeeps the removed edge and node. socket-patch correctly keeps the entry there. I tested every supported release from 0.0.0-19 through rc.13, and those three are the only ones affected. The uninstall leg skips them under a newremoved-dependency-stays-lockedrule indocs/testing/vlt-compatibility.md, and the manifest is regenerated withcheck-vlt-legs.py --derive.Local verification
cargo clippy --workspace --all-features -- -D warnings: clean.e2e_vendor_vlt_buildthroughcheck-vlt-legs: clean on vlt 0.0.0-30, 0.0.0-32, rc.14 and 1.2.0 (34/34). The new legs also pass on 0.0.0-19 … 0.0.0-29, rc.1/5/9/13, rc.32 and 1.0.4.e2e_redirect_vlt_build38/38,mode_migration_vlt14/14, pluse2e_safety_vltande2e_vltpassing.scan_vendor_e2epasses 33/33.cargo test --workspace --all-features --no-fail-fast: 208 binaries pass. 12 tests in 4 binaries fail identically onorigin/main, because the sandbox runs as root and their read-only-directory setups don't block writes. Those arecovgap_commands_vendor(3),in_process_redirect(3),repair(2) and thesocket-patch-corelib (4); this PR doesn't touch core.*_productionvlt legs can't run in this sandbox (the CLI's TLS roots reject the proxy's CA), so CI covers them.cargo fmt --all -- --checkreports drift in 127 files onorigin/mainitself, and CI doesn't run fmt. I left the repo's formatting as is.CI notes
native (...)jobs fail one random cell per run against the live patch API, and the cell changes every time. This is the same flake PR Fix lock-only requirements.txt discovery (#412, #523) #530 cleared on a rerun; details are in the PR comments. I re-ran the failed jobs once.npm/,pypi/,gem/) changes are needed: they only dispatch to the binary.🤖 Generated with Claude Code
https://claude.ai/code/session_01VEgj84zEsgqYoVSqrt7CqJ
Generated by Claude Code