Skip to content

fix(scan): surface yarn PnP refusal instead of silent success-0 across all modes - #201

Open
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/yarn-pnp-scan-silent-noop
Open

fix(scan): surface yarn PnP refusal instead of silent success-0 across all modes#201
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
mainfrom
fix/yarn-pnp-scan-silent-noop

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

P0: yarn Plug'n'Play projects silently no-op in every mode

On any yarn PnP project (yarn 2/3/4, plain PnP and zero-install), all three modes silently did nothing:

  • scan --json --yes --mode agent|hosted|vendored exited 0 with status: "success", scannedPackages: 0, no warnings (hosted additionally printed an empty redirect block). The user believed they were protected; nothing was checked.
  • apply --json exited 0 with status: "noManifest" — the documented loud refusal (errorCode: yarn_pnp_unsupported, pinned by tests/e2e_safety_yarn_pnp.rs) was unreachable, because scan (which cannot discover PnP packages) never writes a manifest.

Matrix evidence (adversarially verified, 11 cells, one root cause)

Empirically reproduced with real production patch data on yarn 2.4.3 / 3.8.7 / 4.6.0 across agent/hosted/vendored × plain-PnP/zero-install: every PnP cell was a silent success-0 no-op + apply noManifest. The 18 supported (node-modules linker) cells passed, and yarn 2/3 nm refusal paths were clean — this is PnP-specific, one root cause. Intact repro dirs live under worktrees/yarn-matrix-e2e/target/matrix/ (e.g. y4-agent/y4-agent-pnp, y4-hosted/y4-hosted-zero).

Root cause (at da58dee)

  • crates/socket-patch-core/src/vendor/npm_flavor.rs:125-149detect_npm_lock_flavor correctly diagnoses PnP via the .pnp.* markers (vendor_yarn_berry_unsupported / vendor_pnpm_pnp_unsupported) before any lockfile sniff.
  • crates/socket-patch-core/src/vendor/lock_inventory.rs:109-115inventory_npm_lock matched Err(_) and fell through to the Rush fallback returning None, discarding the diagnosis, so the lockfile-supplement discovery leg (scan/discovery.rs) returned empty with no warnings channel.
  • Under PnP node_modules/ is absent, so the crawler leg is also empty; scan hit the package_count == 0 early-return (scan/mod.rs) and printed success before any mode-specific gate ran.
  • apply's PnP check (apply.rs, detect_npm_pkg_managerYarnBerryPnP) sat below the noManifest early-return, so it never fired without a manifest.

