GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Match Composer patch versions like Composer does by mikolalysenko · Pull Request #270 · SocketDev/socket-patch

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 26, 2026 •
edited by cursor Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator

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

The CLI now matches Composer patches by release version the way Composer does. Composer patches stay installed, and VEX reports them fixed, on every Composer version from 1.10 to 2.10 on Linux, macOS and Windows.

The depscan counterparts are SocketDev/depscan#26866 (implementation) and SocketDev/depscan#26864 (fixtures), which replace #26854.

Version matching

A Composer patch's base purl can carry the padded version Socket's SBOM ingestion stores (pkg:composer/psr/[email protected]), while composer.lock and installed.json say 3.0.2 or v3.0.2. The CLI compared the two as strings after stripping v, so it failed in four ways:

  • apply and vendor reported the patch "not found";
  • the hosted redirect refused it as a version mismatch;
  • VEX rejected it;
  • scan --prune deleted it.

What changed:

  • Version module: utils::composer_version ports composer/semver 3.4.4 VersionParser::normalize.
  • Every comparison uses it: crawler, purl_eq, vendor lock lookup, hosted rewriter, redirect ledger, VEX discovery and sources, scan discovery, prune and gc, and the vendored ownership checks in apply, rollback and scan.
  • Stored spellings don't change.
  • Synced with Add in-memory hosted engine and napi addon #276: merged origin/main (afa72533, the in-memory hosted engine) with a plain merge commit; it had no conflicts. hosted_memory does not copy the composer rewriter. It calls core's rewrite_registry_redirect_with_pipenv_version, so composer_source and composer_version reach it through core. Its vendored-takeover check only covers cargo, npm and golang purls, so composer ownership by identity doesn't apply there.
  • Shared vectors: tests/fixtures/composer-version-vectors.json was generated from real Composer 2.10.3 and is byte-identical to depscan's copy.

Making sure patches are actually installed

  • Hosted redirect:
    • removes source wherever it sits in the entry, not only right before dist, because Composer 1.x–2.9 silently fall back to the pristine git source;
    • strips dist.mirrors, including a list placed before url;
    • refuses a dist whose only URL is a mirror.
  • Vendored copies: neutralize root .gitignore and export-ignore rules, which make Composer's path mirror drop files. Vendoring refuses when a patched file would be dropped.
  • Vendored lockfile edits: only the patched entry in composer.lock is edited. CRLF, mixed line endings, \/ escapes and indentation survive, and vendor --revert is byte-identical.
  • VEX: no longer reports a package as fixed when Composer will install it from source. That happens when preferred-install resolves to source, or when it's auto with a dev version on a 1.x/2.0 lock.
  • Next-step hints: composer install, with Composer 1 and dist-only reinstall guidance naming the vendor/ dir to remove.

CI

  • Exact-release matrix: composer-compatibility.yml covers Composer 1.10.28, 2.0.14, 2.1.14, 2.2.30, 2.5.8, 2.8.12, 2.9.8 and 2.10.3.
    • Linux: every version.
    • macOS and Windows: 1.10.28, 2.2.30 and 2.10.3.
    • Windows also: 2.9.8.
  • Triggers: the workflow runs on every composer-related path change.
  • Pinned Docker image: Dockerfile.composer installs an exact, checksum-verified Composer.
  • Docker tests: Composer Docker tests fail instead of skipping when they're required.
  • Docs: the ci.yml comment on the git-source fallback is corrected.

Rebased onto main after #257

