Skip to content

[PR] fix(dsh-ledger): ignore pnpm's peer suffix when comparing lockfile versions, preserve it when rewriting | fix(dsh-ledger): 比较 lockfile 版本时忽略 pnpm 的 peer 后缀,改写时保留它 - #6314

Open
BBleae wants to merge 1 commit into
ranxianglei:masterfrom
BBleae:2026-11-12_dsh-ledger-pnpm-peer-suffix

Conversation

@BBleae

@BBleae BBleae commented Nov 11, 2026 •

Copy link
Copy Markdown

Fixes #6320

修的是什么

包本身(或它依赖的某个包)有 peer、而项目又满足了这个 peer 时,pnpm 会在 importer 条目里解析出的版本后面写一段 peer 解析后缀:

importers:
  .:
    dependencies:
      billion-context:
        specifier: 0.1.213
        version: 0.1.213(@types/node@16.9.1)

src/dsh-ledger.ts 原来把这一整串都当成版本:

  • readPnpmLockResolution 把 0.1.213(@types/node@16.9.1) 作为 lockVersion 返回,dshProfileLedgerFindings 拿它和副本的 0.1.213 按字符串比较。健康的 profile 因此出一条 copy-vs-lock finding,bili doctor --fix(fixDshLedgers,master 上这项审计的调用方)再把它当成漂移去修。
  • patchNestedEntries 也做同样的比较,所以 convergeDshLedger(bili doctor --fix,以及原地更新之后的收敛)会把这一行改写成不带后缀的 version: 0.1.213。这是在什么都没漂移的 profile 上写了一次文件,而且写完 importer 指向 billion-context@0.1.213,snapshots: 下的键却还是 billion-context@0.1.213(@types/node@16.9.1)。

这个后缀在真实的 DSH profile 里就有(pnpm v9 lockfile,(@types/node@…)),不是少见的形状。

