Skip to content

Plan pnpm rewires with one core for both modes (lock-model 2b) - #2029

Draft
Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
lock-model/2a-pnpm-readersfrom
lock-model/2b-pnpm-planner
Draft

Mikola Lysenko (mikolalysenko) wants to merge 7 commits into
lock-model/2a-pnpm-readersfrom
lock-model/2b-pnpm-planner

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Stack: PR 2b of the lock-model stack, based on #2009.

Hosted and vendored pnpm mode now plan with one core. They differ only in where a rewired entry points: a Socket URL, where hosted splices the instance's resolution:, or a committed .socket/vendor/ path, where vendored rekeys the entry and mirrors the override. The code is 202 implementation lines smaller than the base.

The shared core: formats/pnpm/plan.rs (pure)

  • Lock versions: one table for both targets (N16).
    • hosted_refuses_lock holds hosted's only refusal: pnpm 1's early shrinkwrap.
    • Vendored takes the grammars its rekey was captured against (sniff_lock_grammar: 5.4, 6.0, 9.0), and PnpmDialect::check_lock routes each dialect from it.
    • This replaces grammar::unsupported_early_shrinkwrap, formats::pnpm::check_v9_lock_version and the legacy check_lock_version.
  • match_key: one rule for how a key names name@version.
    • It finds the registry entry with its peer suffix, or a vendored rekey. A rekey is v9's name@file: or legacy's bare file:, read with origin::vendor_ref plus the artifact leaf.
    • It serves the hosted lookup and the hosted "vendored?" hint (vendored_packages is deleted), and in both vendored dialects target_key, lock_has_target_package, target_block (with the split-entry refusal) and the rekey edits.
  • Target::repoints: the per-instance check of the all-or-nothing gate. Hosted splices any peer context it can parse; vendored rekeys only the plain entry.
  • governing_pnpm_lock / planned_locks: which of a directory's pnpm-lock.yaml and shrinkwrap.yaml pnpm reads (N11, below). Hosted and discovery both use it.

