Repository navigation
Plan pnpm rewires with one core for both modes (lock-model 2b) - #2029
Mikola Lysenko (mikolalysenko) merged 17 commits into
Conversation
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>
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>
|
Burn-down agent: merged
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>
|
Burn-down agent: merged
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
… lock-model/2b-pnpm-planner
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
|
bugbot run Generated by Claude Code |
- 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>
|
Merged
bugbot run Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ 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
… 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>
|
Not merged to |
8b59a5a
into
lock-model/2a-pnpm-readers
- 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>
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)hosted_refuses_lockholds hosted's only refusal: pnpm 1's early shrinkwrap.sniff_lock_grammar: 5.4, 6.0, 9.0), andPnpmDialect::check_lockroutes each dialect from it.grammar::unsupported_early_shrinkwrap,formats::pnpm::check_v9_lock_versionand the legacycheck_lock_version.match_key: one rule for how a key namesname@version.name@file:or legacy's barefile:, read withorigin::vendor_refplus the artifact leaf.vendored_packagesis deleted), and in both vendored dialectstarget_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'spnpm-lock.yamlandshrinkwrap.yamlpnpm reads (N11, below). Hosted and discovery both use it.The vendored v9 and legacy dialects now share more code:
DepPair);revert_block, which re-sorts legacy's barefile:rekey);dep_entries), used by the importer edit, the 6.0 root-dep edit, both ref gates andLockIndex::build;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.rsand stays at 3 forvendor/pnpm_lock.rs.Behavior changes (each pinned by a test that fails before this PR)
pnpm-lock.yamlorpnpm-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 withvendor_lockfile_crlf_unsupported.crlf_lock_is_wired_like_its_lf_twin_and_stays_crlfmixed_line_ending_lock_refuses_naming_line_endingscrlf_workspace_file_gains_the_override_in_its_own_sectioncrlf_checkout_of_a_vendored_project_reverts_cleanlycrlf_two_document_lock_revertsmixed_line_ending_revert_fails_before_any_writecrlf_is_wired_and_foreign_versions_refuseonly_a_mixed_line_ending_hosted_pnpm_lock_is_refused(takeover preview)scan_vendored_over_hosted_pnpm_crlf_lock_takes_over_and_stays_crlfpreflight_agrees_with_the_loop…now use a mixed-ending lock.../.socket/vendor/…rekey as this project's entry. Before,parse_vendor_pathfound the anchor anywhere in the path. Test:a_sibling_checkouts_rekey_is_not_a_target. The reverter still finds its wiring withparse_vendor_path, because values such as pnpm 7/8's absolute specifier need it.version:line.version:line now getsredirect_pnpm_entry_vendored.version:line now getsredirect_pnpm_entry_not_found.pnpm_vendored_diagnosis_reads_the_rekeyed_entry_through_origin.lockfileVersionnow 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.shrinkwrap.yaml(owner decision: shadowed). Of a directory'spnpm-lock.yamland pnpm <= 2shrinkwrap.yaml, the planner rewrites only the one pnpm reads; the other is shadowed (governing_pnpm_lock).pnpm-lock.yaml. pnpm 1 / 2 read onlyshrinkwrap.yaml, so when the project pins pnpm 1 / 2 (node_modules/.modules.yaml, else package.jsonpackageManager), the straypnpm-lock.yamlis the shadowed one.pnpm-lock.yamlgoverns.recognize_ignored.shrinkwrap.yamlgovern, it refuses withvendor_lockfile_version_unsupported(it does not write the shrinkwrap grammar) instead of wiring the straypnpm-lock.yaml. With no pin it is unchanged. Test:a_pnpm_2_pin_beside_a_shrinkwrap_refuses, which fails without the gate.shrinkwrap.yamlis rewritten as before. A nested shrinkwrap is shadowed only by its own sibling.formats/registry.rskeeps itsHOSTEDread 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.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.pnpm_shrinkwrap_only_without_pnpm_lock, now with a pnpm 2 pin.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 bylock_versions_each_target_rewrites.Unchanged: N14 (
lockfile=falsestays target-specific).Review findings (
/code-review high)---\rfailed every revert of a CRLF two-document lockcrlf_two_document_lock_revertsfailed)read_pnpm_textfirst.mixed_line_ending_revert_fails_before_any_writefailed)read_pnpm_text(root, file)serves the vendoring reads and the revert pre-read of the lock and the workspace file.is_mixedandread_refusalmap its errors.shrinkwrap.yamlgoverning_pnpm_lockreads the pin (utils::pnpm_workspace::pinned_pnpm_major, whichpnpm_major_fromnow reuses), and hosted and discovery share it. There are tests for all three cases.'v10'lost the update-socket-patch remedylock_version_remedy_is_version_aware.SectionIndex::buildallocatedformat!("@{key}")per blockstarts_with("file:")andmatch_indices("@file:")on the key directly.dep_entriesandimporter_refsare lazy and borrowed; the gate stops at the first refusal.matching_blocksclassifies once;target_blockreturns the block and its rekey flag; the curried closure is gone.Target::check_lock'sOptionforced a dead.unwrap_or(V9)hosted_refuses_lockand the vendored dialect'ssniff_lock_grammarcheck; no dead default.Target::repointsearly return was hard to readRestack 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:redirect_copy_unpatched) uses the sameTreePart::withheld_byrule (from Read and plan pnpm locks through one core (lock-model 2) #2009), so an env pin withheld by a projectfile:copy is named and kept out of the in-run VEX.vendor_config_dependency_unpatchedwarning still names the config dependency's own copy.Also fixed while restacking:
tests/pnpm_hosted.rs::early_pnpm1_refuses_non_durable_shrinkwrap_across_all_locksstill asserted the pre-N11 rule. It failed on this branch from the N11 change on, but draft CI never ran it. It is nowearly_pnpm1_shrinkwrap_refuses_only_when_it_governs: a lone early shrinkwrap refuses, and beside apnpm-lock.yamlit is shadowed.Review findings, second pass (
/code-review high, 2026-11-09)redirect_unattributableconfirmskips locksplanned_locksleaves out.hosted::engine::tests::a_shadowed_pnpm_lock_confirms_nothingfails beforeEditCtx::is_oursstill usesparse_vendor_path, said to accept.socket/vendor/anywhereparse_vendor_pathis root-anchored on main (vendor::path::tests::ownership_parse_is_root_anchoredrejectsfile:../.socket/vendor/…)pnpm-lock.yaml, so under a pnpm 1 / 2 pin a stray v9 lock planned atrustLockfilewriteplanned_locks).a_shadowed_pnpm_lock_is_no_heal_rootfails beforeio::ErrorKind::Unsupported, so an OSENOTSUPread error read as a line-ending refusalMixedLineEndingserror type, matched by typepackage.json+.modules.yaml) was read for every candidate file (planned_locks), every vendoring run and every discoverypnpm-lock.yamlandshrinkwrap.yamlexist, the one case it decidesplan::Target::check_lockread_project's doc still said it refuses CRLFread_pnpm_textre-implemented LF normalizationutils::line_endings::to_lfTests 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.in_process_redirect_pnpm42,in_process_vendor_pnpm_takeover14,in_process_vendor_pnpm_parent_child6,in_process_redirect136,hosted_memory_engine49,in_process_vendor144,in_process_rollback_vendored19,hosted_memory_parity42 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'spnpm_lock_file_copies) and belongs in the hosted engine, not the pure planner.Golden diffs
None.
committed_fixture_corpus_matches_goldenpasses unchanged, and no file undertests/fixtures/redirect/npm/pnpmchanges.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
formatsarchitecture guard now bans the host policy (OriginPolicy,hosted_uuid(,SOCKET_PATCH_SERVER_HOST). It still allowscrate::origin::vendor_ref, whichplan::match_keyuses: that function is path grammar and applies no host policy.The merge also restores
crates/socket-patch-core/src/origin.rsto thelock-readersgroup. The earlier routine merge had dropped it. Counted with the fixed counter: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.--test in_process_redirect_pnpm: 41 passed.--test in_process_vendor_pnpm_takeover: 16 passed.test_loc_budget.py: OK.Size budget (
scripts/loc-budget.py --lower, then--check)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.in_process_vendor_pnpm_takeover: 16 passedin_process_redirect_pnpm: 41 passedin_process_vendor_pnpm_parent_child: 8 passedin_process_vendor: 148 passedin_process_rollback_vendored: 23 passedscan: 248 passedhosted_memory_parity: 42 passedcargo clippy --locked --workspace --all-features -- -D warningsandcargo fmt --all -- --check: clean.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 toname@version, and which peer suffixes each target can repoint—while edits still differ (hosted splicesresolution:, vendored rekeys tofile:).N11 — shadowed
shrinkwrap.yaml: When bothpnpm-lock.yamland legacyshrinkwrap.yamlexist, only the file pnpm actually reads is rewritten (defaultpnpm-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 samegoverning_pnpm_lock/planned_lockslogic.N15 — line endings: Vendored mode now accepts uniform CRLF
pnpm-lock.yaml(and keeps CRLF on write/revert). Mixed CRLF/LF still refuses withvendor_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 viavendor_ref) instead of heuristics; sibling../.socket/vendor/…rekeys are ignored. Duplicate lock-version checks ingrammarconsolidate intoplan. 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