Skip to content

Commit 1d3edc2

Browse files
Document vendor --force as variant-probe bypass (#1346)
Since vendoring became service-only it commits the patch server's verified artifact and never reads the installed files, yet vendor --force help and CLI_CONTRACT.md still promised a missing-file tolerance and a vendor_content_mismatch_overwritten warning that no backend implements. Users set --force for nothing. The help and contract now say --force only bypasses the installed- variant probe, mark the warning code as no longer emitted, and fix the stale test docs. A help test pins the new text, and the core test now shows a missing file vendors alike with and without force (#923). Assisted-by: Claude Code:claude-opus-5-5 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 43af719 commit 1d3edc2

6 files changed

Lines changed: 58 additions & 37 deletions

File tree

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Every subcommand accepts the same set of "global" flags via a single shared `Glo
7979

8080
`--offline` means the same thing on every command (v3.0): never contact the network, fail loudly when a required local source is missing. On `repair`, `--offline` and `--download-only` are mutually exclusive (exit 2). `scan` and `get` need remote data for their core function (patch discovery / patch fetch), so `--offline` refuses them up front — exit 1 with an error naming the offline gate (JSON: `status: "error"`), before any crawl, client build, or network contact. This covers `scan --mode vendored` too: offline vendored staging is `vendor --offline`'s job.
8181

82-
The `--strict` mismatch policy applies to the in-place apply paths (apply/get/scan --mode agent/hook/go redirect). DEFAULT (v3.4): a file whose on-disk content matches neither the patch's beforeHash nor its afterHash is overwritten with the FULL verified patched content (blob writes are hash-gated to exactly afterHash; the missing blob is downloaded on demand) and surfaced as a `content_mismatch_overwritten` stderr warning + Skipped event (agent-mode `get` / `scan --json`: a `warnings[]` entry, see "Agent-mode mismatch overwrites"). A file the patch adds (empty beforeHash) that already exists with other content is the same case. `--strict` turns that case into a hard error. Rollback of an added file deletes it; the content it replaced is not kept. `--force` overrides `--strict` and additionally skips missing files. Vendor staging is unaffected (it always auto-overwrites into its private stage).
82+
The `--strict` mismatch policy applies to the in-place apply paths (apply/get/scan --mode agent/hook/go redirect). DEFAULT (v3.4): a file whose on-disk content matches neither the patch's beforeHash nor its afterHash is overwritten with the FULL verified patched content (blob writes are hash-gated to exactly afterHash; the missing blob is downloaded on demand) and surfaced as a `content_mismatch_overwritten` stderr warning + Skipped event (agent-mode `get` / `scan --json`: a `warnings[]` entry, see "Agent-mode mismatch overwrites"). A file the patch adds (empty beforeHash) that already exists with other content is the same case. `--strict` turns that case into a hard error. Rollback of an added file deletes it; the content it replaced is not kept. `--force` overrides `--strict` and additionally skips missing files. Vendoring is unaffected: it commits the patch server's verified artifact and never reads the installed bytes.
8383

8484
## Per-subcommand arguments
8585

@@ -89,7 +89,7 @@ Beyond the globals above, each subcommand defines a small set of local arguments
8989
|---|---|---|---|
9090
| `apply` | `--force` / `-f` | — | Bypass beforeHash check |
9191
| `apply` | `--check` | — | Read-only audit that every in-scope (`--ecosystems`) manifest patch is in place, for CI / GitHub-App auditing (v5.0: previously Go-only, which passed on any unpatched non-Go tree). Local Go patches: the committed `.socket/go-patches/` copies and `go.mod` `replace` directives match the manifest (`go_redirect_drift`). Every other patch: each installed copy hashes to the record's `afterHash` — the `vex` verifier over the `vex` copy lookup; the copies are narrowed by `apply`'s own rules: a release variant (a qualified purl such as `?artifact_id=` / `?platform=`) is judged only on the copies holding its distribution, matched against every variant of its base as `apply` matches them, and an installed copy that holds none of them is drift of the base purl (`no_matching_variant`, the copy `apply` fails with "no matching variant found"; a Gradle / Ivy cache dir is exempt, as in `apply`); for gem, once a bundle-store copy exists the `gem env` fallback-home copies are not judged (`apply` treats them as best-effort). Drift is a `failed` event per patch with `errorCode` `not_applied` (still unpatched), `hash_mismatch` (neither the original nor the patched bytes), `file_not_found` or `no_matching_variant`, status `partialFailure`, exit 1, and the human `Error: Patches are OUT OF SYNC:` report (printed even under `--silent`), which ends with the remedy ``Run `socket-patch apply <scope>` to regenerate them.``: `<scope>` repeats the check's own `--cwd`, `--manifest-path`, `-g` / `--global-prefix` and `--ecosystems` (each shell-quoted when needed), so running it verbatim heals the tree that was checked (v5.0, #1219). In sync is exit 0: `Patches are in sync (N checked).` and, under `--json`, a `skipped` event per verified patch (`errorCode: already_patched`). A patch with no installed copy is skipped as `apply` skips it (`package_not_installed`; the human line adds `M not installed, skipped`). Vendor-owned patches are excluded (`vendor --check` audits them). Lock-free, fetch-free, offline-safe; it never writes. An unreadable manifest is drift (`manifest_unreadable`, exit 1) |
92-
| `vendor` | `--force` / `-f` | — | Tolerate missing patch-target files in the stage + bypass the variant probe. A beforeHash mismatch no longer needs it: vendor staging auto-overwrites with the verified patched content (`vendor_content_mismatch_overwritten` warning) |
92+
| `vendor` | `--force` / `-f` | — | Bypass the installed-variant probe for multi-release ecosystems. That is its only effect: vendoring commits the patch server's verified artifact and never reads the installed files' content, so a missing or mismatched installed file vendors the same with or without `--force` (no warning) |
9393
| `vendor` | `--revert` | `SOCKET_VENDOR_REVERT` | Undo vendoring: restore recorded original lockfile fragments + remove `.socket/vendor/` artifacts. Works without a manifest. A package vendored over a hosted pin returns to its upstream registry entry, never to hosted (see "Takeover reconciliation") |
9494
| `vendor` | `--check` | — | Offline, read-only artifact and wiring audit; exits 1 on drift. Conflicts with `--revert`. |
9595
| `vendor` | `--local-repo <path>` | — | With `--check`, also inspect suffixed Maven jar/POM copies in this cache for conflicts. |
@@ -1343,7 +1343,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
13431343
| `vendor_yarn_berry_mixed_line_endings` | `failed` | vendor (yarn berry): `yarn.lock` or the root `package.json` mixes CRLF and LF line endings (or holds a bare CR) — no single ending can be kept, and yarn rewrites such a file wholesale on its next install (a mixed lock also fails `--immutable`, YN0028). Refused before any write; `yarn install` normalizes the files. A uniformly CRLF pair is vendored in CRLF. A hosted→vendored takeover (`vendor`, `scan`/`get --mode vendored`) raises this — and the berry `vendor_yarn_berry_cache_unsupported` gates — BEFORE restoring the hosted pin's upstream entry (dry run too), so a refused purl stays hosted. |
13441344
| `vendor_override_conflict` | `failed` | vendor (pnpm/yarn-berry): a user-authored override/resolution for the package already exists. pnpm exception: a user exact pin equal to the vendored version, under the bare `<name>` or the `<name>@<version>` key, in package.json `pnpm.overrides` or pnpm-workspace.yaml `overrides:` (#854; there a quoted `'1.3.0'` or a trailing `# comment` is the same pin), is taken over in place (its value becomes the vendored `file:` spec under the user's own key on every override surface) and `vendor --revert` restores it byte-for-byte. A range, another version, a `>` selector, several same-name keys, or package.json and pnpm-workspace.yaml pinning under different keys still refuse; the detail names the file that carries the override. |
13451345
| `vendor_integrity_unverified` | `skipped` (warning) | vendor (pipenv): the lockfile format does not hash-check file entries; the committed wheel bytes are the protection. |
1346-
| `vendor_content_mismatch_overwritten` | `skipped` (warning) | vendor: a staged file matched NEITHER beforeHash nor afterHash (patch built against different bytes, or local edits); the stage was overwritten with the verified patched content and the vendor succeeded. |
1346+
| `vendor_content_mismatch_overwritten` | — | **No longer emitted** (service-only vendoring): `vendor` commits the patch server's verified artifact without reading the installed bytes, so it has no mismatch to report. Kept so consumers that match on it know it will not appear. |
13471347
| `vendor_vlt_transitive_unsupported` | `failed` | vendor (vlt): the target has an inbound edge from another package in `vlt-lock.json` (the detail names it); vendored mode rewires only direct dependencies of the root or a workspace member, because vlt silently reverts transitive lock surgery. Remedy: `--mode hosted`. Refused before any download or write, dry runs included (`would_refuse`). |
13481348
| `vendor_vlt_lock_out_of_sync` | `failed` | vendor (vlt): an importer's `package.json` is missing, unparseable, or declares a spec for the dependency that differs from the lock's importer edge. Remedy: `vlt install` first. Refused before any write. |
13491349
| `vendor_vlt_build_scripts_unsupported` | `failed` | vendor (vlt): the package declares a `preinstall`, `install`, `postinstall` or `prepare` script, or ships a `binding.gyp`. vlt builds a registry copy in the untracked store, but a vendored `file:` dependency in place, so `vlt build` would rewrite the committed artifact (a platform binary over a JS shim, say) and every later vendor, repair and `vex` would treat it as tampered. Remedy: `--mode hosted`. Refused before any write. |

‎crates/socket-patch-cli/src/commands/vendor.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ pub struct VendorArgs {
7171
#[command(flatten)]
7272
pub common: GlobalArgs,
7373

74-
/// Tolerate missing patch-target files in the staged copy (skip them
75-
/// instead of failing) and bypass the variant probe for multi-release
76-
/// ecosystems. Not needed for a beforeHash mismatch: vendoring always
77-
/// overwrites mismatched content with the verified patched bytes and
78-
/// warns (`vendor_content_mismatch_overwritten`).
74+
/// Bypass the installed-variant probe for multi-release ecosystems
75+
/// (vendor every recorded release variant, not just the one whose
76+
/// bytes match the installed copy). Vendoring never reads the
77+
/// installed files' content: it commits the patch server's verified
78+
/// artifact, so a missing or locally edited file needs no flag.
7979
#[arg(
8080
short = 'f',
8181
long,

‎crates/socket-patch-cli/tests/help_text_hygiene.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,21 @@ fn short_help_lists_about_eight_options_and_long_help_lists_all() {
285285
"{long}"
286286
);
287287
}
288+
289+
/// #923: service-only vendoring never reads the installed files, so
290+
/// `vendor --force` only bypasses the installed-variant probe. Its help
291+
/// must not promise a missing-file tolerance or a mismatch warning that
292+
/// no vendored backend implements.
293+
#[test]
294+
fn vendor_force_help_describes_only_the_variant_probe_bypass() {
295+
let help = long_help(&["vendor"]);
296+
let force = help
297+
.split("--force")
298+
.nth(1)
299+
.and_then(|rest| rest.split("\n -").next())
300+
.expect("vendor --help documents --force");
301+
assert!(force.contains("variant probe"), "{force}");
302+
for stale in ["Tolerate missing", "vendor_content_mismatch_overwritten"] {
303+
assert!(!force.contains(stale), "stale {stale:?} in: {force}");
304+
}
305+
}

‎crates/socket-patch-cli/tests/in_process_vendor.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,9 +2880,10 @@ fn vendor_after_in_place_apply_emits_applied_event() {
28802880

28812881
/// Installed content matching NEITHER hash (a patch built against different
28822882
/// bytes than the installed artifact — the flatted@3.3.1 case) still vendors:
2883-
/// the stage is overwritten with the verified patched content, the run exits
2884-
/// 0 with an `applied` event, and the overwrite surfaces as a
2885-
/// `vendor_content_mismatch_overwritten` warning event.
2883+
/// vendoring commits the server's verified artifact without reading the
2884+
/// installed bytes, so the run exits 0 with an `applied` event and the
2885+
/// `vendor_prebuilt_downloaded` advisory, and the installed tree is left
2886+
/// untouched.
28862887
#[test]
28872888
fn mismatched_install_does_not_change_the_server_artifact() {
28882889
let fx = npm_fixture();

‎crates/socket-patch-cli/tests/scan_vendor_e2e.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,8 +1407,8 @@ async fn scan_vendor_prune_reconciles_unwired_entry_on_an_empty_crawl() {
14071407

14081408
/// Interactive (non-JSON) `scan --mode vendored` pre-verifies patch baselines:
14091409
/// installed content matching NEITHER hash is annotated before vendoring
1410-
/// starts, and the run still vendors (auto-force) with the
1411-
/// `vendor_content_mismatch_overwritten` warning on stderr.
1410+
/// starts, and the run still vendors the server's verified artifact (no
1411+
/// mismatch warning: vendoring never reads the installed bytes).
14121412
#[tokio::test]
14131413
async fn scan_vendor_annotates_mismatched_baseline_and_vendors_anyway() {
14141414
let mock = MockServer::start().await;

‎crates/socket-patch-core/src/vendor/npm_lock.rs‎

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2322,32 +2322,34 @@ mod tests {
23222322
);
23232323
}
23242324

2325-
/// `vendor --force` keeps its missing-file tolerance (strict superset
2326-
/// of the auto-force policy).
2325+
/// Vendoring ignores the installed copy's content: a missing patch
2326+
/// target vendors the same with or without `force` (#923).
23272327
#[tokio::test]
2328-
async fn vendor_force_still_skips_missing_files() {
2329-
let fx = fixture().await;
2330-
tokio::fs::remove_file(fx.installed().join("index.js"))
2331-
.await
2332-
.unwrap();
2328+
async fn vendor_ignores_missing_installed_files_with_or_without_force() {
2329+
for force in [false, true] {
2330+
let fx = fixture().await;
2331+
tokio::fs::remove_file(fx.installed().join("index.js"))
2332+
.await
2333+
.unwrap();
23332334

2334-
let blobs = fx.root().join(".socket/blobs");
2335-
let sources = PatchSources::blobs_only(&blobs);
2336-
let outcome = crate::vendor::test_support::vendor_npm(
2337-
&fx.purl(),
2338-
&fx.installed(),
2339-
fx.root(),
2340-
&fx.record,
2341-
&sources,
2342-
"2026-06-09T00:00:00Z",
2343-
false,
2344-
/*force=*/ true,
2345-
None,
2346-
)
2347-
.await;
2348-
let (result, entry, _) = expect_done(outcome);
2349-
assert!(result.success, "{:?}", result.error);
2350-
assert!(entry.is_some());
2335+
let blobs = fx.root().join(".socket/blobs");
2336+
let sources = PatchSources::blobs_only(&blobs);
2337+
let outcome = crate::vendor::test_support::vendor_npm(
2338+
&fx.purl(),
2339+
&fx.installed(),
2340+
fx.root(),
2341+
&fx.record,
2342+
&sources,
2343+
"2026-06-09T00:00:00Z",
2344+
false,
2345+
force,
2346+
None,
2347+
)
2348+
.await;
2349+
let (result, entry, _) = expect_done(outcome);
2350+
assert!(result.success, "force={force}: {:?}", result.error);
2351+
assert!(entry.is_some(), "force={force}");
2352+
}
23512353
}
23522354

23532355
/// A package already patched IN PLACE by `apply` vendors cleanly: the

0 commit comments

Comments
 (0)