The vendored v9 and legacy dialects now share more code:

  • one dep-pair revert (DepPair);
  • one block revert (revert_block, which re-sorts legacy's bare file: rekey);
  • one lazy dep-entry reader (dep_entries), used by the importer edit, the 6.0 root-dep edit, both ref gates and LockIndex::build;
  • one set of predicates for the v9 ref gate, whether it reads the lock index or scans;
  • one file reader for every vendored pnpm text read: vendoring, its pre-flight, the revert and the workspace revert (read_pnpm_text).

The refusal codes are unchanged. The 2a architecture test's writer allowlist shrinks from 6 to 4 walks for vendor/pnpm_lock_legacy.rs and stays at 3 for vendor/pnpm_lock.rs.

Behavior changes (each pinned by a test that fails before this PR)

  1. N15, CRLF. Vendored mode now edits a CRLF pnpm-lock.yaml or pnpm-workspace.yaml (a Windows autocrlf checkout) like its LF twin and writes it back CRLF, in both vendoring and the revert. This includes pnpm 11's two-document lock. Before, it refused these files with vendor_lockfile_crlf_unsupported.
    • A file that mixes CRLF and LF is still refused with the same code, because no single line ending writes it back byte-faithfully.
    • The revert now fails on such a file before any write (dry run included), and leaves every surface and the artifact as they are.
    • Tests:
      • crlf_lock_is_wired_like_its_lf_twin_and_stays_crlf
      • mixed_line_ending_lock_refuses_naming_line_endings
      • crlf_workspace_file_gains_the_override_in_its_own_section
      • crlf_checkout_of_a_vendored_project_reverts_cleanly
      • crlf_two_document_lock_reverts
      • mixed_line_ending_revert_fails_before_any_write
      • legacy crlf_is_wired_and_foreign_versions_refuse
      • only_a_mixed_line_ending_hosted_pnpm_lock_is_refused (takeover preview)
      • CLI scan_vendored_over_hosted_pnpm_crlf_lock_takes_over_and_stays_crlf
    • The CLI refusal cases and preflight_agrees_with_the_loop… now use a mixed-ending lock.
  2. N5, vendored planner. The vendored planner no longer treats a sibling checkout's ../.socket/vendor/… rekey as this project's entry. Before, parse_vendor_path found the anchor anywhere in the path. Test: a_sibling_checkouts_rekey_is_not_a_target. The reverter still finds its wiring with parse_vendor_path, because values such as pnpm 7/8's absolute specifier need it.
  3. Hosted "vendored?" hint. The hint now judges a rekey by its artifact leaf, which is how the vendored planner rekeys, instead of the entry's version: line.
    • A rekey with no version: line now gets redirect_pnpm_entry_vendored.
    • Another release's artifact under a matching version: line now gets redirect_pnpm_entry_not_found.
    • Tests: new cases in pnpm_vendored_diagnosis_reads_the_rekeyed_entry_through_origin.
  4. Message only. A lock with no lockfileVersion now reads "has no lockfileVersion in its head" and gets the re-lock remedy. An unparseable major (for example 'v10') keeps the update-socket-patch remedy. The error codes are unchanged.
  5. N11, shrinkwrap.yaml (owner decision: shadowed). Of a directory's pnpm-lock.yaml and pnpm <= 2 shrinkwrap.yaml, the planner rewrites only the one pnpm reads; the other is shadowed (governing_pnpm_lock).
    • pnpm >= 3 reads only pnpm-lock.yaml. pnpm 1 / 2 read only shrinkwrap.yaml, so when the project pins pnpm 1 / 2 (node_modules/.modules.yaml, else package.json packageManager), the stray pnpm-lock.yaml is the shadowed one.
    • With no evidence either way, pnpm-lock.yaml governs.
    • The shadowed file is never rewritten by hosted mode and is left out of the all-or-nothing gate. Before, an unparseable key in it, or pnpm 1's early-shrinkwrap head, refused the dependency in every lock.
    • Discovery picks the same governing file and sweeps the shadowed one with recognize_ignored.
    • Vendored mode reads the same decision. When a pnpm 1 / 2 pin makes shrinkwrap.yaml govern, it refuses with vendor_lockfile_version_unsupported (it does not write the shrinkwrap grammar) instead of wiring the stray pnpm-lock.yaml. With no pin it is unchanged. Test: a_pnpm_2_pin_beside_a_shrinkwrap_refuses, which fails without the gate.
    • A lone shrinkwrap.yaml is rewritten as before. A nested shrinkwrap is shadowed only by its own sibling.
    • formats/registry.rs keeps its HOSTED read role, which a lone shrinkwrap needs. The hosted engine's candidate list is unchanged; the planner drops the shadowed file. The hosted unwind can still restore an older run's pin in it.
    • Tests:
      • pnpm_shrinkwrap_beside_a_pnpm_lock_is_shadowed: a plain entry, an unparseable key, an early-shrinkwrap head, the nested case, a pnpm 2 pin and a lone shrinkwrap.
      • the_pinned_pnpm_major_picks_the_governing_lock: no evidence, pnpm >= 3, pnpm 2, and the install record outranking package.json.
      • discovery pnpm_shrinkwrap_only_without_pnpm_lock, now with a pnpm 2 pin.
      • All three fail with the old selection.
    • Docs: CLI_CONTRACT.md (pnpm hosted paragraph), docs/ecosystems.md, and divergences rows N11, now "Resolved (owner: shadowed) in PR 2b".

N16: kept as target-specific. Hosted reads every pnpm lock version except early shrinkwrap. Vendored keeps refusing versions other than 5.4, 6.0 and 9.0, because no captured pnpm file: override fixture proves the rekey for 5.3, 5.x≤5.2, 6.1 or 7.0. This is pinned by lock_versions_each_target_rewrites.

Unchanged: N14 (lockfile=false stays target-specific).

Review findings (/code-review high)

# Finding Verdict Fix
1 The revert's document check read raw CRLF text, so ---\r failed every revert of a CRLF two-document lock Confirmed (crlf_two_document_lock_reverts failed) The revert reads the lock through read_pnpm_text first.
2 Revert wrote mixed-ending lock and workspace files back all-LF Confirmed (mixed_line_ending_revert_fails_before_any_write failed) The revert fails before any write, as vendoring does.
3 One reader with one mixed-ending policy Done read_pnpm_text(root, file) serves the vendoring reads and the revert pre-read of the lock and the workspace file. is_mixed and read_refusal map its errors.
4 pnpm 1 / 2 read only shrinkwrap.yaml Confirmed governing_pnpm_lock reads the pin (utils::pnpm_workspace::pinned_pnpm_major, which pnpm_major_from now reuses), and hosted and discovery share it. There are tests for all three cases.
5 'v10' lost the update-socket-patch remedy Confirmed Restored for unparseable majors and pinned in lock_version_remedy_is_version_aware.
6 SectionIndex::build allocated format!("@{key}") per block Confirmed It now uses starts_with("file:") and match_indices("@file:") on the key directly.
7 The non-index ref gate built every importer entry before checking Confirmed dep_entries and importer_refs are lazy and borrowed; the gate stops at the first refusal.
8 Each block was classified 3–4 times Confirmed matching_blocks classifies once; target_block returns the block and its rekey flag; the curried closure is gone.
9 Target::check_lock's Option forced a dead .unwrap_or(V9) Confirmed Split into hosted_refuses_lock and the vendored dialect's sniff_lock_grammar check; no dead default.
10 Target::repoints early return was hard to read Done Now an explicit match per target.

Back-ports to hosted

I found no pnpm lock shape that vendored rewires and hosted does not. One vendored-only behavior is deferred: the warning about an unpatched file: copy of the same release (vendor_copy_unpatched, #935). It needs project I/O (discovery's pnpm_lock_file_copies) and belongs in the hosted engine, not the pure planner.

Golden diffs

None. committed_fixture_corpus_matches_golden passes unchanged, and no file under tests/fixtures/redirect/npm/pnpm changes.

Restack (2026-11-06)

Merge 1fec27a brings in #2002's review fixes and #2001's later counter fixes, through #2009. Nothing in this PR's code needed changing.

The formats architecture guard now bans the host policy (OriginPolicy, hosted_uuid(, SOCKET_PATCH_SERVER_HOST). It still allows crate::origin::vendor_ref, which plan::match_key uses: that function is path grammar and applies no host policy.

The merge also restores crates/socket-patch-core/src/origin.rs to the lock-readers group. The earlier routine merge had dropped it. Counted with the fixed counter:

Group Base (#2009) This PR
lock-readers 30,767 30,766
lock-writers 88,315 88,114

That is 202 lines net below the base. The table in the Size budget section below is from the old counter.

Tests after the restack:

  • cargo test -p socket-patch-core --lib -- origin:: vex:: formats:: vendor::lock_inventory:: vendor::pnpm patch::redirect:: hosted::: 2,537 passed. That includes the golden test.
  • CLI --test in_process_redirect_pnpm: 41 passed. --test in_process_vendor_pnpm_takeover: 16 passed.
  • clippy (workspace, all features): clean. fmt: clean.
  • test_loc_budget.py: OK.

Size budget (scripts/loc-budget.py --lower, then --check)

group base (#2009) this PR
lock-readers 31,207 31,206
lock-writers 88,314 88,113
combined 119,521 119,319 (−202)

Fixing the review findings and adding the N11 pin evidence grew the counts past this PR's first push (31,201 / 88,093), so I set the maxima to the new counts by hand in scripts/loc-budget.json, then ran --lower. Both stay below the base's maxima.

Tests

  • cargo test -p socket-patch-core --lib -- formats::pnpm vendor::pnpm vendor::lock_inventory:: vex::discover:: patch::redirect:: hosted:: vendor::pnpm_takeover utils::pnpm_workspace: 1,974 passed.
  • vex::discover::testing::golden::tests::committed_fixture_corpus_matches_golden: passed.
  • CLI targets:
    • in_process_vendor_pnpm_takeover: 16 passed
    • in_process_redirect_pnpm: 41 passed
    • in_process_vendor_pnpm_parent_child: 8 passed
    • in_process_vendor: 148 passed
    • in_process_rollback_vendored: 23 passed
    • scan: 248 passed
    • hosted_memory_parity: 42 passed
  • cargo clippy --locked --workspace --all-features -- -D warnings and cargo fmt --all -- --check: clean.
  • I did not run the e2e targets that need a real pnpm (e2e_vendor_pnpm_build, e2e_redirect_pnpm_build).

Generated by Claude Code

Add formats/pnpm/plan.rs, the planning core hosted and vendored mode
share. They now differ only in where a rewired entry points:

* Target::check_lock: one lock-version table. Hosted takes every lock
  but pnpm 1's early shrinkwrap. Vendored takes 5.4 / 6.0 / 9.0 and
  routes each dialect from it. This replaces
  unsupported_early_shrinkwrap, check_v9_lock_version and the legacy
  check_lock_version.
* match_key: one lookup of how a key names name@version, used by the
  hosted lookup, the hosted "vendored?" hint, and both vendored
  dialects' target, split-entry and rekey checks. It finds the registry
  entry with its peer suffix, or a vendored rekey read with
  origin::vendor_ref plus the artifact leaf.
* Target::repoints: the all-or-nothing gate's per-instance predicate.
  Hosted splices any parsable peer context; vendored rekeys the plain
  entry only.

Within the vendored planner, the v9 and legacy dialects now share the
dep-pair revert (DepPair), the block revert (revert_block, re-sorting
legacy's bare file: rekey), target_blocks, and dep_entries. dep_entries
also serves the importer edit, the 6.0 root-dep edit, the ref gate and
the lock index. The ref gate's index and scan paths run one set of
predicates.

Behavior changes, each pinned by a test:

* N15: the vendored planner and reverter edit a CRLF pnpm-lock.yaml or
  pnpm-workspace.yaml like its LF twin and write it back CRLF. Only a
  file mixing CRLF and LF is still refused
  (vendor_lockfile_crlf_unsupported).
* N5 (vendored planner): a sibling checkout's ../.socket/vendor rekey is
  no longer taken for this project's entry.
* The hosted "vendored" hint now judges a rekey by its artifact leaf, as
  the vendored planner does, instead of the entry's version: line.

N16 is kept as a target-specific refusal: no lock version beyond 5.4 /
6.0 / 9.0 has a captured fixture that proves the vendored rekey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
N5's vendored-planner part and N15 are resolved in PR 2b. N16 stays
target-specific for pnpm vendored mode. scripts/loc-budget.py --lower:
lock-readers 31207 -> 31201, lock-writers 88314 -> 88093.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision on divergence N11: pnpm never reads a pnpm <= 2
shrinkwrap.yaml it has a pnpm-lock.yaml for, so that file is shadowed.
The planning core's planned_locks leaves it out, so hosted mode never
rewrites it and it no longer takes part in the all-or-nothing gate.
Before, an entry the splice cannot repoint, or pnpm 1's early-shrinkwrap
head, refused the dependency in every lock. Discovery keeps sweeping it
for stale Socket identities, and vendored mode never reads it. A
shrinkwrap.yaml that is the only pnpm lock (nested ones are shadowed
only by their own sibling) is rewritten as before. No redirect golden
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read every vendored pnpm text file through one reader, read_pnpm_text.
  It normalizes CRLF and rejects mixed endings. The revert now reads a
  CRLF two-document lock: it used to fail on `---\r`. A lock or workspace
  file that mixes CRLF and LF now fails the revert before any write, as
  vendoring refuses it; the revert used to rewrite it all-LF.
- N11: a project that pins pnpm 1 / 2 (install record or package.json
  packageManager) reads only shrinkwrap.yaml, so there the stray
  pnpm-lock.yaml is the shadowed one. Hosted (planned_locks) and
  discovery share governing_pnpm_lock. The pin reader moves to
  utils::pnpm_workspace::pinned_pnpm_major, which pnpm_major_from reuses.
- sniff_lock_grammar: an unparseable lockfileVersion major keeps the
  update-socket-patch remedy.
- Perf: SectionIndex::build no longer formats a string per block.
  dep_entries is lazy and borrowed, so the non-index ref gate stops at
  the first refusal. matching_blocks classifies each block once, and
  target_block returns it with its rekey flag.
- Target::check_lock is split into hosted_refuses_lock and the vendored
  dialect's sniff_lock_grammar check, with no dead default.
  Target::repoints is an explicit match per target.

Budget, against the base: lock-readers 31206 (base 31207), lock-writers
88115 (base 88314).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vendored mode now reads the same plan::governing_pnpm_lock decision as
hosted mode and discovery. When the project pins pnpm 1 or 2 (install
record, else package.json packageManager), pnpm installs from
shrinkwrap.yaml and ignores pnpm-lock.yaml. Vendored mode does not write
the shrinkwrap grammar, so it now refuses with
vendor_lockfile_version_unsupported before any write, instead of wiring
the stray pnpm-lock.yaml. With no pin, nothing changes. Documented in
CLI_CONTRACT.md and the N11 divergence row.

The mixed-ending read error is now an io::ErrorKind::Unsupported error
instead of a custom type, which keeps lock-writers within budget
(88113).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Nov 6, 2026
Clears the conflicts left when the base took the 2026-11-05 owner
decisions and the loc-budget counter fix.

- lock-model-divergences.md: keep this PR's "Resolved in PR 2a" detail
  for N4, N5, N12 and N14, tagged with #2009, plus the base's PR links
  (#2025, #2029). N11 takes the base's owner decision (shadowed), which
  replaces "pending owner decision".
- loc-budget.json: the base re-measured lock-readers with the fixed
  counter (30259). This PR grows the group, so the max is raised by hand
  to its measured count under the new counter (30482).

Co-Authored-By: Claude <noreply@anthropic.com>
Clears the conflicts left when the base merged lock-model/1-origin.

- lock-model-divergences.md: keep this PR's PR 2b resolutions (N5, N11,
  N15, N16) and tag them #2029, take the base's #2009 tags for the PR 2a
  rows, its #2025 note on N5, and its P4 note on out-of-root includes.
- loc-budget.json: set both groups to their measured counts under the
  base's fixed counter (this PR shrinks both).

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merged lock-model/2a-pnpm-readers (1d27a20, which had just taken lock-model/1-origin) into this branch at cf29118 to clear conflicts (no force-push).


Generated by Claude Code

Brings in #2002's review fixes and #2001's later counter fixes through
2a. Restores crates/socket-patch-core/src/origin.rs to the lock-readers
group (lost in the earlier 1-origin merge). Budget at this branch's
counts: lock-readers 30766, lock-writers 88114, both below 2a's 30767
and 88315.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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