Skip to content

fix(vendor): keep artifacts and ledger when revert drift-skips the lock restore - #205

Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
fix/vendor-revert-driftskip-keep-artifacts
Open

fix(vendor): keep artifacts and ledger when revert drift-skips the lock restore#205
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
fix/vendor-revert-driftskip-keep-artifacts

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Root cause (residual #131, adversarially re-confirmed on main 2026-08-18)

Two-stage lossiness when vendor --revert drift-skips the lock restore:

  1. Backendcrates/socket-patch-core/src/vendor/yarn_classic_lock.rs:352-354: revert_yarn_classic ran remove_tree(.socket/vendor/npm/<uuid>/) unconditionally after the restore loop, even when revert_recorded_block (:404-413) drift-skipped EVERY wiring record with vendor_lock_entry_drifted "left alone" (the ownership gate fails for any resolved URL outside our uuid dir — a hosted patch.socket.dev overlay and a registry re-resolve alike). Same shape in the npm-family twins: npm_lock.rs:460, pnpm_lock.rs:593, yarn_berry_lock.rs:631, bun_lock.rs:345.
  2. Orchestratorcrates/socket-patch-cli/src/commands/vendor.rs (run_revert :1366-1370, reconcile_dropped :1319-1326): RevertOutcome had no "kept" channel, so the all-skipped case was plain success ⇒ PatchAction::Removed + state.entries.remove(purl) + save_state. Sub-bug: the skip events pushed by record_warning bypass env.record, so summary.skipped stayed 0 while the events array showed the skip.

End state (matrix cell E1, edgey1-layer-revert): yarn.lock still pointed at patch.socket.dev, but the artifacts, the ledger entry holding the only pre-vendor original lock fragments, and the tarball that redirect-state.json's recorded edit original references were all deleted — silent-success data loss plus a dangling-replay hazard for any future hosted-edit restore.

Fix

Reuses the orphan sweep's invariant (vendor.rs:1392-1413): never delete what something still references.

  • RevertOutcome (core vendor/mod.rs) grows kept_artifact: bool + drift_skipped() / keep_artifact() helpers. keep_artifact() pushes an honest vendor_artifact_kept warning naming the kept dir and the remediation ("undo the drift and re-run vendor --revert").
  • All five npm-family backends with the unconditional-remove shape (yarn_classic_lock, npm_lock, pnpm_lock, yarn_berry_lock, bun_lock) skip the remove_tree and mark the keep whenever any wiring record was drift-skipped.
  • run_revert / reconcile_dropped honor the signal: state.json entry retained, outcome recorded as a counted Skipped (vendor_revert_kept) via env.record — fixing summary.skipped == 0 — never a Removed; counts stay uninflated per the fix(vendor): count skipped packages, not advisory warnings #166 precedent (advisory warnings keep bypassing the counters). Human output gains a Kept N drifted package(s)… line.
  • Other RevertOutcome construction sites get kept_artifact: false (zero behavior change outside the npm family). Once the drift is undone, the next vendor --revert completes exactly as before (lock restored byte-for-byte, artifacts + ledger pruned) — proven by test legs.

socket-patch remove <purl> (the hosted-mode-prescribed cleanup) still prunes the manifest + ledger by explicit user intent; with this fix the kept artifact dir simply survives until the next vendor run's orphan sweep instead of being deleted while still referenced.

Tests (TDD, RED first — commit 841f8a4, fix 7a2013f)

  • Flipped the deliberate fix(redirect): fail closed on non-npm: berry descriptors + yarn layering regression suite #131 pinning test classic_vendor_revert_after_hosted_overlay_is_drift_skipped_and_lossy…_keeps_blob (yarn_layering_tests.rs), per its own doc-comment instruction; now also proves the follow-up revert completes once the drift is undone.
  • Extended the five backends' revert_leaves_drifted_* unit tests: artifact dir kept + vendor_artifact_kept surfaced.
  • New CLI orchestration tests (in_process_vendor.rs): revert_after_drift_keeps_artifact_and_ledger_then_completes and reconcile_keeps_drifted_entry — ledger retained, counted Skipped (summary.skipped == 1, removed == 0), no Removed event, second revert completes.
  • New e2e capstone (e2e_vendor_yarn_classic_dev_flow.rs): real yarn 1.22.22 via corepack — vendored → hosted-URL overlay → vendor --revert keeps tarball/ledger → drift undone → kept tarball still installs the patched bytes → second revert completes → pristine registry install.

RED evidence: 6 core + 2 CLI + 1 e2e tests failed on main behavior (summary.skipped: 0, removed: 1, artifact dirs deleted).

Matrix / manual evidence (old vs new binary, throwaway copies)

Replayed the brief's verify repro (matrix2/verify/C-revert-driftskip-lossy-131, real prod scan --vendor of minimist@1.2.2, anonymous free tier) on throwaway copies:

binary revert exit summary .socket/vendor after
old (main) 0 removed 1, skipped 0 empty — tarball, inventory, state.json all deleted; lock still drifted
this PR 0 removed 0, skipped 1 minimist-1.2.2.tgz, socket-patch.vendor.json, state.json all kept

Synthetic E1 (hosted patch.socket.dev overlay over vendored left-pad) shows the same flip, and the recovery leg on the new binary (restore vendored lock → revert) ends with removed 1, vendor tree fully pruned, registry lock restored, yarn install exit 0 with pristine bytes.

Gates

  • cargo test -p socket-patch-core — clean (22/22 binaries ok)
  • cargo test -p socket-patch-cli — clean
  • cargo clippy --workspace --all-features -- -D warnings — clean
  • fmt: only touched files formatted

Sibling-PR conflict notes (parallel campaign)

🤖 Generated with Claude Code


Note

Medium Risk
Changes vendoring revert semantics and ledger persistence for drift-skipped lock restores across npm-family backends and CLI orchestration; behavior is well-tested but affects data retention and JSON summary counts on a sensitive path.

Overview
Fixes residual #131: vendor --revert used to leave drifted lock entries untouched but still delete .socket/vendor/ artifacts and prune state.json, reporting success with Removed / summary.removed instead of an honest skip.

Core: RevertOutcome adds kept_artifact plus drift_skipped() / keep_artifact() (with vendor_artifact_kept warnings). The npm-family revert backends (yarn_classic, npm_lock, pnpm, yarn_berry, bun) skip remove_tree when any wiring record was drift-skipped.

CLI: run_revert and reconcile_dropped retain ledger entries and emit a counted Skipped (vendor_revert_kept) via env.record—not Removed—and human output adds a “Kept N drifted package(s)…” line when summary.skipped > 0.

After undoing drift, a follow-up vendor --revert still completes as before (lock restored, vendor tree pruned). Tests flip the prior lossy pinning test and add e2e/in-process coverage for keep + recovery.

Reviewed by Cursor Bugbot for commit 7a2013f. Configure here.

… (residual #131)

Flip the deliberate #131 pinning test
classic_vendor_revert_after_hosted_overlay_is_drift_skipped_and_lossy into
the fixed contract (..._keeps_blob): when every wiring record is
drift-skipped ("left alone"), the blob dir must SURVIVE the revert and the
keep must be surfaced (vendor_artifact_kept) — plus a recovery leg proving
a second revert completes once the drift is undone.

Same contract asserted across the npm-family twins' existing drift tests
(npm_lock / pnpm_lock / yarn_berry_lock / bun_lock / yarn_classic_lock:
revert_leaves_drifted_* now require the artifact dir kept + the keep
warning), two new CLI orchestration tests in in_process_vendor.rs
(run_revert and reconcile must retain the state.json entry, record a
COUNTED Skipped vendor_revert_kept — summary.skipped was 0 even when the
skip event was emitted — and never a Removed), and a new yarn-classic e2e
capstone leg (vendored -> hosted overlay -> vendor --revert keeps the
tarball, the kept tarball still installs after the drift is undone, and
the follow-up revert completes to pristine).

RED: 6 core + 2 CLI + 1 e2e tests fail on current main behavior
(remove_tree runs unconditionally after the restore loop; success prunes
the ledger entry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ck restore

Residual #131 (strapi campaign, adversarially re-confirmed on main): when
vendor --revert found every recorded lock entry re-resolved (a hosted
patch.socket.dev overlay, a registry re-resolve — anything failing the
uuid-dir ownership gate), revert_recorded_block correctly left the lock
alone with vendor_lock_entry_drifted warnings, but the backend still ran
remove_tree(.socket/vendor/npm/<uuid>/) unconditionally after the restore
loop, and run_revert/reconcile_dropped treated the warning-only success as
"removed": PatchAction::Removed + state.entries.remove + save_state. End
state: the lock still pointed at the drifted target while the artifacts,
the ledger entry holding the only pre-vendor originals, and the tarball
the redirect ledger's recorded `original` fragments reference were all
destroyed — a silent-success data loss with a delayed broken-install
hazard.

Fix, per the orphan sweep's invariant (never delete what something still
references):

- RevertOutcome grows `kept_artifact` plus drift_skipped()/keep_artifact()
  helpers (vendor/mod.rs). keep_artifact() pushes an honest
  `vendor_artifact_kept` warning naming the kept dir and the remediation.
- All five npm-family backends with the unconditional remove shape
  (yarn_classic_lock, npm_lock, pnpm_lock, yarn_berry_lock, bun_lock) now
  skip the remove_tree and mark the keep whenever any wiring record was
  drift-skipped.
- run_revert and reconcile_dropped (cli vendor.rs) honor the signal: the
  state.json entry is retained and the outcome is a COUNTED Skipped event
  (`vendor_revert_kept`) recorded via env.record — fixing the sub-bug
  where summary.skipped stayed 0 while skip events were emitted — never a
  Removed; counts stay uninflated (#166 precedent: advisory warnings keep
  bypassing the counters). Human output gains a "Kept N drifted
  package(s)" line.
- Other RevertOutcome construction sites gain `kept_artifact: false`
  (no behavior change outside the npm family).

Once the drift is undone (vendored lock restored — e.g. a future hosted
revert replaying its recorded originals), the next `vendor --revert`
completes exactly as before: lock restored byte-for-byte, artifacts and
ledger pruned.

GREEN: the 6 flipped core tests, the 2 new CLI orchestration tests, and
the new yarn-classic E1 e2e capstone (drift-skip keep -> kept tarball
still installs -> follow-up revert completes to pristine) all pass;
cargo test -p socket-patch-core clean; clippy --workspace --all-features
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: GC ignores artifact-keep signal
    • Added kept_artifact checks to both manifest-dropped and lockfile-unused paths in run_vendor_gc to prevent ledger removal when backends deliberately retain artifacts during drift-skip keeps.

Create PR

Or push these changes by commenting:

@cursor push dfde23a211
Preview (dfde23a211)
diff --git a/crates/socket-patch-cli/src/commands/vendor.rs b/crates/socket-patch-cli/src/commands/vendor.rs
--- a/crates/socket-patch-cli/src/commands/vendor.rs
+++ b/crates/socket-patch-cli/src/commands/vendor.rs
@@ -1568,10 +1568,13 @@
                 continue;
             }
             let entry = state.entries.get(&purl).cloned().expect("listed above");
-            if dispatch_revert_one(&entry, &common.cwd, false)
-                .await
-                .success
-            {
+            let outcome = dispatch_revert_one(&entry, &common.cwd, false).await;
+            if outcome.success {
+                if outcome.kept_artifact {
+                    // Drift-skip keep: backend kept the uuid dir, so the ledger
+                    // entry must survive too (same contract as reconcile_dropped).
+                    continue;
+                }
                 state.entries.remove(&purl);
                 out.dropped_reverted.push(purl);
             } else {
@@ -1601,13 +1604,16 @@
             out.unused_reverted.push(purl);
             continue;
         }
-        if !dispatch_revert_one(&entry, &common.cwd, false)
-            .await
-            .success
-        {
+        let outcome = dispatch_revert_one(&entry, &common.cwd, false).await;
+        if !outcome.success {
             out.failed.push(purl);
             continue;
         }
+        if outcome.kept_artifact {
+            // Drift-skip keep: backend kept the uuid dir, so the ledger
+            // entry must survive too (same contract as reconcile_dropped).
+            continue;
+        }
         state.entries.remove(&purl);
         if let Some(m) = manifest.as_mut() {
             let base = strip_purl_qualifiers(&entry.base_purl).to_string();

You can send follow-ups to the cloud agent here.

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

Reviewed by Cursor Bugbot for commit 7a2013f. Configure here.

`vendor --revert` to finish.",
env.summary.skipped
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GC ignores artifact-keep signal

High Severity

run_vendor_gc still treats any successful revert as a full removal and prunes the ledger whenever success is true. It never checks kept_artifact, unlike reconcile_dropped and run_revert. On a drift-skip keep, the backend retains the uuid dir, GC drops the ledger entry, then the trailing orphan sweep deletes the artifacts because the drifted lock no longer points into that dir — recreating residual #131 via scan --prune.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7a2013f. Configure here.

…s are not drift

The keep gate (drift_skipped) keyed on vendor_lock_entry_drifted, but the
npm-family backends emitted that same code for records a PREVIOUS partial
revert had already restored or removed: a restored fragment's registry
value fails the uuid ownership gate ("re-resolved since vendoring"), a
removed berry resolutions entry hits "no longer exists; nothing to
remove", and a rekeyed pnpm/berry block restored under its pre-vendor key
misses the recorded-key lookup ("no longer exists; nothing to restore").
After any partial restore, every later `vendor --revert` therefore
re-classified the converged records as drift and kept the artifact dir +
ledger entry forever — the CLI's own "undo the drift and re-run `vendor
--revert` to finish" remediation could never be satisfied.

Teach every left-alone branch in the five keep-gate backends (npm, bun,
pnpm, yarn classic, yarn berry) to recognize the ALREADY-CONVERGED state
as a silent no-op before warning:

- a live fragment equal to the recorded pre-vendor `original` (restored
  in place: npm/bun entries, classic/berry blocks, pnpm overrides /
  importer deps / snapshot refs, berry takeover resolutions);
- for Added records with no original, the key being absent (berry
  resolutions entry/table, pnpm overrides in package.json /
  pnpm-workspace.yaml / the lock's overrides section);
- for the rekeying restores (berry lock entries, pnpm packages/snapshots
  blocks), the recorded original block being live verbatim under its own
  pre-vendor key when the recorded key no longer matches.

drift_skipped() now fires only on genuine third-party drift the user can
still undo, so the kept artifacts + ledger entry are released the moment
the drift is undone — run 2 completes the revert and prunes everything.

Tests: each backend's drift unit test gains a second-run leg (undo ONLY
the drift; already-reverted records must be silent and the revert must
complete), the CLI keep tests now drift to a value that is neither ours
nor the original, and a new CLI test pins that a lock manually restored
to the pre-vendor original converges (counted Removed, ledger pruned) on
the first revert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant