Skip to content

Read and plan pnpm locks through one core (lock-model 2) - #2009

Merged
Mikola Lysenko (mikolalysenko) merged 56 commits into
mainfrom
lock-model/2a-pnpm-readers
Nov 9, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 56 commits into
mainfrom
lock-model/2a-pnpm-readers

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Problem

Lock-model 2: pnpm locks were read by several separate walks (inventory, discovery, store crawler, hosted planner, vendored planners). Hosted and vendored rewires each had their own planner. They disagreed on the env lockfile document, Rush locks, lockfile=false, vendored rekeys, CRLF and a shrinkwrap.yaml beside pnpm-lock.yaml (divergences N4, N5, N11, N12, N14–N16).

Fix

  • One reader. formats::pnpm::PnpmLock walks every document once and tags each entry Project or Env (the env document is the first one, when its importers carry only config/packageManager deps). Every reader goes through it, and an architecture test enforces that.
  • Env document = its own install tree (TreePart::PnpmEnv), never contested across locks; an env twin of a project ref is dropped.
  • One planner core (formats/pnpm/plan.rs): match_key, lock-version checks, governing_pnpm_lock. Hosted splices the resolution, vendored rekeys to file:.
  • N11 a shrinkwrap.yaml beside pnpm-lock.yaml is shadowed unless pnpm 1/2 is pinned. N12 Rush locks always inventoried. N14 lockfile=false drops hosted integrity. N15 vendored edits CRLF files (mixed endings refused).

Owner decision: withhold env-document refs

An env-document (config-dependency) ref is not attested when the project document of the same lock resolves the same name@version elsewhere: a VEX statement covers the release, not one tree. Via TreePart::withheld_by (project copies reach env refs, never the reverse), a registry entry makes it rewirable, a file: copy shadowed, a bundled copy Unattested. The hosted copy check uses the same rule; rollback restores env entries bare.

Where to look

  1. vex/discover/mod.rs: TreePart::withheld_by, contest_within_locks, drop_env_twins
  2. formats/pnpm/mod.rs PnpmLock; grammar.rs documents
  3. formats/pnpm/plan.rs: match_key, governing_pnpm_lock, planned_locks / shadowed_locks / root_lock
  4. vendor/pnpm_lock.rs: PnpmDialect, read_pnpm_text, shared revert
  5. hosted/engine.rs: confirm, pnpm_trust, copy check

Testing

  • Core lib (vex:: formats:: origin patch::redirect vendor:: hosted:: crawlers::npm_crawler utils::pnpm_workspace): 5254 passed. The discovery golden corpus is unchanged.
  • Core pnpm_hosted, hosted_inventory, redirect_golden, upstream_restore_golden; CLI in_process_redirect_pnpm, …_vendor_pnpm_takeover, in_process_redirect, hosted_memory_engine, in_process_vendor: pass. clippy, fmt clean.
  • LOC: scripts/loc-budget.py is over budget on both groups, as main already is (31,756 / 89,445); this PR adds +294 / −150 over main. CI only warns; loc-budget.json unchanged.

Folds #2029

#2029 (pnpm planner) is folded in here and closed.

Follow-ups: #2044

Review history

Fixed:

  • Revert: CRLF ---\r two-document locks; mixed endings now fail before any write
  • Architecture guard skipped code after an early test module
  • An env-shaped later document opted out of contests
  • Hosted "vendored?" hint matched name only and re-parsed per dep
  • Env instance displaced a project instance in the inventory collapse
  • Entries tagged by body offset; env twin used a pseudo path
  • Rush: flavor label, double stat, shadowed by the root lock
  • Env twin survived a withheld project ref; env bundle mark withheld the project twin
  • Hosted copy check ignored env pins; rollback gave env entries a tarball:
  • Shadowed lock confirmed by substring; pnpm 1/2 pin let a stray lock drive the trust write
  • Mixed endings overloaded io::ErrorKind::Unsupported; pin read when one lock existed
  • 'v10' lost its remedy; stale pnpm_hosted test; dead vendored_in_use; simplify's lone-shrinkwrap.yaml root regression

Declined:

🤖 Generated with Claude Code

Lockfile discovery (vex/discover), the lock inventory
(vendor/lock_inventory) and the hosted and vendored writers each read the
same project files their own way: two Socket-origin classifiers, three
Python precedence rules, second walks of package-lock, yarn, vlt and pnpm
locks, a second parse of uv/pylock/script locks, five Cargo.toml
dependency-table walks, three readers of Maven's checksums.sha256. The
inventory also has no NuGet, JVM, Hatch or Deno models.

docs/design/lock-model-unification.md describes the target, one
per-project model whose entries carry an Origin (Registry, Hosted,
Vendored, Other, Rejected), file selection by install tree (precedence
within a tree, union across trees), and the stack of PRs that gets there.

scripts/loc-budget.py counts implementation lines (no comments, no
cfg(test) items, no test-only modules) for two groups in
scripts/loc-budget.json: lock-readers (30,898 lines today) and
lock-writers (88,363). The lint-ecosystems job runs it with --warn, which
annotates a group over budget but never fails, so bug fixes in these files
are not blocked; each PR in the stack lowers the budget as it deletes
code.

No product code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
formats/ models are guarded to stay free of I/O and of the host policy
(formats/mod.rs architecture_tests bans hosted_patch_uuid there), so the
classifier is a top-level `origin` module and a project layer applies it.
Spell out the three layers and where each part of the classifier moves
from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decisions: inconsistencies between discovery, the inventory and the
two modes are bugs to remove, not behavior to preserve; hosted and
vendored share one core and differ only in where a rewired entry points;
when in doubt the hosted mode's behavior wins, and every such call is
listed in the doc so it can be overruled. Behavior changes are allowed
per PR, listed, pinned by tests and shown as golden diffs.

First decision recorded: pnpm 11's env-lockfile document counts as its
own install tree, as the hosted planner already treats it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/design/lock-model-divergences.md lists every question on which
discovery, the inventory, hosted and vendored mode answer differently
today, across all ecosystems, with the resolution under the principles
and flags for false attestations and target-specific differences.

Owner decisions recorded: the modes are ground truth over the readers;
vendored mode's Maven profile and reactor support and its scala-cli wiring
are back-ported to hosted as the planners merge (the JVM PR splits into
readers, Maven planner, and Gradle/sbt/scala-cli). One call is pending:
whether hosted keeps rewriting a shrinkwrap.yaml that pnpm never reads.

Corrects the install-tree section: npm-shrinkwrap.json does not shadow
package-lock.json; every consumer but the inventory already reads both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decisions: a dependency the user patched themselves is never
patched or attested by default, in every mode and package manager, unless
the user explicitly overrides; and no reader or writer reads project files
outside the git repository root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discovery's classifier (the memoized hosted_uuid fast path, vendor_ref
and its variants, locate, Wired, the socket-patch-<uuid> name grammar)
and the redirect's hosted URL rule (hosted_patch_uuid,
hosted_patch_url_uuids, SOCKET_PATCH_SERVER_HOST) now live in
crates/socket-patch-core/src/origin.rs, with their tests.

The code moves verbatim. DiscoverCtx holds an OriginPolicy (the
per-run origin allowlist and its memoized host set) and delegates
hosted_uuid and locate to it. vex::discover and patch::redirect
re-export the moved items, so no caller changes.

origin.rs sits outside formats/ on purpose: format models are guarded to
apply no host policy (formats/mod.rs architecture_tests).

Budget: lock-readers 30,898 -> 30,976 (49 lines moved in from
patch/redirect, ~30 lines of OriginPolicy and re-export glue);
lock-writers 88,363 -> 88,314.

No behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PnpmLock is now the one pnpm entry walk, over every document of the
lock. Each entry is tagged with its document (PnpmDocument::Project or
Env), and documents are told apart by content: in a multi-document
lock, one whose importers carry only configDependencies /
packageManagerDependencies is pnpm 11+'s env lockfile, every other
document is a project one. grammar::main_document (the text after the
last `---`) is gone, so a crafted lock can no longer pick its own
"project" and hide the rest (lock-model divergence N4).

Behavior changes (owner decision: the env document counts and is its
own install tree):

- The inventory (PnpmLock::entries), PnpmLock::resolves, the legacy
  vendored in-use check (vendored_in_use) and InstalledPackages read the
  env document too, as the hosted planner and the v9 in-use check
  already did. env_document_is_not_in_the_inventory pinned #1949's skip;
  it becomes env_document_is_read_as_its_own_tree.
- Discovery emits refs for Socket wiring in the env document. Its
  records are keyed `<lock> (env lockfile)` while the contests run, so
  the project document's registry entries, file: copies and bundled
  copies never contest its refs (and the reverse), its wiring does not
  exempt the project document from contesting another lock, and it is
  skipped by the cross-lock contest both ways (config dependencies
  install apart from every node_modules). finalize maps the key back to
  the lock path. Pinned by pnpm_env_document_is_its_own_install_tree.
- A lock with several non-env documents reads all of them as project
  documents (before: only the last). Pinned by
  documents_are_identified_by_content_not_position.

InstalledPackages::from_lock_text is rebuilt on PnpmLock (keeping its
last-wins name:/version: rule and its None cases). pnpm writes the
current lockfile (node_modules/.pnpm/lock.yaml) as the project lock
alone and installs config dependencies outside the virtual store, so an
env document is not expected there; if one appears its entries keep
their store entries, the side that never hides an installed package.
The vendored set is computed lazily (OnceLock), so the crawler does not
pay for it. lock_text_has_member_importers, pnpm_settings_major and the
hosted unwind's tarball evidence read documents by content too (the
latter through PnpmLock, project documents only, as before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hosted pnpm planner names a dep it cannot find "vendored" instead of
"not found" when the lock carries the vendor backend's wiring for it. It
decided that with a substring test over every line: any line starting
`<name>@file:.socket/vendor/`, or an `<name>@<version>:` line containing
both `file:` and `.socket/vendor/`. It now reads the lock's packages
entries through PnpmLock and asks crate::origin::vendor_ref (pure path
grammar, no I/O, no host policy, so allowed in formats/): a v9
`<name>@file:<path>` key, or a legacy bare `file:<path>` key whose
`name:` line names the dep, with `<path>` this project's vendored npm
artifact. That is the rule discovery reads vendored pnpm keys with
(lock-model divergence N5, pnpm part).

Behavior changes, pinned by
pnpm_vendored_diagnosis_reads_the_rekeyed_entry_through_origin (the
backends' real v9 and legacy shapes stay "vendored", per the existing
tests):

- A `file:` path without a canonical uuid dir, or under another
  ecosystem's vendor dir, is redirect_pnpm_entry_not_found now.
- An `overrides:` line alone, with no rekeyed packages entry behind it,
  is redirect_pnpm_entry_not_found now: pnpm mirrors overrides from
  package.json whether or not anything matches (the vendored in-use
  check already treats it that way).