Rebased onto 8c381ecf (#257's concurrent, parallel-crawl, single-pass rewriters). Every #270 behavior sits on #257's paths:

  • Version identity everywhere, oracles too: composer_versions_equivalent is used at every call site, including Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's composer crawler oracle (crawlers/composer_crawler/oracle.rs) and the hosted rewriter oracle (patch/redirect/composer_equivalence_tests.rs). Both had encoded the old v-strip compare, so they now use Match Composer patch versions like Composer does #270's semantics.
  • Hosted source and mirrors: the rewriter keeps Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's walk and in-place splice: test the name first, walk bytes, replace_range per edit. composer_source::apply_dist_edit does the splice, and its recorded edit spans the dist plus the removed source wherever it sits, so the fragment revert stays byte-exact. Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's source-before-dist block (composer_source_before_dist, redirect_composer_source_kept for a non-adjacent source) is gone.
    • The rewriter oracle calls the same helper, keeping its char walk and fresh copy per edit, and has Match Composer patch versions like Composer does #270's already-redirected heal and strip-before-check for mirrors listed before the url.
    • Its random locks now also cover mirrors before the url, a mirror-only dist (redirect_composer_no_dist_url), source before name, and padded or v-prefixed patch versions.
    • Each lock is also checked to revert byte for byte when its edits are undone newest first.
  • Vendored lock splice on Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's write path: lock_text::replace_entry output goes through atomic_write_bytes_preserving_mode, which is both the per-run group commit and the durable writer. The parse memo is re-seeded only when the written bytes equal the doc's canonical render; otherwise it is invalidated. A unit test covers this.
    • ComposerPrelude now carries the lock text it parsed.
    • Match Composer patch versions like Composer does #270 adds no refusal before the service call; the mirror-filter conflict comes after the copy. So Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's composer preflight gate and prestaging need no change.
  • Mirror filters: neutralizing runs on Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's in-sync hot path, the wired-but-stale rebuild and the fresh vendor. The refusal when a patched file would be dropped is kept.
  • VEX source-install veto: reads composer.json through ctx.read_text.
  • Vendored ownership by identity: purl_keys_cover and friends are merged with Speed up hosted and vendored scans: concurrent API requests, parallel crawl, single-pass rewriters #257's concurrent scan, discovery and vendor imports.
  • Hints, CI, docker: unchanged. composer-compatibility.yml also triggers on crawlers/composer_crawler/**, utils/group_commit.rs and utils/durability.rs.
  • Shared vectors: composer-version-vectors.json is byte-identical (cmp) to depscan's workspaces/lib/src/composer/patch-identity-vectors.json on SocketDev/depscan#26866 (4ecd8543d3).

Synced with main after #268 and #269

  • Merge: merged origin/main (b32711f7: Fix four vendored/hosted correctness bugs found while profiling #268 correctness fixes and Support vlt in hosted, vendored and agent modes #269 vlt) with a plain, signed merge commit. The only conflict was the scan/hosted.rs next-step hints, which keep both the Composer composer install hint and vlt's vlt ci line.
  • Date releases: adopted depscan's final identity vectors from #26866 (4ecd8543d3) byte for byte. That ports the rule that a date release (a major of 6+ digits, like 20200101) drops its trailing .0 parts from its identity, because SBOM padding erases whether the lock said X, X.0 or X.0.0. Versions Composer rejects get their own \u{1} key space, matching depscan's composerVersionIdentityKey, so they can't collide with a normalized version.
  • Test fix: drop_superseded_purl's Composer test used a fake edit kind that main's new unknown-kind guard refuses, so it now uses a real kind.

🤖 Generated with Claude Code


Note

Medium Risk
Changes core PURL matching, scan pruning, hosted lock rewrites, and VEX wiring across many CLI paths; risk is mitigated by extensive oracles and a multi-OS Composer CI matrix, but regressions could still mis-apply or drop patches for non-Composer ecosystems if identity helpers were misused.

Overview
Treats Composer patch PURLs and lock/crawler spellings (@3.0.2, @v3.0.2, padded @3.0.2.0) as the same release via a new composer_version normalizer, wired through crawler lookup, purl_eq, scan discovery/prune/GC, apply/rollback vendored checks, hosted redirect ledger, and VEX source resolution—so padded API purls no longer miss apply, get pruned, or fail redirects.

Hosted and vendored Composer behavior is tightened: redirects drop git source and dist.mirrors (not only adjacent source), vendored copies neutralize mirror-skipping ignore rules, lock edits preserve CRLF/structure, and CLI reinstall hints explain Composer 1 / dist-only reinstall quirks. A dedicated composer-compatibility.yml matrix runs checksum-pinned composer.phar on Linux/macOS/Windows (plus Docker legs); e2e harness adds SOCKET_PATCH_COMPOSER_PHAR, Git CRLF isolation, and broad integration tests.

Reviewed by Cursor Bugbot for commit 185f91a. Configure here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Mikola Lysenko (mikolalysenko) and others added 15 commits September 27, 2026 08:15
@mikolalysenko @claude
Match composer patch versions by release identity
35f5e2e
A composer patch's base purl can carry the padded version Socket's
SBOM ingestion stores (pkg:composer/psr/[email protected]), while the
project's installed.json and composer.lock say 3.0.2 or v3.0.2. The
CLI compared those as strings after stripping a leading v, so such a
patch was "not found" by apply and vendor, refused by the hosted
redirect as a version mismatch, rejected by VEX discovery, and deleted
by scan --prune.

Compare composer versions the way Composer does. The new
utils::composer_version ports composer/semver 3.4.4
VersionParser::normalize (padding, v tags, stability spellings such as
-rc.1 and RC1, +build, x-dev branches); a spelling Composer rejects
only matches itself. purl_eq, the crawler, the vendor lock lookup, the
hosted lock rewriter, the redirect ledger, VEX discovery and sources,
scan discovery and the prune step now use it for composer purls.
Stored spellings (manifest keys, vendored leaf dirs, ledger keys) are
unchanged.

The shared vector file tests/fixtures/composer-version-vectors.json is
generated from real Composer 2.10.3 and is byte-identical to depscan's
copy, so the CLI and the server agree on every case; the port also
matches Composer on 12,129 fuzzed inputs. New tests cover the crawler,
lock lookup, ledger, VEX leaf, prune, a padded-version redirect golden,
and apply, vendor (with VEX) and hosted runs against a mock API that
serves only the padded spelling.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko
Test exact Composer releases on every OS
b700809
Composer patching was only proven on Ubuntu against three floating
releases, so a regression on macOS, Windows, or a specific 2.x line
(2.9 still falls back to the git source; 2.10 does not) went unseen.

composer-compatibility.yml runs the real vendored and hosted capstones
plus the composer VEX cells against checksum-pinned 1.10.28, 2.0.14,
2.1.14, 2.2.30, 2.5.8, 2.8.12, 2.9.8 and 2.10.3 phars on Ubuntu, and
the 1.10/2.2/2.9/2.10 lines on Windows and macOS. A Docker job runs the
vendored Docker capstone on exact 2.2.30 and 2.10.3 images.

The capstones can now run a given composer.phar through php, which is
also what makes them work on Windows. Dockerfile.composer installs an
exact, checksum-verified Composer instead of the latest one, and the
ci.yml comment on the git-source fallback is corrected.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko @claude
Keep Composer patches installed on every version
bbf1949
Hosted redirects now remove a patched package's source wherever it
sits in its composer.lock entry, and strip dist mirrors. Before, a
failed or skipped hosted download let Composer 1 through 2.9 quietly
install the unpatched upstream code from git. Locks that an older
CLI or the GitHub app already redirected are healed on the next run.

Vendored copies no longer lose files when installed. Composer's path
mirror skipped anything matched by the copy's .gitignore (Composer 1
to 2.1), .hgignore (Composer 1) or .gitattributes export-ignore rules
(every version), so a patched file could silently go missing. Those
rules are now neutralized in the copy, and re-runs heal copies
vendored earlier. A patch that edits one of those files is refused.

vendor, scan and get now tell users to run composer install, and to
remove the package directory first on Composer 1, which does not
reinstall a changed package. Real-Composer tests cover the new cases
on 1.10 through 2.10, and docs/testing/composer-compatibility.md
records what each version does.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Keep CRLF composer.lock line endings when vendoring
6fd093b
Vendoring a Composer patch (vendor, scan --mode vendored, get --mode
vendored) rewrote a CRLF composer.lock, as a Windows or core.autocrlf
checkout has, with LF line endings. The commit diff covered every
line instead of the patched entry, and vendor --revert also wrote LF,
so it did not restore the original bytes.

Both writes now keep the line endings of the lock they replace, as the
hosted mode already did. A CRLF lock now round-trips byte for byte
through vendor and vendor --revert. Unit tests and a binary e2e cover
vendor, scan --vendor and get --mode vendored on a CRLF lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Match SBOM-padded Composer date versions
e59b605
Socket's SBOM pads every numeric Composer version to four parts, so a
date release locked as 20231001 reaches the CLI as 20231001.0.0.0.
Composer rejects that spelling (a 6+ digit major is only valid as a
date), so the version key fell back to the raw text and never matched
the lock: apply, vendor, hosted redirect and VEX reported the patch as
not found for its own package.

A 2-4 part numeric spelling Composer rejects is now normalized with
its trailing .0 parts dropped. The shared vector file, copied from
depscan, gains the padded date cases checked against Composer 2.10.3.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Strip Composer dist mirrors before redirecting
452e2d9
A dist that lists mirrors before its url had the mirrors' url
rewritten instead of the dist's own, and a dist whose only url is a
mirror is now refused with redirect_composer_no_dist_url. Also drop
the dead composer_source_before_dist helper.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Edit only the vendored entry in composer.lock
98d705f
Vendoring rewrote the whole composer.lock through serde_json, so a
lock with \/ escapes, \uXXXX escapes or mixed line endings did not
revert byte for byte. Splice only the patched entry's text, keeping
the lock's indentation, line endings and escaping.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Match vendored Composer ownership by release
b9fb886
Vendored ownership checks compared composer purls as exact strings,
so a ledger keyed pkg:composer/psr/[email protected] never covered the lock's
@3.0.2 and apply, rollback, gc and scan treated a live vendored patch
as foreign or dead. Match composer purls by release identity. The
vendor hint now reads composer.lock only when this run wired
composer, and never blocks on a special file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Do not report source-installed Composer as fixed
fe912c0
Composer installs from source first when preferred-install resolves
to source for the package, or when it is auto with a dev version on a
Composer 1 or 2.0 lock. A leftover source entry then means pristine
bytes, so VEX no longer reports those entries as fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Name the vendor dir in Composer reinstall hints
165cebb
After a hosted redirect that removed no source entry, Composer 2
keeps an existing vendor/ copy, so the hint now names the vendor
directory to remove on every Composer version.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Run Composer compatibility on every related change
5408586
The composer compatibility workflow now also runs for vendor, patch,
manifest, lockfile and command changes. Docker composer tests fail
instead of skipping when SOCKET_PATCH_DOCKER_E2E_REQUIRED=1, and the
source-fallback test disables autocrlf so Windows compares LF bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Satisfy clippy in the Composer lock splice
0cf30f9
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Read composer.json through the VEX discovery context
ff64d02
The source-install veto read composer.json directly, which the VEX
discovery rules forbid: every extractor reads through DiscoverCtx so
unreadable files are diagnosed and Socket identities are recognized.
Also add the golden entries for the dist-mirrors-before-url fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Check Composer source and mirror layouts against the redirect oracle
9343c4f
The in-place composer.lock rewrite oracle now also generates locks
with dist mirrors listed before the url or as the only url, a source
member placed before name, and patched versions spelled padded or
v-prefixed. Every randomized lock is also checked to revert byte for
byte when each recorded fragment is undone, newest first, the way the
ledger reverts it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Run Composer compatibility when the crawler oracle or group commit ch…
68c8c82
…anges

The Composer crawler is now a directory module with its equivalence
oracle, and vendored Composer lock writes go through the per-run group
commit and the durable writer, so changes to any of them run the exact
release matrix too.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko
Mikola Lysenko (mikolalysenko) force-pushed the fix/composer-patch-annotations branch from 791848d to 68c8c82 Compare September 27, 2026 12:46
Mikola Lysenko (mikolalysenko) and others added 3 commits September 27, 2026 10:33
@mikolalysenko @claude
Merge origin/main (#276) into the Composer patch fold
dacaf8f
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Merge origin/main (b32711f) into fix/composer-patch-annotations
7abd927
Resolve the scan/hosted.rs next-steps conflict by keeping both sides:
the Composer reinstall hint from this branch and main's vlt ci line
(#269). Update the vlt next-steps test for the edits argument.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko @claude
Treat a Composer date release's trailing .0 parts as padding
185f91a
Adopt depscan's final shared vectors (4ecd8543d3) byte for byte. A date
release (6+ digit major) now drops its trailing .0 parts from its
identity, since SBOM padding erases whether the lock said X, X.0 or
X.0.0. Versions Composer rejects key into their own space, matching
depscan's composerVersionIdentityKey, so purl identity keys cannot
collide with a normalized one.

Also point the drop_superseded_purl composer test at the real
redirect_composer_dist edit kind: main's unknown-kind guard (#269)
correctly refuses the made-up kind it used.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review September 27, 2026 16:37
cursor[bot]
cursor Bot reviewed Sep 27, 2026
View reviewed changes

@cursor cursor Bot left a comment •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

There was a problem hiding this comment.

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 2 potential issues.

Fix All in Cursor

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

  • ✅ Fixed: Filter conflict misses package-prefixed keys
    • Added normalize_file_path() call to strip 'package/' prefix from patch keys before comparing against filter file names, ensuring conflicts are properly detected.

Create PR

Or push these changes by commenting:

@cursor push 3446b3ea64
Preview (3446b3ea64)
diff --git a/crates/socket-patch-cli/src/commands/get.rs b/crates/socket-patch-cli/src/commands/get.rs
--- a/crates/socket-patch-cli/src/commands/get.rs
+++ b/crates/socket-patch-cli/src/commands/get.rs
@@ -7272,8 +7272,11 @@
         let installed = |name: &str, body: &[u8]| {
             let dist = site.path().join(format!("{name}-1.0.0.dist-info"));
             std::fs::create_dir_all(&dist).unwrap();
-            std::fs::write(dist.join("METADATA"), format!("Name: {name}\nVersion: 1.0.0\n"))
-                .unwrap();
+            std::fs::write(
+                dist.join("METADATA"),
+                format!("Name: {name}\nVersion: 1.0.0\n"),
+            )
+            .unwrap();
             std::fs::write(site.path().join(format!("{name}.py")), body).unwrap();
             compute_git_sha256_from_bytes(body)
         };
@@ -7317,7 +7320,10 @@
         mount(uuid("bs"), "beta_sdist.py".into(), "0".repeat(64), 0).await;
         for n in ["gw", "gs"] {
             Mock::given(method("GET"))
-                .and(wm_path(format!("/v0/orgs/test-org/patches/view/{}", uuid(n))))
+                .and(wm_path(format!(
+                    "/v0/orgs/test-org/patches/view/{}",
+                    uuid(n)
+                )))
                 .respond_with(ResponseTemplate::new(500))
                 .expect(0)
                 .mount(&server)

diff --git a/crates/socket-patch-cli/src/commands/scan/discovery.rs b/crates/socket-patch-cli/src/commands/scan/discovery.rs
--- a/crates/socket-patch-cli/src/commands/scan/discovery.rs
+++ b/crates/socket-patch-cli/src/commands/scan/discovery.rs
@@ -2094,8 +2094,12 @@
                 "pkg:npm/[email protected]",
                 std::path::PathBuf::from("/nonexistent"),
             ),
-            crawled_pkg("alpha", "pkg:npm/[email protected]", installed("alpha", "alpha.js")),
             crawled_pkg(
+                "alpha",
+                "pkg:npm/[email protected]",
+                installed("alpha", "alpha.js"),
+            ),
+            crawled_pkg(
                 "embedded",
                 "pkg:npm/[email protected]",
                 installed("embedded", "embedded.js"),

diff --git a/crates/socket-patch-cli/src/commands/setup.rs b/crates/socket-patch-cli/src/commands/setup.rs
--- a/crates/socket-patch-cli/src/commands/setup.rs
+++ b/crates/socket-patch-cli/src/commands/setup.rs
@@ -247,8 +247,7 @@
     }
     let mut hooked = Vec::new();
     for loc in found.files.iter().filter(|loc| !loc.is_root) {
-        if let Ok(content) = socket_patch_core::utils::fs::read_regular_to_string(&loc.path).await
-        {
+        if let Ok(content) = socket_patch_core::utils::fs::read_regular_to_string(&loc.path).await {
             let status = is_setup_configured_str(&content);
             if status.postinstall_configured || status.dependencies_configured {
                 hooked.push(loc.path.clone());

diff --git a/crates/socket-patch-cli/src/commands/update.rs b/crates/socket-patch-cli/src/commands/update.rs
--- a/crates/socket-patch-cli/src/commands/update.rs
+++ b/crates/socket-patch-cli/src/commands/update.rs
@@ -159,7 +159,11 @@
 
 /// The result line after a successful install, naming the same action as
 /// [`confirm_prompt`].
-fn installed_message(current: &semver::Version, target: &semver::Version, path: &std::path::Path) -> String {
+fn installed_message(
+    current: &semver::Version,
+    target: &semver::Version,
+    path: &std::path::Path,
+) -> String {
     let path = path.display();
     if target < current {
         format!("Downgraded socket-patch {current} \u{2192} {target} ({path})")
@@ -500,9 +504,18 @@
 
     #[test]
     fn cancel_and_result_lines_match_the_prompt() {
-        assert_eq!(cancelled_message(&v("4.0.0"), &v("9.9.9")), "Update cancelled.");
-        assert_eq!(cancelled_message(&v("4.0.0"), &v("3.0.0")), "Downgrade cancelled.");
-        assert_eq!(cancelled_message(&v("4.0.0"), &v("4.0.0")), "Reinstall cancelled.");
+        assert_eq!(
+            cancelled_message(&v("4.0.0"), &v("9.9.9")),
+            "Update cancelled."
+        );
+        assert_eq!(
+            cancelled_message(&v("4.0.0"), &v("3.0.0")),
+            "Downgrade cancelled."
+        );
+        assert_eq!(
+            cancelled_message(&v("4.0.0"), &v("4.0.0")),
+            "Reinstall cancelled."
+        );
         let p = std::path::Path::new("/opt/sp/socket-patch");
         assert_eq!(
             installed_message(&v("4.0.0"), &v("9.9.9"), p),

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
@@ -3864,7 +3864,11 @@
         .unwrap();
         let packages = [
             ("pkg:composer/psr/[email protected]", "psr/cache", UUID_A),
-            ("pkg:composer/psr/[email protected]", "psr/http-message", UUID_B),
+            (
+                "pkg:composer/psr/[email protected]",
+                "psr/http-message",
+                UUID_B,
+            ),
             ("pkg:composer/psr/[email protected]", "psr/log", UUID_C),
         ];
         let mut all_packages: Vec<(String, StagedSource)> = Vec::new();

diff --git a/crates/socket-patch-cli/src/hosted_memory/redirect.rs b/crates/socket-patch-cli/src/hosted_memory/redirect.rs
--- a/crates/socket-patch-cli/src/hosted_memory/redirect.rs
+++ b/crates/socket-patch-cli/src/hosted_memory/redirect.rs
@@ -19,8 +19,7 @@
     NPMRC_REL,
 };
 use socket_patch_core::patch::redirect::{
-    rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, RewriteResult,
-    RewriteWarning,
+    rewrite_registry_redirect_withholding_vlt, DepOverride, FileEdit, RewriteResult, RewriteWarning,
 };
 use socket_patch_core::utils::purl::{purl_parts, strip_purl_qualifiers};
 use socket_patch_core::vendor::lock_inventory::{MemoryEntry, MemoryProject};

diff --git a/crates/socket-patch-cli/tests/apply_network.rs b/crates/socket-patch-cli/tests/apply_network.rs
--- a/crates/socket-patch-cli/tests/apply_network.rs
+++ b/crates/socket-patch-cli/tests/apply_network.rs
@@ -1075,10 +1075,7 @@
         v["summary"]["applied"], 1,
         "the drifted nested copy must be warn-overwritten.\nstdout={v:#}"
     );
-    assert_eq!(
-        v["summary"]["failed"], 0,
-        "no copy may fail.\nstdout={v:#}"
-    );
+    assert_eq!(v["summary"]["failed"], 0, "no copy may fail.\nstdout={v:#}");
 
     // The nested copy's blob was fetched on demand…
     let requests = mock.received_requests().await.unwrap();

diff --git a/crates/socket-patch-cli/tests/cli_config_fallback.rs b/crates/socket-patch-cli/tests/cli_config_fallback.rs
--- a/crates/socket-patch-cli/tests/cli_config_fallback.rs
+++ b/crates/socket-patch-cli/tests/cli_config_fallback.rs
@@ -59,8 +59,7 @@
     let mut cmd = Command::new(BINARY);
     // Human mode: core's proxy advisory (the oracle below) is muted under
     // `--json`/`--silent`.
-    cmd.args(["scan", "-e", "npm", "--cwd"])
-        .arg(project);
+    cmd.args(["scan", "-e", "npm", "--cwd"]).arg(project);
     for (key, _) in std::env::vars_os() {
         let name = key.to_string_lossy();
         if name.starts_with("SOCKET_") {
@@ -298,7 +297,9 @@
     json_cmd.arg("--json");
     let json_out = run(json_cmd);
     assert!(
-        json_out.stderr.contains("could not parse socket-cli config"),
+        json_out
+            .stderr
+            .contains("could not parse socket-cli config"),
         "the parse warning must reach stderr under --json too; got:\n{}",
         json_out.stderr
     );

diff --git a/crates/socket-patch-cli/tests/cli_parse_list.rs b/crates/socket-patch-cli/tests/cli_parse_list.rs
--- a/crates/socket-patch-cli/tests/cli_parse_list.rs
+++ b/crates/socket-patch-cli/tests/cli_parse_list.rs
@@ -1202,7 +1202,10 @@
     assert_eq!(v["error"]["code"], "manifest_not_found", "envelope={v}");
     let warnings = v["warnings"].as_array().expect("warnings[] present");
     assert_eq!(warnings.len(), 1, "envelope={v}");
-    assert_eq!(warnings[0]["code"], "redirect_ledger_corrupt", "envelope={v}");
+    assert_eq!(
+        warnings[0]["code"], "redirect_ledger_corrupt",
+        "envelope={v}"
+    );
     assert!(
         out.stderr.is_empty(),
         "--json must keep stderr clean: {}",
@@ -1214,7 +1217,10 @@
     let stderr = String::from_utf8_lossy(&out.stderr);
     assert_eq!(out.status.code(), Some(1));
     assert!(stderr.contains("Warning: "), "stderr={stderr}");
-    assert!(stderr.contains("Error: Manifest not found at "), "stderr={stderr}");
+    assert!(
+        stderr.contains("Error: Manifest not found at "),
+        "stderr={stderr}"
+    );
 }
 
 #[test]

diff --git a/crates/socket-patch-cli/tests/cli_parse_rollback.rs b/crates/socket-patch-cli/tests/cli_parse_rollback.rs
--- a/crates/socket-patch-cli/tests/cli_parse_rollback.rs
+++ b/crates/socket-patch-cli/tests/cli_parse_rollback.rs
@@ -378,7 +378,11 @@
 /// relied on the rejection get a test-visible flip instead of a silent one.
 #[test]
 fn multiple_targets_parse_in_order() {
-    let args = parse_rollback(&["pkg:npm/foo@1", "packages/api/**", "b0630680-4da6-45f9-bba8-b888e0ffd58c"]);
+    let args = parse_rollback(&[
+        "pkg:npm/foo@1",
+        "packages/api/**",
+        "b0630680-4da6-45f9-bba8-b888e0ffd58c",
+    ]);
     assert_eq!(
         args.targets,
         vec![

diff --git a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs
--- a/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs
+++ b/crates/socket-patch-cli/tests/coverage_fix_apply_silent_mute_exit.rs
@@ -150,7 +150,9 @@
     );
     let chatter = stderr_chatter(&stderr);
     assert!(
-        chatter.iter().any(|l| l.contains("could not be downloaded")),
+        chatter
+            .iter()
+            .any(|l| l.contains("could not be downloaded")),
         "--silent must keep the download-failure error (errors only, \
          never nothing); stderr was: {stderr:?}"
     );

diff --git a/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs b/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs
--- a/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs
+++ b/crates/socket-patch-cli/tests/coverage_fix_repair_vendor_predelete.rs
@@ -234,12 +234,9 @@
     let gemfile_wired = std::fs::read(tmp.path().join("Gemfile")).unwrap();
 
     std::fs::remove_file(tmp.path().join(".socket/vendor/state.json")).unwrap();
-    std::fs::remove_file(
-        tmp.path()
-            .join(format!(
-                "vendor/bundle/ruby/3.4.0/gems/{GEM_NAME}-{GEM_VERSION}/lib/padlock.rb"
-            )),
-    )
+    std::fs::remove_file(tmp.path().join(format!(
+        "vendor/bundle/ruby/3.4.0/gems/{GEM_NAME}-{GEM_VERSION}/lib/padlock.rb"
+    )))
     .unwrap();
 
     mount_blob(&mock).await;
@@ -280,8 +277,11 @@
         &std::fs::read_to_string(tmp.path().join(".socket/vendor/state.json")).unwrap(),
     )
     .unwrap();
-    assert_eq!(state["entries"][GEM_PURL]["uuid"], GEM_UUID, "state={state}");
     assert_eq!(
+        state["entries"][GEM_PURL]["uuid"], GEM_UUID,
+        "state={state}"
+    );
+    assert_eq!(
         std::fs::read(tmp.path().join("Gemfile")).unwrap(),
         gemfile_wired,
         "the wired Gemfile is untouched"

diff --git a/crates/socket-patch-cli/tests/covgap_commands_update.rs b/crates/socket-patch-cli/tests/covgap_commands_update.rs
--- a/crates/socket-patch-cli/tests/covgap_commands_update.rs
+++ b/crates/socket-patch-cli/tests/covgap_commands_update.rs
@@ -10,10 +10,10 @@
 //! self_update_e2e.rs / interactive_prompts_e2e.rs (do not edit those
 //! files).
 
+#[path = "common/mod.rs"]
+mod common;
 #[path = "common/pty_io.rs"]
 mod pty_io;
-#[path = "common/mod.rs"]
-mod common;
 #[path = "common/update_fixture.rs"]
 mod update_fixture;
 
@@ -275,9 +275,8 @@
         let mut child = pair.slave.spawn_command(cmd).expect("spawn in PTY");
         drop(pair.slave);
 
-        let reader_handle = crate::pty_io::PtyOutput::spawn(
-            pair.master.try_clone_reader().expect("clone reader"),
-        );
+        let reader_handle =
+            crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader"));
 
         let mut killer = child.clone_killer();
         std::thread::spawn(move || {
@@ -345,7 +344,8 @@
             "a declined update exits 1 (codebase convention); got: {output}"
         );
         assert!(
-            !output.contains("Updated socket-patch") && !output.contains("Reinstalled socket-patch"),
+            !output.contains("Updated socket-patch")
+                && !output.contains("Reinstalled socket-patch"),
             "a declined update must not report a swap; got: {output}"
         );
 

diff --git a/crates/socket-patch-cli/tests/covgap_commands_vex.rs b/crates/socket-patch-cli/tests/covgap_commands_vex.rs
--- a/crates/socket-patch-cli/tests/covgap_commands_vex.rs
+++ b/crates/socket-patch-cli/tests/covgap_commands_vex.rs
@@ -544,14 +544,24 @@
         ])
         .output()
         .expect("invoke vex");
-    assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
+    assert!(
+        out.status.success(),
+        "{}",
+        String::from_utf8_lossy(&out.stderr)
+    );
     let env: Value = serde_json::from_slice(&out.stdout).expect("envelope JSON on stdout");
     let w = env["warnings"]
         .as_array()
-        .and_then(|ws| ws.iter().find(|w| w["code"] == "product_multiple_manifests"))
+        .and_then(|ws| {
+            ws.iter()
+                .find(|w| w["code"] == "product_multiple_manifests")
+        })
         .unwrap_or_else(|| panic!("product_multiple_manifests warning expected: {env}"));
     assert!(
-        w["detail"].as_str().unwrap().contains("Multiple project manifests"),
+        w["detail"]
+            .as_str()
+            .unwrap()
+            .contains("Multiple project manifests"),
         "{w}"
     );
     let stderr = String::from_utf8_lossy(&out.stderr);

diff --git a/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs b/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs
--- a/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs
+++ b/crates/socket-patch-cli/tests/covgap_ecosystem_dispatch.rs
@@ -254,8 +254,11 @@
         .unwrap_or_else(|e| panic!("rollback envelope must parse ({e}); stdout={stdout}"));
     let code = out.status.code().unwrap_or(-1);
 
-    assert_eq!(code, 0, "rollback --ecosystems=deno: expected exit 0; env={env}");
     assert_eq!(
+        code, 0,
+        "rollback --ecosystems=deno: expected exit 0; env={env}"
+    );
+    assert_eq!(
         env["status"], "success",
         "rollback --ecosystems=deno: expected success; env={env}"
     );
@@ -295,7 +298,8 @@
     // The decisive check: the on-disk bytes are restored to ORIGINAL.
     let restored = std::fs::read(&verify_file).unwrap();
     assert_eq!(
-        restored, ORIGINAL,
+        restored,
+        ORIGINAL,
         "rollback --ecosystems=deno: {} was not restored to its original bytes",
         verify_file.display()
     );

diff --git a/crates/socket-patch-cli/tests/covgap_output.rs b/crates/socket-patch-cli/tests/covgap_output.rs
--- a/crates/socket-patch-cli/tests/covgap_output.rs
+++ b/crates/socket-patch-cli/tests/covgap_output.rs
@@ -173,9 +173,8 @@
         .expect("spawn socket-patch in PTY");
     drop(pair.slave);
 
-    let reader_handle = crate::pty_io::PtyOutput::spawn(
-        pair.master.try_clone_reader().expect("clone reader"),
-    );
+    let reader_handle =
+        crate::pty_io::PtyOutput::spawn(pair.master.try_clone_reader().expect("clone reader"));
 
     // Watchdog: detached kill after `timeout`; a no-op if the child exits
     // naturally first.
@@ -266,7 +265,10 @@
         "\n",
         Duration::from_secs(15),
     );
-    assert_eq!(code, 0, "remove with bare Enter must succeed; got: {output}");
+    assert_eq!(
+        code, 0,
+        "remove with bare Enter must succeed; got: {output}"
+    );
     // The interactive confirm MUST have run — otherwise this test passes
     // vacuously against a regression that drops the TTY gate and
     // auto-proceeds. Match the distinctive prompt verbatim (the loose

diff --git a/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs b/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs
--- a/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs
+++ b/crates/socket-patch-cli/tests/covgap_setup_composer_mod.rs
@@ -126,7 +126,10 @@
     write(&cwd.join("composer.json"), MALFORMED_COMPOSER_JSON);
 
     let (code, v) = run_json(cwd, &["setup", "--remove", "--yes", "--json"]);
-    assert_eq!(code, 1, "remove on a malformed composer.json must fail: {v}");
+    assert_eq!(
+        code, 1,
+        "remove on a malformed composer.json must fail: {v}"
+    );
     assert_eq!(v["status"], "error", "{v}");
     assert_eq!(v["removed"], 0, "{v}");
     assert_eq!(v["errors"], 1, "{v}");

diff --git a/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs b/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs
--- a/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs
+++ b/crates/socket-patch-cli/tests/covgap_setup_gem_mod.rs
@@ -71,7 +71,10 @@
         &["setup", "--yes", "--json", "--ecosystems", "gem"],
         &[],
     );
-    assert_eq!(code, 0, "gem setup must succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+    assert_eq!(
+        code, 0,
+        "gem setup must succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}"
+    );
     let v = common::parse_json_envelope(&stdout);
     assert_eq!(v["status"], "success", "{v}");
     assert!(
@@ -110,7 +113,14 @@
     // Step 3: unwire with BUNDLE_APP_CONFIG set (child-only env injection).
     let (code, stdout, stderr) = common::run_with_env(
         root,
-        &["setup", "--remove", "--yes", "--json", "--ecosystems", "gem"],
+        &[
+            "setup",
+            "--remove",
+            "--yes",
+            "--json",
+            "--ecosystems",
+            "gem",
+        ],
         &[("BUNDLE_APP_CONFIG", "bundle-config")],
     );
     assert_eq!(

diff --git a/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs b/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs
--- a/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs
+++ b/crates/socket-patch-cli/tests/covgap_setup_pypi_detect.rs
@@ -45,7 +45,10 @@
 fn write_pm_shim(bin_dir: &Path, name: &str, log: &Path) {
     use std::os::unix::fs::PermissionsExt;
     std::fs::create_dir_all(bin_dir).expect("create shim dir");
-    let body = format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n", log.display());
+    let body = format!(
+        "#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n",
+        log.display()
+    );
     let p = bin_dir.join(name);
     std::fs::write(&p, body).expect("write shim");
     std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755)).expect("chmod shim");
@@ -80,11 +83,7 @@
 /// through the shared hermetic runner (the seed-then-scrub of the ambient
 /// `SOCKET_*` surface is load-bearing: SOCKET_DRY_RUN=true would fake every
 /// edit, SOCKET_ECOSYSTEMS=npm would hide the Python branch entirely).
-fn run_setup_with_shims(
-    cwd: &Path,
-    bin_dir: &Path,
-    extra: &[&str],
-) -> (i32, serde_json::Value) {
+fn run_setup_with_shims(cwd: &Path, bin_dir: &Path, extra: &[&str]) -> (i32, serde_json::Value) {
     let path_env = format!(
         "{}:{}",
         bin_dir.display(),

diff --git a/crates/socket-patch-cli/tests/e2e_cargo.rs b/crates/socket-patch-cli/tests/e2e_cargo.rs
--- a/crates/socket-patch-cli/tests/e2e_cargo.rs
+++ b/crates/socket-patch-cli/tests/e2e_cargo.rs
@@ -209,8 +209,7 @@
         "Expected human scan to report exactly 'Found 2 packages (2 cargo)', got:\n{combined}"
     );
     assert!(
-        !combined.contains("No packages found")
-            && !combined.contains("No packages found"),
+        !combined.contains("No packages found") && !combined.contains("No packages found"),
         "scan reported no packages despite a populated registry:\n{combined}"
     );
 
@@ -267,8 +266,7 @@
         "Expected human scan to report exactly 'Found 1 package (1 cargo)', got:\n{combined}"
     );
     assert!(
-        !combined.contains("No packages found")
-            && !combined.contains("No packages found"),
+        !combined.contains("No packages found") && !combined.contains("No packages found"),
         "scan reported no packages despite a populated vendor dir:\n{combined}"
     );
 

diff --git a/crates/socket-patch-cli/tests/e2e_maven.rs b/crates/socket-patch-cli/tests/e2e_maven.rs
--- a/crates/socket-patch-cli/tests/e2e_maven.rs
+++ b/crates/socket-patch-cli/tests/e2e_maven.rs
@@ -182,8 +182,7 @@
     // the word "packages", which is exactly what let the old assertion
     // pass when discovery was disabled.
     assert!(
-        !combined.contains("No packages found")
-            && !combined.contains("No packages found"),
+        !combined.contains("No packages found") && !combined.contains("No packages found"),
         "scan reported zero packages — Maven discovery did not run:\n{combined}"
     );
     assert!(

diff --git a/crates/socket-patch-cli/tests/e2e_nuget.rs b/crates/socket-patch-cli/tests/e2e_nuget.rs
--- a/crates/socket-patch-cli/tests/e2e_nuget.rs
+++ b/crates/socket-patch-cli/tests/e2e_nuget.rs
@@ -232,7 +232,8 @@
     // masked.
     assert!(
         !combined.contains("No packages found")
-            && !combined.contains("No packages found") && !combined.contains("No global packages found"),
+            && !combined.contains("No packages found")
+            && !combined.contains("No global packages found"),
         "scan failed to discover the fake global cache:\n{combined}"
     );
     // Exactly the two packages we planted (Newtonsoft.Json, System.Text.Json),
@@ -291,7 +292,8 @@
     );
     assert!(
         !combined.contains("No packages found")
-            && !combined.contains("No packages found") && !combined.contains("No global packages found"),
+            && !combined.contains("No packages found")
+            && !combined.contains("No global packages found"),
         "scan failed to discover the legacy packages/ layout:\n{combined}"
     );
     // Exactly the single legacy package we planted (Newtonsoft.Json.13.0.3),

diff --git a/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs b/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs
--- a/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs
+++ b/crates/socket-patch-cli/tests/get_edge_cases_e2e.rs
@@ -508,8 +508,16 @@
     // parseable (scripts get that explicit error) but is not advertised.
     assert!(!stdout.contains("--one-off"), "{stdout}");
     // Help text is for users: no implementation notes from the source.
-    for leak in ["value_parser", "parse_bool_flag", "No env binding", "locally- installed"] {
-        assert!(!stdout.contains(leak), "get --help leaks {leak:?}: {stdout}");
+    for leak in [
+        "value_parser",
+        "parse_bool_flag",
+        "No env binding",
+        "locally- installed",
+    ] {
+        assert!(
+            !stdout.contains(leak),
+            "get --help leaks {leak:?}: {stdout}"
+        );
     }
 }
 

diff --git a/crates/socket-patch-cli/tests/global_packages_e2e.rs b/crates/socket-patch-cli/tests/global_packages_e2e.rs
--- a/crates/socket-patch-cli/tests/global_packages_e2e.rs
+++ b/crates/socket-patch-cli/tests/global_packages_e2e.rs
@@ -211,8 +211,11 @@
             r["skipped"], "package_not_installed",
             "a no-op rollback may carry only not-installed markers; envelope={v}"
         );
-        assert!(r["path"].is_null(), "marker path must be null; envelope={v}");
         assert!(
+            r["path"].is_null(),
+            "marker path must be null; envelope={v}"
+        );
+        assert!(
             r.get("success").is_none() && r.get("error").is_none(),
             "markers carry no success/error keys; envelope={v}"
         );

diff --git a/crates/socket-patch-cli/tests/help_text_hygiene.rs b/crates/socket-patch-cli/tests/help_text_hygiene.rs
--- a/crates/socket-patch-cli/tests/help_text_hygiene.rs
+++ b/crates/socket-patch-cli/tests/help_text_hygiene.rs
@@ -61,7 +61,11 @@
     names.extend(cmd.get_subcommands().map(|s| s.get_name().to_string()));
     let mut failures = Vec::new();
     for name in &names {
-        let path: Vec<&str> = if name.is_empty() { vec![] } else { vec![name.as_str()] };
+        let path: Vec<&str> = if name.is_empty() {
+            vec![]
+        } else {
+            vec![name.as_str()]
+        };
         let text = long_help(&path);
         let found = leaks(&text);
         if !found.is_empty() {
@@ -166,8 +170,10 @@
         "{text}"
     );
     assert!(
-        text.lines().any(|l| l
-            == "  repair    Download missing patch artifacts and clean up unused ones [aliases: gc]"),
+        text.lines().any(|l| {
+            l
+            == "  repair    Download missing patch artifacts and clean up unused ones [aliases: gc]"
+        }),
         "{text}"
     );
     let repair = long_help(&["repair"]);

diff --git a/crates/socket-patch-cli/tests/hosted_memory_engine.rs b/crates/socket-patch-cli/tests/hosted_memory_engine.rs
--- a/crates/socket-patch-cli/tests/hosted_memory_engine.rs
+++ b/crates/socket-patch-cli/tests/hosted_memory_engine.rs
@@ -963,7 +963,8 @@
         .and_then(|w| w["detail"].as_str())
         .expect("the preflight warning is reported");
     assert!(
-        detail.contains("/patch/npm/<redacted>/") && detail.contains(": offline; nothing was written"),
+        detail.contains("/patch/npm/<redacted>/")
+            && detail.contains(": offline; nothing was written"),
         "the offline refusal quotes the redacted URL"
     );
     assert!(output.changed_files.is_empty());

diff --git a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs
--- a/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs
+++ b/crates/socket-patch-cli/tests/in_process_redirect/vlt.rs
@@ -335,11 +335,15 @@
     let detail = warning_detail(&doc, UNVERIFIABLE);
     let redacted = url.replace(&format!("/{TOKEN}/"), "/<redacted>/");
     assert!(
-        detail.starts_with(&format!("vlt would fail to verify {redacted}: fetch error "))
-            && detail.ends_with(&format!("; nothing was written for {PURL}")),
+        detail.starts_with(&format!(
+            "vlt would fail to verify {redacted}: fetch error "
+        )) && detail.ends_with(&format!("; nothing was written for {PURL}")),
         "the fetch-error refusal quotes the redacted URL"
     );
-    assert!(!detail.contains(TOKEN), "the grant token never reaches the warning");
+    assert!(
+        !detail.contains(TOKEN),
+        "the grant token never reaches the warning"
+    );
 }
 
 async fn redirect_chain(hops: usize) -> (Value, tempfile::TempDir) {

diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs
--- a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs
+++ b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs
@@ -109,7 +109,9 @@
         .mount(server)
         .await;
     Mock::given(method("GET"))
-        .and(path_regex(format!("^/v0/orgs/{ORG}/patches/by-package/.+$")))
+        .and(path_regex(format!(
+            "^/v0/orgs/{ORG}/patches/by-package/.+$"
+        )))
         .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
             "patches": [{
                 "uuid": UUID, "purl": RECORD_PURL,
@@ -337,8 +339,10 @@
         PYPROJECT,
         "pyproject untouched"
     );
-    let ledger: serde_json::Value =
-        serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap();
+    let ledger: serde_json::Value = serde_json::from_str(&read(
+        &tmp.path().join(".socket/vendor/redirect-state.json"),
+    ))
+    .unwrap();
     assert!(
         ledger["records"][RECORD_PURL].is_object(),
         "ledger keyed by the artifact-qualified purl: {ledger}"
@@ -363,7 +367,11 @@
     // 2. Idempotent re-scan: no further edits, lock byte-identical.
     let code = run(hosted_args(tmp.path(), server.uri(), None)).await;
     assert_eq!(code, 0);
-    assert_eq!(read(&lock_path), redirected, "re-scan must not touch the lock");
+    assert_eq!(
+        read(&lock_path),
+        redirected,
+        "re-scan must not touch the lock"
+    );
 
     // 3. The committed state, manifest-less, attests (and only while wired).
     assert_manifestless_vex(tmp.path(), LOCK);
@@ -422,8 +430,10 @@
         pyproject,
         "pyproject untouched"
     );
-    let ledger: serde_json::Value =
-        serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap();
+    let ledger: serde_json::Value = serde_json::from_str(&read(
+        &tmp.path().join(".socket/vendor/redirect-state.json"),
+    ))
+    .unwrap();
     assert!(
         ledger["records"][RECORD_PURL].is_object(),
         "the pdm redirect must be confirmed and recorded despite the hatch backend: {ledger}"
@@ -443,7 +453,11 @@
     })
     .await;
     assert_eq!(code, 0, "rollback must succeed");
-    assert_eq!(read(&lock_path), LOCK, "rollback must restore the pristine lock");
+    assert_eq!(
+        read(&lock_path),
+        LOCK,
+        "rollback must restore the pristine lock"
+    );
 }
 
 /// The legacy `[metadata.files]` lock (lock_version 2) redirects the package
@@ -461,8 +475,10 @@
     assert_eq!(code, 0);
     let redirected = read(&lock_path);
     assert!(redirected.contains(HOSTED_URL), "{redirected}");
-    let ledger: serde_json::Value =
-        serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json"))).unwrap();
+    let ledger: serde_json::Value = serde_json::from_str(&read(
+        &tmp.path().join(".socket/vendor/redirect-state.json"),
+    ))
+    .unwrap();
     assert_eq!(
         ledger["edits"].as_array().unwrap().len(),
         2,

diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs
--- a/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs
+++ b/crates/socket-patch-cli/tests/in_process_redirect_pipenv.rs
@@ -54,7 +54,8 @@
 
 const LOCK: &str =
     include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile.lock");
-const PIPFILE: &str = include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile");
+const PIPFILE: &str =
+    include_str!("../../socket-patch-core/tests/fixtures/pipenv/2026.8.0/Pipfile");
 
 /// The upstream and patched bytes of the record's one file, so the venv
 /// tests can materialize a real `Ready` (upstream) install.
@@ -118,7 +119,9 @@
         .mount(server)
         .await;
     Mock::given(method("GET"))
-        .and(path_regex(format!("^/v0/orgs/{ORG}/patches/by-package/.+$")))
+        .and(path_regex(format!(
+            "^/v0/orgs/{ORG}/patches/by-package/.+$"
+        )))
         .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
             "patches": [{
                 "uuid": UUID, "purl": RECORD_PURL,
@@ -308,7 +311,10 @@
         serde_json::json!([format!("sha256:{}", sha256())]),
         "{redirected}"
     );
-    assert!(entry.get("version").is_none() && entry.get("index").is_none(), "{entry}");
+    assert!(
+        entry.get("version").is_none() && entry.get("index").is_none(),
+        "{entry}"
+    );
     assert_eq!(
         entry["markers"],
         urllib3_entry(LOCK)["markers"],
@@ -316,11 +322,19 @@
     );
     let before: serde_json::Value = serde_json::from_str(LOCK).unwrap();
     let after: serde_json::Value = serde_json::from_str(&redirected).unwrap();
-    assert_eq!(after["_meta"], before["_meta"], "the Pipfile content hash stays");
-    assert_eq!(read(&tmp.path().join("Pipfile")), PIPFILE, "Pipfile untouched");
-    let ledger: serde_json::Value =
-        serde_json::from_str(&read(&tmp.path().join(".socket/vendor/redirect-state.json")))
-            .unwrap();
+    assert_eq!(
+        after["_meta"], before["_meta"],
+        "the Pipfile content hash stays"
+    );
+    assert_eq!(
+        read(&tmp.path().join("Pipfile")),
+        PIPFILE,
+        "Pipfile untouched"
+    );
+    let ledger: serde_json::Value = serde_json::from_str(&read(
+        &tmp.path().join(".socket/vendor/redirect-state.json"),
+    ))
+    .unwrap();
     assert!(
         ledger["records"][RECORD_PURL].is_object(),
         "ledger keyed by the artifact-qualified purl: {ledger}"
@@ -340,17 +354,34 @@
     let vex: serde_json::Value = serde_json::from_str(&read(&vex_path)).unwrap();
     let statements = vex["statements"].as_array().expect("statements");
     assert_eq!(statements.len(), 1, "{vex}");
-    assert_eq!(statements[0]["vulnerability"]["name"].as_str(), Some(GHSA), "{vex}");
-    assert_eq!(statements[0]["status"].as_str(), Some("not_affected"), "{vex}");
+    assert_eq!(
+        statements[0]["vulnerability"]["name"].as_str(),
+        Some(GHSA),
+        "{vex}"
+    );
+    assert_eq!(
+        statements[0]["status"].as_str(),
+        Some("not_affected"),
+        "{vex}"
+    );
 
     // 2. Idempotent re-scan: no further edits, lock byte-identical.
     let code = run(hosted_args(tmp.path(), server.uri(), None)).await;
     assert_eq!(code, 0);
-    assert_eq!(read(&lock_path), redirected, "re-scan must not touch the lock");
... diff truncated: showing 800 of 2236 lines

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 185f91a. Configure here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/socket-patch-core/src/vendor/composer_lock/mirror_filters.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread crates/socket-patch-core/src/vendor/composer_lock/mirror_filters.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mikolalysenko @claude
Match package/-prefixed patch keys and guard reads in mirror filters
8b88583
The conflict check compared patch file keys to `.gitignore` /
`.hgignore` / `.gitattributes` verbatim, but API records key files as
`package/<path>`, so a patch rewriting a filter file slipped past it and
neutralization broke the patched file's afterHash. Normalize the key
first.

Read the copy's filter files through the FIFO-safe
`read_regular_to_bytes` instead of a bare `tokio::fs::read`, so a FIFO
at the path is skipped instead of wedging the vendor run.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@mikolalysenko Mikola Lysenko (mikolalysenko) mentioned this pull request Sep 27, 2026
Give the CI test job 50 minutes so Windows can finish #278
Merged
@mikolalysenko
Mikola Lysenko (mikolalysenko) enabled auto-merge (squash) September 27, 2026 21:50
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Sep 27, 2026
@mikolalysenko @claude
ci: give the test job 50 minutes so Windows can finish (#278)
5e1ebac
Windows test runs today took 25-32 minutes when green, and slower runners
cut off three #270 runs and a release/v5-prerelease run at the 35-minute
limit with every test still passing. The build step alone ran 4-5 minutes
longer than usual, which left no headroom.

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
mikolalysenko
Mikola Lysenko (mikolalysenko) commented Sep 28, 2026
View reviewed changes

@mikolalysenko Mikola Lysenko (mikolalysenko) left a comment

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

v5 review at 8b885839 — this is a worthwhile correctness prerequisite for hosted mode. Composer version identity, removing source fallback, and checking actual installer behavior belong in v5. The two earlier Bugbot findings have fixes at this head; I am not reopening them, and I did not independently confirm a new blocker in this PR.

For integration/simplification:

  • Land the Composer identity/installer behavior once, then make #281's Composer model and #280's upstream restoration reuse it. Otherwise padded-version equivalence, source removal and mirror handling will drift across three implementations.
  • The hosted rewrite still leaves dist.reference at the upstream identity, and the new hints compensate by telling users to remove installed directories. Investigate using the patch UUID as the hosted dist reference, as vendored mode already does. Verify initial install, A→B patch updates, and rollback on the supported Composer matrix before relying on it; retain only guidance that remains necessary, including Composer 1 behavior.
  • Prefer one byte-span-aware Composer entry model for hosted/vendored/discovery/VEX. The expanded hosted scanner still has positional assumptions (name/dist order); valid JSON should either be handled consistently or produce one explicit refusal through that model.
  • Keep the real Composer install tests and shared identity vectors. This is compatibility evidence worth preserving; the simplification target is duplicated parsing and orchestration.

I reviewed the code/diff and current CI results; I did not rerun the Composer toolchain matrix locally.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mikolalysenko Mikola Lysenko (mikolalysenko) mentioned this pull request Sep 28, 2026
v5 prerelease: scan → vex → vendor workflow, hosted by default #277
Merged
Wenxin-Jiang
Wenxin Jiang (Wenxin-Jiang) approved these changes Sep 28, 2026
View reviewed changes
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 1a8608b into main Sep 28, 2026
487 of 491 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/composer-patch-annotations branch September 28, 2026 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@cursor cursor[bot] cursor[bot] left review comments
@Wenxin-Jiang Wenxin Jiang (Wenxin-Jiang) Wenxin-Jiang approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@mikolalysenko @Wenxin-Jiang
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.