Fix

  1. core: inventory_npm_lock now returns Result<Option<…>, UnsupportedNpmLayout>, propagating the two PnP diagnosis codes instead of conflating "unsupported layout" with "no lockfile". Every other probe error (missing lock, bun.lockb, unsupported versions) keeps the Rush fallback and the calm Ok(None). New inventory_project_diagnosed exposes the refusals; inventory_project keeps its signature (vendor/repair callers unchanged).
  2. scan: additive top-level warnings[] ({code, detail} objects, omitted when empty) on both the empty-discovery and the non-empty (polyglot) JSON envelopes, plus a stderr Warning (<code>): … line on the human path — in all three modes. Codes: yarn_pnp_unsupported (one vocabulary with apply's refusal; remedy yarn patch <pkg>) and pnpm_pnp_unsupported (the node-linker=pnp twin, surfaced through the same channel).
  3. apply: the YarnBerryPnP refusal is hoisted above the noManifest early-return (extracted into refuse_yarn_pnp; identical envelope + exit 1 as the with-manifest gate), so a PnP checkout refuses loudly even when no manifest exists.
  4. CLI_CONTRACT.md: documents the scan warnings[] channel and the deliberate noManifest carve-out.

Exit semantics — deliberately conservative

  • scan: exit code and status unchanged (exit 0 / success + warnings entry). Precedent: hosted refusals already exit 0 with redirected: 0 (known, deliberately-unchanged residual from the pnpm sweep). The warning is the machine-readable signal that nothing was checked.
  • apply: keeps the existing yarn_pnp_unsupported semantics (exit 1) — now also on the no-manifest path. Note: a PnP project with an install hook and no manifest previously "passed" installs silently; it now fails them, exactly as it already did once a manifest existed.

Tests (TDD, RED first — see commit order)

  • Commit 1 (7e442ba) adds 7 e2e tests that fail on main (7 failed / 20 passed / 3 ignored): scan agent/hosted/vendored JSON warning + whole-tree no-mutation snapshot, human-mode stderr, the pnpm twin, apply no-manifest JSON + human refusal.
  • Commit 2 fixes and rewrites the lock_inventory unit test that pinned the swallow (unsupported_flavors_yield_none) into pnp_layouts_propagate_the_diagnosis_instead_of_yielding_none (PnP → propagated diagnosis; genuinely-None cases kept). The unit rewrite rides the fix commit because the new assertion cannot compile against the old signature.
  • Green: full cargo test -p socket-patch-core (22 binaries, 0 failures incl. 2265 lib tests), full default cargo test -p socket-patch-cli (0 failures; e2e_safety_yarn_pnp 27 passed), cargo clippy --workspace --all-features -- -D warnings clean. Formatted only touched files (no cargo fmt --all).
  • Manual before/after on a throwaway copy of the real yarn-4 PnP repro (y4-agent-pnp): old binary → silent success-0 + noManifest exit 0; new binary → warnings[]: yarn_pnp_unsupported in all three modes (hosted keeps its empty redirect block) and apply --jsonerror.code: yarn_pnp_unsupported, exit 1; no files touched, no .socket/ created.

🤖 Generated with Claude Code


Note

Medium Risk
Behavior change for PnP projects: install-hook apply without a manifest now exits 1 instead of silent 0, and scan contract adds machine-readable warnings while keeping success exit codes—intentional but user-visible for CI and hooks.

Overview
Fixes a P0 where yarn and pnpm Plug'n'Play projects looked healthy while npm deps were never discovered: empty crawler + lockfile inventory that swallowed PnP diagnosis produced scan exit 0 / scannedPackages: 0 with no warnings, and apply fell through to calm noManifest exit 0.

Coreinventory_npm_lock now returns Result and propagates UnsupportedNpmLayout for PnP loaders instead of treating them like “no lockfile.” inventory_project_diagnosed exposes those refusals alongside other ecosystems’ lock entries; inventory_project unchanged for vendor/repair callers.

scan — Lockfile supplement collects layout refusals and surfaces them as additive top-level warnings[] ({code, detail}) on JSON envelopes (empty and polyglot non-empty paths) plus Warning (<code>): … on stderr. Codes align with apply: yarn_pnp_unsupported, pnpm_pnp_unsupported. Exit code and status stay exit 0 / success (same posture as hosted refusals).

apply — Yarn berry PnP detection runs before the missing-manifest early return via shared refuse_yarn_pnp, so PnP checkouts get exit 1 / yarn_pnp_unsupported even when scan never created .socket/.

CLI_CONTRACT.md documents the scan warning channel and the noManifest carve-out. E2e coverage in e2e_safety_yarn_pnp.rs pins all three scan modes, human stderr, pnpm twin, and no-manifest apply.

Reviewed by Cursor Bugbot for commit cd894fe. Configure here.

…efusal

On any yarn Plug'n'Play project, all three scan modes exit 0 /
status=success / scannedPackages=0 with no warning (the crawler leg is
empty because node_modules is absent, and the lockfile-supplement leg
swallows the flavor probe's PnP diagnosis), and apply exits 0 with the
calm noManifest status because the documented yarn_pnp_unsupported
refusal sits below the noManifest early-return — unreachable when scan
never wrote a manifest.

These tests fail on current main (7 failed / 20 passed / 3 ignored):
- scan --json --yes --mode agent|hosted|vendored on a PnP fixture must
  surface an explicit warnings[] refusal (exit semantics deliberately
  unchanged) and touch no file
- human-mode scan must print the refusal to stderr
- the pnpm node-linker=pnp twin surfaces its own diagnosis
- apply --json / human with NO manifest must refuse loudly with the
  existing yarn_pnp_unsupported envelope, not silent noManifest

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s all modes

Root cause (verified on da58dee):
- vendor/npm_flavor.rs detect_npm_lock_flavor correctly diagnoses PnP
  loaders (vendor_yarn_berry_unsupported / vendor_pnpm_pnp_unsupported)
  before any lockfile sniff, but lock_inventory.rs inventory_npm_lock
  matched Err(_) and fell through to the Rush fallback returning None,
  DISCARDING the diagnosis — so scan's lockfile-supplement leg came back
  empty with no warnings channel.
- Under PnP node_modules/ is absent, so the crawler leg is also empty;
  scan hit the package_count==0 early-return and printed status=success
  / scannedPackages=0 in all three modes (hosted: plus an empty
  redirect block).
- apply's YarnBerryPnP check sat BELOW the noManifest early-return, so
  the documented loud yarn_pnp_unsupported refusal was unreachable when
  scan (which cannot discover PnP packages) never wrote a manifest.

Fix:
- core: inventory_npm_lock now returns Result<Option<..>,
  UnsupportedNpmLayout>, propagating the two PnP diagnosis codes;
  every other probe error keeps the Rush fallback and the calm
  Ok(None). New inventory_project_diagnosed exposes the refusals;
  inventory_project keeps its signature.
- scan: an additive top-level warnings[] ({code, detail}, omitted when
  empty) rides both the empty-discovery and the non-empty (polyglot)
  JSON envelopes, plus a stderr 'Warning (<code>): ...' line on the
  human path. Codes: yarn_pnp_unsupported (same vocabulary as apply's
  refusal, remedy 'yarn patch <pkg>') and pnpm_pnp_unsupported (the
  node-linker=pnp twin, pnpm remedies). Exit code and status field are
  DELIBERATELY unchanged (exit 0 / success — same posture as hosted
  refusals, which exit 0 with redirected=0).
- apply: the YarnBerryPnP refusal is hoisted above the noManifest
  early-return (extracted into refuse_yarn_pnp, same envelope + exit 1
  as the with-manifest gate). CLI_CONTRACT.md documents the carve-out.

Tests: the previous commit's 7 RED e2e tests now pass (scan agent/
hosted/vendored JSON warning + no-mutation, human stderr, pnpm twin,
apply no-manifest JSON + human refusal); the lock_inventory unit test
that pinned the swallow now asserts the propagated diagnosis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.

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

Reviewed by Cursor Bugbot for commit cd894fe. Configure here.

// yarn-PnP silent success-0 no-op.
if !layout_refusals.is_empty() {
result["warnings"] = layout_refusal_json(&layout_refusals);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Conflicting install hint after PnP

Low Severity

On an empty human-mode yarn PnP scan, the new yarn_pnp_unsupported warning is followed by No packages found. Run … install first. Running install does not create a crawlable node_modules/ under Plug'n'Play, so that second message conflicts with the refusal's yarn patch remedy and can send users down a dead end.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cd894fe. Configure here.

#[test]
fn apply_without_manifest_on_pnp_project_refuses_in_human_mode() {
let dir = tempfile::tempdir().unwrap();
make_yarn_berry_project(dir.path());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bare unwraps in new tests

Low Severity

New PnP e2e helpers and cases use bare .unwrap() on tempdir, fixture writes, and JSON field access. Team guidance for new test code prefers .expect("…") with a step-specific message so CI failures name the setup or assertion that broke instead of a generic unwrap panic.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Prefer .expect("context") over bare .unwrap() in test code

Reviewed by Cursor Bugbot for commit cd894fe. 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

Development

Successfully merging this pull request may close these issues.

1 participant