Skip to content

Fix agent mode skipping npm-aliased copies (#356) - #738

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-npm-agent-alias-copies
Oct 5, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-npm-agent-alias-copies

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

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

Fixes #356

Summary

Agent mode now treats an npm alias install ("lp": "npm:left-pad@1.3.0", which puts the real left-pad@1.3.0 at node_modules/lp) as an installed copy of pkg:npm/left-pad@1.3.0. apply, rollback and vex all go through the same resolver, so all three now cover alias copies under npm, yarn, Bun and pnpm's hoisted linker.

Before: an alias-only project got package_not_installed from apply. A project with a plain copy plus an alias got only the plain copy patched, success, and a VEX not_affected statement while require('lp') loaded unpatched code.

Root cause

NpmCrawler::find_by_purls (crates/socket-patch-core/src/crawlers/npm_crawler.rs) only probes <node_modules>/<purl-name>, and visit_resolver_dir requires the dir name to equal the package.json name. An alias dir's name never equals its package's name, so it was never a candidate.

Change

  • visit_resolver_dir (importer-tree visits only) adds alias_copies: real package dirs, plain or under a @scope, whose own package.json name@version is a pending target while the dir is named otherwise. It also checks nested node_modules, because the BFS visits them.
  • Links never count. A link is a dependency edge into a store, a workspace member or an npm link target, so pnpm's isolated layout and Fix agent mode patching linked first-party source (#626) #634's first-party-link handling are unchanged. A dir whose name is its own package name (in any ASCII case) is left to the direct probe, so one physical dir is never recorded twice on case-insensitive filesystems.
  • The plain copy stays first (root-copy-first order), so single-representative consumers (get, vendor) still pick it.
  • The sequential equivalence oracle (npm_crawler/oracle.rs) mirrors the rule, so the randomized tree tests keep comparing like with like. Their generator already produces alias installs.
  • Hosted VEX's separate alias walk (vex_consumed::npm_alias_copies) now mostly re-finds paths the installed-tree lookup already returns, so its results are merged without duplicates.
  • CLI_CONTRACT documents alias installs as copies.

Wrappers (npm/, pypi/, gem/) only dispatch to the binary, so they need no change.

Test evidence

Red→green (each new test was run with the alias_copies call disabled, then enabled):

Issue variant Test Without fix With fix
#356 alias-only (package_not_installed) npm_crawler::tests::find_by_purls_resolves_an_alias_only_install FAIL pass
#356 plain + alias, nested alias npm_crawler::tests::find_by_purls_returns_alias_copies_beside_the_plain_copy FAIL pass
#356 scoped alias / alias of a scoped pkg npm_crawler::tests::find_by_purls_resolves_scoped_alias_installs FAIL pass
#356 apply patches every alias copy (in-run --vex) e2e_embedded_vex::apply_vex_patches_npm_alias_copies FAIL pass
#356 vex refuses while an alias copy is unpatched e2e_vex::verify_mode_requires_npm_alias_copies_patched FAIL pass
links are not alias copies npm_crawler::tests::find_by_purls_does_not_take_a_link_as_an_alias_copy pass pass

Real toolchains (Linux, hand-staged manifest + blobs, apply --offline --vex):

  • npm 10.9 with left-pad + lp + @x/pad: applied 3, every copy patched, require('lp') loads patched bytes, VEX not_affected. rollback --offline restores all three. With only node_modules/left-pad patched, vex exits 1: omitting pkg:npm/left-pad@1.3.0 … (not_applied).
  • pnpm 10.28 node-linker=hoisted (lp + left-pad): applied 2, both patched.
  • Bun 1.3.14 (lp + left-pad): applied 2, both patched.

Local checks:

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo fmt: changed hunks are formatted. main itself is not fmt-clean (498 diffs), so I only formatted my own hunks.
  • cargo test --workspace --all-features: 213 suites ok. The 12 failures are all permission/write-failure tests (chmod 0555-based, e.g. covgap_commands_vendor::*_state_write_failure_*, vlt_heal unremovable-lock, copy_tree relax loop) that cannot fail as root in this sandbox. They don't touch the resolver, and CI runs them as non-root. The 13th, ecosystem_dispatch::tests::npm_crawl_snapshot_matches_the_crawls_it_replaces, pinned the old "alias is missing" behavior and is updated in 07824bd.
  • node --test npm/socket-patch/bin/socket-patch.test.mjs: 4/4.

Follow-ups (not in this PR)


Note

Medium Risk
Changes core npm package discovery used by apply, rollback, and VEX; incorrect alias/link handling could patch wrong dirs or miss copies, but behavior is heavily tested and scoped to importer-tree alias detection.

Overview
Fixes #356 by teaching the npm installed-tree resolver to treat npm alias installs (e.g. "lp": "npm:left-pad@1.3.0" → real package at node_modules/lp) as additional copies of the target PURL, alongside plain node_modules/<name> paths.

NpmCrawler::find_by_purls / visit_resolver_dir now runs alias_copies on importer-tree node_modules: it scans real package dirs (including scoped layouts) whose package.json name@version matches a pending target while the directory name differs, skips symlinks and dirs that already match their package name (avoids double-counting), and keeps plain copies first in the result order. The async oracle mirrors the same rule for randomized equivalence tests.

apply, rollback, and vex all use this resolver, so alias-only trees no longer get package_not_installed, mixed plain+alias trees get every copy patched/verified, and VEX no longer attests not_affected while require('lp') still loads pristine bytes. Hosted VEX path merging dedupes alias walk results against paths the resolver already returned. CLI_CONTRACT documents alias installs as supported copies.

Coverage adds unit tests for alias-only, plain+alias+nested, scoped aliases, and “links are not alias copies”, plus e2e tests for apply --vex and vex verify mode; snapshot/dispatch tests expect left-pad resolved via node_modules/lp.

Reviewed by Cursor Bugbot for commit 07824bd. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
An npm alias such as "lp": "npm:left-pad@1.3.0" installs the real
left-pad@1.3.0 at node_modules/lp. Agent mode only looked for the
package at node_modules/left-pad, so:

- an alias-only project got package_not_installed from apply;
- a project with a plain copy and an alias patched only the plain
  copy, reported success, and vex attested not_affected while
  require('lp') still loaded the unpatched file.

The resolver now also treats a real package dir whose own
package.json names the patched name@version as a copy of it, under
any dir name (plain or scoped). Links still never count, so pnpm's
isolated layout and workspace links are unchanged. apply, rollback
and vex all share this resolver, so all three now cover alias copies
under npm, yarn, Bun and pnpm's hoisted linker.

Fixes #356

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
The installed-tree lookup now returns npm alias installs itself, so
hosted VEX's own alias walk mostly finds paths that lookup already
returned. Merge them without duplicates. The dispatcher test that
pinned the old "alias is missing" behavior now expects the resolver
to find node_modules/lp directly.

Refs #356

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 4, 2026 02:19
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit 07824bd. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 4, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review — head 07824bd00d83a1a2eb3dfb2bf7a8fc2a0941a5f7

  • CI: all check runs green on this head (0 failing, none pending; skipped matrix legs only)
  • Mergeable: yes, no conflicts with main
  • Bugbot: reviewed 07824bd with no findings; no open review threads
  • Reviewer focus: alias_copies in crates/socket-patch-core/src/crawlers/npm_crawler.rs. Links are never alias copies, and a dir whose name matches its own package name is skipped so it isn't counted twice. The hosted-VEX alias walk is now mostly redundant (noted as a follow-up).

Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit f99ba5c into main Oct 5, 2026
502 of 503 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-npm-agent-alias-copies branch October 5, 2026 11:34
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
Resolves conflicts with main's npm alias (#738), first-party link
(#634) and pnpm store (#698) changes:

- CLI_CONTRACT.md: keep main's hosted row and this PR's agent row.
- vex_consumed.rs: drop aliases the installed lookup already found
  (main), then store-expand only the new ones (this PR), so already
  expanded copies are not scanned again.
- Tests: keep both sides' new multicopy and e2e_vex regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174mrknEY9ge42c94RNRRBx
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
Main has been red since 4646693 (#605): two
commands::vex_consumed tests built for #738 assume the name-keyed
resolver never returns npm-aliased copies, which #605 changed. This is
the same test-only change as #851 and becomes a no-op once that lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXCFoPbraq7rNKJpXEP2n
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main has been red since #605 taught the npm copy resolver to probe
bundled store trees: two vex_consumed alias tests (#738) still assumed
the resolver never returns npm-aliased copies, so the CLI lib tests
fail on every PR's merge ref. This ports #851's tests-only fix so the
PR's CI reflects its own change; it no-ops once #851 lands on main.

Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main has been red since #605 (4646693). Two vex_consumed tests from
#738 assumed the name-keyed copy resolver never returns npm-aliased
copies, and #605 taught it to. This ports #851's tests-only fix
unchanged so this PR's coverage and macOS test jobs can go green; it
no-ops once #851 lands on main.

Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main fails commands::vex_consumed::tests::hosted_expands_alias_only_copies
and hosted_reuses_expanded_npm_copies_and_merges_alias_variants since
#605 landed alongside #738; #851 fixes the tests. Carry the same change
so this PR's CI (coverage, test-release) is green; it no-ops once main
has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPHxnE5P1rkfCpHFFCwzFR
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main went red when #605 taught the name-keyed resolver to find pnpm
store copies, which the #738 alias tests assumed it missed. Same
change as #851; it no-ops once main carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEpjVvY7X41jPuVuoiCLVz
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #806, #821

Assisted-by: Claude Code:claude-opus-5-5

* Unwind uv vendoring after a relock

After vendoring, an ordinary uv relock (`uv add --dev x`, `uv add y`)
re-serializes the lock arrays that hold our element: the dev group's
requires-dev line and `[manifest] overrides`. Revert matched those
arrays by their exact recorded text, so it saw drift and kept
uv.lock wired, but still reverted pyproject.toml. The pair then
failed `uv sync --locked` while `vendor --revert` reported success.

Revert now finds our unchanged element inside the live array under
the same key and restores or removes just that element, rendering
the array the way uv writes it. A pair gate also writes neither
file when any record is genuinely drift-kept, so pyproject.toml and
uv.lock always stay consistent.

Fixes #806, #821.

Assisted-by: Claude Code:claude-opus-5-5

* Test uv revert after a relock with real uv

Vendor six, run the uv command that re-serializes the lock array
around our element (`uv add --dev zipp` for a dev group, `uv add
idna` beside user overrides), then revert. Both files must be
unwired with no drift warning, and `uv lock --check` must pass.

Refs #806, #821.

Assisted-by: Claude Code:claude-opus-5-5

* Document uv revert after a relock

Refs #806, #821.

Assisted-by: Claude Code:claude-opus-5-5

* Anchor uv array reverts on their key

A [manifest] overrides record holds the bare array, and the old
convergence shortcut searched the whole lock for it. When the root
requires-dist happened to match the user's overrides array, revert
treated our element as already gone, left it in uv.lock and deleted
the artifact it points at. Every whole-array record now reverts
through its own key: an untouched array is restored verbatim,
otherwise just our element is.

Refs #806.

Assisted-by: Claude Code:claude-opus-5-5

* Fail closed when a uv lock array can't be read

Revert treated any miss locating a whole-array record as convergence,
including a key spelled differently or an unbalanced array. A lock
that still routed through the vendored wheel could then lose the
wheel. Only a key or section that is provably absent now counts as
converged. Anything unreadable is drift, which keeps both files and
the artifact.

Refs #806, #821.

Assisted-by: Claude Code:claude-opus-5-5

* Start fix for #840

Assisted-by: Claude Code:claude-opus-5-5

* Test uv revert after a declaration edit

A vendored uv revert writes back the lock specifier it recorded when
vendoring. If the user changed the package's requirement in
pyproject.toml in the meantime, the lock no longer matches and
`uv sync --locked` fails. These tests pin the expected behaviour
for requires-dist, requires-dev groups and [manifest] constraints.

Refs #840

Assisted-by: Claude Code:claude-opus-5-5

* Re-derive uv specifiers on vendored revert

When six is vendored, uv.lock records it as a path source with no
version specifier. If the user then changes six's requirement in
pyproject.toml (uv add "six>=1.16"), the lock stays byte-identical,
and vendor --revert, remove and rollback wrote back the specifier
recorded at vendoring time. The revert reported success, but
`uv sync --locked` then failed.

The revert now writes the specifier pyproject.toml declares now, using
the same derivation the hosted unwind uses. This covers requires-dist
(each extra separately), requires-dev groups and [manifest]
constraints. An unchanged declaration still restores byte-for-byte.
When uv's spelling can't be derived, such as a multi-clause range whose
clause order varies between uv releases, the revert keeps both files
and warns vendor_lock_entry_drifted instead of breaking the lock.

Fixes #840

Assisted-by: Claude Code:claude-opus-5-5

* Document uv revert after a declaration edit

Refs #840

Assisted-by: Claude Code:claude-opus-5-5

* Adapt uv specifier re-derivation to main

#625 on main changed the uv declaration reader to report each
optional-dependencies member's extra. The merge of main into this branch
no longer compiled. Use that reader for requires-dist instead of the
local extras walk. Dev groups now also pick up main's group-name
normalization and include-group expansion.

Refs #840

Assisted-by: Claude Code:claude-opus-5-5

* Pick the declaration a uv lock entry mirrors

When a package is declared twice, for example under two environment
markers, or directly and through an include-group, the revert kept the
recorded specifier whenever any one declaration still matched it. Edit
just one of them and the stale pin came back, with the same broken
`uv sync --locked` as #840.

The revert now picks the declaration by the entry's own marker, as the
hosted unwind does. Declarations that still disagree after that are
treated as drift, and both files are kept.

Refs #840

Assisted-by: Claude Code:claude-opus-5-5

* Tighten uv revert specifier re-derivation

Two cases Bugbot found on the vendored uv revert:

- A same-name declaration that isn't a plain version range, such as an
  extra pinned with ===, stopped every entry from following its edited
  declaration. Now only the entry whose own declaration is unreadable
  keeps its recorded spelling.
- After a bound was dropped, the restored { name = "six" } element
  also matched another dependency entry in uv.lock, so a drifted wiring
  could pass as already reverted. The check now looks only in the root
  unit's requires-dist array.

Refs #840

Assisted-by: Claude Code:claude-opus-5-5

* Format the uv revert re-derivation changes

Assisted-by: Claude Code:claude-opus-5-5

* Port #851: fix vex alias tests broken on main

main has been red since #605 (4646693). Two vex_consumed tests from
#738 assumed the name-keyed copy resolver never returns npm-aliased
copies, and #605 taught it to. This ports #851's tests-only fix
unchanged so this PR's coverage and macOS test jobs can go green; it
no-ops once #851 lands on main.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Fix vendored gem rewrite breaking positional args

Vendoring a gem declared with a splat, constant or method-call
version (`gem "rack", *V`, `gem "rack", VERSION`, `ENV.fetch(...)`)
wrote that argument after the new `path:` keyword. Ruby rejects that,
so every later `bundle` command failed to parse the Gemfile even
though vendor reported success and VEX attested the patch.

The exact pin supersedes these constraints, so they are now dropped
like quoted ones. Keyword options such as `require: false` and a
trailing comment still follow `path:`. A real-bundler e2e checks the
rewritten Gemfile installs frozen and loads the vendored copy.

Fixes #847

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
main has been red since #605 taught the name-keyed resolver to return
npm-aliased copies, which broke two vex_consumed tests added by #738.
Port #851's test update so this PR's CI goes green; it no-ops once
#851 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 5, 2026
#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
…files (#590, #417) (#598)

* Start fix for #590, #417

Assisted-by: Claude Code:claude-opus-5-5

* Refuse hosted runs from a workspace member

Hosted scan and get read locks only in --cwd. Run from a pnpm
workspace member (or a project whose lockfile-dir puts pnpm-lock.yaml
elsewhere), they pinned nothing and still reported success, so pnpm
kept installing the unpatched package (#590). Run from a cargo
workspace member, they rewrote the member as a lockless project and
broke every build of the workspace (#417).

Both layouts are now refused before any takeover or write, exit 1,
naming the directory to run from: redirect_pnpm_lockfile_elsewhere
for pnpm, and the vendored cargo_manifest_not_workspace_root check,
now shared, for cargo.

Assisted-by: Claude Code:claude-opus-5-5

* Document the hosted workspace-member refusals

Assisted-by: Claude Code:claude-opus-5-5

* Honor lockfileDir set by the workspace root

A workspace root can move pnpm-lock.yaml with lockfileDir, and the
key may be written quoted in pnpm-workspace.yaml. Hosted runs from a
member of such a workspace, or of one with a quoted key, still
reported success while pinning nothing. Both are now refused like
any other member whose lock lives elsewhere.

Assisted-by: Claude Code:claude-opus-5-5

* Honor pnpm workspace lockfile configuration precedence

* Drop CHANGELOG entry from this PR

Release notes are written when a release is cut, from the merged PR
log and the code, so PRs no longer edit CHANGELOG.md.

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

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5

* Read lockfileDir past a BOM and take the last

A pnpm-workspace.yaml saved with a UTF-8 BOM, or one that sets
lockfileDir twice, could hide a relocated lock from the member check,
so a hosted run from a member still reported success while pinning
nothing. The reader now skips the BOM and uses the last assignment.

Assisted-by: Claude Code:claude-opus-5-5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #652

Assisted-by: Claude Code:claude-opus-5-5

* Refuse gem lines pulled from git sources

Hosted scans moved a gem declared with `gitlab:`, a custom
`git_source(:name)` key or a string-keyed `"git" =>` option into the
Socket source block. The option still overrode the block, so bundler
kept loading the unpatched git checkout while scan reported the gem
redirected and VEX attested it not_affected. String-keyed options
such as `"require" => false` were also silently dropped.

Both hosted and vendored modes now read gem options through one
shared reader that understands every key spelling and treats any key
outside bundler's non-source options as a source. Hosted mode also
refuses a gem the lock resolves from a GIT, PATH or plugin section.

Fixes #652

Assisted-by: Claude Code:claude-opus-5-5

* Add e2e and escape tests for gem git sources

Adds a real-bundler capstone where the gem comes from a custom
`git_source` key: the hosted scan must refuse it, write nothing and
attest nothing, and bundler must still install the project. The
option reader now also honors backslash escapes in single-quoted
strings, so a quote inside a value cannot hide a later git option.

Refs #652

Assisted-by: Claude Code:claude-opus-5-5

* Read the gem lock's git sections once per scan

The new git/path refusal re-parsed Gemfile.lock for every patched
gem, which made hosted bundler scans about 15% slower on the bench
fixture (800 gems, 20 patched). The lock is now parsed once per
rewrite; our own edits only touch GEM sections and CHECKSUMS, so
the GIT/PATH membership read up front stays accurate.

Refs #652

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

* Retry PDM backtest cases on transport errors

The PDM matrix runs against production PyPI and the public patch API.
Over the last 7 days 25 pdm-compatibility runs failed on one random
cell each, on unrelated PRs. The version, OS, shape, mode and check
differed every time (rescanIdempotent, appliedExactlyOne,
rescanAfterRelockApplies, ...). Each check judges a CLI scan, install
or rollback. `Run` retries a command once, and only on a non-zero
exit. The CLI usually reports an exhausted patch API fetch in its
JSON while exiting zero, so the cell just fails a later check.

Port backtest-poetry.py's case-level retry (#596). A case is re-run
from a fresh directory, at most three attempts, only when every
failed check recorded transport evidence from the operation it
judged. Evidence is a failed command's request error, PyPI give-up,
patch API 5xx or exhausted 429, or the same in the CLI's JSON error
records. Functional failures are never retried, even when a later
step raises a transport error. Failed attempts' logs go under
attempts/ and are uploaded. A failing case now prints its failed
checks' notes, since the job log alone never said why.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV
(cherry picked from commit 4329170)

* Judge PDM rollback and VEX checks by their run

Bugbot: the final hosted/vendored rollback checks, the unverifiable-
write rollback, the refused-lock VEX and the reverted-lock VEX runs
named no operation, so a transport failure there never made the case
retryable. installedBytesPatched fails together with a blipped pdm
sync and blocked the retry the same way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV
(cherry picked from commit 6b0302a)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #632

Assisted-by: Claude Code:claude-opus-5-5

* Pin yarn catalog deps in hosted mode

A dependency declared "catalog:" in package.json was never patched
by scan --mode hosted: the resolutions entry was keyed by the lock's
expanded npm: range, but yarn matches resolutions before it expands
the catalog. The scan reported success, then yarn install --immutable
failed (YN0028) and a plain yarn install kept the unpatched release.

Also route name@catalog: / name@catalog:<named> for every
.yarnrc.yml catalog that maps the package to a pinned range. A re-run
adds the selector to a pin written by an earlier release.

Fixes #632

Assisted-by: Claude Code:claude-opus-5-5

* Test yarn catalog pins end to end

Add a real-yarn check that a fresh checkout of a hosted-pinned
catalog dependency installs the patched bytes under --immutable,
an in-process scan + rollback round trip, and document catalog
pins in the yarn berry hosted notes.

Assisted-by: Claude Code:claude-opus-5-5

* Drop unrelated rustfmt churn

cargo fmt --all also reformatted 127 files this fix doesn't touch
(main isn't rustfmt-clean). Restore them and the untouched hunks of
the edited files to main, so the PR only carries the catalog fix,
its tests and the docs note.

Assisted-by: Claude Code:claude-opus-5-5

* Keep unquoted yarn catalog ranges as their source text

berry_catalog_selectors parsed .yarnrc.yml catalogs into serde_json
Values, so an unquoted range like `1.10` became the number 1.1 and
never matched the lock's `npm:1.10`: the `catalog:` selector was
dropped while the pin was still confirmed. Yarn reads .yarnrc.yml with
the failsafe schema, so deserialize the catalog tables as string
tables instead, which keeps each scalar's source text.

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

* Port #851: fix vex alias tests broken by store-copy merge

main fails commands::vex_consumed::tests::hosted_expands_alias_only_copies
and hosted_reuses_expanded_npm_copies_and_merges_alias_variants since
#605 landed alongside #738; #851 fixes the tests. Carry the same change
so this PR's CI (coverage, test-release) is green; it no-ops once main
has it.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #756, #772

Assisted-by: Claude Code:claude-opus-5-5

* Report writes to pnpm/vlt store twin copies

When a package has more than one pnpm or vlt peer-variant store copy,
apply and rollback already patch (or restore) every copy, but they only
reported what happened to the first one. A run that fixed only a twin
copy said "already patched" (applied: 0), and a rollback that restored
only a twin said "already original" (rolledBack: 0).

Apply and rollback now share one store-copy fan-out and one fold, which
merges each copy's per-file records into the result under the copy's
on-disk path. The two private folds, which had drifted on which
advisories they kept, are gone; both directions now carry only the
ownership advisory from a copy.

Fixes #756, #772.

Assisted-by: Claude Code:claude-opus-5-5

* Test CLI reporting of store twin writes

End-to-end regression for #756 through the real binary, on hand-built
pnpm and vlt store layouts: apply that patches only a twin copy reports
it as applied, and rollback that restores only a twin counts it as
rolled back. Documents the copy-qualified file paths in CLI_CONTRACT.

Assisted-by: Claude Code:claude-opus-5-5

* Keep --force skips in a twin copy local

Under --force, a store twin missing a patched file skips it and still
succeeds. The fold already dropped that copy's "all files skipped" note,
but it carried the skipped file's NotFound record, so a package whose
primary copy was already patched was reported as "applied" with no
files instead of "already patched". Those records now stay with the
copy, like its note.

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #798

Assisted-by: Claude Code:claude-opus-5-5

* Refuse npm VEX when the twin lock lacks the pkg

With both npm-shrinkwrap.json and package-lock.json committed,
lockfile-only `vex` attested a patch that only one lock wired when the
other lock had no entry for the package at all. npm 12 installs from
package-lock.json and re-resolves a missing entry from the registry,
so the checkout installed unpatched bytes while the VEX document said
`not_affected`.

A twin lock with no entry for the package now contests the wiring the
same way a registry entry does (`patched_ref_unattributable`), in both
directions and for hosted and vendored wiring. A twin that holds the
package only at another version still contests nothing: npm installs
that version, not unpatched bytes of the patched one.

Fixes #798

Assisted-by: Claude Code:claude-opus-5-5

* Make the dual-lock read test use agreeing twins

The test that proves both npm locks are read wired each package in only
one lock. After #798 such a pair is contested (npm re-resolves the
package missing from the other lock), so the fixture now has each lock
wire both packages. It still proves both locks are read (4 refs) and
that the v2 legacy mirror adds nothing.

Refs #798

Assisted-by: Claude Code:claude-opus-5-5

* Keep patch refs out of a vex test's messages

CodeQL flagged the new dual-lock test for printing the wired patch
reference (which holds the patch uuid) in an assertion message. The
message now names the wiring mode instead.

Refs #798

Assisted-by: Claude Code:claude-opus-5-5

* Fix vex alias tests broken by store-copy merge

#605 taught the name-keyed npm resolver to probe bundled store
trees, so it now finds aliased copies (node_modules/lp) and a nested
host's store peers itself. Two vex_consumed tests from #738 assumed
that set never held aliases, so main's CI went red after both merged.

The tests now feed the alias-free set explicitly to keep covering
alias expansion, and also check the resolver's own set reaches the
same copies with no duplicates. No production code changes.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 40dac07)

* Contest a twin npm lock that lacks the wired version

A twin lock that held the package only at another version still let the
wired ref through. npm keeps that entry only while it satisfies
package.json, and otherwise fetches the wired version unpatched from the
registry, so the lock alone can't vouch for it. The twin now contests
unless it has an entry for the same name@version at any path. Lock pairs
the rewriters keep in sync share that set, so they are unaffected.

Refs #798

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 5, 2026
* Start fix for #804

Assisted-by: Claude Code:claude-opus-5-5

* Fix rollback of pip-written pylock.toml

`pip lock` writes PEP 751's array-of-tables spelling
(`[[packages.wheels]]` with a `[packages.wheels.hashes]` sub-table),
but the hosted upstream restore only read inline `wheels = [{ ... }]`
arrays. Every pip sibling looked artifact-free, so `rollback`, `remove`
and the hosted -> vendored takeover always refused a pip lock with "no
sibling registry package shows ...", leaving users with a hosted patch
they could not undo.

The restore now reads artifacts in either spelling, writes the entry
back in the siblings' spelling, and, since pip records only the one
artifact it selected, restores only the release's wheel (or its sdist
when it has no wheel), refusing a release with several wheels. The
refusal no longer blames "this uv release" for a pip-written lock.

Fixes #804

Assisted-by: Claude Code:claude-opus-5-5

* Port vex alias test fix from #851

main has been red since #605 taught the npm copy resolver to probe
bundled store trees: two vex_consumed alias tests (#738) still assumed
the resolver never returns npm-aliased copies, so the CLI lib tests
fail on every PR's merge ref. This ports #851's tests-only fix so the
PR's CI reflects its own change; it no-ops once #851 lands on main.

Assisted-by: Claude Code:claude-opus-5-5

---------

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

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

npm agent-mode apply never patches an npm-aliased install (lp@npm:left-pad), yet VEX attests the package not_affected

3 participants