Skip to content

Fix vendored scans failing on nested npm projects (#1534) - #1543

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-vendored-nested-project-copies
Oct 14, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-vendored-nested-project-copies

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1534

Summary

Some repositories hold an independent npm project below the root, such as tools/x/ with
its own package-lock.json and node_modules. A root scan --mode vendored in such a
repository used to fail on every run (exit 1, partialFailure) and skip the in-run VEX.
It now leaves that project's packages to that project's own run and prints a
nested_project_skipped warning naming the PATH to pass. The exit code and status are
unchanged.

Root cause

The scan crawls every node_modules below the root, including a nested project's.
Agent and report-only scans assign each crawled copy to the project that owns it
(judge_nested_roots, #554). Hosted and vendored scans skip that step, so a vendored scan
passed tools/x's copy of left-pad to the root's package-lock.json backend. The root
lock has no entry for it, and the backend treats a missing entry as the hard
vendor_lock_entry_not_found refusal. The suggested fix (npm install) can't clear that,
so the run fails every time. Hosted mode only skipped the package (redirect_unconfirmed).

Fix

Test evidence

New crates/socket-patch-cli/tests/scan/scan_nested_project_e2e.rs, run with
cargo test -p socket-patch-cli --all-features --test scan scan_nested_project_e2e.
On main with the tests committed, 3 fail (red); with the fix, all pass (green).

#1534 symptom test main this PR
root vendored scan exits 1 on every run with vendor_lock_entry_not_found root_vendored_scan_warns_about_a_nested_projects_copies (2 runs) FAIL ok
--vex writes no in-run document root_vendored_scan_with_vex_writes_the_document FAIL ok
scan --mode vendored . tools/x stays red vendored_scan_of_root_and_nested_path_succeeds FAIL ok
control: a package the root also locks is still vendored at the root package_the_root_also_locks_is_still_vendored_at_the_root ok ok
control: a workspace member's stray lock is not a nested project workspace_member_with_stray_lock_is_not_a_nested_project ok ok
the remedy path is relative to the shell's directory under --cwd warning_names_the_path_from_the_shells_directory n/a ok

Unit tests: governing_root::tests::nested_npm_project_finds_the_independent_lock_root and
scan::discovery::tests::shell_word_quotes_only_what_the_shell_would_split.

Local checks:

  • cargo fmt --all -- --check: ok.

  • cargo clippy --workspace --all-features -- -D warnings: ok.

  • cargo test -p socket-patch-cli --all-features --test e2e_redirect_npm_build -- --ignored
    (real npm): 6 passed.

  • cargo test --workspace --all-features: every failure from this change is fixed. 10 tests
    still fail locally, all for environment reasons:

    • the sandbox runs as root, so read-only and write-failure injection has no effect. This
      covers the *_write_failure_* tests, repair_*_unremovable, copy_tree's read-only
      parent, and vlt heal_invalidation_failure;
    • e2e_bun_lockb fetches a blob from the live patch API, which isn't reachable here.

    None of them touch scan's npm discovery. CI runs them as a non-root user.

Review (/code-review high)

Fixed:

  • the suggested PATH was relative to the leg's directory and wasn't quoted;
  • errors fell open and dropped packages; they now keep every package with the root;
  • the plural wording is computed once.

Not fixed:

  • An independent nested project with no lock of its own is not detected. Agent mode's Agent-mode scan ignores socket.yml includePaths / ignorePaths (and the built-in tests/ default) for nested npm projects, patching every nested project's node_modules #554
    rule uses lock markers too, and without a lock there is no project boundary to read. A root
    vendored scan still refuses such a copy (vendor_lock_entry_not_found), as before. This
    is a narrower case than the one reported, which has a lock.
  • The . leg of scan . tools/x still warns about tools/x even though the next leg
    patches it. The warning is accurate for that leg, and silencing it would mean passing sibling
    PATHs into each leg.
  • nested_npm_project and ScanPolicy::nested_owner share a walk shape but use
    different rules. Agent mode doesn't exclude stray-lock members. Unifying them would change
    agent-mode ownership, which is out of scope here.
  • The second node_modules walk happens only when some patched package is a candidate,
    which is rare. Reusing NpmCrawlSnapshot would mean threading it through run_scan's
    agent path.
  • No per-directory memoization in nested_npm_project. It runs once per patched
    candidate, not once per crawled package.
  • scannedPackages still counts the nested copy. It counts what the crawl found; the
    copy just isn't selected. The in-run VEX test confirms the copy isn't attested.

🤖 Generated with Claude Code

https://claude.ai/code/session_0165v5yieeSffKsNoz1RbLKx


Generated by Claude Code


Note

Medium Risk
Changes npm discovery and lock-backend inputs for hosted/vendored scans; behavior is gated and covered by e2e tests, but wrong nested/root attribution could skip patching or still mis-wire locks.

Overview
Fixes #1534: root scan --mode hosted / vendored no longer treats patched npm packages that live only under a nested, independent npm project (own lockfile, not a workspace member) as root lock targets.

Behavior: After patch discovery, hosted/vendored root scans call nested_project_skips, which uses nested_npm_project to attribute crawled copies to nested lock roots, drops those purls when the root lock does not list them and the root install has no other copy, and emits one nested_project_skipped warning per nested project with a copy-pasteable socket-patch scan --mode <mode> <dir> path (shell-relative, POSIX-quoted when needed). Exit/status stay success instead of vendor_lock_entry_not_found / partial failure; in-run VEX no longer omits attestations for the root-only work.

Docs: New nested_project_skipped row in CLI_CONTRACT.md and npm crawl note in docs/ecosystems.md.

Tests: scan_nested_project_e2e.rs plus unit tests for nested_npm_project and shell_word.

Reviewed by Cursor Bugbot for commit 695464d. Configure here.

Assisted-by: Claude Code:claude-opus-5-5
A root scan --mode vendored over a repository with an independent
nested npm project (tools/x with its own lock) fails every run with
vendor_lock_entry_not_found and drops its in-run VEX (#1534). These
tests capture that, plus the scan . tools/x form and two controls: a
package the root also locks, and a workspace member's stray lock.

Assisted-by: Claude Code:claude-opus-5-5
Hosted and vendored scans rewire only the scan root's lockfiles, but
the crawl also reads node_modules of nested, independent npm projects.
A vendored root scan handed those copies to the root lock backend,
which refused them, so the run failed with exit 1 every time and wrote
no in-run VEX, with a remedy (npm install) that could not clear it.

Now a patched npm package is left out of a hosted or vendored run when
the root's lockfiles do not list it, its copy sits in a nested project
with its own lock (not a workspace member whose lock is ignored), and
the root's own install has no copy. One nested_project_skipped warning
per project names it and the PATH to pass. Exit and status unchanged.

Fixes #1534

Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the nested_project_skipped warning:

- the PATH it suggests is relative to the directory the command runs
  in (or absolute), not to a PATH-list leg or --cwd, and is quoted
  when a shell would split it;
- an unreadable node_modules keeps every package the root's instead of
  dropping one the root may install;
- npm purls are classified through Ecosystem::from_purl, the remedy
  spells each mode out so the remedy-command check parses it, and the
  e2e reads the envelope through common::envelope.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 14, 2026 19:18
@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 695464d. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants