Skip to content

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

Merged
Mikola Lysenko (mikolalysenko) merged 17 commits into
lock-model/2a-pnpm-readersfrom
lock-model/2b-pnpm-planner
Nov 9, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 17 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.

Restack onto #2009's withhold decision (2026-11-09)

Merged lock-model/2a-pnpm-readers (a303dd6) twice; only the divergences N4/N5 rows conflicted (kept 2a's N4 and this PR's N5). #2009 now withholds a pnpm env-document (config-dependency) ref when the project document resolves the same release elsewhere. The planner side of that:

  • Hosted: the planner splices every instance across both documents (all or nothing), so a run either wires the project entry too (the env twin is then dropped and the project ref attests) or wires neither. The engine's copy check that names an unpatched copy (redirect_copy_unpatched) uses the same TreePart::withheld_by rule (from Read and plan pnpm locks through one core (lock-model 2) #2009), so an env pin withheld by a project file: copy is named and kept out of the in-run VEX.
  • Vendored: unchanged. It wires the project document only, and env copies never withhold a project ref. The existing vendor_config_dependency_unpatched warning still names the config dependency's own copy.

Also fixed while restacking: tests/pnpm_hosted.rs::early_pnpm1_refuses_non_durable_shrinkwrap_across_all_locks still asserted the pre-N11 rule. It failed on this branch from the N11 change on, but draft CI never ran it. It is now early_pnpm1_shrinkwrap_refuses_only_when_it_governs: a lone early shrinkwrap refuses, and beside a pnpm-lock.yaml it is shadowed.

Review findings, second pass (/code-review high, 2026-11-09)

# Finding Verdict
1 The hosted confirmation probe read a shadowed pnpm lock, so a stale Socket URL there "confirmed" a dependency and the attribution gate then dropped it as redirect_unattributable Fixed: confirm skips locks planned_locks leaves out. hosted::engine::tests::a_shadowed_pnpm_lock_confirms_nothing fails before
2 EditCtx::is_ours still uses parse_vendor_path, said to accept .socket/vendor/ anywhere Not a bug: parse_vendor_path is root-anchored on main (vendor::path::tests::ownership_parse_is_root_anchored rejects file:../.socket/vendor/…)
3 The pnpm trust/heal post-pass hard-coded the root pnpm-lock.yaml, so under a pnpm 1 / 2 pin a stray v9 lock planned a trustLockfile write Fixed: the root lock is the governing one (planned_locks). a_shadowed_pnpm_lock_is_no_heal_root fails before
4 Mixed line endings were signalled as io::ErrorKind::Unsupported, so an OS ENOTSUP read error read as a line-ending refusal Fixed: a dedicated MixedLineEndings error type, matched by type
5–7 The pnpm pin (package.json + .modules.yaml) was read for every candidate file (planned_locks), every vendoring run and every discovery Fixed: read only when both pnpm-lock.yaml and shrinkwrap.yaml exist, the one case it decides
8 Divergences N16 named the removed plan::Target::check_lock Fixed
9 read_project's doc still said it refuses CRLF Fixed (mixed endings only)
10 read_pnpm_text re-implemented LF normalization Fixed: uses utils::line_endings::to_lf

Tests run (2026-11-09, head 8b59a5a)

  • cargo test -p socket-patch-core --lib -- vex:: formats:: origin patch::redirect vendor:: hosted:: crawlers::npm_crawler utils::pnpm_workspace: 5251 passed, 0 failed (golden corpus included, unchanged).
  • cargo test -p socket-patch-core --test pnpm_hosted --test hosted_inventory --test redirect_golden --test upstream_restore_golden: 6 + 21 + 1 + 58 passed.
  • CLI: in_process_redirect_pnpm 42, in_process_vendor_pnpm_takeover 14, in_process_vendor_pnpm_parent_child 6, in_process_redirect 136, hosted_memory_engine 49, in_process_vendor 144, in_process_rollback_vendored 19, hosted_memory_parity 42 passed.
  • cargo clippy --locked --workspace --all-features -- -D warnings: clean. cargo fmt --all -- --check: clean.

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

Note

Medium Risk
Changes which pnpm lock files are rewritten and vendored CRLF/shrinkwrap rules—incorrect selection could miss pins or leave installs inconsistent, but behavior is narrowly scoped and heavily tested.

Overview
Adds formats::pnpm::plan, a shared planning layer for hosted and vendored pnpm rewires. Both modes now agree on which lock files to touch, how package keys map to name@version, and which peer suffixes each target can repoint—while edits still differ (hosted splices resolution:, vendored rekeys to file:).

N11 — shadowed shrinkwrap.yaml: When both pnpm-lock.yaml and legacy shrinkwrap.yaml exist, only the file pnpm actually reads is rewritten (default pnpm-lock.yaml; pnpm 1/2 pins flip to shrinkwrap). The shadowed file is skipped for the all-or-nothing gate so debris there cannot block redirects. Discovery and the hosted engine use the same governing_pnpm_lock / planned_locks logic.

N15 — line endings: Vendored mode now accepts uniform CRLF pnpm-lock.yaml (and keeps CRLF on write/revert). Mixed CRLF/LF still refuses with vendor_lockfile_crlf_unsupported (including revert pre-flight). Takeover tests shift from “all CRLF refused” to “mixed refused, pure CRLF succeeds.”

Other behavior: Hosted “vendored?” detection uses match_key (artifact leaf via vendor_ref) instead of heuristics; sibling ../.socket/vendor/… rekeys are ignored. Duplicate lock-version checks in grammar consolidate into plan. Vendored v9/legacy planners share more lookup/revert helpers (~200 LOC net reduction in lock-writer group per PR notes).

Contract docs (CLI_CONTRACT.md, lock-model-divergences.md, ecosystems.md) and extensive unit/CLI tests cover the above.

Reviewed by Cursor Bugbot for commit 97bee85. Configure here.


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>
… lock-model/2b-pnpm-planner

