Skip to content

fix(scan): parse pnpm v5.4/v6 lock grammars in the lockfile supplement - #203

Merged
Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
fix/pnpm-lock-inventory-legacy
Aug 19, 2026
Merged

fix(scan): parse pnpm v5.4/v6 lock grammars in the lockfile supplement#203
Mikola Lysenko (mikolalysenko) merged 8 commits into
mainfrom
fix/pnpm-lock-inventory-legacy

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Problem

scan's lockfile-only supplement (the fresh-clone / partial-install rescue path) discovered zero packages from pnpm 7 and pnpm 8 projects, silently: inventory_pnpm_lock_at parsed only the v9 name@version key grammar. v6 keys (/name@1.2.8:) produced a leading-slash name that is_safe_npm_name dropped fail-closed, and v5.4 keys (/name/1.2.8:) were skipped by the rfind('@') guard entirely. Confirmed against real pnpm-7/8-emitted locks in the 2026-08-18 e2e matrix.

Fix

  • New split_pnpm_key(base, legacy) handles all three grammars: peer-paren suffix trimmed first (v6/v9), one leading / stripped and remembered as legacy. Legacy keys may use the v5 name/version form — the segment after the last /, truncated at the first _ (v5 peer/hash suffix), must be digit-leading with no @ — which correctly parses v5 peered keys like /styled-components/5.3.3_react@17.0.2 and scoped v5 keys. Anything unparseable is skipped, never guessed (the function keeps its fail-closed character).
  • The probe-failure fallback that reads a root pnpm-lock.yaml is narrowed to the pnpm-specific version refusal (vendor_lockfile_version_unsupported). Previously it fired on any flavor-probe error, so a stale pnpm-lock.yaml left behind by a pnpm→yarn-berry/bun migration was inventoried as the project's live dependency set. (Post-merge with fix(scan): surface yarn PnP refusal instead of silent success-0 across all modes #201, both PnP-loader refusals — vendor_yarn_berry_unsupported and vendor_pnpm_pnp_unsupported — propagate as the UnsupportedNpmLayout diagnosis instead of falling back: under a PnP loader the installed-tree crawl is structurally empty, so the honest scan outcome is the surfaced refusal, not a lock-only inventory posing as a served project.)

Verification

  • Grammar tests quoted verbatim from real captured 5.4 and 6.0 locks (mkdirp/minimist shapes), plus synthetic scoped-v5, v5 _-peered, v6 paren-peered, and v9-unchanged cases asserting exact (name, version) pairs.
  • Migration regressions: stale pnpm-lock behind a .pnp.cjs marker or a bun lock is not inventoried (the PnP marker now surfaces the fix(scan): surface yarn PnP refusal instead of silent success-0 across all modes #201 layout diagnosis); a legacy 5.4 lock alone still is; a pnpm-PnP layout propagates vendor_pnpm_pnp_unsupported instead of inventorying the lock.
  • Full core suite green.

🤖 Generated with Claude Code


Note

Medium Risk
Changes read-only lock discovery and fallback routing for scan supplements; incorrect parsing or overly broad fallback could misreport dependencies on fresh clones, though wiring/patch paths remain gated by the flavor probe.

Overview
Lockfile-only scan no longer returns an empty dependency set for pnpm 7/8 projects whose pnpm-lock.yaml uses v5.4 (/name/version) or v6 (/name@version) package keys.

inventory_pnpm_lock_at now routes keys through split_pnpm_key, which strips peer parentheses, handles the leading /, and for legacy keys parses v5 slash-separated names/versions (including _peer/_hash suffix stripping) while still failing closed on unparseable keys.

When the npm flavor probe refuses the project, direct root pnpm-lock.yaml inventory runs only for pnpm-specific codes (vendor_lockfile_version_unsupported, vendor_pnpm_pnp_unsupported)—so stale locks left after pnpm→yarn/bun migrations are not treated as the live set. Rush common-lock fallback on other probe failures is unchanged.

Tests cover real v5/v6 lock shapes, exact v9 entry sets, migration regressions (yarn PnP / bun markers), and pnpm-PnP layouts that should still inventory.

Reviewed by Cursor Bugbot for commit 6596621. Configure here.

Fresh-clone scan of a pnpm 7 or pnpm 8 project discovered ZERO packages:
inventory_pnpm_lock_at parsed only the v9 `name@version` key grammar, so
v6 keys (`/name@1.2.8:`) produced a leading-slash name dropped fail-closed
by is_safe_npm_name, and v5.4 keys (`/name/1.2.8:`) were skipped outright.
Confirmed against real pnpm-7/8-emitted locks (2026-08-18 matrix).

- split_pnpm_key handles all three grammars: peer-paren suffix trimmed,
  one leading slash stripped and remembered as legacy; legacy keys may use
  the v5 name/version form (segment after the last '/', truncated at the
  first '_' peer/hash suffix, digit-leading, no '@') — correctly parsing
  v5 peered keys like /styled-components/5.3.3_react@17.0.2; scoped v5
  keys parse via the same rule.
- The probe-failure fallback that reads a root pnpm-lock.yaml is narrowed
  to pnpm-specific refusals (vendor_lockfile_version_unsupported,
  vendor_pnpm_pnp_unsupported): a stale pnpm-lock.yaml left behind by a
  pnpm->yarn/bun migration is no longer resurrected as the live dependency
  set.

Tests: grammar cases quoted verbatim from real captured 5.4/6.0 locks,
plus migration regressions (stale lock behind .pnp.cjs / bun lock is not
inventoried; legacy lock alone still is).

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 1 potential issue.

Fix All in Cursor

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

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

Reviewed by Cursor Bugbot for commit 6596621. Configure here.

Comment thread crates/socket-patch-core/src/vendor/lock_inventory.rs
…ouch trustLockfile, takeover reconciliation, revert guards (#213)

* feat(pnpm): full pnpm 7-12 vendor+hosted support — legacy lock grammars, zero-touch trustLockfile, takeover reconciliation, revert guards

Built and verified against real corepack-pinned pnpm 7.33.5 / 8.15.9 /
9.15.9 / 10.34.5 / 11.22.0 / 12.0.0-rc.7 (plus legacy 1-6 probes) in the
2026-08-18 e2e campaign.

HOSTED, pnpm 7/8: the v5.4/v6.0 refusal is replaced by a real rewrite —
every instance key of a dep is spliced (v5 /name/ver and _peer-suffixed,
v6 (peer)-parenthesized; each owns its resolution), one ledger edit per
instance; a post-splice residual detector refuses the dep set-wide if any
instance shape the splice regex cannot claim remains (no partial
rewrites). Frozen installs from empty stores land patched bytes on both
majors; tamper fails ERR_PNPM_TARBALL_INTEGRITY.

HOSTED, pnpm 11/12 zero-touch: rewriting a v9 root lock now auto-writes
trustLockfile: true into pnpm-workspace.yaml (create with scaffold or
byte-preserving append; ledger-recorded as redirect_pnpm_workspace_trust;
--no-trust-lockfile-config opt-out; only ErrorKind::NotFound creates —
an unreadable existing file falls back to guidance, never overwrite;
re-scans heal a missing config on already-redirected locks). pnpm 11.22
and 12-rc frozen installs succeed with no flags and no CI changes; 9/10
ignore the key (verified); the sha512 pin still fails closed under trust.
Warnings name the actual spliced host (userinfo stripped) and both
per-major error codes, and pre-empt pnpm 12's own rebuild-the-lock advice
that silently unpatches.

VENDOR, pnpm 7/8: new pnpm-legacy backend (flavor-stamped so older
binaries fail closed) — package.json pnpm.overrides + legacy lock surgery
emitting exactly what those majors serialize (byte-stable under pnpm's
own re-lock). pnpm <= 8 absolutizes file: specifiers, so frozen installs
are path-bound: surfaced as vendor_pnpm_legacy_absolute_specifier, with
plain `pnpm install --offline` as the moved-checkout path (marker bytes
verified). Windows-shaped canonical paths are normalized (verbatim prefix
stripped, forward slashes).

CONVERSIONS + SAFETY: vendoring over a hosted-redirected npm-family purl
now reconciles the redirect ledger (artifact-uuid-anchored matching —
version-exact, v5 underscore keys claimed; degraded ledgers keep edits
fail-closed), firing vendor_supersedes_redirect exactly once; vendor
--revert byte-restores the hosted lock from the wiring originals. All six
npm-family vendor backends refuse to delete an artifact the live lock
still references when a repair-reconstructed entry has no wiring
(vendor_wiring_unknown_revert_blocked — the revert-brick fix); repair
stamps detected flavors and preserves corrupt artifacts when no rebuild
source exists. Legacy-era diagnostics: shrinkwrap.yaml projects get
pnpm-flavored no-lockfile guidance and join the lockfile-only supplement;
vendored lock entries get redirect_pnpm_entry_vendored instead of
entry-not-found.

Tests: e2e_redirect_pnpm_build.rs (new hosted capstone: pnpm 7-11 real
corepack legs incl. the zero-touch pnpm 11 proof, tamper negative,
hermetic v5/v6 legs), e2e_vendor_pnpm_build.rs ladder (@9/@10/@11 + real
pnpm 7/8 lifecycle legs), takeover/reconciliation/guard/heal unit + e2e
suites — all RED-verified where behavior changed. CLI_CONTRACT.md and
docs/ecosystems.md updated.

Stacked on #203 (lock-inventory legacy grammars); trivially overlapping
test hunks with #204/#208 carry identical content.

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

* fix(pnpm-legacy): moved-checkout recovery needs --no-frozen-lockfile (pnpm defaults frozen on under CI)

CI caught what local runs could not: pnpm turns --frozen-lockfile ON when
CI=true, and the pnpm <= 8 moved-checkout recovery works precisely by
re-resolving the path-bound absolute specifier — frozen semantics skip
that re-resolution (pnpm 8: ERR_PNPM_OUTDATED_LOCKFILE; pnpm 7:
stale-path install). The lifecycle legs' recovery step now passes
--no-frozen-lockfile explicitly, and the vendor_pnpm_legacy_absolute_
specifier remedy (warning text, module doc, CLI_CONTRACT.md,
docs/ecosystems.md) recommends `pnpm install --offline
--no-frozen-lockfile` so real CI users get working advice.

Verified: the full capstone (10 legs incl. real pnpm 7/8 lifecycles)
passes under CI=true locally.

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

* test(pnpm-legacy): lock oracle uses the real path normalizer (Windows byte-exactness)

Windows CI proved the production normalizer right and the test oracle
wrong: the hermetic splice legs built their expected absolute specifier
with raw canonicalize().display() — the \\?\C:\ verbatim form the
normalizer exists to strip. normalize_canonical_root is now pub and the
oracle consumes it at both assertion sites, so the expected string is
built by the same transformation the backend writes and cannot drift.

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

* test(pnpm-legacy): in-file oracles also use the shared root normalizer

Windows CI surfaced the same oracle-drift bug in the module's own unit
tests: the fixture helper handed raw canonicalize().display() (verbatim
\\?\C:\ form) to the ROOT_TOKEN substitution, the no-leak contains probe,
and the moved-checkout fixture builder. All three now go through a
canon_root_str() helper built on normalize_canonical_root, so every
oracle spells the root exactly as the splice writes it.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… embedded vex advisories ride the envelope

Review feedback, two fixes:

1. The probe-failure discovery fallback inventoried a root pnpm-lock.yaml
   on vendor_lockfile_version_unsupported without looking at siblings — a
   pnpm→yarn/npm migration leaving an old refused lock behind surfaced
   DEAD pnpm resolutions as the live dependency set. The fallback now
   probes sibling live locks in the flavor router's own precedence
   (bun.lock, yarn.lock classic/berry, npm-shrinkwrap/package-lock)
   first; a sibling with entries wins, a present-but-empty sibling
   suppresses the legacy read entirely (blind beats dead resolutions),
   and only a genuinely lone legacy lock is inventoried as before.
   RED-verified: pre-fix, stale-lock-beside-live-yarn returned the dead
   pnpm dep. Note: supported legacy versions (5.4/6.0) route to the
   pnpm-legacy flavor at the probe and are governed by the router's
   documented pnpm-beats-yarn precedence (vendor_multiple_lockfiles
   warning) — this fix covers the versions the probe refuses.

2. Embedded --vex advisories (product_not_iri, vendored_tree_out_of_sync)
   were machine-invisible under a host's --json: note_warning silences
   stderr there and only the standalone vex envelope copied them out.
   VexSummary gains an additive warnings field (skip-if-empty; same
   RunWarning shape as the standalone envelope) populated by the apply,
   scan, and vendor hosts. Pinned by
   in_process_vendor::vendor_json_vex_warnings_ride_in_envelope
   (product advisory + the tree-sync disclosure + no-false-positive
   control), RED-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit's host hunks read VexWriteSummary.warnings, which is
introduced by the vex diagnostics change (vex.rs note_warning plumbing +
core verify.rs out-of-sync flag). Carrying those two files here keeps
this branch self-contained; content is byte-identical to the sibling vex
PR, so either merge order resolves cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflicts:
- crates/socket-patch-cli/src/commands/vendor.rs: kept the branch's
  doc comment on the VexSummary warnings channel (code identical on
  both sides).
- crates/socket-patch-cli/tests/in_process_vendor.rs: both sides
  appended tests at the same point — kept BOTH the branch's
  hosted_to_vendor_conversion module (#213) and main's section-12
  drift-skip revert tests (#205).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er pre-revert

The hosted→vendored conversion test pinned the pre-#206 semantics: vendor
over a live hosted redirect fired the vendor_supersedes_redirect warning
once and embedded the HOSTED splice as the wiring original. Main's #206
changed the contract — vendor now PRE-REVERTS the redirect
(vendor_takeover_reverted_redirect), records the PRISTINE registry
fragments as its originals, and --revert restores the registry lock.

Rewritten as the pnpm twin of mode_migration_npm.rs's assertions: takeover
advisory fires, supersede warning never does, no hosted residue in lock or
vendor ledger, revert round-trips to the pristine registry lock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflicts (crates/socket-patch-core/src/vendor/lock_inventory.rs):

Main's #201 rewired inventory_npm_lock to
Result<Option<...>, UnsupportedNpmLayout>, propagating the PnP-loader
refusals (vendor_yarn_berry_unsupported, vendor_pnpm_pnp_unsupported) as a
layout diagnosis instead of the calm None. This branch's #203/#213 work
added lockfile fallbacks on the pnpm refusal codes, including reading the
root lock under pnpm's node-linker=pnp.

Resolution: main's semantics win for BOTH PnP codes — under a PnP loader
the installed-tree crawl is structurally empty, so the honest scan outcome
is the propagated diagnosis, not a lock-only inventory posing as a served
project. The branch's fallbacks survive for the non-PnP refusals:
vendor_lockfile_version_unsupported (live-sibling check, else direct root
pnpm-lock.yaml read) and vendor_lockfile_missing (pnpm <=2 shrinkwrap.yaml).

Test reconciliation:
- pnpm_pnp_layout_still_inventories_root_lock →
  pnpm_pnp_layout_propagates_the_diagnosis (asserts the Err).
- stale_pnpm_lock/stale_shrinkwrap behind a yarn-berry PnP marker now
  assert the propagated yarn diagnosis instead of None (same protective
  intent, new channel).
- unsupported_flavors_yield_none's v6 comment updated: post-#213 the probe
  passes 6.0; the None comes from the dep-less lock.
- Branch tests mechanically adapted to the Result signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 635d1d9 into main Aug 19, 2026
42 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/pnpm-lock-inventory-legacy branch August 19, 2026 16:35
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