The depscan-sized equivalence fixture's vendored entry had no uuid dir,
a shape the backend never writes; it now carries one, and its golden is
re-blessed (both digests change because the input text changed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two npm-family inventory rules now follow lockfile discovery (and so
what hosted mode pins):

- Rush's common and subspace pnpm locks are separate install trees,
  read whenever rush.json is present, beside whatever lock the flavor
  router chose. Before, the inventory read them only as a fallback when
  the root had no lock (lock-model divergence N12). The listing itself
  is unchanged (#2003 aligns it separately). root_pnpm_lock_wins_over_
  rush_fallback pinned the old shadowing; it becomes
  rush_locks_are_read_beside_the_root_lock.
- pnpm's `lockfile` setting off (lockfile_disabled, not under Rush, as
  in discovery): pnpm neither reads nor writes the root or member locks.
  Discovery keeps a hosted ref from such a lock but withholds its lock
  pin; the inventory now keeps the entry (its coordinates and hosted url,
  which ledger liveness reads) and drops its integrity, so vendor's
  fetch and repair never verify against a pin the install does not use.
  Registry entries keep their integrity: a registry release's hash
  verifies its content whether or not pnpm reads the lock (N14). Pinned
  by pnpm_lockfile_off_withholds_hosted_pins.

"Hosted" here is the default Socket host (origin::hosted_patch_uuid with
no configured origins): the inventory has no per-run origin list yet;
the install-tree PR moves it onto the single classifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Outside formats/pnpm, pnpm lock entries are read through PnpmLock (one
walk, every document tagged). A new test in formats/pnpm/mod.rs fails on
any production use of grammar::entries / pnpm_packages or a hand-rolled
`packages:` line walk elsewhere, except the writers that splice by byte
range or line position, each allowlisted with a label and a use count:

- patch/redirect/upstream/npm.rs (2): the hosted unwind's resolution
  splices (restore_pnpm_locks);
- vendor/pnpm_lock.rs (3): the v9 vendored planner's preflight and line
  splices;
- vendor/pnpm_lock_legacy.rs (6): the 5.4 / 6.0 vendored planner's
  preflight, splices and revert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/design/lock-model-divergences.md records what PR 2a resolved.

Budget: lock-readers 30,976 -> 31,117 (+141), raised by hand:
- vex/discover/mod.rs +46: the env document's tree key
  (pnpm_document_tree), mapping it back to the lock path in finalize,
  and skipping it in the cross-lock contest;
- formats/pnpm/grammar.rs +51: the content-based document split
  (documents, is_env_document) replacing main_document;
- vendor/lock_inventory/npm_family.rs +35: Rush locks beside the routed
  lock and the lockfile=false pin withholding;
- vex/discover/npm.rs +13: one extraction pass per document;
- formats/pnpm/workspace.rs +6, lines.rs +1;
- formats/pnpm/mod.rs -4 (InstalledPackages on PnpmLock instead of its
  own line walk), formats/pnpm/hosted.rs -7 (the vendored check).
lock-writers 88,314 -> 88,313 (--lower).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. The pnpm architecture guard cut each file at its first
   `#[cfg(test)] mod`, so a file declaring an out-of-line test module
   near the top (crawlers/npm_crawler.rs, patch/redirect/mod.rs,
   vendor/mod.rs) went mostly unchecked. It now drops only the gated
   items: a `mod x;` line, or an inline item through its closing brace,
   counted outside comments and string, raw string and char literals
   (which span lines). production_drops_only_test_gated_items pins it,
   including late production code of those three files.
2. The env document must be the FIRST document as well as env-shaped,
   as pnpm and split_project_document read it. An env-shaped later
   document is a project one, so it cannot skip the cross-lock contest,
   and pnpm_settings_major no longer reports 11 for it.
   documents_are_identified_by_position_and_content (was
   ..._by_content_not_position) and pnpm_include_tarball_ignores_the_env_
   lockfile_document pin it.
3. The hosted "vendored?" check compares the version too: another
   version vendored is redirect_pnpm_entry_not_found. The depscan-sized
   equivalence fixture's vendored entry gains the `version:` line the
   backend always writes; golden re-blessed.
4. LockfileEntry gains `pnpm_env`, and dedup_prefer_integrity never lets
   an env-document instance displace a project one (project first, then
   a verifier). env_document_instances_never_displace_project_instances.
5. Entries are tagged by their KEY line's offset, so an empty-bodied
   last key of a document keeps its document.
   an_entry_with_an_empty_body_keeps_its_document.
6. No pseudo path: discovery records carry an explicit TreePart (File or
   PnpmEnv) beside the real file (PatchedRef, ResolvedElsewhere,
   UnpatchedCopy, UnwiredCopy scope); the contests compare it, and the
   finalize remap is gone. An env ref the same file's project document
   also wires (same purl, uuid, mode) is dropped in finalize, so the
   two never both survive, whatever their pins.
7. InstalledPackages walks keys only (PnpmLock::parse_keys, no
   resolution parse) on the store crawler's hot path.
8. The hosted planner builds each lock's vendored (name, version) set
   once, from the unspliced text (a splice never touches a key or a
   name:/version: line), dropped when materialize replaces the text.
9. Rush locks are no flavor's: the routed lock keeps its flavor alone
   (inventory_npm_lock), and inventory_npm_trees_raw_in unions it with
   Rush's trees; rush.json is stat'd once through the view (the Rush
   reader no longer re-checks it), no bool/Vec shadowing.

Budget: lock-readers 31,117 -> 31,207 (+90: TreePart and its helpers
+30 in vex/discover/mod.rs, PnpmTree +22 in vex/discover/npm.rs, the
key-only walk +16 in formats/pnpm/mod.rs, the vendored cache +13 in
formats/pnpm/hosted.rs, `pnpm_env: false` in 21 LockfileEntry literals,
golden render +7, minus 15 in the inventory's npm_family/pnpm/mod).
lock-writers 88,313 -> 88,314 (+1: hosted/engine.rs compares the part).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Review of #2001 found the counter miscounting in several shapes, each
now pinned by a test in scripts/tests/test_loc_budget.py:

- a cfg(test) struct field, enum variant, parameter or match arm drove
  the scan below the item's depth and swallowed the code after it; the
  item now ends at a top-level `,` or before the bracket closing its
  container
- `#[cfg(test)] use a::{..};` left its trailing `;` line counted
- cfg predicates are parsed: `all(unix, test)` and nested `all` count,
  `not(test)` doesn't, and a file opening with `#![cfg(test)]` is
  test-only
- `#[path]` is found among any attributes around the item and resolves
  against the declaring file's directory, as rustc does
- `mod x;` inside a test-only inline module (vex::discover's
  `testing { mod golden; }`) marks that file test-only
- `git ls-files -z` keeps paths with spaces whole, and only the group
  files plus their module declarers are read
- `--files` with an unknown group is a usage error, not a KeyError

golden.rs (435 lines) and two `};` lines leave the counts, so the
budget drops to 30462 / 88362.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #2001: the Decisions table said every read is bounded to the
repository root, while G1, J13 and P4 still said to read an ancestor
go.work, parent NuGet configs and every include. The owner narrowed
the rule on 2026-11-05, and the table now records it with the rest of
that day's calls:

- the bound applies to workspace runs only; --global / -g /
  --global-prefix runs are unbounded, and without a git checkout upward
  lookups behave as before
- a marker file above the repository root (below home) makes hosted and
  vendored refuse with a message naming it
- out-of-repository -r includes stay bounded in workspace runs
- N11: shrinkwrap.yaml beside pnpm-lock.yaml is shadowed, unless the
  project pins pnpm 1 / 2, where it governs and vendored refuses
- discovery withholds pnpm root and member-lock attestations whenever a
  pnpm-lock.<branch>.yaml exists
- user patches: the override is --allow-patch-conflicts, and vex never
  attests under a user patch

G1, J9, J13, P4, P9, X12, N1, N2, N4, N5, N11, N12 and N14 name the open
PRs that fix them, and the fixes-on-main table gains a PR column. The
problem table now counts four checksums.sha256 readers, as J9 does. The
stack table splits PR 2 into 2a / 2b with their numbers, and the size
budget section says why CI only warns and gives the corrected counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the scripts/loc-budget.json conflict: the base fixed the
counter's cfg(test) and module bugs, so both budgets are recounted
with the fixed counter and lowered to the exact current totals
(lock-readers 30259, lock-writers 88313), keeping this PR's
'both groups at their budget' intent.

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: merged lock-model/1-origin (4242ebc) into this branch at 1d27a20 to clear conflicts (no force-push).

  • docs/design/lock-model-divergences.md: kept this PR's "Resolved in PR 2a" detail for N4, N5, N12, N14 (now tagged Read and plan pnpm locks through one core (lock-model 2) #2009) plus the base's PR links; N11 takes the base's owner decision (shadowed).
  • scripts/loc-budget.json: the base's counter fix re-measured lock-readers at 30259; this PR grows it, so the max is raised to the new measured count, 30482.
  • cargo check -p socket-patch-core --all-targets clean; cargo test -p socket-patch-core: 7126 passed, 4 failed. The 4 rely on unwritable files or symlinks and fail only because the container runs as root. The merge didn't touch them.

Generated by Claude Code

Claude (claude) and others added 4 commits November 6, 2026 08:26
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>
…g code

loc-budget.py only recognized `pub name: Type` as a field, so a
#[cfg(test)] on a `crate name: Type` field or a `const N: usize`
generic parameter was read as an item. _item_end then didn't stop at the
comma and excluded the rest of the struct or parameter list from the
implementation count.

Treat `crate` followed by a name like `pub`, and read `const` as a
parameter when the attribute follows `<` or `,`, where no item can
start. A `crate::path` statement and a plain const item keep their
current handling. Reported by Bugbot on #2001.

Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up PRs grep for the names this doc cites. OriginPolicy::hosted_uuid
exists nowhere in the tree; the function is DiscoverCtx::hosted_uuid (its
memoized fast path is the hosted_hosts cell). The npm flavor router lives
at vendor/lock_inventory/npm_family.rs, since doc paths are relative to src/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aeNYq2GHyrsNnPBepcC
Brings in the counter's crate-field and const-generic fix and the doc
symbol fixes. Also restores crates/socket-patch-core/src/origin.rs to
the lock-readers group: the previous merge (4242ebc) resolved the
loc-budget.json conflict with the base's path list and dropped it, so
the 281 lines this PR moves into origin.rs went uncounted (30259). With
origin.rs counted, lock-readers is 30540 and lock-writers 88313.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision on #2009: a config-dependency (env lockfile document) ref
is not attested while the project document of the same lock resolves
the same name@version from elsewhere. A VEX statement covers the
release, not one install tree, so attesting it would claim the release
is fixed while node_modules holds upstream.

TreePart::withheld_by makes the project tree's copies reach env refs of
the same file, through the existing machinery:
- a registry entry moves the env ref to `rewirable` (a re-run rewires
  that entry, and the project ref then replaces the env twin);
- a `file:` copy (and any project-tree unpatched copy) shadows it;
- a bundled copy marks it Unattested (BundledCopy), like a project ref.

The branch-lock marks no longer need a separate env-scoped copy: the
project-scoped one now covers env refs. pnpm_env_document_is_its_own_
install_tree's first loop now expects withholding, with registry-only,
file:-only and bundled-only cases plus a different-version control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lock-model/2b-pnpm-planner

# Conflicts:
#	docs/design/lock-model-divergences.md
The hosted gate's copy check compared the pin's tree with the copy's,
so a config-dependency (env document) pin withheld by the project
document's `file:` copy was neither named with redirect_copy_unpatched
nor kept out of the in-run VEX. It now asks TreePart::withheld_by, the
rule discovery withholds with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop a pnpm env twin before any contest, so it leaves no Unattested
  mark (keyed by purl and uuid, which withheld the project ref) and no
  diagnostic behind.
- Restore a hosted pin in the env lockfile document bare: pnpm 11+
  records config dependencies without `tarball:` whatever
  lockfileIncludeTarballUrl says, so rollback no longer writes the
  project document's policy into it.
- The Rush reader no longer stats rush.json again; its one caller does.
- Divergences row N4: documents are identified by position and content.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] Review findings (Claude Opus + codex, independent passes):

  1. Env-document refs are never contested by another lock (crates/socket-patch-core/src/vex/discover/mod.rs:1410, with withheld_by at :283; Opus). contest_across_locks only lets TreePart::File refs be contested. Input: root pnpm-lock.yaml env document wiring left-pad@1.3.0 to a Socket tarball, project document without left-pad, and a package-lock.json (or non-shared member / Rush subspace lock) resolving left-pad@1.3.0 from the registry → pkg:npm/left-pad@1.3.0 attested while that tree installs upstream. On main the env document produced no refs, so this path is new.
  2. Env refs skip installed-file verification (vex/discover/npm.rs:1301, codex). Both npm crawler paths skip .pnpm-config (npm_crawler.rs:3279,3690), so an upstream node_modules/.pnpm-config/left-pad yields package_not_found, which VEX excuses (commands/vex.rs:857-883) and emits not_affected.

Minor: withhold_hosted_pins (npm_family.rs:123) knows only the default host; has_packages_section now scans the env document too; inline name@1.0.0: {} keys are no longer counted (tracked in #2044).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[agent] Approving per maintainer decision; see the findings in the review above.

- 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>
Behavior-preserving cleanups over the folded lock-model 2 diff:

- The pnpm architecture test reads production code through the shared
  vendor::test_support::production_sources instead of its own lexer.
- The hosted unwind walks PnpmLock::packages once (document tags and
  resolutions included) instead of two raw entry walks plus its own
  env-span tagging, so it leaves the raw-walk allowlist.
- One PnpmDialect::entry_in_use backs both dialects' in-use checks
  (memoized; the legacy one no longer parses the whole lock per entry).
- plan::shadowed_locks names the pnpm locks pnpm does not read, for the
  engine's confirmation probe and trust/heal root; planned_locks shares
  its per-key rule.
- Cheaper hot paths: the store crawler's key-only walk skips document
  tagging; grammar::suffix no longer allocates; drop_env_twins returns
  early with no env ref; the inventory reads the lockfile setting only
  when a hosted pin exists; PnpmLock::documents needs no sort.
- Comments and the divergences doc state current behavior instead of
  review history; shared test fixtures (env document head, Rush consts);
  three vendored helpers made private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Read pnpm locks through one walk (lock-model 2a) Read and plan pnpm locks through one core (lock-model 2) Nov 9, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 9, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 9, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final reviewer: re-added to the merge queue at aa9f5c9. The queue removed it at 22:11Z when #2018 merged: #2012 ahead of it no longer merged cleanly, and this PR's merge_group run was cancelled with that group. That was a side effect, not a failure of this PR. The head is unchanged, mergeable and green, and the PR is still approved.


Generated by Claude Code

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 9, 2026
#2018's user_patched extractor built UnwiredCopy marks with the old
file-only scope; #2009 scopes them by (file, TreePart). Each mark is
for exactly one ref, so it takes that ref's own (source_file, part), as
npm::mark_user_patched does. A pnpm env-lockfile ref of a user-patched
package is marked too: the index is per package, so a missed
attestation is the safe side.

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

Copy link
Copy Markdown
Collaborator Author

Merged main (9bbb4b7) after the merge-queue dequeue. #2018 added vex/discover/user_patched.rs, which builds UnwiredCopy marks with the old file-only scope: Some(PathBuf). This PR changed that scope to (file, TreePart), hence the E0308 at user_patched.rs:100. Each user-patched mark targets exactly one ref (CopyTarget::Ref), so it now takes that ref's own (source_file, part), the same as npm::mark_user_patched. Its meaning is unchanged for project refs. A pnpm env-document ref of a user-patched package is marked too (fail-closed). No #2018 test changed behavior: the vex:: tests, including user_patched, plus formats::, origin, patch::redirect, patch::user_patches and tests/pnpm_hosted.rs all pass, and CI's clippy is clean. The patch/apply.rs:3 warning and the --all-targets clippy lints are in files this PR doesn't touch; they come from main.


🤖 Generated with Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 7c08fd8 Nov 9, 2026
49 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the lock-model/2a-pnpm-readers branch November 9, 2026 22:37
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.

3 participants