改动

  • 新增 splitPeerSuffix(),在第一个 ( 处把这串切开。后缀说的是这次安装按哪组 peer 解析,不属于版本。
  • readPnpmLockResolution 返回去掉后缀的版本,state 和 finding 里的 lockVersion 都是纯版本号。
  • patchNestedEntries 只比较版本部分。真要挪版本时,只换版本那一段,后缀和这一行原来的引号风格都保留(pnpm 写的是不带引号的值,交给 yamlScalar 会被加上引号)。

扁平格式(lockfile <= 5.4)和 npm lockfile 的路径没动。没有改动 kernel/**。

测试

在 tests/issue3496-dsh-ledger-convergence.test.ts 里加了三个用例,用一个 v9 fixture:importer 的版本带 peer 后缀,packages: 下是不带后缀的键,snapshots: 下是带后缀的键。

  1. 带后缀的健康三本账:lockVersion 读成纯版本,没有 finding,零写入(字节和 mtime 都不变);
  2. 带后缀的真实漂移:只有 importer 里 billion-context 的那两行变了,后缀保留且不加引号,另一个依赖带后缀的版本没被碰,再审计是干净的,第二遍什么都不写;
  3. 直接测 patchPnpmLock:多个串起来的 peer、带引号的值、以及 v6-v8 的嵌套条目。

三个用例在 master 上都失败,加上改动后都通过。这个文件加上改动是 27/27(不加是 24/27)。

npm run typecheck、check:unused-exports、check:test-import-graph 和 git diff --check 都干净。另外在本地 Windows(Node 22,离线)上把整套 tests/*.test.ts 跑了一遍做粗查:这个模块里和它附近的测试没有失败;那次跑出来的几处失败是环境造成的(Windows、离线),都在无关的测试里。整套的结论以 CI 为准。

这个 PR 不做的

packages: / snapshots: 仍然不动,这是 #3509 定下的,跟踪在 #3570(已关闭)。带 peer 后缀时,真挪了版本以后的情况和那边一样:importer 写的是 billion-context@<new>(<peers>),snapshots: 下没有这个键(原来丢掉后缀的做法也避不开:不带后缀的键同样不在 snapshots: 下)。我在本地 fixture 里看了 pnpm 10.20.0 对这种 lockfile 的处理:普通的 pnpm install 先打印 Broken lockfile: no entry for 'billion-context@0.1.213(@types/node@16.9.1)',再打印 The lockfile is broken! Resolution step will be performed to fix it. 并重新解析,和 #3570 记录的 pnpm 9 / 11 的表现一致;pnpm install --frozen-lockfile 停在 ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY。(fixture 是离线的,所以我只看到重新解析开始,没看到它跑完。)

为了让这个 PR 保持小,只做了比较和后缀这两处。如果还是想要 #3570 里三段一起同步的做法,说一声,我可以另开一个 PR 跟进。

…ions and keep it when rewriting

pnpm appends a peer-resolution suffix to an importer's resolved version
when the package or one of its dependencies has a satisfied peer:
`version: 0.1.213(@types/node@16.9.1)`. The ledger code took the whole
scalar for the version. A healthy profile was reported as copy-vs-lock,
and convergence (`bili doctor --fix`, the pass after an in-place update)
rewrote the line to the bare version: a write where nothing had drifted,
leaving the importer without the suffix its snapshots key carries.

- splitPeerSuffix(): split the scalar at the first "(".
- readPnpmLockResolution returns the version without the suffix, so the
  state and the findings carry a plain version.
- patchNestedEntries compares the version part only; when the version
  really moves it swaps that part and keeps the suffix and the quoting
  the line already had.
- tests/issue3496-dsh-ledger-convergence.test.ts: v9 fixture with a peer
  suffix; healthy triple gives no finding and zero writes; real drift
  changes only the two importer lines and keeps the suffix; multi-peer,
  quoted and v6-v8 nested shapes.

packages:/snapshots: stay untouched as before (ranxianglei#3509, ranxianglei#3570).
Copilot AI balanced review requested due to automatic review settings November 11, 2026 23:47
@ranxianglei ranxianglei changed the title fix(dsh-ledger): ignore the pnpm peer suffix when comparing lock versions and keep it when rewriting [PR] fix(dsh-ledger): ignore the pnpm peer suffix when comparing lock versions and keep it when rewriting Nov 11, 2026

Copilot AI 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.

Note

Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.

🟢 Approval recommended

The change is narrowly scoped to handling pnpm peer-suffix metadata in importer version parsing/patching and is accompanied by regression tests for both healthy and drifted cases, with no outstanding review issues indicated.

No open issues 🎉

What changed in this PR

Fixes false-positive DSH ledger drift and unintended pnpm-lock.yaml rewrites when pnpm encodes peer-resolution metadata as a suffix in importer version: scalars (e.g. 0.1.213(@types/node@16.9.1)), by comparing only the resolved version while preserving any suffix during updates.

Changes:

  • Add splitPeerSuffix() and use it to ignore pnpm peer suffixes when reading nested importer version: values.
  • Update pnpm lockfile patching to compare/swap only the version portion while preserving any existing peer suffix and line formatting.
  • Add regression tests covering healthy and drifted profiles with peer-suffixed versions, including quoted and multi-peer suffix variants.
File Description
tests/​issue3496-dsh-ledger-convergence.test.ts Adds pnpm v9 peer-suffix fixtures and convergence test cases to ensure healthy profiles produce no findings/writes and drifted profiles preserve peer suffixes during fixes.
src/​dsh-ledger.ts Implements peer-suffix-aware parsing and patching for pnpm nested importer version: fields so comparisons ignore the suffix while rewrites retain it.

Review effort: Balanced

@BBleae BBleae changed the title [PR] fix(dsh-ledger): ignore the pnpm peer suffix when comparing lock versions and keep it when rewriting fix(dsh-ledger): 比较 lockfile 版本时忽略 pnpm 的 peer 后缀,改写时保留它 Nov 12, 2026
@ranxianglei ranxianglei changed the title fix(dsh-ledger): 比较 lockfile 版本时忽略 pnpm 的 peer 后缀,改写时保留它 [PR] fix(dsh-ledger): ignore pnpm's peer suffix when comparing lockfile versions, preserve it when rewriting | fix(dsh-ledger): 比较 lockfile 版本时忽略 pnpm 的 peer 后缀,改写时保留它 Nov 12, 2026
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

Progress report — review of this PR is in flight (the previous run was force-restarted after a stalled tool call; nothing was lost, all findings so far are below).

Dup check: not a duplicate — no other open issue/PR touches the pnpm peer-suffix handling in src/dsh-ledger.ts; this is the first fix for the #3496 ledger's treatment of version: X(peer) scalars.

Verified so far:

  • Fetched the PR head (single commit 9ae09b3e07, 2 files: src/dsh-ledger.ts +31/−4, tests/issue3496-dsh-ledger-convergence.test.ts +133). Base is current master minus one unrelated web commit — effectively fresh, no rebase risk on the touched files.
  • Read the full diff and the whole module on master. The bug is real and I can trace both facets in the code: readPnpmLockResolution takes the whole version: scalar as lockVersion (so a healthy suffixed profile is a permanent copy-vs-lock finding), and patchNestedEntries compares the whole scalar, so convergeDshLedger (bili doctor --fix + the post-self-update hook in src/update.ts:2156) rewrites the line to the bare version — a write on a healthy profile that leaves the importer pointing at a snapshots: key that doesn't exist (broken lockfile per pnpm).
  • The fix (split at first (, compare/swap the version part only, carry the suffix and the existing quoting) is correct on all edge shapes I checked by hand: chained peers, quoted values, v6–v8 nested entries, no-suffix (identity behavior, unchanged output). Flat ≤5.4 and npm paths correctly untouched (neither format carries peer suffixes there).
  • Callers walked: convergeDshLedger (update.ts self-heal hook, doctor --fix), dshProfileLedgerFindings (startup audit + doctor), patchPnpmLock (tests only). No other pnpm-lock reader in src/ — no divergence risk with feat: detect manifest/lockfile/copy three-way disagreement in dsh profiles (#3224) #3231's detection.

In progress: running the test suite locally — the 27-case file at the PR head, plus a revert-to-master check that the 3 new cases fail without the change. Then CI status via the mirror cache and the final 8-question verdict. One reply with the full review coming shortly.

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

Review of PR #6314 — verdict: mergeable (可合). Dup check: not a duplicate (no other open issue/PR touches the pnpm peer-suffix handling in the #3496 ledger). Labels applied: bug, dsh, scope/other, p2 — a defect in the dsh install-lane ledger bookkeeping; usable-with-workaround severity, not p1.

What I verified independently (not taken from the PR description):

One disclosed behavior change (intended, this is the fix): healthy suffixed profiles now produce zero writes where master wrote a corrupted bare version; details[].before for the version field is now trimmed of trailing whitespace (a whitespace-only difference no longer churns the file — same zero-writes spirit).

结构化八问

  1. 是什么问题: pnpm v6+ lockfile 的 importer version: 行带 peer 后缀(如 0.1.213(@types/node@16.9.1)),dsh 三账本代码把整个标量当版本——健康 profile 被永久报成 copy-vs-lock 漂移,且 bili doctor --fix/自更新后的收敛会把后缀剥掉,写出指向不存在 snapshots key 的坏 lockfile。
  2. 什么原因导致的: 根因在读/改两处的裁决点——把 YAML 标量整体当版本比较与重写;后缀是 peer 集合元数据,不是版本的一部分。修在裁决点(比较 + 重写),不是传输层过滤。
  3. 影响范围与严重程度: 受影响面 = 带 pnpm lockfile 且 billion-context 解析带 peer 后缀的 dsh profile(作者实测真实 DSH profile 常见,pnpm v9 + @types/node)。后果:永久误报(噪音)+ 每次 doctor --fix/自更新收敛写坏 lockfile(pnpm 普通 install 会重新解析自愈,--frozen-lockfile 硬失败)。能用但有明显问题 → p2。
  4. 怎么修复的: splitPeerSuffix() 在第一个 ( 处拆分;读取侧返回纯版本;改写侧只比版本段,真漂移时只换版本段、保留后缀与原有引号风格。flat ≤5.4 与 npm 路径不动(那些格式没有后缀)。
  5. 是否有回归问题: 无。触碰路径的既有行为逐项过:无后缀健康/漂移(行为字节级不变,原 24 个测试全绿)、flat 路径(未动)、npm 路径(未动)、零写契约(健康带后缀 profile 从「误写」变「零写」,即本修复)、details.before 现 trimEnd(纯空白差异不再 churn,同零写精神)。无 fail-fast 变吞错、无语义漂移。CI 30/30 绿。
  6. 是否破坏缓存: 不涉及。
  7. 是否改了设计方向: 否——同一模块、同一 dsh 车道自动更新改全:自愈同步三本账(manifest/lockfile/副本)+ 存量漂移显式修复,自动更新后必须还能用 #3496 三账本契约、同一权限边界;packages:/snapshots: 仍按 feat(dsh-lane): converge all three ledgers on in-place self-heal + bili doctor --fix (#3496) #3509/Converged dsh profile breaks under a user-managed pnpm >= 12: dangling billion-context symlink, permanent "Already up to date" #3570 的已决决定不动(真实版本移动后的 snapshots key 缺口在 PR 里如实披露,与已关闭的 Converged dsh profile breaks under a user-managed pnpm >= 12: dangling billion-context symlink, permanent "Already up to date" #3570 记录一致)。
  8. 是否引入了新的配置: 不涉及。

收工判定:可合——根因修复、双向测试钉死、CI 全绿、diff 干净,无决策依赖项,等 owner 合并即可。

@ranxianglei ranxianglei added bug Something isn't working dsh p2 scope/other labels Nov 12, 2026
@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

Status confirmation (no new work): the full review verdict is already posted above (the "verdict: mergeable (可合)" comment) — bug verified in code, 27/27 tests at the PR head with exactly the 3 new cases failing on master's code, local gates clean, CI 30/30 green on sha 9ae09b3e07, diff clean, no rebase needed. Labels bug/dsh/scope/other/p2 are on the thread. Nothing remains on my side — the PR is ready for the owner to merge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dsh p2 scope/other

Projects

None yet

3 participants