Skip to content

v5 prerelease: scan → vex → vendor workflow, hosted by default - #277

Draft
Mikola Lysenko (mikolalysenko) wants to merge 27 commits into
mainfrom
release/v5-prerelease
Draft

Mikola Lysenko (mikolalysenko) wants to merge 27 commits into
mainfrom
release/v5-prerelease

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

The v5 prerelease centres the CLI on one workflow:

  1. socket-patch scan patches dependencies by rewriting lockfiles to Socket-hosted patched packages. It writes nothing but those lockfile edits.
  2. socket-patch vex emits OpenVEX for vulnerability scanners.
  3. socket-patch vendor ejects the patches into .socket/vendor/ for offline installs.

socket-patch list shows what a project has. The older agent-mode commands (get, apply, setup, rollback, remove, repair) keep working but move after these in help and docs.

This is a draft. It lands in stages, one commit per step.

Done so far

  • Help order: --help lists scan, vex, vendor and list first, then the agent-mode commands, with a "Typical workflow" footer.
  • Hosted is the default: a bare scan runs hosted mode. A path-scoped, --prune or global scan with no --mode only reports, because it has no lockfile to rewire.
  • No prompts in scan: no confirm prompt in any mode and no patch menu.
  • Patch choice: for each package, scan picks the newest merged (multi-advisory) patch the user can download. With no merged patch it picks the highest severity, then the newest.
  • --package filter: scopes a scan by name or purl, and can be repeated or comma-separated.
  • Directory scoping: in hosted and vendored mode, PATHs (apps/*) name project directories, each scanned as if it were --cwd.
  • Update detection from lockfiles: updates[] also counts the hosted pins in the lockfiles, not only ledger records.
  • Shared helpers: GlobalArgs::{crawler_options, is_global, ecosystem_selected, purl_ecosystem_selected} replace 8 hand-copied CrawlerOptions literals and 7 ecosystem-scope predicates.
  • Changelog: the v5 section leads with the workflow.

Still to come

  • Ledger-free hosted mode (blocked on a design decision): scan stops writing .socket/vendor/redirect-state.json. vex, list and vendor read the hosted pins from the lockfiles. rollback rebuilds the originals from the registry. Existing ledgers are still read.
  • vendor ejects hosted pins into vendored artifacts.
  • Shared project setup: one context for scan, vendor, vex and list (crawler options, ecosystem scope, ledgers, lockfile wiring, inventory).
  • Cleanup and docs: tidy comments in the touched modules, then update README, CLI_CONTRACT, docs and CHANGELOG.

Known red

  • e2e_redirect_cargo_build (2 cases): the embedded scan --vex now writes the hosted ledger. These pass once scan stops writing the ledger (the ledger-free step above).

🤖 Generated with Claude Code

List scan, vex, vendor and list first, then the agent-mode commands
(get, apply, setup, rollback, remove, repair), and add a short
"Typical workflow" footer to the root help.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A merged patch folds several advisories into one blob and is the
package's cumulative fix, so the newest one the user can download now
wins outright. Packages with no merged patch keep the old order:
highest severity, then newest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A bare scan now runs hosted mode: it rewrites lockfiles so only the
patched dependencies resolve to Socket-hosted packages. A path-scoped,
--prune or global scan with no mode only reports, since it has no
lockfile to rewire.

scan asks nothing any more: no confirm prompt in any mode and no patch
menu. It takes the top-ranked patch per package.

New --package filter (repeatable or comma-separated, env
SOCKET_SCAN_PACKAGES) scopes a scan to packages by name or purl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan's updates[] now also sees the hosted pins the lockfiles wire, not
only the redirect ledger's records, so a hosted project that never
committed its ledger still reports a superseding patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GlobalArgs gains crawler_options(), is_global(), ecosystem_selected()
and purl_ecosystem_selected(). They replace eight hand-copied
CrawlerOptions literals, seven --ecosystems predicates and seven global
checks across scan, vendor, vex, apply, setup, rollback, get and
repair.

vendor's predicate also matched case-insensitively and accepted
for golang. clap validates the names first, so neither form ever
reached it. It now uses the same exact match as everything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In hosted and vendored mode, and so in a bare scan, positional PATHs
now name project directories: each directory, or directory glob such
as apps/*, is scanned on its own as if it were --cwd, under a
"== <dir> ==" header. The exit code is the worst of the runs. A PATH
that is not a directory is a usage error. --json takes a single
directory so stdout stays one document. Agent-mode PATHs keep their
installed-path glob meaning.

The changelog's v5 section now leads with the scan → vex → vendor
workflow and describes the new scan defaults.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
candidate_supersedes still put severity above merge state, so scan's
updates[] and [UPDATE] marker could name a different patch from the one
scan selects. It now calls a new ranking::batch_supersedes, which uses the
same merged-first key as the selection and ignores the tier and uuid
tiebreaks and missing dates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
socket-patch get <id>, and the bare-UUID shortcut, now redirect the
package to its Socket-hosted patched copy. Agent mode (manifest + in-place
apply) is --mode agent. It stays the default with --save-only, which
records a manifest entry, and with --global/--global-prefix, since those
have no project lockfile to rewrite. The agent-mode test fixtures now pass
--mode agent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README now leads with the scan → vex → vendor workflow: hosted mode
first, vendored for offline installs, agent mode as the older
install-hook flow, and the command reference in that order. The contract
and README now match the code on these points:
- a bare scan and get run hosted mode
- scan never prompts
- --package
- PATH semantics in each mode
- the merged-first patch ranking
- the exit-2 cases
- the env var table

The Bundler plugin README now says setup writes a path: source and that
failures warn by default. The docs/testing notes drop references to files
and flags that no longer exist.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comments and doc comments now describe the current code:
- references to renamed or deleted functions, files, tests and flags are
  fixed;
- notes about pre-v5 scan behavior (prompts, report-only non-TTY scans,
  severity-first ranking, PATH rejection) are gone;
- TODOs for finished work are removed;
- history narration ("used to", bug diaries, PR and audit tags, dated
  verification notes) is rewritten as the invariant it protects;
- doc comments attached to the wrong item are moved.

Two CI path filters that named the deleted vendor/lock_inventory.rs now
match vendor/lock_inventory/**. No code behavior changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- An exported-but-empty SOCKET_SCAN_PACKAGES= filtered every package out
  of a scan. It is now scrubbed like the other local env vars, and blank
  --package specs are ignored.
- Manifest-less VEX liveness for a rebuilt cargo vendor entry (no
  recorded wiring) now also probes the root Cargo.toml, where v5 writes
  the [patch.crates-io] table, not only the pre-v5 .cargo/config files.
- setup names the dependency it writes, socket-patch[hook]. The
  repair_vendor hint and the hosted --prune warning name the current
  flags.
- Drop scan's unreachable patch-menu spacing and the vendored arm's
  leftover blank line from the removed confirm prompt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Release review at 8ae7dc37: hold the release until the integrated hosted lifecycle is working and green. The direction is right, but the stack currently removes some visible CLI surface while adding new restoration/state machinery behind it.

I reviewed all seven open PRs and left specific feedback:

PR Assessment
#279 Keep setup/hook removal. Finish consistent empty-list status, command grouping and one result schema.
#280 Fix offline HTTP, fresh-checkout eject and dropped wiring diagnostics. Share restore grammar; delete the checked-in .orig.
#281 Good registry/model direction; complete shared grammar instead of retaining parallel readers/writers. Reproduced inherited Gem checksum duplication.
#282 Good core/Node boundary. Remove repeated ledger sorting and finish one project snapshot plus ledger-free disk/memory integration.
#283 Good backend consolidation, but two reproduced repair regressions: lost offline source bytes and silently changed lock integrity.
#270 Useful Composer correctness foundation; no new blocker confirmed. Reuse its identity/installer rules in the v5 models and restore path.

The simplification I would ship:

  1. One normal workflow: scan selects and applies hosted patches; scan --vex <file> optionally emits the report in the same run; vendor changes the same selected patch set to committed artifacts. Keep standalone vex/list for inspection. Make targeted get a thin selector over that same execution path, and route undo/remove through the same transition planner. Preserve the planned agent-mode escape hatch without duplicating its orchestration.
  2. One data path: project snapshot → parsed format models → patch selection → typed change plan → verified artifacts → commit → human/JSON rendering. Share inventory/wiring views, keep VEX eligibility separate, and reload/invalidate after writes. Hiding flags is not a substitute for deleting parallel execution branches.
  3. One acquisition path with explicit policy: reuse service artifacts where available; isolate the local-build fallback. Repair can reuse acquisition but must preserve the original artifact identity and lock wiring. Hosted→vendor should not pass through a committed unpatched state when conversion fails.
  4. Delete obsolete hosted write/replay machinery after checking consumers. Keep a small legacy reader. Prefer removing duplicate parsers and event-to-JSON-to-event conversions over cutting supported ecosystems or package-manager versions.
  5. Measure the intended savings: crawl/parse counts, API requests, no-op re-run work and end-to-end latency. Ledgers::owned() currently adds a repeated full sort/scan; ProjectContext still caches two separate parses. These are more useful optimization targets than moving functions between files.

Release gates:

  • All ten pairs of the five sibling cleanup heads produced merge conflicts in git merge-tree, including edits to files another PR deletes. Stack/rebase in a declared order and test the actual combined tree; per-branch results do not validate the release.
  • Run a lifecycle matrix: fresh lock-only checkout → hosted scan → native install → VEX/list → vendor → offline install → repair → rollback. Cover human/JSON/dry-run parity, zero HTTP offline, and failure injection that preserves existing protection.
  • Migrate the remaining tests that assume agent defaults or a hosted ledger. I checked the base Cargo CI failures and a #280 Go job; those include obsolete ledger assumptions, not evidence that every red job is a new product defect. They still need to be green under the new contract before release.
  • Finish the v5 version bump and docs against that integrated behavior. Workspace version is still 4.0.0 in this draft.

The inline comments cover a reproduced multi-project VEX overwrite and a patch-selection invariant that deserves an explicit decision before unattended mutation becomes the default.

Validation: source/diff and CI-log review across all seven PRs; isolated CLI builds/reproductions; 36 upstream restoration goldens passed; repair behavior compared before/after. No full cross-platform matrix or conflict-resolved integration build was run. Cloud delegation was attempted but rejected with 403: GitHub connector is disabled or unavailable, so this review ran locally in isolated worktrees.

}
let mut child = args.clone();
child.paths.clear();
child.common.cwd = dir.clone();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[P2] Give each project's VEX output a distinct destination

Each child keeps the same vex.vex path, and generate_vex writes that path relative to the process working directory. scan apps/* --vex out.vex.json therefore overwrites the first project's document with the second while reporting both writes as successful. A later project's failed generation can also remove the earlier project's output through stale-document cleanup.

Reproduced with two projects carrying distinct root product names: exit 0, two successful-write messages, final document contains only product b. Resolve per-project output explicitly, aggregate into one document, or reject a single output path for multiple projects before making changes. Add a two-project regression including a later failure.

not_paid: p.tier != "paid",
uuid: &p.uuid,
not_merged: !merged,
severity: if merged { 0 } else { severity },

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Selection-policy concern: advisory count does not prove cumulative coverage

This promotes every multi-advisory patch above every single-advisory patch and erases its severity. The new test a_merged_patch_beats_a_higher_severity_single_one deliberately selects a 2020 high+high bundle over a 2026 critical fix; the next test selects a low+low bundle over a critical+high one with different advisory IDs.

Because scan automatically replaces one patch per PURL, the “merged means cumulative” assumption needs an authoritative API contract/selection field, or a coverage check that prevents dropping already-fixed advisories and silently ignoring a newer critical fix. Prefer a server-recommended cumulative patch with explicit supersession/coverage. If no candidate safely supersedes the current patch, report the uncovered conflict instead of inferring it from len() >= 2. This is a critique of the intended policy, not a claim that a production incident has been observed.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Detailed v5 implementation handoff, incorporating Mikola's latest direction from the review discussion.

Target: make depscan the owner of package construction and remove CLI repacking where the hosted artifact contract supports it. Lockfile changes are part of the intended workflow and should remain supported. Mikola is comfortable keeping the entire repacking engine in depscan and prefers deleting the CLI copy if feasible. Avoid breaking or rewriting depscan's working builder merely to achieve code sharing.

This supersedes my earlier suggestion to expose the Rust repacker to both callers. Prefer the smaller migration: preserve depscan's existing implementation, turn the CLI into an artifact consumer, and delete redundant client construction paths.

What is actually shared today

In the inspected depscan checkout (c70d82a4097b61842c8834f8f490c1b5c93c9e80), production goes through buildAndStorePatchPackage → convertPatchToPackage → TypeScript ecosystem repackers and @socketsecurity/patches-shared/archive/repack-utils. Its own repack-* commands reuse those modules. This path does not call socket-patch's Rust repackers.

There is real duplication. Depscan's Berry writer explicitly identifies itself as a port of the CLI implementation. The npm paths also differ: depscan preserves upstream tar entry order and uses epoch timestamps; the CLI sorts paths and uses npm's fixed 1985 timestamp. Equal patched members therefore do not imply equal archive digests. Preserve already-published bytes and identities during this migration.

Suggested implementation sequence

  1. Establish the artifact coverage boundary before deleting code. Inventory each CLI vendor backend's required archive, extracted layout, auxiliary files and native checksums. Match these against the existing service response and serving routes. An ecosystem appearing in depscan's repacker registry is not proof that every variant is downloadable. Its current servable-variant guard rejects Maven classifiers/non-JAR variants and platform-specific gems. Berry sidecars are optional. Check wheel tags, Berry cache versions, Go module flavors, Cargo metadata, gem stub gemspecs, Maven POMs and NuGet metadata against actual supported CLI paths. Report which gaps affect existing supported behavior; do not assume they all do.

  2. Introduce one artifact acquisition path using the existing service contract. Carry immutable artifact identity, variant and expected digest through selection, acquisition, validation and installation. Prefer an already verified committed/cached artifact; otherwise download the exact service artifact when online. Return typed per-package results. Fresh lock-only checkouts must not require an installed pristine dependency tree just to vendor a service artifact. Keep safe extraction, integrity verification and package-manager wiring in the client. Audit callers before deleting shared hashing/extraction helpers merely because they live beside packers.

  3. Make vendor and repair consumers of that path with explicit operation semantics. Vendor may deliberately select a new artifact and edit the lockfile. Repair restores the pinned identity. Acquire into staging, validate against the original expected identity, then replace the artifact; failure must preserve the current project state. Repair must not validate against a fingerprint that the same operation just rewrote. For a historical locally built archive whose digest differs from the service archive, return a precise recovery instruction or require an explicit re-vendor; do not silently substitute different bytes. An intact historical artifact should remain usable.

  4. Remove CLI construction and fallback paths once coverage is demonstrated. Candidates include archive packing, wheel rebuilding, Berry archive generation, pristine-source acquisition used only for building, patch-content staging used only for building, and VendorSource::Auto/Build branches. This is a call-graph audit, not a blanket directory deletion: agent-mode application, rollback/upstream resolution and integrity verification may still consume nearby helpers. Remove dead dependencies and tests specific to the deleted responsibility. Make the v5 change to --vendor-source build explicit in CLI behavior and migration docs; no hidden fallback to local rebuilding after a service error.

  5. Define offline behavior without promising offline reconstruction. Existing committed artifacts and verified cache hits remain usable without HTTP. Missing bytes with no local verified source produce a clear refusal before mutation. A service-only design cannot reconstruct missing historical bytes offline, so document that intentional change. Preserve vendored/offline installation. Test offline behavior at the network boundary, including dry runs, rather than relying on flags propagated through several layers.

  6. Keep depscan stable. Aim for a CLI-only PR against release/v5-prerelease. Do not replace the TypeScript repackers, change existing artifact URLs/digests, regenerate published packages or alter storage/publication behavior to make the CLI refactor easier. If an actual capability gap blocks removal, use a narrowly scoped additive depscan change in a separate dependent PR; preserve existing outputs and consumers. Avoid adding a second server implementation of the Rust engine. Where coverage is incomplete, state the remaining build path explicitly and defer its deletion rather than silently dropping package-manager support.

Other deletions that complement this boundary

  • scan and targeted get should select inputs for the same execution path. vendor should acquire/materialize the chosen artifacts and plan their local references. Hosted→vendor should acquire successfully before committing any removal of hosted protection.
  • Return typed backend outcomes and render human/JSON output afterward. The reviewed WS5: one VendoredBackend for vendored apply/revert/repair; cut repair's ledger rebuild #283 repair adapter creates a scratch Envelope and interprets event codes in EngineOutcome::of; this is an internal dependency on the output protocol. Delete that round trip.
  • Share each format's parsed representation among inventory, wiring, edits, repair and VEX. Keep raw wiring inventory separate from attestation eligibility so an invalid or conflicting reference cannot disappear from management commands.
  • Finish deletion of obsolete hosted ledger writers/replay after auditing library consumers. Keep only the legacy reading/migration behavior still needed. Do not recreate three competing authoritative stores behind a new context type.
  • Keep the shared disk/memory hosted engine from v5 WS4/WS6: one hosted engine for disk + memory; unified Ledgers view #282. Moving package construction to depscan does not imply moving the CLI's project/lockfile engine there.

Coordination and validation

The current sibling PRs are active and overlap heavily. Inspect their latest heads and existing reviews first. In particular, #283 advanced to 6b04d2f with fixes and regression tests for the two repair problems I reproduced at e3717a0; I have not rerun the new tests. Do not duplicate those fixes or treat the older findings as proof that the latest head still fails. The longer-term simplification is to avoid general apply/rewire during repair in the first place.

Use the existing release review and linked per-PR reviews as additional context. Revalidate findings against the integrated tree. Preserve all currently supported package-manager versions and the planned agent-mode escape hatch; update docs/design/v5-plan.md where the newly authorized removal of client building changes the previous contract.

Acceptance checks should cover:

  • Fresh lock-only checkout → hosted selection → native install → vendor → native offline install.
  • Missing/corrupt artifact repair with an exact cache/service match; digest mismatch, pending/unavailable artifact, interrupted acquisition and failure before commit.
  • A historical locally built artifact with a different digest: usable when intact, explicit refusal/migration when unavailable, no silent re-pin.
  • Zero HTTP offline; equivalent operation semantics for human/JSON and dry runs.
  • Required sidecars/variants and native installer compatibility for every backend touched.
  • No-op reruns avoid rebuilding, unnecessary downloads and repeated parsing.
  • Relevant existing tests on the combined branch; report actual results and any matrix not run.

Please push a focused draft PR targeting release/v5-prerelease, describe any dependency on the sibling PRs, list concrete removed paths/dependencies and retained compatibility gaps, and do not merge automatically. The desired result is less client code and fewer execution paths with depscan's existing package builder preserved.

Cloud delegation from this session is unavailable: task creation returned 403: GitHub connector is disabled or unavailable, and no cloud task was created. This comment is the implementation handoff for the agent already working on the release.

…r ejects hosted projects (#280)

* Stop writing the hosted redirect ledger from scan

Hosted scan keeps its edits and records in memory only; the lockfiles
are the record of a redirect. Replays the parked WIP (which was
snapshotted on an older tree) as just its hosted.rs delta.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Restore hosted pins to their upstream registry entries instead of replaying the ledger

Imports the stopped local WS1 agent's work-in-progress (backup/local-v5-
ledger-free-hosted): core patch::redirect::upstream re-resolves npm-family,
cargo and golang registry entries for every hosted pin vex::discover finds
in the lockfiles, and rollback/remove/vendor route their hosted legs
through it instead of the redirect ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Update rollback prompt unit test for the upstream-restore wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Derive list, scan updates and takeover state from lockfile hosted pins

v5 keeps no hosted ledger, so every reader that consulted
.socket/vendor/redirect-state.json now reads the hosted pins lockfile
discovery finds:

- list shows each hosted pin with the lockfiles wiring it (details.lockfiles);
  a pre-v5 ledger only supplies the details of pins it still describes.
- scan's updates[] fold, redirectState block and the agent-flow
  hosted_wiring_retained probe read the pins.
- The hosted-over-vendored takeover classifier reads the pins; the
  vendored-over-hosted ledger reconcile (vendor_supersedes_redirect) is
  gone: once the lock routes a package to .socket/vendor/ no hosted state
  is left to go stale.
- vex treats a malformed pre-v5 redirect ledger as an advisory.

scan/mod.rs unit tests still need rewriting (WIP).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Drop the hosted ledger from get, repair, the in-memory engine and the scan fold

- get's vendored lock-text gates read the lockfiles' hosted pins instead of
  the redirect ledger (DownloadParams carries --patch-server-url for it).
- repair's hosted-only no-op fires for hosted lockfile pins (or a pre-v5
  ledger).
- The in-memory hosted engine neither reads nor emits
  .socket/vendor/redirect-state.json.
- Disk hosted scan no longer folds edits into a throwaway ledger; its
  records feed only the stale-install probes and in-run VEX.
- The cargo vendor backend's hosted_redirect_live refusal names rollback /
  git checkout instead of a ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* vendor ejects a hosted project; vendoring over any hosted pin restores upstream first

WS2: standalone `vendor` with no manifest now takes its patch set from the
lockfiles' hosted pins (purl + the uuid in each hosted URL), fetches each
record from the API, vendors it into .socket/vendor/ through the same step
`scan --mode vendored` and `get --mode vendored` use, and rewires the lock
from hosted to vendored. Without hosted pins it keeps the no-manifest
no-op.

The vendor takeover now restores the upstream registry entry before
vendoring for every ecosystem, not only cargo/npm/golang, so the vendor
ledger always records the upstream entry as its original and
`vendor --revert` returns to upstream rather than to hosted. A pin whose
upstream entry cannot be restored is refused with the checkout remedy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite scan/mod.rs takeover unit tests for lockfile-derived hosted state

v5 hosted mode keeps no ledger, so the scan/mod.rs unit tests now make a
purl "hosted" by writing a lockfile that pins the hosted URL instead of
planting .socket/vendor/redirect-state.json:

- overlap / classify_overlap_takeover tests use hosted package-lock,
  yarn (classic + berry), bun and cargo sparse-index pins; the
  non-default-host test configures --patch-server-url and pins that an
  unconfigured host is no pin.
- New behavior pinned: a pre-v5 ledger on disk is never hosted state;
  a lock routed to vendored (npm or cargo) yields no pin and no overlap;
  an edits-only state names no package; a half-migrated project whose
  locks name both sides stays silent; the redirectState block has no
  ledger/ledgerKey fields.
- hosted_wiring_retained_purls / redirect_state_json tests read the
  lockfile-derived state, keeping the probe's own liveness gate covered.
- Removed tests of retired behavior: note_vendor_supersedes_redirect
  reconcile (wet/npmrc/dry-run/no-op/persist-failure), the edits-only
  fallback (degraded ledger, vlt tilde keys) and following the vendored
  remediation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore hosted gem and composer pins to their upstream entries

Add the RubyGems and Composer restorers to the v5 ledger-free hosted
unwind (`redirect::upstream`).

Gem (Gemfile.lock / gems.locked + Gemfile / gems.rb): a converged Socket
GEM section is removed and its spec moved back, in name order, into the
upstream section (the single remaining one, else the manifest's global
source or rubygems.org, else refused as ambiguous); a bundler <= 2.1
merged section loses only the Socket remote; the DEPENDENCIES `!` pin is
dropped; CHECKSUMS is re-pinned from the rubygems.org compact index. The
Gemfile source block becomes `gem "n", "v"[, opts]` again (the original
constraint is not derivable), or is removed with its DEPENDENCIES entry
only when provably a transitive append. The pre-2.6 mixed state is undone
when a pin is supplied, keeping the untouched lock's own constraint.

Composer: dist {type,url,reference,shasum} and the dropped source block
are rebuilt from packagist p2 metadata (composer/2.0 minified, expanded),
cross-checked against the lock's dist.reference, in the lock's indent,
slash style and line endings. Non-packagist entries are refused.

UpstreamClient gains cached rubygems and packagist lookups
(SOCKET_RUBYGEMS_URL, SOCKET_PACKAGIST_URL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Fix clippy findings in the eject and takeover paths

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore hosted PyPI pins to their upstream registry entries

Add the Python family to the v5 hosted -> upstream restore: Pipfile.lock,
requirements.txt, Hatch direct references (pyproject.toml / hatch.toml),
poetry.lock, pdm.lock, and uv.lock / PEP 723 script locks / PEP 751 pylock
files with their paired pyproject / script metadata.

Each restorer rewrites only entries whose reference is a hosted URL for an
in-scope patch uuid and re-derives what the hosted rewrite overwrote from
PyPI's JSON API (UpstreamClient::pypi_files, base overridable with
SOCKET_PYPI_JSON_API, cached like the other lookups). Where a field is not
derivable the pin is refused instead of guessed: requirements hash-checking
mode that no other line settles, a Pipfile.lock index that is not PyPI,
PDM locks without cross_platform (or uv locks) when the release ships
platform- or interpreter-specific wheels, uv locks with no sibling
registry package to show the artifact shape, several or non-PyPI
registries, exclude-newer / no-binary / no-build filtering, multi-clause
uv specifiers with no spelling evidence, uv 0.2 [[distribution]] locks,
offline runs and registry failures.

The golden harness round-trips the native Poetry (1.0-2.4), PDM (every
supported lock_version) and Pipenv fixtures plus synthetic
requirements/Hatch/uv/pylock projects through the real hosted rewriter;
the shared requirements golden restores modulo the grant's name casing and
the uv golden (no registry sibling) is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore vlt, Maven and NuGet hosted pins to their upstream entries

vlt-lock.json: slot [2] from npm dist.integrity, slot [3] per the lock's
own convention (same-era default-registry siblings, else DepID era and
options.registries), every hosted instance of name@version together.
Maven (no network): base versions back, added dependencyManagement
entries, socket-patch repositories and emptied wrappers removed,
trusted-checksum lines dropped and .mvn files deleted only when nothing
but hosted content is left; module poms and stray suffix uses refuse.
NuGet: socket-patch source and mapping removed, a mapping that only fans
* out to every source dropped; packages.lock.json contentHash re-derived
from nuget.org's catalog packageHash (SOCKET_NUGET_URL), refusing when
the restored config does not resolve the id from nuget.org alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Drop dead fixture fields clippy flags in covgap_commands_rollback

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Retire a pre-v5 hosted ledger when rollback finds nothing else; update the cargo guard test

rollback in a project whose only state is a stale pre-v5
redirect-state.json (no manifest, no vendor ledger, no hosted pin in the
lockfiles) removes that file and exits 0 instead of failing on the missing
manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Feed the hosted scan's in-run VEX this run's records

The in-run `scan --mode hosted --vex` attestation read its hosted records
from the ledger the run had just written. With no ledger, the run's
fetched records reach the VEX builder in memory (VexBuildParams
hosted_records), merged over any pre-v5 ledger's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Refresh doc comments that still described the hosted ledger

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* vendor --revert restores hosted pins a pre-v5 vendor ledger re-creates

A package vendored over hosted wiring before v5 recorded the hosted
fragment as its pre-vendor original, so reverting it wired the lock back to
the patch server. After a wet revert, any hosted pin on a reverted purl is
restored to its upstream registry entry (warning
vendor_revert_restored_upstream; a refused restore is a failed event,
hosted_restore_failed, exit 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Refuse around contested hosted wiring; refuse offline eject before any request

Review follow-ups:

- core HostedInventory keeps raw hosted wiring apart from attributable
  pins: a hosted identity discovery recognizes but cannot attribute (locks
  that disagree, a malformed reference, a lockless registry pin) is
  contested wiring. rollback (unscoped), remove, list and vendor refuse
  around it with hosted_wiring_contested, naming the files and the
  git checkout remedy, instead of reporting a bare project.
- vendor's eject refuses offline (flag or env, wet or dry) with
  offline_eject_unavailable before it builds any request.
- Drop the stray upstream/client.rs.orig merge backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Make eject one transaction: plan, restore upstream, vendor, or roll back

Review follow-ups on the hosted -> vendored eject:

- Every patch record is fetched first; one the API cannot serve refuses
  the whole eject (eject_refused) before anything is touched.
- The upstream restore is planned first (a dry resolve of every pin); a pin
  that cannot be restored refuses the whole eject with its remedy.
- A dry run stops at the verified plan (eject_planned events) and writes
  nothing, not even .socket/.
- The wet run takes the apply lock once, snapshots every file the eject
  can touch (root files, pin and restore files, cargo/maven config, the
  vendor ledger, vendored uuid dirs), restores the pins upstream BEFORE the
  vendor engine inventories sources (so a fresh checkout with nothing
  installed resolves the pristine registry package), vendors, and on any
  failure puts the snapshot back (eject_rolled_back): a failed eject
  leaves the project hosted, byte for byte.

Adds failure-injection and dry-run coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite hosted rollback/remove coverage-gap tests to the v5 upstream restore

Hosted state is the lockfile pin: rollback/remove restore the default
upstream registry entry (mock npm registry via SOCKET_NPM_REGISTRY), a
refused pin (offline, registry 404, missing integrity) fails closed with
the git-checkout remedy, and a pre-v5 ledger is never replayed but
retired once no pin remains. Ledger-persist failure tests become
restored-lockfile write failure tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite hosted rollback/remove/vex-step tests to the ledger-free v5 contract

- in_process_rollback_hosted (+ vlt): hosted pins come from the lockfiles
  and are restored from a mock npm registry; per-pin refusal, scoped
  restores, legacy-ledger retirement (never replayed), preserve-state,
  and the vlt heal following restored pins. A vlt project re-locked onto
  the registry has no hosted state left to roll back.
- coverage_fix_rollback_ecosystem_scoped_replay -> ..._scoped_hosted:
  --ecosystems never restores another ecosystem's pin nor retires the
  pre-v5 ledger while a pin remains.
- e2e_golang_hosted_state: rollback and vendor takeover restore go.sum
  from a mock checksum database; offline refuses.
- redirect_npm_allow_remote: no ledger records the .npmrc edit; restore
  deletes only a pristine scaffold .npmrc and reports a kept
  allow-remote=all line (npm_allow_remote_left).
- hosted_symlinked_files, repair_invariants: no ledger is written; a
  lockfile-pin-only project takes the redirect_only_project skip.
- vex_pipenv_pip_steps: a reverted checkout with no ledger is the plain
  manifest_not_found error; apply --vex fetches the record online when
  no ledger supplies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite hosted scan/get integration tests for the ledger-free v5 contract

Hosted scan/get write no .socket/vendor/redirect-state.json: assertions on
the ledger become "no ledger" plus lockfile facts; pre-v5 (incl. corrupt)
ledgers are pinned as ignored and left byte-identical; ledger-write failure
tests become "a read-only .socket/vendor or a squatting dir no longer
blocks the run"; re-runs are pinned idempotent on the lock bytes.

Rollback round trips now name the mock host with --patch-server-url and
mock the upstream registry (SOCKET_NPM_REGISTRY / SOCKET_PYPI_JSON_API);
berry CRLF/BOM, bun digestless, pnpm, poetry, pdm and pipenv restores are
checked against the pristine locks. bun.lockb rollback pins the refusal.
Manifest-less VEX legs attest from lock + API, and use a synthesized
pre-v5 ledger as the extra local record source for the offline and
redirect_unwired legs. scan redirectState/hosted_wiring_retained/updates
are pinned to lockfile pins; vendor_supersedes_redirect is gone.

vlt_hosted_common gains assert_no_ledger, legacy_record_from_view and
write_legacy_ledger (additive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Tidy hosted remove/rollback messages and legacy-ledger residue

- remove's hosted_revert_failed message is the restore's own refusal (it
  already names the pin and the remedy) instead of wrapping it twice.
- Retiring a pre-v5 ledger prunes the emptied .socket/vendor/.
- The in-run hosted VEX summary says patches are attested from their patch
  records, not from a ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite list and vex ledger tests for lockfile-derived hosted state

list: hosted entries are lockfile pins (details.mode hosted,
details.lockfiles, no details.ledger); a pre-v5 ledger only details a
matching pin and never lists a record by itself. vex: a malformed pre-v5
redirect ledger is the redirect_ledger_corrupt warning, the run proceeds
and the file is left byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite manifest-less VEX suites for the ledger-free hosted mode

v5 hosted mode writes no .socket/vendor/redirect-state.json, so the shared
harnesses (vex_pdm_hatch_common, vex_pipenv_pip_common, vex_e2e_common/bun
and vlt, npm_e2e_common/manifestless) now assert the hosted wiring run left
NO ledger, while vendored keeps its .socket/vendor/state.json cells.
Hosted cells that relied on the ledger now pin the new contract: offline
with no local record is record_unavailable, online attests from the API,
and a reverted hosted lock leaves nothing to discover. vendor over hosted
pins is the eject flow, so the manifest-less embedded cells drive apply
for hosted checkouts and a new cell pins the eject path. One focused cell
shows a committed pre-v5 ledger still lets a hosted pin attest offline
(new additive helpers: write_legacy_redirect_ledger,
assert_no_hosted_ledger, LEGACY_REDIRECT_LEDGER).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Teach the real-uv VEX matrix the ledger-free hosted mode

A hosted uv flow writes no ledger: assert that, run the embedded steps
online (no local record), expect nothing discovered once the wiring is
reverted, and take a hosted production leg's record from the public
patch API instead of the removed ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Run the real-uv hosted revert against the v5 rollback contract

A hosted uv flow commits no .socket/, so copy_tree tolerates a missing
source; rollback needs --patch-server-url for a pin on the mock origin,
and restores each pin to its upstream registry entry or refuses it with
the version-control hint (asserting nothing was written) instead of
replaying ledger bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Delete the hosted ledger replay engine; keep a read-only legacy loader

v5 derives hosted state from lockfile pins and restores upstream entries
by re-resolving them from the registry, so nothing reverts recorded ledger
fragments any more. Remove the replay machinery and the writers behind it:

- patch/redirect/replay.rs and takeover.rs (fragment replay, per-purl
  npm/cargo/golang revert, redirect_revert_supported)
- state.rs persist_redirect_state, drop_superseded_purl, quarantine and the
  unclassified-edit guards; load_redirect_state stays for migration reads
  and save_redirect_state stays doc(hidden) for laying down pre-v5 fixtures
- npmrc unwind planners, bun.lockb snapshot restore, pipenv/vlt/bun text
  inverses and the EOL fragment respelling that only replay used
- tests that only exercised replay; rewrite round-trips in the poetry/uv
  suites keep their forward and idempotency assertions (the upstream
  restore of those formats is covered by upstream_restore_golden)

hosted_url_names/hosted_url_version move to a small hosted_url module
shared by the bun rewriter and VEX discovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Document the ledger-free hosted contract and hosted eject

Update CLI_CONTRACT.md, README.md, CHANGELOG.md [Unreleased] and the
testing/ecosystem docs to v5's WS1/WS2 behaviour: hosted mode writes no
redirect ledger; rollback/remove restore hosted pins to their upstream
registry entries (per-format coverage, refusals incl. --offline and
bun.lockb, the git checkout remedy); list/vex/scan/repair derive hosted
state from the lockfiles (details.lockfiles, the new redirectState shape,
redirect_ledger_corrupt as a warning); vendor ejects a hosted project and
vendor --revert returns to upstream; vendor_supersedes_redirect and
hosted_revert_unsupported are gone; the pre-v5 ledger is read for
migration only and retired by rollback; new registry env overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite vendor takeover covgap tests to lockfile hosted pins; add WS2 eject tests

covgap_commands_vendor: a pre-v5 redirect ledger (even malformed) is now
ignored; the takeover guard is driven by hosted pins in package-lock.json
on a --patch-server-url origin (dry-run advisory against a mock registry,
wet takeover + revert back to the upstream registry entry, offline refusal
redirect_revert_failed, unconfigured origin is not hosted). The
redirect-ledger write-failure test has no subject anymore and is removed.

vendor_eject (new): standalone vendor in a hosted npm project ejects the
pins into .socket/vendor/ (no ledger, no manifest), vendor --revert returns
to upstream, a failed view fetch is patch_fetch_failed/exit 1, and no pins
keeps the no-manifest no-op.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite in_process_vendor hosted takeovers to the v5 upstream restore

The berry CRLF takeover and the pnpm/package-lock hosted->vendored
conversions no longer see a redirect ledger: vendor runs online against a
mock npm registry (SOCKET_NPM_REGISTRY) with the patch-server origin
configured, restores the upstream entry, and vendor --revert / rollback
land on the upstream registry entry. Adds an offline-refusal test
(redirect_revert_failed with the checkout remedy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* mode_migration_npm: hosted takeovers restore the upstream registry entry

Hosted mode writes no ledger; vendor over a hosted yarn pin now runs
online with --patch-server-url and restores the upstream entry first
(classic legs read a registry document mirrored from the pristine lock via
SOCKET_NPM_REGISTRY, so the unwind is hermetic). The reverse classic leg
replaces the ledger-originals check with a rollback back to the pristine
registry lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* mode_migration_cargo: hosted takeovers restore the crates.io entry

Vendor over a hosted cargo pin runs online with --patch-server-url and a
sparse-index mirror (SOCKET_CRATES_INDEX) of the pristine checksum; the
ledger-deletion fail-closed test becomes an offline-refusal test
(redirect_revert_failed + checkout remedy) keeping the half-reverted
hosted_redirect_live backstop, which now names rollback / git checkout.
The hosted leg of the manifest-less VEX matrix fetches its record from the
API (no hosted ledger).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* mode_migration_bun: no hosted ledger; unwinds restore the upstream 4-tuple

Every run carries SOCKET_PATCH_SERVER_URL (the mock patch origin) and
SOCKET_NPM_REGISTRY (a mirror of the pristine lock's integrities), so
vendor takeovers, rollback and remove restore the registry 4-tuple; the
ledger record/edit assertions become no-ledger assertions. VEX over a
stale manifest may name the superseded uuid only in vex_record_superseded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* in_process_vendor_bun_takeover: hosted pins without a ledger

bun: every run names http://patch.test the patch server and points
SOCKET_NPM_REGISTRY at one shared registry mirror of the pristine
integrities; hand-written hosted fixtures are the lock's URL 3-tuples
only, and the ledger record/edit assertions become no-ledger assertions
(the unwinds restore the registry 4-tuple byte-exactly).

vlt: scan/vendor/get/rollback over a hosted pin run online against the
mock origin and a registry mirror; the fixture lock records
options.registries so the upstream restore re-derives slot [3] exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_bun_lockb: binary hosted pins are refused by rollback and vendor takeover

v5 restores a hosted pin's upstream entry instead of replaying a ledger,
which a binary bun.lockb cannot get: vendor over the hosted pin (dry and
wet) and rollback of it now refuse with the git-checkout remedy and write
nothing; the tests apply that remedy and continue the round trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_bun_build: no hosted ledger; rollback restores the upstream 4-tuple

The hosted run writes only bun.lock (asserted: no ledger). Rollback and
its dry run carry the mock origin as the patch server and a registry
mirror of the pristine integrity, so the upstream restore lands on the
pre-redirect lock byte for byte; the repeat-run heal is checked in the
lock alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_yarn_classic_build: hosted run writes only yarn.lock

Assert no redirect ledger is written, and carry .socket/ into the fresh
checkout only when it exists (v5 hosted mode writes nothing there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_gem_stale_install: no ledger fallback in hosted mode

The re-fire-from-ledger test becomes a re-scan with a failing record fetch:
record_fetch_failed with the v5 VEX-omission detail, exit 0, wiring
byte-identical, no ledger. Manifest-less VEX drops the ledger-kept cells:
a stale unconverged pair and a reverted pair attest nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_pnpm_build: no hosted ledger; rollback restores the resolution

The hosted run and its idempotent re-run write no ledger (the trust
setting and lock splice are checked on disk). The v5/v6 synthetic legs
replace the ledger-original checks with a rollback that re-resolves the
upstream integrity from a registry mirror and lands on the pristine lock
byte for byte; the pinned matrix rollback runs online the same way, and
its vex checks fetch the record from the API (no ledger to read offline).
.socket/ is copied into fresh checkouts only when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_cargo_shapes: remove restores crates.io entries without a ledger

The post-install vex fetches records from the API (no hosted ledger), and
remove runs with a sparse-index mirror of the pristine checksums and the
mock origin named the patch server, restoring every shape byte for byte.
One inherent v5 difference is pinned: a .cargo config that lacked a final
newline gets it back, since without a ledger fragment the rewriter's
separator is indistinguishable from a terminated file's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e_redirect_cargo_build: the three-file rewrite is the whole hosted state

Assert no ledger after scan/get --mode hosted; the post-install vex and
the manifest-less matrix fetch the record from the API (the ledger-kept
cells go), and reverted locks attest nothing. .socket/ travels into the
fresh checkout only when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* golang hosted e2e: go.mod/go.sum is the whole hosted state

get --mode hosted in a module and in a go.work root now asserts no
redirect ledger is written; the manifest-less VEX tail already covers the
ledger-less shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Format the rewritten vendor/hosted test files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Pin that a transactional eject emits no per-purl takeover warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Document the transactional eject, offline refusal and contested wiring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Drop a stale comment on the hosted unwind error

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Prove eject from a fresh hosted checkout; verify hash-pinned requirements

The eject restores every hosted pin's upstream registry entry before the
vendor engine takes its source inventory, so a fresh checkout (no
node_modules, empty CARGO_HOME, no virtualenv) fetches the pristine
source from the registry and verifies it against the restored checksum.
New subprocess tests pin that for npm, cargo and pypi.

The pypi case exposed that requirements.txt `==` pins never carried their
`--hash=sha256:` digests into the lock inventory, so a hash-pinned pin
with no installed copy was unverifiable. Carry them as the entry's
integrity (any-of, like Pipfile.lock) while the file resolves from the
public index; an index option keeps every pin unverifiable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Give the stale-Pipfile rollback test a derivable hash mode

A requirements.txt whose every line is a hosted pin is refused by the
upstream restore (hash-checking mode is not derivable). Add an unhashed,
unpatched sibling so the test still pins a successful restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* node addon smoke: a wet hosted session writes no ledger

The in-memory hosted engine emits only lockfile/config edits in v5, so
assert no .socket/ output instead of requiring the redirect ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* covgap scan_hosted: unreadable-workspace case asserts no ledger

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Rewrite the CI-only e2e suites and backtests to the ledger-free hosted contract

v5 hosted mode writes no .socket/vendor/redirect-state.json: every
#[ignore]d real-toolchain capstone and every compatibility backtest that
read the redirect ledger now asserts it is absent and reads the facts
from the lockfile (or the public API record) instead.

- npm / yarn berry / composer / gem / maven / nuget / rush / poetry /
  pdm / hatch / pypi-real / yarn-classic matrices: no-ledger assertions;
  vex offline with no local record is record_unavailable; a reverted
  hosted lock names the patch nowhere (manifest_not_found, exit 2);
  a manifest-less `vendor --vex` is no longer run over hosted pins
  (it ejects them now).
- Mock-origin hosted pins pass --patch-server-url to vex / rollback /
  remove / vendor / scan --mode vendored; the vlt harness gains
  rollback_upstream / Fixture::rollback with SOCKET_NPM_REGISTRY pointed
  at the harness registry.
- Rollback legs expect the upstream registry entry back: npm (real
  registry, JSON-equal lock, .npmrc removed), composer 2 (byte-identical
  from packagist; composer 1's inline repository refuses with the
  git checkout remedy), maven (--offline, byte-identical pom), poetry
  (real PyPI), vlt (byte-exact per era); the production npm leg gains a
  real-registry rollback on a copy.
- vlt: URL-less / update-dropped pins now find no state ("Manifest not
  found"); the TS-written lock restores to its input and the pre-v5
  ledger is retired; mode migrations restore upstream before vendoring.
- Backtests (bun, pdm, pipenv, poetry, vlt, uv docs): hosted records come
  from the public /patch/view/<uuid>; noLedger checks are unconditional
  for hosted; rollback checks expect the upstream entry (bun custom
  registry slot comes back as "", bun.lockb refuses with the remedy);
  the vlt downgrade leg asserts the pin with no ledger.

Core fix: the vlt upstream restore added a slot [3] tarball URL to a
single-node lock whose options record `registry` (vlt rc.33 .. 1.0.4
with config.registry), which vlt itself never writes (save.ts omits the
resolved URL when it starts with the configured registry); rollback was
not byte-exact on those eras. The no-siblings fallback now honours that
rule (unit-tested).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Hold the vlt configured-registry slot [3] rule pending real-vlt evidence

The rule in 44240a2 drops slot [3] when a lock records options.registry,
matching vlt rc.33..1.0.4, but it breaks the vlt-lock-v1-both-registry-keys
golden, and newer vlt releases have not been checked yet. Restore the
previous behaviour until real captures decide it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Backtests: send a User-Agent on public patch-view fetches

The hosted backtest legs now read the patch record from the public proxy's
/patch/view/<uuid>, and CI got HTTP 403 for Python's default urllib
User-Agent (the poetry 1.4-2.x native legs). Send an explicit agent, as the
vlt backtest's api_get already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Re-land the vlt configured-registry slot [3] rule on real-vlt evidence

vlt's lockfile save (identical in 1.0.0-rc.33, 1.0.4, 1.0.10 and 1.2.0)
writes a default-registry node's resolved URL (slot [3]) only when no
`registry` is configured or the URL does not start with it, and records
that `registry` in `options.registry`. Real installs of left-pad@1.3.0
with `config.registry` + `registries.npm` set to the default registry
produce a byte-identical 3-tuple lock on all four releases.

- upstream::vlt: the no-siblings fallback of `records_url` no longer
  adds slot [3] under a recorded `options.registry` the node resolves
  under (unit test covers both sides).
- fixtures: `lock-v1-both-registry-keys` was hand-written with slot [3];
  its input (and expected-edits original) now match the real lock byte
  for byte, and the real capture is added as
  `capture-1.2.0-config-registry` (vex-discover golden extended).
- docs/testing/vlt-compatibility.md records the rule and the evidence.

Without the rule the real-vlt legs that restore a single-node lock of a
`config.registry` project (the harness configures it for rc.33 .. 1.0.4)
are not byte-exact: hosted idempotence / crlf_lock and the migration
scoped_unwind / rollback_from_mixed / agent_rollback_after_takeovers
legs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* hosted-e2e uv leg: serve the record of the uuid the lock pins

pypi_uv_lock_hosted_install_proof handed all three PYPI_UUIDS to
uv_vex::production_manifestless. With no local record (v5 hosted keeps
no ledger) production_record takes the FIRST of those the public proxy
answers for, so the VEX stand-in served de58c8b8 while the resolver had
granted (and uv.lock pinned) e828efa5: every manifest-less cell then
asked the stand-in for e828efa5 and got `vex_record_not_found` /
`record_unavailable`. Pass exactly the wired uuid instead; if production
ever grants a uuid its public view will not serve, production_record now
fails naming it rather than masking it behind another patch's record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* PDM static_urls restore writes files in URL order; bun backtest User-Agent on artifact fetches

PDM orders a static_urls entry's files by URL (the sdist under 0c/39
precedes the wheel under b0/53 for urllib3 1.26.18); the upstream restore
wrote them in filename order, so hosted rollback wasn't byte-exact. The
golden now uses real bucketed URLs so the order differs from filename order.

The bun backtest's lockb digest and asset downloads now send the same
User-Agent as the patch-view fetch (patch.socket.dev 403s urllib's default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* covgap rollback: macOS-only blob-pin check derives hashes from the fixture bytes

The fixture no longer carries before_hash/after_hash; the macos-gated
manifest-write-failure test still read them and broke the macOS build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Round-trip hosted rollback of unpopulated Poetry 1.0/1.1 locks

Poetry 1.0/1.1 record `[metadata.files] <name> = []` against today's PyPI
JSON API, while locks written earlier list every release file. The hosted
rewrite replaced either with the one-entry patched array, so rollback could
not tell them apart and always re-derived the full release list: the
backtest's `direct` and `crlf` shapes (literal `urllib3 = []`) failed
rollbackRestoresLockBytes on Poetry 1.0.10 and 1.1.15.

The rewriter now keeps that bit in the patched entry's layout: one file per
line (Poetry's own rendering) when the original listed files, inline when
it was `[]`; a re-run keeps the layout it finds. The restore reads it back
and writes the full release list or `[]`.

Adds a golden round-trip over every native fixture generation (1.0.10 to
2.4.3), LF and CRLF, alongside the existing populated-shape one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* yarn berry e2e: manifest-less VEX matrix expects no hosted ledger

The berry matrix still asserted the pre-v5 .socket/vendor redirect ledger
after a hosted flow. v5 hosted mode writes none: the offline cell now
expects record_unavailable, a reverted hosted checkout discovers nothing
(exit 2 manifest_not_found), and manifest-less apply --vex is a calm
no-op. The yarn4 pnpm-linker and workspaces fresh checkouts copy .socket/
only when it exists, as the node-modules berry test already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* e2e vex poetry: accept the one-file-per-line metadata.files pin

A populated lock-1.0/1.1 [metadata.files] entry now keeps Poetry's
multi-line layout after the hosted rewrite (so rollback can tell it from
an originally empty one); the pin-spelling matrix strips that form too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Keep Pipenv's index on hosted entries so rollback restores it exactly

The hosted Pipfile.lock rewrite dropped each entry's `index`, and the
ledger-free upstream restore then guessed it from sibling registry
entries (none -> the PyPI source name; all siblings index-less -> none).
Pipenv's own choice cannot be re-derived from the lock or the Pipfile:
for the same Pipfile it depends on the release and the locking
environment (measured with real `pipenv lock`):

  shape            2018.11.26  2020-2022  2023.12.1-2026.8.0
  direct           pypi        pypi       pypi
  extras table     pypi        pypi       (none)
  marker-excluded  pypi        (none)     (none)
  transitive       (none)      (none)     (none)

So the backtest's rollbackRestoresLockBytes failed on 2022.12.19
extras (transitive pysocks sibling has no index -> index dropped) and
on 2022.12.19 / 2026.8.0 marker-excluded (no siblings -> "pypi"
added), and rollbackAfterRelockRetires failed on 2026.8.0
marker-excluded: the relock hybrid (our `file` kept, `version` and
registry `hashes` restored, no `index`) was restored with an `index`
Pipenv never wrote.

The hosted rewrite now keeps `index` exactly as Pipenv wrote it
(present or absent) and only drops `version`; the restore carries the
entry's `index` back unchanged instead of choosing one, refusing when
it (or the Pipfile's explicit index) does not name a PyPI source in
`_meta.sources`. A `file` entry carrying `index` installs the
referenced wheel itself (direct_url.json present) on Pipenv
2018.11.26, 2020.11.15, 2021.11.23, 2022.12.19, 2023.12.1, 2024.4.1,
2025.1.3 and 2026.8.0 (`install --deploy`, `sync`, `verify`), and a
marker-excluded one stays uninstalled; Pipenv 7-11 ignore `index` on a
`path` entry (convert_deps_to_pip skips it for file/path deps).

Tests: real Pipenv 2018.11.26 / 2022.12.19 / 2026.8.0 locks for the
extras and marker-excluded shapes (tests/fixtures/pipenv-shapes) round
trip byte for byte in LF and CRLF, and the 2026.8.0 marker-excluded
relock hybrid restores the pristine bytes. The backtest's
allCategoriesRewritten now expects hosted entries to keep the pristine
`index` (vendored still drops it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Restore hosted bun.lockb pins natively for the vendor takeover

With the hosted ledger gone, `vendor` over a live hosted pin in a binary
bun.lockb (Bun 0.8.1-1.1.x's default lock, Bun 1.2's legacy lock) was
refused `redirect_revert_failed`: format_of() mapped bun.lockb to
Unsupported, so the backtest matrix's hosted-then-vendored cell exited
partial_failure.

The upstream restore gains a bun.lockb restorer
(patch/redirect/upstream/bun_lockb.rs). It rebuilds each hosted
remote-tarball record as Bun's npm registry record for name@version,
from the registry's dist.tarball / dist.integrity (SOCKET_NPM_REGISTRY
aware), via the new BunLockb::set_registry_package. That function
re-interns the URL (re-using the original pool offset), drops the
hosted URL string from the pool tail and re-derives the metadata hash.
The staged restore view now carries binary files.

To make the rebuild byte-exact, set_package keeps the registry record's
inactive bytes (padding, semver) when it writes a remote tarball
record. Early writers leave uninitialized padding there (Bun 0.8.1), so
this matters. A re-pin to a later grant's URL now drops the superseded
URL from the pool. A record without the retained bytes is rebuilt the
way Bun writes one; prerelease versions are refused in that case.

The rebuild is exact for every fixture writer except a format-1 lock
(kept promoted) and workspace locks (behaviors kept normalized). So
only the vendor takeover and eject opt in (RestoreOptions::bun_lockb):
their vendor ledger records the rebuilt record, and `vendor --revert`
returns the pre-hosted bytes. `rollback` / `remove` keep refusing a
hosted bun.lockb pin with the `git checkout -- bun.lockb` remedy, as
the harness's rollbackLockbRefused expects. An offline vendor still
refuses.

Tests: core restorer tests over every real fixture, codec tests for the
rebuild (retained and zeroed records, re-pin), a hermetic CLI test
(vendor_eject_bun_lockb.rs: takeover, eject, rollback and offline
refusals on Bun 0.8.1 / 1.1.38 / 1.2.0 locks), and the real-Bun
e2e_bun_lockb takeover now vendors online and reverts byte-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* bun backtest: inject the custom-registry slot with byte I/O

On Windows, Path.write_text turned the injected bun.lock's LF into CRLF, so
the LF pre-injection bytes could never match the (EOL-preserving) upstream
restore: custom-registry hosted rollbackOriginalFiles failed on Windows only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Demote hosted format-1 bun.lockb locks exactly on the vendor takeover

The bun backtest's binary job failed only for the format-1 writers (Bun
0.1.1 / 0.1.6, readers 0.5.9 and 1.4.2) after 65aa074: the hosted rewrite
promotes a format-1 lock to format 2 (format 1 has no URL column), and
the new native upstream restorer rebuilt the registry record inside that
promoted lock. The vendor ledger then recorded the promoted bytes as its
pre-vendor original, so `vendor --revert` returned a format-2 lock and
e2e_bun_lockb's "the revert restores the pre-hosted bytes exactly"
assertion failed. A promoted lock is byte-for-byte indistinguishable from
one Bun 0.1.7+ wrote (same pool order, same metadata hash), and hosted
mode keeps no ledger, so the restorer could not tell.

The codec now marks a lock whenever an edit had to normalize it: seven
magic bytes and a flag byte in the last eight bytes of the root package's
resolution (the root resolution's value union, which no Bun reader reads;
early writers leave uninitialized bytes there, and 1.4.2 / 0.5.9 read such
locks). For a promoted lock those bytes are new, so the mark overwrites
nothing. promote_legacy_format sets NORMALIZED_FORMAT_1;
normalize_workspace_behaviors sets NORMALIZED_WORKSPACE when it changes a
dependency behavior or workspace literal. The vendor ledger's
layout_original path normalizes the original the same way, so its exact
revert is unchanged.

The bun.lockb upstream restorer then:
- demotes a NORMALIZED_FORMAT_1 lock back to format 1 once every hosted
  record is rebuilt (BunLockb::demote_legacy_format, which drops the URL
  column and the URLs the promotion appended to the pool, and succeeds
  only if promoting the result again reproduces the lock byte for byte).
  Otherwise the pins are refused with the `git checkout -- bun.lockb`
  remedy.
- refuses a NORMALIZED_WORKSPACE lock outright with that remedy, since
  clearing the workspace behavior bit cannot be undone. It no longer
  takes the lock over non-exactly.

Tests: the upstream restorer's byte-exact test now covers 0.1.1 / 0.1.6.
Workspace-normalized extension locks refuse, and so does a lock whose mark
carries an unknown flag. The codec rebuild test checks the exact
demotion. vendor_eject_bun_lockb adds the 0.1.1 / 0.1.6 takeover with an
exact revert and a workspace-lock refusal. Locally with real Bun, the
1.4.2 reader x 0.1.1 / 0.1.6 writer cells now pass the takeover and the
exact revert. They then stop at the 0.5.9 legacy reader, which segfaults
on any networked install in this sandbox. The 1.1.45 / 1.2.0 / 1.4.2
cells pass everything except the `extensions` shape, which needs
api.github.com (403 here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* ci: give the Windows test leg a 50-minute budget

The Windows leg runs the same suite ~1.6x slower than macOS. On the base
branch it already took 34m40s of the flat 35-minute budget, and with this
PR's added tests it was cancelled at the limit mid-run (no failures).
Linux and macOS keep 35 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

* Check out the Poetry and Pipenv lock fixtures byte-exact on Windows

The Windows test leg's CRLF checkout (core.autocrlf) turned the LF-committed
tests/fixtures/poetry, pipenv and pipenv-shapes locks into CRLF, so the
byte-exact upstream-restore round trips (and their derived CRLF variants,
which became \r\r\n) and the Poetry VEX pin-spelling test failed on
Windows only. Mark them -text like the other captured-lock fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…'s ledger rebuild (#283)

* Consolidate vendored apply/revert/repair into one VendoredBackend

vendor, scan/get --mode vendored, vendor --revert, rollback's vendored
leg, remove and repair now go through one VendoredBackend { apply,
revert, repair } over the shared engine (vendor_records_reusing,
dispatch_revert_one_opts). The boxed_* scan shims collapse into one
boxed_vendor_step; the engine future stays boxed inside apply for the
Windows 1 MiB main-thread stack.

repair no longer re-synthesizes vendor ledger entries from lockfiles. A
lockfile reference with no ledger entry fails with vendor_ledger_missing
(artifact-level event: uuid + details.{ecosystem,path}); the remedy is
restoring state.json from version control. Missing or corrupt artifacts
are re-vendored through the same engine as vendor, so the patch
service's prebuilt artifact is downloaded first under --vendor-source
auto, with the local build as the fallback. The fingerprint post-verify,
set-aside of corrupt bytes and carried-inventory refresh are kept.

Removed with the rebuild: repair_vendor.rs, gem Gemfile wiring
reconstruction, and registry_fetch::fetch_npm_unverified. The packing
code (npm_pack, pypi_wheel, berry_zip, registry_fetch, prestage) stays:
depscan does not call it (verified against depscan master 784013d6), but
it is the CLI's own --vendor-source build/auto fallback.

Tests for the reconstruction path are replaced by vendor_ledger_missing
pins per flavor; CLI_CONTRACT, README, CHANGELOG and the v5 plan are
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Point the vlt coverage map at the renamed repair test

vlt-coverage.json still named vlt_repair_reconstructs_the_ledger_from_the_lock,
and vendor_vlt_lock_out_of_sync lost the only assertion the coverage
check could see when the lock-only reference test switched to
vendor_ledger_missing. vendor_vlt_out_of_sync now asserts the refusal
detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Drop the ledger-less mirror leg from the bun binary workspace test

native_binary_alias_and_transitive still expected repair to rebuild a
workspace mirror after deleting state.json. Repair now reports that as
vendor_ledger_missing (pinned in native_binary_hosted_vendored_takeover_roundtrip),
so the leg is gone; the missing/corrupt mirror legs keep running and
assert the ledger stays byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Pass --mode agent to the gated agent-mode get fixtures

get defaults to hosted since 5e5f5ed, which moved the agent-mode
fixtures to --mode agent but missed the #[ignore]d real-toolchain
suites (e2e_vlt, e2e_npm, e2e_pypi, e2e_gem, e2e_safety_pnpm). Their
plain `get <uuid>` now redirects instead of applying in place, so e.g.
vlt_pinned_matrix_agent_get_and_remove saw the copy Absent. This PR
touches the vlt-compatibility path filter, which surfaced it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Keep an unpersistable inventory refresh loud, drop reconstruction persist tests

A carried-inventory refresh whose ledger write fails now reports
vendor_inventory_refreshed next to vendor_state_write_failed and keeps
the member-verified rebuild on disk, instead of falling through to
vendor_artifact_rebuild_failed and removing it. The persist-failure
tests for the removed backfill / anchored / soft reconstruction paths
go with them; they only run as non-root, which is why the root sandbox
skipped them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Restore the macOS-only PatchedFixture hash fields

The dead-field clippy fix removed before_hash/after_hash, but the macOS
immutable-flag rollback tests read them, so test (macos-latest) no
longer compiled. Keep the fields and allow dead_code off macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Keep repair on the recorded artifact identity

Review of #283 found two regressions in the shared-engine repair:

- A corrupt artifact was moved aside before staging, so its
  afterHash-verified members were no longer harvested: an offline repair
  that the previous implementation completed failed with "no local
  source". The members are now harvested first and passed as the seed.
- The post-verify reloaded the ledger the re-vendor had just written, so a
  service archive with different bytes (same members, new gzip mtime) was
  committed as `rebuilt` with a rewired lock and new fingerprint. Repair
  now verifies against the original entry; when the result is not the
  recorded artifact, that candidate's wiring files and ledger entry are
  put back from a pre-run snapshot, and a service copy falls back to a
  build-only rebuild. Legitimate backend migrations of a verified rebuild
  (the cargo version retag) are kept.

Both reproductions are pinned in repair_vendor_e2e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

* Make repair's wiring undo FIFO-safe, atomic and symlink-preserving

Bugbot on #283: the identity-undo snapshot read lockfiles with bare
tokio::fs::read (a FIFO at a wiring path blocks open(2)) and skipped
symlinked lockfiles, and the put-back wrote them in place (no
stage+fsync+rename, mode bits dropped).

The snapshot now reads through read_regular_to_bytes and records a
symlinked lockfile's link text; the undo re-links a link that the
engine's rename replaced, then writes the target through
atomic_write_bytes_preserving_mode. Pinned by
repair_identity_undo_follows_a_symlinked_lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMhWChtaNX5FJYNDq3NBJa

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Landing: #283 (WS5 VendoredBackend) landed on release/v5-prerelease as 06437d2; next up #281 (needs re-merge of the base). #288 fast lane still draft.


Generated by Claude Code

/// prints even under `--silent` (`json` mutes it: the envelope carries it).
fn fail(env: &mut Envelope, json: bool, purl: &str, code: &str, detail: String) {
if !json {
eprintln!("{}", format_repair_failure(purl, &detail));
// uuid and the referenced path instead.
let detail = format_ledger_missing(eco, uuid);
if !common.json {
eprintln!("Error: Cannot repair vendored artifact {path}: {detail}");
* Cancel only superseded PR runs in CI

A CI or compatibility run is now cancelled only when a newer push to
the same pull request replaces it. Push, dispatch and scheduled runs
always finish, so a manually dispatched run on the v5 base branch (its
only CI verdict, since push CI runs on main alone) is no longer killed
by a later dispatch or by the non-main cancel rule, and main keeps
finishing its rust-cache saves.

CI now groups PR runs by PR number, like the compatibility workflows
already do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Expect native path separators in scan headers

scan_hosted_paths_run_once_per_project_directory compared the
per-directory `== apps/a ==` header against a literal forward-slash
path, but scan prints the directory glob matched, which Windows
spells `apps\a`. The Windows test leg failed on this since 62f07c7,
and because every e2e job waits on `test`, the whole e2e tier was
skipped on v5 PRs. Build the expected header from path components.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Run the vlt agent get test in agent mode

get defaults to hosted mode since 5e5f5ed, so the real-vlt
get_and_remove leg ran a hosted get and found the installed copy
unpatched (Absent, expected Patched). Pass --mode agent, as the other
agent-mode fixtures already do. Same change as cc5f1b6 on #285; it
blocked the e2e tier's e2e_vlt jobs now that they run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Run the pnpm safety e2e gets in agent mode

get defaults to hosted mode since 5e5f5ed, so the three pnpm safety
tests ran a hosted redirect and found proj_a's installed copy
unpatched and no pnpm-layout note. They test the in-place apply
path, so pass --mode agent. These e2e-tier tests had not run since
that change because a red base test skipped the tier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

* Let the NuGet hosted e2e run without .socket/

v5 hosted mode writes no `.socket/` directory (the lock pins are the
whole hosted state), so the hosted leg's fresh_checkout panicked with
NotFound copying a tree that no longer exists. Copy it only when the
run left one; the vendored leg still carries its ledger through.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp

---------

Co-authored-by: Claude <noreply@anthropic.com>
* Drop unused fields from the rollback covgap fixture

`cargo clippy --all-targets -- -D warnings` failed on the dead
before_hash/after_hash fields of PatchedFixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

* Add an opt-in NuGet fallback vendoring layout

Setting SOCKET_PATCH_NUGET_LAYOUT=fallback vendors a patched NuGet
package under a Socket-only version V' (the upstream version plus a 4th
part derived from the patch uuid). The package is committed already
extracted, as a NuGet fallback package folder under
.socket/vendor/nuget/<uuid>/. It is wired through a generated
socket-patch.targets file (imported via CustomAfterDirectoryBuildTargets
from a Directory.Build.props block) and byte-exact packages.lock.json
splices.

Because nothing else can produce V', the patched bytes never enter or
collide with the shared global packages folder, and nuget.config and
source mapping are left alone. Restore-time and build-time guards fail
closed on a tampered, incomplete or missing seed, or an unpatched
resolution (SOCKETPATCH001/002/005/007).

Supported shapes: SDK-style solutions with per-project lock files under
--locked-mode, and Central Package Management with or without transitive
pinning. Other shapes are refused with explicit codes.

The default layout is unchanged. A repo is opted in only by the env var
or an existing nuget-fallback ledger entry.

Tests:
- unit tests;
- lock goldens captured from real dotnet;
- an ignored real-SDK e2e (sln_locked, cpm_locked, cpm_pinning,
  sln_patch_update).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

* Add the NuGet vendoring design

docs/design/nuget-vendoring.md covers:
- the verified NuGet facts behind the design;
- the four candidate designs and the judge scores;
- the decision (unique-version fallback seed);
- the mechanism, supported shapes, failure modes, signing policy and
  migration;
- the server/CLI split, the test plan and the adversarial review
  dispositions;
- where the prototype overrides the design.

The raw research and review notes are committed alongside in
nuget-vendoring-research/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

* Run the vlt agent get test in agent mode

get now defaults to hosted mode (5e5f5ed), so the real-vlt
get_and_remove leg ran a hosted get and found the installed copy
unpatched. It now passes --mode agent, like the other agent-mode
fixtures that commit updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

* Gate the rollback fixture's blob hashes to macOS

d9a3f53 removed PatchedFixture's before_hash/after_hash as dead code,
but a macOS-only test reads them, which broke the macOS build. Restore
the fields under #[cfg(target_os = "macos")], so they exist where they
are read and clippy stays clean elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

* Move the NuGet fallback prototype to a follow-up branch

Vendored NuGet is future work for v5, so the opt-in fallback layout
prototype (9 new modules, routing hooks and a real-dotnet e2e) moves
to v5/nuget-vendoring-prototype, unmerged. This branch keeps the
design doc, its research notes and the two test fixes. The doc now
points at the prototype branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A

---------

Co-authored-by: Claude <noreply@anthropic.com>
Keep the v5 waste review and the repacking-to-depscan design from
PR #286 as reference docs. #286 is being closed without merging, and
its findings now belong to follow-up PRs. Each doc starts with a line
that links the triage map on #286, which gives every finding's owner.

Co-authored-by: Claude <noreply@anthropic.com>
* Plan staged patch rollout for v5

Adds the design for rolling Socket patches out gradually: a
`patches:` block in socket.yml that narrows what scan may patch
(paths, ecosystems, packages, severity floor, on/off), and a
severity-ordered per-run cap on new patches so each scan lands the
next few most critical fixes.

The plan splits the work into two parallel items with a frozen
interface, lists every hard-coded filter and where it belongs, and
covers the depscan autopatch follow-up. configuration.md now records
that socket-patch reads socket.yml for selection policy only, with
the trust boundary unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Revise rollout plan after adversarial review

Three independent reviews (ambiguity, churn, trust boundary) found
gaps that would have let the two implementations disagree or let a
repo file widen or stall the rollout. The plan now:

- matches paths against marker files with the backend's top-down
  gitignore rules, so projectIgnorePaths means the same everywhere
- uses one data source for severity, supersession and ordering
- spends the budget only on patches the planning pass proves can
  land, and admits nothing new when a lookup failed
- uses the merged recorded view in every mode and engine
- has depscan read the policy from the base commit for PR jobs
- hardens file handling (regular files, aliases, size, encoding,
  trusted repo root) and reports what a policy hides

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Close interface gaps in the rollout plan

A final consistency pass found places where the two work items
would have produced incompatible code: base purls admitted in one
directory being charged again in the next, no defined hand-off of
the unfiltered offers from the severity filter to classification,
no shared repo-relative path helper, and override sources the JSON
must report but the interface could not carry. The shared contract
now defines each of these, and the parity tests match each engine's
budget scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Clarify who shapes scan's selection output

The plan said both that the selector returns the shared offers
struct (work item A) and that it returns admitted/deferred rows
(work item B). The selector now returns the offers, and B adds the
rollout stage after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Apply socket.yml policy in memory path selection

Bugbot on #290: the plan had the in-memory engine's path selector
apply only the built-in test/fixture ignores, because it runs before
socket.yml is read. A negation such as `!/e2e/tests/` could then
never bring those trees back in depscan, while it works on disk.

Selection is now two-phase: the caller fetches the root policy
file(s) first and passes their text to selectHostedScanPaths, which
applies the full path policy. A listed policy file that is not passed
fails closed. A new memory test covers the negation case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Discovery's sweep recognizes every uuid-shaped segment of a hosted URL,
so it also records the pin's grant token. HostedInventory counted that
token as contested wiring in two ledger-free shapes:

- uv: the paired pyproject.toml repeats the uv.lock pin's URL in
  [tool.uv.sources] (the transitive override case), but pyproject.toml
  is not a pin file.
- vlt: a vlt-lock.json whose pins are refs but withheld from the lock
  basis (a lock some vlt release discards) is a flagged pin file.

Unscoped rollback then failed with hosted_wiring_contested after it had
already restored the pin. The token of an attributed pin's own URL is
now excused in a file that also names that pin's patch uuid. Any other
unpinned uuid, a half-reverted pair and a contradicted lock are still
contested.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Drop never-read fields from the rollback covgap fixture

clippy -D warnings rejects the dead before_hash/after_hash fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Read pnpm-lock.yaml through one format model

formats::pnpm owns the pnpm lock grammar in every generation (shrinkwrap,
5.x block, 5.4/6.0/9.0 flow, Rush nested locks): PnpmLock::parse once, then
entries(), resolves(), wired_refs(), wired_integrity(), vendored_in_use(),
plan_hosted() and the restore_upstream() hook. The redirect rewriter's pnpm
leg, the lock inventory, lockfile discovery, repair's integrity anchor and
flavor sniff, the vendored v9/legacy planners and get's pnpm-PnP probe all
read through it instead of their own walkers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Derive every wiring-file list from formats::registry()

The hosted candidate list, repair's vendored-reference search space,
lockfile discovery's vendored-liveness probe, the in-memory engine's root
markers and file ecosystems, the npm flavor probe guard and pnpm detection
now filter one table instead of keeping five parallel lists. The hosted
candidate order is pinned by value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Read Cargo.lock through one format model

formats::cargo owns the lock read model (LockedPackage, v1 [metadata]
checksums, [[patch.unused]], dependency references) behind CargoLock:
entries() for the inventory, packages() for lockfile discovery and the
vendor probes, dependents() for the hosted planner's unpinnable-dependents
refusal (which re-parsed the lock with its own walker), vendored_in_use()
for the vendored-copy claim, and the restore_upstream() hook.

The hosted planner's Cargo.lock splice moves to formats::cargo::hosted with
the line-grammar probes the rewriter reads with (is_locked,
locked_versions), replacing three copies of the header probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Read composer.lock through one format model

formats::composer owns the entry walk (ComposerLockPackage, now with its
ownership gate wired_to) behind ComposerLock: entries() for the inventory
and packages() for lockfile discovery and the vendored backend. The
vendored backend's entry_is_wired and repair's recorded-fragment reader,
which read dist fields straight off the JSON, go through the entry model.
The hosted planner's byte scanner moves verbatim to
formats::composer::hosted; its equivalence oracle stays green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Read Bundler locks through one format model

vendor::gemfile_lock becomes formats::gem: GemfileLock::parse plus
entries() for the inventory (moved out of lock_inventory::gem, which keeps
only its view I/O and ledger recovery's remote set), the restore_upstream()
hook, and gem_download_url. The hosted planner's lock-source convergence
moves verbatim to formats::gem::hosted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Keep the rollback fixture's hashes for the macOS-only test

The blob-retention test that reads before_hash/after_hash is
#[cfg(target_os = "macos")]; allow the fields as dead elsewhere instead of
dropping them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Route the yarn grammar split and bun.lock prelude through formats

formats::yarn owns the one yarn.lock grammar decision: sniff_grammar (the
head sniff the vendor flavor probe, the lock-inventory view and repair's
reference flavor each re-derived) and is_berry_lock (the whole-file check
the hosted rewriters and discovery share; the classic vendored backend's
refusal gate now uses it too, so a BOM'd berry lock no longer slips past
it). formats::bun::BunTextLock is the gate + split + packages parse five
bun.lock readers copied; each keeps its own refusal wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Read repair's yarn/bun trust anchor through the entry models

wired_vendor_integrity scanned yarn.lock and bun.lock with a six-line
forward window from any line naming the artifact. It now reads the entry
models lockfile discovery uses: live classic blocks' integrity, berry
checksum (yarn 4.0.x bare hex promoted under cacheKey 10c0), and bun's
tarball tuple. That fixes a berry block whose carried dependencies pushed
checksum out of the window (no anchor), a bare-hex checksum (no anchor), a
shadowed classic block being read, and bun's digest-less re-save borrowing
the next package's sha512 (a wrong anchor). Entries that disagree yield no
anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Pin gem CHECKSUMS through the shared entry grammar

The hosted writer found the dep's CHECKSUMS row with a regex that only
matched a lowercase terminal sha256, while the discovery reader accepts
uppercase digests, extra digest tokens and bare entries. Any of those fell
through to the insert branch and added a second, conflicting row (which
the reader then treats as no pin). The writer now locates the entry with
formats::gem's split_checksum_entry inside the CHECKSUMS section and
replaces that row in place, keeping its line ending and recording the old
row verbatim for revert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Answer pnpm in-use from the model's parse; drop the placeholder hook

PnpmLock computes its vendored-uuid set at parse time with one walk over
the packages/snapshots block keys (the vendored planners' key grammar,
each line's \r dropped). pnpm_entry_in_use memoizes the same set per lock
bytes instead of keeping LockIndex's copy and the LF-only
vendored_in_use_lines scan. A CRLF lock now answers like its LF twin (in
use) instead of undeterminable; the unwired-revert guard still refuses.

formats::LockModel with its default-unsupported restore_upstream() is
removed: hosted upstream restoration belongs to the ledger-free hosted
workstream and should land on these models, not beside them as a
placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Move the maven pom and nuget config readers into formats

vendor::maven_pom becomes formats::maven and nuget_config's routing reader
becomes formats::nuget; the NuGet config file names and the stat-only
same-file check stay in vendor::nuget_config with the callers' I/O.
Lockfile discovery, their only reader, imports the models directly; the
purity guards follow the files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

* Splice Cargo.lock at the spans of its one parse

CargoLock::parse now reads the lock with toml_edit's spanned Document and
records each [[package]]'s header, version/source/checksum value spans and
string values, every table header, the [root] strings and the [metadata]
lines. The hosted planner (CargoLock::plan_hosted) splices at those spans,
and the rewriter's is_locked / locked_versions probes answer from the same
parse, so the separate `[[package]]\nname = ...` text grammar, its regexes
and block walkers are gone.

The previous line-grammar planner is kept test-only as the oracle:
randomized v1 and v3/v4 locks (plus twins, [root], sourceless v1, bare
blocks, 1.2k-block locks) plan to identical bytes and FileEdits for every
package, a re-run, and a second package over the result. The one allowed
difference is a lock the old grammar could not read (a final block with no
newline after `version`), which the span planner now finds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BYsX2VfVNKHvfFAJnFryc

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #281 (WS3 lock models) landed on release/v5-prerelease as 73c0c4f; next in order is #279, which needs a re-merge of the base.


Generated by Claude Code

* Remove the setup subcommand and its install hooks (v5 WS7)

`socket-patch setup` (and --check/--remove/--exclude) is gone, with every
install hook it wired: npm postinstall/dependencies scripts, the
socket-patch[hook] .pth wheel, the in-tree Bundler plugin + Gemfile block,
and Composer post-install/update scripts. `apply` stays; agent mode in CI
is `scan --mode agent` once, then `socket-patch apply` after each install.

Deleted: commands/setup.rs, core setup/** and the setup-only package_json
helpers, the setup tests and setup-matrix suites, the setup-e2e feature,
the setup-matrix CI job, tests/setup_matrix and scripts/setup-matrix.sh.
vex's install-hook "Property 7" filter goes with it. The socket-patch-hook
wheel and socket-patch-bundler gem are dropped from the build and publish
workflows (sources kept, frozen, pending an owner decision).

Also the plan's small follow-ups: drop the core crate's deprecated
re-export aliases (and the CI grep that guarded them), the unused
utils::process::tool_command, the vacuous e2e_cargo/e2e_golang CI rows,
add the merged 01019627 and 9c2b4925 gem patches to the vendored
production e2e, and retire the backtest-poetry "known crawler gap" label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Streamline the patch UI (v5 WS8)

- `-h` lists about eight options per command (`cli_command()` marks the
  rest hide_short_help; `--help` is unchanged); `scan --apply/--vendor`
  are hidden (still accepted).
- Human warnings drop the `(code)` tag (`Warning: …`, `GC: skipped: …`);
  JSON keeps every code. Error lines keep theirs.
- Human text says "hosted", not "redirect" (JSON keys unchanged).
- npm's allow-remote notice is one line; `--verbose`/JSON keep the full
  policy text.
- One `ui::next_steps` renderer for hosted and vendored results.
- Hosted and vendored `get` never prompt: top-ranked patch per package,
  like scan, in JSON too. Agent-mode `get` keeps its picker and confirm.
- `list` with nothing to list says `No patches in this project. Run
  \`socket-patch scan\`.` (exit codes unchanged: 1 missing, 0 empty).
- One cancel line (`ui::CANCELLED`) and one paid upsell (`ui::PAID_UPGRADE`).
- `get`'s self-enforced flag conflicts and `rollback --one-off` exit 2,
  like every other usage error.

Docs: CLI_CONTRACT (human output conventions, exit codes, get prompts),
README, CHANGELOG [Unreleased], v5 plan status. Tests updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Pin the real-vlt get_and_remove leg to --mode agent

`get <uuid>` defaults to hosted since v5, so the leg's in-place
patched/pristine assertions need agent mode spelled out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Match the hosted-ledger persist-failure wording in two covgap tests

These chmod-guarded tests skip under root, so the WS8 wording change
("hosted redirect ledger" -> "hosted ledger") only showed up in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Empty list exits 0; group help by task; document the setup upgrade

Review follow-ups:
- `list` on a project with no manifest and no ledger record is an empty
  list: exit 0, the empty-project line (human) or the success envelope
  with `events: []` (`--json`). Only an unreadable or invalid manifest
  fails. Hosted mode writes no manifest, so this is the normal case.
- Root help groups the commands by task (patch, undo, ship, agent mode)
  instead of calling get/rollback/remove "older agent-mode commands";
  the subcommand list follows the same order. `-h` keeps --cwd,
  --ecosystems and --offline, and moves `scan --prune` to --help.
- README gains "Upgrading from `setup`": move to hosted or keep agent
  mode, and the exact hook to delete per ecosystem. The CHANGELOG and
  the frozen hook/plugin READMEs link it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Carry the hosted wording and code-free warnings onto #280's new tests and docs

#280 added tests and contract lines with the pre-WS8 human strings
("Would redirect", "<purl> redirected, but its patch record ...",
`Warning (<code>): ...`). Switch them to this branch's conventions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

* Give the cargo safety VEX baseline a vulnerability to attest

With setup's install-hook filter gone, the manifest-backed agent-mode
cargo patch attests, but the staged minimal manifest carries no
vulnerabilities, so vex ended no_applicable_patches (exit 1). Add one
vulnerability to the entry before the baseline run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #279 landed as f6bdad5 (CI reds all base-inherited: yarn-classic mode_migration_npm, vlt install-proof 0.0.0-*). Next: #282, which needs to merge the base again.


Generated by Claude Code

…#282)

* Share one hosted engine between disk and memory

Extract the plan -> rewrite -> edits stages of `run_redirect_selected`
into `socket_patch_core::hosted::engine`, a set of pure functions over a
`ProjectView` (build_candidates, bun_lockb_symlinked, withhold_everywhere,
read_candidate_files, wheel_targets, rewrite, guard). The disk flow
(`scan`/`get --mode hosted`) keeps only the apply lock, the host probes
(pipenv version, gem/python/vlt stale installs), the vendored takeover,
the symlink refusal and the commit of the rewritten files.

The in-memory engine moves to `socket_patch_core::hosted::memory` and
runs the same stages over `ProjectView::Memory`; its duplicated
redirect.rs / ledger.rs orchestration is deleted. The pnpm trust / npm
allow-remote planners move to `hosted::guidance`, the vlt preflight to
`hosted::vlt`, and the redirect-ledger delta to `hosted::ledger`, which
the engine never calls, so removing the hosted ledger only touches the
two callers. `socket-patch-node` now depends on socket-patch-core only;
the CLI re-exports `hosted_memory` for `hosted-bundle` and the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

* Split the vendored takeover out of run_redirect_selected

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

* Undo unrelated rustfmt churn from the takeover split

The previous commit ran `cargo fmt --all` over a tree that is not
rustfmt-clean, reformatting ~30 files it does not otherwise touch.
Restore those files; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

* Read the patch stores through one Ledgers view and ProjectContext

`socket_patch_core::ledgers` holds the one owner-precedence rule for the
manifest, the vendor ledger and the hosted redirect ledger (manifest >
vendored > hosted by ledger key; a manifest key claims every vendor entry
filed under it or naming it as base purl) and the views every reader
derives from it: `owned` (one group per owner key, losers as alternates),
`listed` (every copy worth showing), `matching` (remove/rollback
identifiers) and `hosted_vendored_overlap`. It replaces list's
combined_entries, fold_vendor_records / vendor_record_is_unowned, scan's
merge_ledger_records_for_updates, vex_sources' build_candidates and
overlap_from_states, and rollback/remove's per-store matching loops.
`LoadedLedgers::load` loads the three stores once, each with its own
outcome so every caller keeps its error posture.

`commands::context::ProjectContext` lazily loads the stores, the
lockfile inventory and the wiring discovery at most once per run; scan's
discovery phase, list and get read through it. `get`'s installed-version
narrowing now reuses scan's lockfile and vendored-ledger supplements
instead of its own inventory and ledger reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

* Index vendor-ledger claims once per view

`Ledgers::owned` re-sorted and scanned every vendor key for each
manifest key. Group the vendor entries under their claiming manifest key
in one pass over the sorted ledger (`claims`), so each view is
O(V log V + M) instead of O(M * V log V). Same ordering and alternates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

* Return typed warnings from the hosted engine; JSON in hosted::render

The engine's own warnings (rush repo-state, pnpm trustLockfile, npm
allow-remote, vlt artifact-unverifiable, record_fetch_failed, the
in-memory takeover refusal) were built as serde_json values inside
orchestration. They are now RewriteWarning values; the new
hosted::render module holds the only JSON spelling (warnings, skipped
entries, the nested redirect block), consumed by the disk adapter and
the in-memory engine. No output change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

* Read the lock set and discovery through one per-run snapshot

ProjectView gains a Snapshot variant: the disk under a read-through
cache (DiskSnapshot), so each lock or config file is read at most once
per run and every reader sees the same bytes; probes that are not
content reads still go to the disk. Lockfile discovery's guarded reads
now go through a ProjectView (discover_patched_refs_in), and
ProjectContext backs both locks() and discovery() with one snapshot of
--cwd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #282 (one hosted engine) landed on release/v5-prerelease as 14a9cb0 — core four (#283, #281, #279, #282) are all in; next up #287, #291, #292, #293, #294 once re-merged and ready.


Generated by Claude Code

* Build e2e test binaries once and tier PM legs

The e2e matrix recompiled the CLI and its test binary in each of its
176 legs. e2e-build now compiles every CLI test target once per OS and
the legs run the downloaded binaries from the same checkout path. That
compile also replaces the --all-features --no-run pass in test and
test-release, so each of those compiles one feature set.

PR runs keep every named version boundary, the oldest and newest
release of each tool and every vlt era. The 31 middle e2e rows, 2
yarn-berry releases and 10 cargo toolchain x lock cells move to *-full
jobs that run on main pushes, a new nightly schedule and dispatch.

- e2e-docker (a subset of coverage-docker) runs nightly only.
- Dockerfile.base is built once per run and loaded by each docker leg.
- The hermetic maven/nuget crawl tests run in `test`; their rows and
  e2e_composer's are gone. e2e_safety_cargo_build rides cargo-vex.
- pdm-compat builds the capstone once, skips the 7 cells ci.yml runs,
  drops the Windows native rows (they never ran), and fails a cell
  whose bootstrap fails or whose rows all skip.
- vlt-compat install-proof leaves ci.yml's identical cells to it.
- npm/pnpm compatibility are path-filtered on PRs; the disarmed vlt
  serve watchdog runs daily instead of every 6 hours.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK6zFsizxHRUtyxPvdhM4c

* Address review: env parity, filters, PR cells

- The legs that run test binaries directly set SOCKET_NO_CONFIG and
  SOCKET_NO_UPDATE_CHECK, which cargo's [env] gave `cargo test`.
- cargo 1.93.1 with its own lock (the pinned toolchain the removed
  e2e_safety_cargo_build rows ran) stays on PRs; 1.82.0 own-lock moves
  to the full tier.
- Poetry 2.0.1, the first lock 2.1 writer, stays on PRs.
- e2e-build gets 60 minutes on Windows.
- npm/pnpm filters also watch .cargo/config.toml and cache_env.rs.
- vlt install-proof notes a cell left entirely to ci.yml.
- pdm-compat saves its build cache from main only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK6zFsizxHRUtyxPvdhM4c

* Run the full tier on v5 pushes; review fixes

- ci.yml also runs on pushes to release/v5-prerelease, whose PRs skip
  the full tier and which has no nightly; e2e-docker runs there too.
- cargo 1.93.1 with its own lock runs on macOS and Windows on PRs,
  the cell the old e2e_safety_cargo_build rows ran there.
- e2e-build and pdm-compat print rendered compile errors
  (json-render-diagnostics).
- vlt-compat and pdm-compat also trigger on ci.yml changes, and the
  vlt dedupe only counts ci rows with the same test filter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK6zFsizxHRUtyxPvdhM4c

* Run the vlt agent get test in agent mode

get now defaults to hosted mode (5e5f5ed), so the real-vlt
get_and_remove leg ran a hosted get and found the installed copy
unpatched. It now passes --mode agent, like the other agent-mode
fixtures that commit updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R11RZRvYFL3fzmEnkFAU4A
(cherry picked from commit cc5f1b6)

* Give the nightly CI run its own concurrency group

A concurrency group holds one pending run. Sharing main's group let a
queued main push and the nightly cancel each other, and the nightly is
the only automatic run of e2e-docker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HK6zFsizxHRUtyxPvdhM4c

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #291 (v5 CI: build e2e binaries once and tier the PM matrix) landed on release/v5-prerelease as f9cb7e1, ahead of #287. Its only red checks (4 vlt install-proof cells, yarn-classic 1.0.2/1.6.0/1.7.0/1.9.4) fail the same way on the base. #287 is still blocked: it is a draft and nobody has pushed the docs-only reduction. Next up: #292.


Generated by Claude Code

* Fix apply of created files from diff caches

A diff archive has no delta for a file the patch creates, yet the
disk stager counted a cached diff archive as covering the whole
patch. With only diffs on disk, `apply --offline` passed the gate,
patched the modified files, then failed on the created file's
missing blob and left the package half-patched; online `apply`
never fetched that blob at all.

Coverage is now per file: a diff covers only files with a
before-hash, and created files need their blob. Online, a cached
diff archive no longer suppresses the download, and the top-up
fetches just the created files' blobs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Batch vendor package-reference requests

A vendor run asked the patch service for each package's download
reference in its own request, though the endpoint takes 500 uuids
at once: N round trips and N quota units for N packages.

The run's download plan now resolves every planned uuid in one
request, sent by the first planned call in place of its own and
with the same retries, so an outage costs what it did before.
Each package takes its answer from that batch at its turn; one
still building is asked again then, as before. Hosted scan's
reference lookup is chunked at the endpoint's 500-uuid cap,
which it used to exceed with a 400.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Skip downloading pypi sdists vendoring rejects

pypi vendoring is wheel-based, yet a pypi patch the service serves
as an sdist (every patch without a file qualifier) was downloaded
in full, then rejected because it is not a .whl.

The service's reference already names the artifact, so a pypi
reference whose artifact is not a wheel is now refused before the
download, in the vendor loop and in its download plan alike. The
outcome is unchanged: `auto` warns and builds the wheel locally,
and `service` refuses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Repair downloads created files' blobs

A default (diff-mode) `repair` downloaded only diff archives, but a
diff has no delta for a file the patch creates. After such a repair
`apply --offline` still could not apply a patch that creates files.

In diff mode, repair now also downloads the blobs of created files
(and lists them under `--offline` and `--dry-run`), reported as
their own blob download.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Defer pristine fetches the service makes moot

With the patch service on, `vendor` deferred the registry download
of a not-installed package only for cargo; npm, golang and composer
packages were downloaded and verified up front even when the
service's prebuilt artifact made the pristine copy unnecessary.

Those backends also ask the service first and read the pristine
tree only on a local-build fallback, so their download is now
deferred the same way. A package is deferred only when its fetch
would really download: the fetchers' pre-download refusals (a
foreign yarn berry cacheKey, a go module go fetches without a
proxy, a composer entry with no dist URL) are now one shared check
that both the fetch and the deferral use. pypi and gem keep the
up-front fetch, which their installed-variant probe reads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Run the vlt get e2e leg in agent mode

A bare `get` defaults to hosted mode since v5, so the real-vlt
get_and_remove leg found the installed copy unpatched. Pass
`--mode agent` as #283 does, which this ports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Address review of the waste-review fixes

- repair --json no longer counts created-file blobs twice (once
  under the diff-mode event); the closing line names both failure
  counts when both passes fail.
- The vendor reference batch names the plan from the first call's
  position on, so a package the loop passed over is never granted.
- The npm and yarn classic registry views no longer take a non-http
  resolution's integrity (a local tarball's hash, a git commit id)
  as a registry integrity, so such a package is never deferred
  behind, or vendored from, the service's registry build.
- CHANGELOG entries for the new behavior, and the repair event row
  in CLI_CONTRACT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

* Keep yarn git deps out of the registry view

A yarn classic block resolved to a git repository over plain https
(`https://…/repo.git#<commit>`, or a codeload tarball) passed as a
registry tarball: its commit id became a sha1 integrity, and an
`integrity` field on any git block was kept. With npm now deferring
behind the patch service, such a lockfile-only git dependency could
be vendored from the service's registry build instead of refusing
`vendor_fetch_unverifiable`.

A git resolution, over any protocol, now carries no URL and no
integrity in the registry view, like npm's non-registry entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzdTEhubve9yWfqBE7vAsB

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #292 landed on release/v5-prerelease as a7b0d00. Next up: #296, then #297, #293 and #294, each after it merges the new base and is ready. #287 is still skipped (draft, not yet reduced to docs-only).


Generated by Claude Code

* Share one hosted-URL check for pdm and poetry

The pdm and poetry lockfile rewriters each carried an identical copy of
the rule that decides whether an existing pin is an earlier hosted
redirect of the same artifact. They now use one copy in python_lock, so
the two rewriters cannot drift apart. No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018JsczaHn8e6YrCpxs1NznQ

* Remove the never-implemented --one-off flag

`get --one-off` and `rollback --one-off` (and SOCKET_ONE_OFF) never
did anything: they only failed with a "not yet implemented" usage
error. v5.0 drops them. Passing `--one-off` is now an ordinary
unknown-flag error (still exit 2), and SOCKET_ONE_OFF is ignored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop blanket dead-code allow on vlt lock parser

The whole vlt lockfile text module was exempt from dead-code checks,
which hid an unused edge-rendering method. The exemption is gone, the
unused method is deleted, and the macOS-only global node_modules
helper now opts out of the lint only on builds where it is truly
unused. No behavior change for vlt users.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop reading never-written package archives

Nothing has written `.socket/packages/<uuid>.tar.gz` for several
releases, yet apply, vendor and repair still probed and overlaid that
directory and the patch pipeline tried it before the diff archive.
v5.0 drops the read path and the `appliedVia: "package"` JSON value.
The GC sweeps (scan --prune, rollback, remove, repair) now remove
any leftover `.socket/packages/` files whole, so old projects are
cleaned up; the `removedPackageArchives` counter keeps reporting them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop v3 env names and legacy scan spellings

v5.0 removes compatibility surface kept since v3/v4:
- The legacy env names SOCKET_PATCH_PROXY_URL, SOCKET_PATCH_DEBUG and
  SOCKET_PATCH_TELEMETRY_DISABLED are no longer read (use SOCKET_*).
- The hidden `scan --redirect` flag (use `--mode hosted`) and the
  hidden no-op `scan --detached` flag are gone; both are now unknown
  flags (exit 2).
- The hidden `--mode` values `host`, `redirect` and `vendor` (scan
  and get) are rejected; only hosted, vendored and agent remain.
The hidden `scan --apply` and `scan --vendor` spellings stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Delete uncalled public helpers in core vendor code

Several public functions in the core crate had no callers anywhere in
the workspace: the bun workspace artifact snapshot, the vlt lock sniff
wrapper, the in-memory project's binary/symlink/path helpers, and the
disk snapshot's invalidate, sync text read and disk-root accessors.
They are removed. The in-memory project's text/present/entries helpers
that only tests use are now test-only. No user-visible behavior change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Drop the never-read berry zip URL from hosted

The hosted engine copied the yarn-berry cache-zip URL into every
redirect entry, but no rewriter ever read it: berry pins only the
zip's checksum, which is still taken from the patch reference. The
field is gone from DepOverride; references that still send it parse
as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Note the v5 dead-code removals in the changelog

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Share one service fallback policy across backends

Every service-backed vendor backend (npm tarball and directory, pypi,
cargo, composer, golang, gem, and the frozen maven/nuget path) decided
on its own when a patch-service miss falls back to a local build, when
`--vendor-source=service` refuses, and when tampered bytes are fatal.
Seven hand-copied versions of that policy could drift apart.

The policy now lives once in service_fetch.rs: ServicePolicy maps the
Pending, Unavailable, Failed and IntegrityMismatch outcomes, and each
backend keeps only its own handling of a ready artifact. Warnings,
refusal codes, messages and check order are unchanged, including the
npm tarball backend's wording for a failed request in service mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Align tests and docs with package-archive removal

Three integration tests still expected a leftover package archive to
be a patch source or to survive GC; they now pin the v5.0 rule (not a
source, always swept). Also: a rollback --one-off rejection test, test
names that no longer mention the removed --detached flag, a dead
berryZipUrl branch in the hosted memory harness, and contract rows
that still listed `--download-mode package`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #296 (remove dead code and v3 shims) landed on release/v5-prerelease as 1e3ace6. Its only red checks were base-inherited: yarn-classic ×4 mode_migration_npm and vlt install-proof 0.0.0-1/0.0.0-11 ×4. Next up: #297, then #293 and #294, each after it merges the new base. #287 is still skipped (draft, not yet reduced to docs-only).


Generated by Claude Code

* Pin the #257 oracle sweeps with golden digests

The randomized equivalence sweeps that compare the blocking-pool
crawlers and single-pass rewriters with their pre-#257 oracles now
also record, per case, a digest of the generated input and of the
output both implementations agreed on, in
crates/socket-patch-core/tests/equivalence/*.golden. The oracles
still run, so every recorded output is proven equal to the oracle's.
The next commit deletes the oracles and keeps the goldens.

The crawler goldens were blessed on Linux as a non-root user: the
sweeps strip permissions and plant symlinks, so they replay only
there (other platforms and root still run the sweeps, without the
golden comparison).

Also moves the crawler sweeps' xorshift test RNG to
crate::test_rng, and renames crawlers::oracle_support to
crawlers::test_tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9YdJqaGiT9Jf5LN1hDFhB

* Retire the #257 crawler and rewriter oracles

The equivalence sweeps now replay against the golden digests pinned
in the previous commit instead of re-running the verbatim previous
implementations: the crawler oracles (cargo, composer, go, maven,
npm, nuget, python), the hosted rewriter oracles and the reference
POM parser are deleted. Every sweep still generates the same inputs
from the same seeds, so a changed output is still caught per case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9YdJqaGiT9Jf5LN1hDFhB

* Pin the pdm and poetry rewrites by golden

The pdm plan, pdm and poetry parse-reuse rewrites were checked
against #257's verbatim fresh-parse implementations. Their sweeps
now record per-case digests, blessed while those oracles still
agreed with production, and the oracles are deleted.

Golden keys with whitespace are normalized so a case label can
never split a golden line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9YdJqaGiT9Jf5LN1hDFhB

* Merge 67 CLI test binaries into command suites

The subprocess-only integration tests for apply, get, remove,
rollback, repair, scan, vendor, update and the global CLI now build
as one binary per command instead of 67, so CI links and runs 58
fewer test binaries. Each file keeps its name as a module, and the
shared helpers are declared once per suite, so their self-tests run
once per suite instead of once per file.

Binaries a workflow names, the docker e2e suites, crawl_fd_limit_e2e
and the in-process suites (which share process-global notice and
config state) stay separate. Doc links to the moved files are
updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9YdJqaGiT9Jf5LN1hDFhB

* Point docs at the merged test suites

Rows that named a moved test binary now name its suite and module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9YdJqaGiT9Jf5LN1hDFhB

* Keep the crawler oracles

A crawler's output follows the directory order the filesystem hands
back, which differs between filesystems: every crawler golden blessed
on ext4 failed when replayed on tmpfs, and macOS and Windows skipped
them. The per-call oracles walk the same tree as the crawler, so they
check the parallel walk on every platform; restore them and drop the
crawler goldens. The rewriter goldens are pure text and stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9YdJqaGiT9Jf5LN1hDFhB

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #297 (one suite per command, retire #257 oracles) landed on release/v5-prerelease as b97a1c2. Its only red checks were base-inherited: yarn-classic ×4 and vlt install-proof 0.0.0-1/0.0.0-11 ×4. Next up: #293, then #294, each after it merges the new base. #287 is still skipped (draft, not yet reduced to docs-only).


Generated by Claude Code

* Plan staged patch rollout for v5

Adds the design for rolling Socket patches out gradually: a
`patches:` block in socket.yml that narrows what scan may patch
(paths, ecosystems, packages, severity floor, on/off), and a
severity-ordered per-run cap on new patches so each scan lands the
next few most critical fixes.

The plan splits the work into two parallel items with a frozen
interface, lists every hard-coded filter and where it belongs, and
covers the depscan autopatch follow-up. configuration.md now records
that socket-patch reads socket.yml for selection policy only, with
the trust boundary unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Revise rollout plan after adversarial review

Three independent reviews (ambiguity, churn, trust boundary) found
gaps that would have let the two implementations disagree or let a
repo file widen or stall the rollout. The plan now:

- matches paths against marker files with the backend's top-down
  gitignore rules, so projectIgnorePaths means the same everywhere
- uses one data source for severity, supersession and ordering
- spends the budget only on patches the planning pass proves can
  land, and admits nothing new when a lookup failed
- uses the merged recorded view in every mode and engine
- has depscan read the policy from the base commit for PR jobs
- hardens file handling (regular files, aliases, size, encoding,
  trusted repo root) and reports what a policy hides

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Close interface gaps in the rollout plan

A final consistency pass found places where the two work items
would have produced incompatible code: base purls admitted in one
directory being charged again in the next, no defined hand-off of
the unfiltered offers from the severity filter to classification,
no shared repo-relative path helper, and override sources the JSON
must report but the interface could not carry. The shared contract
now defines each of these, and the parity tests match each engine's
budget scope.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Clarify who shapes scan's selection output

The plan said both that the selector returns the shared offers
struct (work item A) and that it returns admitted/deferred rows
(work item B). The selector now returns the offers, and B adds the
rollout stage after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Add the socket.yml selection policy to core

New socket_patch_core::policy module: the SelectionPolicy, Offers and
repo-root helpers that the staged-rollout plan freezes as the shared
contract between the socket.yml work and the --max-new-patches work.

It reads the repo root's socket.yml/socket.yaml strictly and fails
closed: bad YAML, a misspelled `patches` block, unknown keys (with a
did-you-mean hint), wrong types, empty allowlists, bad globs and
anchors or aliases inside the keys we read are all errors that name
the key path. Path lists match marker files with npm `ignore`
semantics, walked top-down; a golden fixture generated from the npm
package pins that. The built-in test/fixture ignores become
overridable defaults for discovered roots.

--package matching moves to core so scan and the policy share it.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Honor socket.yml patch policy in disk scans

scan now reads the repo root's socket.yml before any write and applies
its patches block in hosted, vendored and agent mode, --dry-run
included: path filters on project roots (PATH-glob matches also get
the built-in test/fixture ignores), ecosystem and package filters on
crawled packages, and a severity floor on the patches a package may
receive. An invalid or ambiguous file fails the run with exit 1 and
an errorCode before any request.

Packages that already carry a patch are never removed, upgraded or
replaced by the policy: they are held and reported under
policy.retained. A recorded patch below a new floor stays in place.
patches.enabled: false reports what would be patched and writes
nothing.

New flags: --no-socket-yml / SOCKET_NO_SOCKET_YML and
--min-severity / SOCKET_MIN_SEVERITY. Every successful scan --json
result gains a top-level policy block. A PATH outside the repository
root is a usage error.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Apply socket.yml policy in the in-memory engine

selectHostedScanPaths now streams the root socket.yml/socket.yaml and
returns them as policyPaths; it drops test and fixture trees through
the policy's built-in default ignores instead of a hard-coded segment
list (structural excludes like node_modules and vendor stay fixed).

The session reads the policy before any root is processed: path
filters run before the project limit, ecosystem and package filters
on each root's packages, and the severity floor before selection. A
listed policy file that arrives without content, or an invalid one,
yields policyError with no root processed and no file changed. New
options noSocketYml, minSeverity and policyPaths; the result gains a
policy block. hosted-bundle and index.d.ts carry the new fields.

get now warns policy_bypassed when the repo's socket.yml would have
skipped the package it patches.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Document the socket.yml patch policy

CLI_CONTRACT.md gains a "socket.yml patch policy" section (grammar,
precedence, paths, lookup, validation, commands, error codes and the
policy JSON block), the flag and env rows, and the "narrow or pace"
half of the trust-boundary rule. README adds a "Roll out gradually"
section with copyable socket.yml recipes, CHANGELOG lists the new
policy and the breaking scan changes, and the design docs record the
decisions made while building it.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Harden the socket.yml policy after review

Fixes from an adversarial self-review of the policy work:

- A non-ASCII package spec no longer panics validation, and error
  text, warnings and verbose lines drop terminal escapes and bidi or
  zero-width characters.
- Ignore lists that fail to compile together are an error instead of
  silently matching nothing, and the policy file is opened by its
  resolved path without following a swapped-in symlink.
- A top-level key that looks like a misspelled `patches` (`patchs`),
  a top-level merge key or an aliased key now fails closed.
- Repo-root lookup follows a `.git` symlink and trusts the checkout
  owner under root and sudo, so CI containers keep the policy.
- The severity floor always reports the patch it held back, report-
  only --json runs report what a floor or `enabled: false` hides, a
  root skipped as a whole is always one entry and no longer prints
  "No packages found", warnings print once per invocation, and the
  policy line is omitted when there is nothing to say.
- policy entries are sorted and use canonical purls on disk and in
  memory; the in-memory engine applies a socket.yml negation of a
  built-in ignore to roots it was given, and policyError carries no
  CLI-only remedy.
- A lockfile-less disk root matches path filters by its manifests.

Tests cover each fix, plus the prune universe, agent path filters, a
vendored package held byte-identical, and tighter oracles; docs are
corrected where they overstated what the human output names.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Give the release test job more time

The optimized test job now has to build two more crates and one more
test binary for the socket.yml policy. It ran out of time on its last
40 minutes, about a minute short, and a docs-only change already takes
38. Raise the limit to 50 minutes so it can finish.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Apply socket.yml paths during memory selection

The merged plan makes in-memory path selection two-phase: the host
fetches the root socket.yml first and passes its text to
selectHostedScanPaths. Selection now applies the full path policy, so
a negation such as `!/e2e/tests/` brings a test tree back in memory
exactly as on disk. A listed policy file that is missing, symlinked
or invalid returns policyError with nothing selected.

Selection returns policySha256, and the session fails closed when the
policy it reads differs. Roots the policy excludes keep their marker
files presence-only, so the session still lists them as filtered.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep excluded roots out of the memory stream

Review follow-ups to two-phase selection:

- Roots the policy excludes are reported in ignoredSample instead of
  streamed presence-only, so a repo with many fixture lockfiles no
  longer runs into the session's file limit.
- A session that bypasses socket.yml while selection applied it now
  fails closed instead of processing roots it never fetched.
- The docs say policy text must decode losslessly (TextDecoder drops
  a BOM) and when policySha256 is null.

Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep scan -h short with the policy flags

The v5 help rules cap each command's short help at about eight
options. `--no-socket-yml` and `--min-severity` pushed `scan -h` to
ten, so they now appear only in `scan --help`, like the other
advanced scan flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3

* Fail report-only JSON when detail queries fail

A report-only `scan --json` (`--prune` with no mode) with a severity
floor or `enabled: false` fetches patch details to fill
`policy.filtered[]`. If every query failed it still printed a success
envelope and exited 0. It now reports the error and exits 1, like the
agent and vendored runs.

Human-mode policy warnings now print `Warning: <detail>` like the rest
of `scan`; the code stays in the JSON `warnings[]` entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3

---------

Co-authored-by: Claude <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Coordinator: #293 (socket.yml patch rollout config and filtering) landed on release/v5-prerelease as b9e106d. Next: #294 (needs a re-merge of the base); #287 is still a draft; #295 is queued at the end.


Generated by Claude Code

This branch has not been deployed

No deployments
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.

2 participants