# Conflicts:
#	scripts/loc-budget.json
The refusals test merged in from main (#2015) expected a pure CRLF pnpm
lock to be refused with vendor_lockfile_crlf_unsupported. This branch
teaches the pnpm rewriter to wire pure CRLF locks byte-faithfully, so
that refusal now applies only to locks that mix CRLF and LF. Give the
test a mixed-ending lock so it still checks that a hosted pin's
takeover meets the pnpm lock-text gates.

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merged lock-model/2a-pnpm-readers (ad1abb4) into this branch to fix the merge conflict. New head is 64dfe54.

  • scripts/loc-budget.json: both sides changed the max values. I kept the base's numbers. Both groups are already over budget after the merge (31413 / 88426), and CI only warns on that.
  • Merging in Refuse vendored patches the same way everywhere #2015's vendor/refusals.rs brought a test that expects a pure CRLF pnpm lock to be refused with vendor_lockfile_crlf_unsupported. This PR now writes pure CRLF locks back byte-faithfully, so the test fails after the merge. I switched it to a lock that mixes CRLF and LF endings, which is still refused, so it still checks that a hosted pin's takeover goes through the pnpm lock-text gates. Please confirm that hosted takeovers of pure CRLF pnpm locks should now go through.
  • Local checks: cargo check and workspace clippy pass, and the refusals tests pass. Four other core tests failed locally: copy_tree symlink, vlt_heal unremovable lock, poetry and requirements write-failure. They test file permissions, so they fail because this sandbox runs as root. This merge doesn't touch them.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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.

Stale Bugbot comment from a previous run.

The hosted pnpm planner picks which of a root pnpm-lock.yaml /
shrinkwrap.yaml pair to rewrite from the pnpm major the project pins:
node_modules/.modules.yaml, else package.json's packageManager. The
hosted engine only read package.json beside an npm or yarn lock, so on
a fresh clone that pins pnpm 1 or 2 in packageManager (no install
record) hosted rewrote the stray pnpm-lock.yaml while pnpm installs from
shrinkwrap.yaml. Vendored mode and discovery read the pin from disk and
already chose shrinkwrap.yaml. Read the manifest as advisory input
whenever both root pnpm locks are present, so all three agree.

Addresses Bugbot 4500674838 on #2029.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aePC6sLFkqRM2XPWq4P
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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.

Stale Bugbot comment from a previous run.

- vendor/pnpm_lock_legacy.rs: kept the PR's move of the legacy revert
  paths onto the shared DepPair / revert_block helpers, and carried the
  base's (#2025) pnpm <= 8 machine-absolute ownership parse into them.
  DepPair gains a `parse` field (like the base's DepRef) and revert_block
  a `parse` argument; legacy passes parse_vendor_path_or_machine_absolute,
  v9 passes parse_vendor_path. Imports take the PR's list minus the now
  unused vendor_value_is_for.
- vex/discover/npm.rs: kept the PR's governing_pnpm_lock selection and
  added the base's `read` list of locks read (#556 branch-lock marks),
  recording the governing lock. The base's per-tree UnwiredCopy scopes
  merged cleanly.

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

Copy link
Copy Markdown
Collaborator Author

Merged lock-model/2a-pnpm-readers (f5215a4) into this branch. The new head is 97bee85.

  • vendor/pnpm_lock_legacy.rs and vendor/pnpm_lock.rs: kept this PR's shared DepPair/revert_block path. I carried the base's Never read or rewrite project files outside the git repository root #2025 machine-absolute vendored-path parse into it as a new parse field and argument. Legacy locks use parse_vendor_path_or_machine_absolute; v9 locks use parse_vendor_path.
  • vex/discover/npm.rs: kept governing_pnpm_lock. I added the base's read list, which the Hosted scan with pnpm gitBranchLockfile pins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556 branch-lock marks need. The base's per-tree UnwiredCopy scoping merged without conflict.
  • Local results:
    • cargo test -p socket-patch-core pnpm: 507 passed.
    • cargo test -p socket-patch-core vex: 804 passed.
    • The CI clippy invocation passes.
    • The full core suite fails only 4 tests. They rely on read-only directories, and these runs were as root.
  • Open question for review: formats::pnpm::plan::match_key (used by target_block and lock_has_target_package) matches rekeyed file: package keys by their root-relative path only. Before this PR, legacy also accepted the machine-absolute spelling there. pnpm writes package keys as relative paths, so I left it as is. No test covers it.

bugbot run


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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 97bee85. Configure here.

… lock-model/2b-pnpm-planner

# Conflicts:
#	docs/design/lock-model-divergences.md
… lock-model/2b-pnpm-planner

# Conflicts:
#	docs/design/lock-model-divergences.md
early_pnpm1_refuses_non_durable_shrinkwrap_across_all_locks asserted
the pre-N11 rule, where pnpm 1's early shrinkwrap refused the
dependency even beside a pnpm-lock.yaml. Beside one it is shadowed now,
so it refuses nothing and only pnpm-lock.yaml is rewritten; a lone one
still refuses. The test failed on this branch since the N11 change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The hosted confirmation probe skips a pnpm lock pnpm does not read
  (N11): a stale Socket URL in a shadowed shrinkwrap.yaml confirmed the
  dependency, which the attribution gate then dropped as
  redirect_unattributable.
- The pnpm trust/heal post-pass takes the governing root lock from
  planned_locks, so under a pnpm 1 / 2 pin a stray v9 pnpm-lock.yaml no
  longer plans a trustLockfile write.
- Mixed line endings get their own error type instead of overloading
  io::ErrorKind::Unsupported, which an OS ENOTSUP read also returns;
  read_pnpm_text normalizes through utils::line_endings::to_lf.
- The pnpm pin is read only when both pnpm-lock.yaml and
  shrinkwrap.yaml exist (planned_locks, vendoring, discovery).
- Docs: divergences N16 names the two checks; read_project refuses
  mixed endings, not CRLF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review November 9, 2026 21:09
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Not merged to main. GitHub marked this merged because the stack was folded: lock-model/2a-pnpm-readers was fast-forwarded to this PR's head. The pnpm stack is now reviewed and landed as #2009.

@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 8b59a5a into lock-model/2a-pnpm-readers Nov 9, 2026
80 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the lock-model/2b-pnpm-planner branch November 9, 2026 21:17
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Nov 9, 2026
- plan::root_lock picks the root's governing pnpm lock directly: the
  simplify pass derived it from the shadowed set, which named a missing
  pnpm-lock.yaml when shrinkwrap.yaml is the only lock. One
  pnpm_lock_leaf helper backs planned_locks, shadowed_locks and governs.
- Remove PnpmLock::vendored_in_use and its cache, which no production
  code calls since both dialects use PnpmDialect::entry_in_use; the
  tests read vendored_npm_uuids directly.
- parse_keys documents that it leaves every entry tagged Project, and
  PnpmLock::documents keeps collecting the tags actually present.
- drop_env_twins reuses its list array; the unification doc notes that
  #2029 was folded into #2009.

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