diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index 54bf2197c..af828cf20 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -319,7 +319,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc * **Hosted and vendored mode (bare `scan` included) — project directories** (`run_project_dirs`). Each PATH is a directory, or a glob (`*?[`) matching directories, relative to `--cwd`; the set is sorted and deduplicated, and each directory is scanned on its own exactly as if it were `--cwd` (its own lockfiles, ledgers and `.socket/`). With more than one directory, each run is headed `==
): ` lines behind the `--silent` gate). Interactive mode prompts "Download and vendor N patches?" (singular for one).
+`scan --mode vendored` swaps the in-place apply for the vendor pipeline: discover → download the selected patch records **into memory** (no manifest write) → vendor every selected dependency via the same engine as the `vendor` command (under the same lock). Vendored mode is **manifest-free (v5.0)**: `.socket/manifest.json` is never written or read by a vendored run; each ledger entry carries `detached: true` plus an embedded copy of the patch record (`record`) as its verification source, and the run's footprint is `.socket/vendor/**` only. The vendor step's scope is what discovery selected — the former "whole manifest is vendored" re-vendor on an empty discovery is retired (`repair` verifies and redownloads committed vendored state; `scan --prune` reconciles ledger entries whose dependency left the lockfile). The vendor-ledger discovery supplement (the fresh-clone rule: a ledger entry with no installed copy stays discoverable because its committed artifact IS the dependency) holds only while the lockfile still resolves through that artifact: an entry the lockfile in-use probe (the one `--prune` reverts by) proves unwired, because the dependency was upgraded or removed, is NOT discovered and so is never re-vendored. A run without a non-hosted `--prune` reports it through the run-level `vendor_ledger_entry_unwired` warning; a `--prune` run reverts it in its GC and exits 0. That GC runs even when the crawl found no packages, as its vendored half alone (the manifest prune stays skipped there). A package the ledger holds at an older patch uuid is still **re-vendored automatically** when discovery selects the newer patch (its old uuid dir is removed — `vendor_stale_artifact_removed`) — unless the patch service has no prebuilt artifact for the newer patch yet (or at all): the package then keeps the older patch and is a `skipped` `vendor_prebuilt_pending` / `vendor_prebuilt_unavailable` event, not a failure, in every ecosystem (#954, #1235); same-uuid re-runs reuse the embedded record, skip the patch-view fetch, and are `already_vendored` skips. **Legacy manifest-mode entries**: when a vendored run vendors a purl that also has a `.socket/manifest.json` record (a project vendored by a pre-5.0 binary, or by standalone `vendor` from an agent-mode manifest), that manifest record is dropped in the same run — the ledger becomes the owner (migration write); an emptied manifest is left as `{"patches": {}}`, never deleted. The migration is reported through the run-level `warnings[]` (stderr in human mode), never as a run error: `vendor_manifest_record_migrated` (`N manifest records moved to the vendor ledger (vendored mode is manifest-free): `) or `vendor_manifest_migration_failed` (the manifest or the ledger could not be read or rewritten; the legacy records were left in place) — so a corrupt `.socket/manifest.json` no longer fails a vendored run (standalone `vendor`, the one manifest-driven writer, still fails closed on it). With `--prune`, GC runs **after** the vendor step (the step never reads the manifest, and running the sweep last lets it reclaim what the run itself orphaned — a migrated legacy record's blobs, a superseded uuid dir). Under `--json` (v5.0, MAJOR) the download phase and the vendor engine record into the ONE scan envelope (no nested `download` / `vendor` objects): a fetched patch is a `downloaded` event (the patch metadata in `details`, plus `details.oldUuid` when the ledger holds the purl at another uuid — the re-vendor the vendor step then performs — whose human `[fetch]` line reads ` (replacing )`), a refused or failed one a `failed` event with its code, a patch the ledger already holds at the selected uuid no download event at all (reused, no fetch); then the vendor engine's events (`applied`, `rebuilt`, `skipped`, `failed`, `removed`, its uncounted advisories) follow with their warnings and sidecars. Every one of these events carries `details.mode: "vendored"`. Patch blobs are held in memory (see "Patch sources stay in memory" under the vendor contract). `--dry-run` previews per patch: `verified` (to vendor; `oldUuid` on a re-vendor), `skipped` / `already_vendored` (in sync), or `skipped` with the refusal code for an npm purl the wet run's Bun preflight (see the `get --mode vendored` bullet below) would refuse, or whose hosted pnpm pin the takeover would be refused on (see "Takeover reconciliation"), or for a PyPI purl the PyPI vendored backend's wiring guards would refuse before building anything (the project routing and lock guards a standalone `vendor --dry-run` reports, such as `pypi_no_requirements` or `pypi_uv_no_sources`, on the project lane and the PEP 723 script lane alike, #891; a PyPI purl with a live hosted pin is not judged, since the wet run restores its upstream entry before those guards run) — without network downloads or disk writes; the preview never flips status or exit (the human path — `scan` and `get` alike, through one shared printer — prints `[would-refuse] (): ` lines behind the `--silent` gate). Interactive mode prompts "Download and vendor N patches?" (singular for one).
**Vendored entries and the rest of the CLI.** Because nothing is in the manifest, vendored patches are invisible to `apply` (nothing to apply in place) but fully visible to `list` (listed from the ledger, labeled `Mode: vendored (recorded in .socket/vendor/state.json)` in human mode, exit 0 on a vendored-only project), `vex` (attested from the embedded records while a lockfile still wires the artifact — see "Manifest-less VEX"), `repair` (health-checked and rebuilt from the ledger), and `scan --prune` (lockfile-driven reconcile). They are exempt from standalone `vendor`'s manifest reconcile (`reconcile_dropped` never touches `detached` entries) and exit via `remove ` (which reverts them), `vendor --revert`, or `rollback`, whose vendored leg reverts every in-scope ledger entry (unscoped and identifier-scoped runs; path-scoped runs reach them only when an installed copy matches).
@@ -1568,7 +1568,7 @@ command's integration tests check its output.
| `path_scope_excluded_supplements` | scan `warnings[]` | path-scoped scan (v5.0): lockfile-only / vendor-ledger supplement packages have no installed path and were excluded from the scoped scan; the detail carries the count. |
| `vendor_commit_failed` | top-level error (`vendor`, and the nested vendor envelope of `scan` / `get --mode vendored`); also `failed` (per package) | v5.0 group commit: the run's lockfile / manifest / ledger edits could not be written (the detail names the I/O error). Exit 1; the project's lockfiles and `.socket/vendor/state.json` are left as they were before the run (a partially-applied commit is put back). As for `redirect_symlinked_file_unsupported` (#898), the artifact dirs the run added under `.socket/vendor/` are removed (any that cannot be are named in the detail), each package the run vendored is a `failed` event with this code, not `applied`, its `skipped` advisories from the run are dropped, and the human run prints no "Vendored N packages" count for them and no "Next steps:". When putting a partially-applied commit back fails too, the journal is kept instead, the detail says the next socket-patch command in the project finishes the commit, and the per-package events stand (that commit does complete). |
| `redirect_symlinked_file_unsupported` (vendored) | top-level error (`vendor`, and the nested vendor envelope of `scan` / `get --mode vendored`) | v5.0 group commit (#627): a file the run would rewrite — a lockfile, `package.json`, `pnpm-workspace.yaml`, `nuget.config`, … — is a symbolic link. The commit stages each file and renames it over the path, which would replace the link with a detached copy and leave its target (the lock other checkouts read) unpatched, so it refuses before writing anything — the same code and message as the hosted guard. Exit 1; the link, its target and `.socket/vendor/state.json` are left as they were, the artifact dirs the run added under `.socket/vendor/` are removed (any that cannot be are named in the message, with `vendor --revert` as the remedy), and each package the run vendored is a `failed` event with this code, not `applied` (`summary.applied` does not count it); the `skipped` advisories the run recorded for it (`vendor_prebuilt_downloaded`, `vendor_artifact_reused`, …) are dropped, since they describe the vendoring that was refused. The human run prints no "Vendored N packages" count for them and no "Next steps:" (#898). Backends that check their own targets first (Hatch, uv, Poetry, PDM, Pipenv, requirements, PEP 751 locks, Cargo) refuse per package with this same code (see the per-package row below); a symlinked `bun.lockb` keeps `vendor_bun_lockb_invalid`. |
-| `vendor_would_refuse_symlinked_file` | `skipped` (advisory event) under `vendor --dry-run`; a top-level `warnings[]` entry (detail prefixed `: `) for each package the `scan` / `get --mode vendored --dry-run` preview would vendor (human: an `[warning] (): ` line) | dry run (#627): a dry run captures no writes, so for each package it would vendor (not one previewed as in sync, whose re-run writes nothing) it names every symlinked file of that package's ecosystem a vendored run may rewrite (the registry's vendored rewrite targets plus `pnpm-workspace.yaml`, `nuget.config`, `packages.lock.json`, the root `pom.xml`, `.mvn/maven.config` and `hatch.toml`; files a vendored run only reads, such as `.yarnrc.yml` or `vlt.json`, are never named); the wet run refuses with `redirect_symlinked_file_unsupported` if it must rewrite one. Does not change the exit code. |
+| `vendor_would_refuse_symlinked_file` | `skipped` (advisory event) under `vendor --dry-run`; a top-level `warnings[]` entry (detail prefixed `: `) for each package the `scan` / `get --mode vendored --dry-run` preview would vendor (human: an `[warning] (): ` line) | dry run (#627): a dry run captures no writes, so for each package it would vendor (not one previewed as in sync, whose re-run writes nothing) it names every symlinked file of that package's ecosystem a vendored run may rewrite (the registry's vendored rewrite targets plus `pnpm-workspace.yaml`, `nuget.config`, `packages.lock.json`, the root `pom.xml`, `.mvn/maven.config` and `hatch.toml`; for PyPI also every variable-name lock in the project root — `pylock.toml`, `pylock..toml`, a PEP 723 `