dsh-m 0.9.93 → 0.9.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.94 重构+修复:变更结局描述客户端单源化(op-outcome)+ 补丁映射 YAML 引号宽容与重复键根治
12
+
13
+ - **需求(2026-10-17 架构巡查 + grill 九决议)**:变更结局的呈现无 owning module——守卫拦截文案装配 main.jsx 三处逐字重复(0.9.93 toWire 收敛了线格式、client 侧呈现是已知残尾,`repairBasis` 后加时的多点位手补复发已实证)、升级成功文案两处逐字、builds 后缀四处拼装、superseded 五处;另:事务补偿对 workspace `patchedDependencies` **非双引号在场形态**(pnpm 的单引号/无引号书写)逐字判重失明——逐字判重可自造重复键文件、复读检查可假 committed(同包链经 strip 先摘在生产不可达,本项属防御面收敛与 fail-closed 补洞)。
14
+ - **方案**:新纯模块 `src/client/op-outcome.js`(GLOSSARY「变更结局描述」入册)——工厂 `createOpOutcome(lookup)` 出成品文案(`createMarkdown(h)` 同款注入先例),`guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed` 四函数;六处接线站点(五类投影)一行接线,流程特有分支(兼容弹窗/能力表拒绝/乐观提示)与 ToolCardRow 短版留守注记。D9:`verifyPatchMapping` workspace 支改 yaml 键成员判定(解析失败 fail-closed 报「复读失败」);`restorePatchMapping` 追加前解析去重(同键任意引号形态在场即跳过,解析失败退化逐字判重)。
15
+ - **验证**:全量 1567 测试全绿(新增 client-op-outcome 参数化矩阵 32 例——guardBlockedText/buildsSuffix/upgradeOkText to×activation 3×3 全交叉/mutationFailed × verb 五枚,期望串全手写字面量;事务补丁映射五钉例——单引号去重/无引号去重/模拟 pnpm 解析炸端到端/坏 YAML 假 committed→fail-closed/restore 退化不抛,夹具异包前提钉死)、typecheck 零错误;收敛锚 main.jsx `join(" | ")` 3→0、`mutationFailed` ≥ 6、`upgradeNotify` 解构随调用点清除;实施经计划评审四轮 15 项收敛 + 硬停点执行评审两轮(基线独立树复跑 1530 对照 + TDD 红→绿独立复演)后放行;实施计划见 `docs/plans/2026-10-17-op-outcome-and-verify-quote-fix-implementation-plan.md`。
16
+
11
17
  ### 0.9.93 重构:装后守卫线格式收敛至 InstallGuardError.toWire——加守卫字段编辑点 8 → 2(纯重构,线格式语义零变化)
12
18
 
13
19
  - **需求(2026-10-16 架构巡查摩擦点 3,巡查首推)**:`InstallGuardError` → 线格式的投影在手写 5 处逐字重复(host-api 409 payload / tools install+upgrade 两臂 / cli install+upgrade 两臂)+ client 再拼一次——加一个守卫字段要碰 8 处(类 + 5 投影 + client 读取 + 测试),`repairBasis` 后加时各点手补已实证过一次复发。
@@ -655,6 +661,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
655
661
 
656
662
  ## English
657
663
 
664
+ ### Refactor + fix in 0.9.94 — client-side mutation-outcome copy single-sourced (op-outcome) + patch-mapping YAML quote tolerance and duplicate-key elimination
665
+
666
+ - **Need (2026-10-17 architecture walk + nine grilled decisions)**: the presentation of mutation outcomes had no owning module — the guard-interception copy was assembled verbatim three times in main.jsx (0.9.93's toWire converged the wire format while the client-side presentation remained the known leftover; the multi-point hand-patching recurrence when `repairBasis` was added had already been demonstrated once), the upgrade-success copy twice verbatim, the builds suffix four times, superseded five times; separately: the transaction's compensation was blind to **non-double-quoted in-place forms** of workspace `patchedDependencies` (pnpm's single-quoted/unquoted spellings) via verbatim duplicate checks — the verbatim check could fabricate a duplicate-key file and the read-back check could false-pass (the same-package chain is unreachable in production because strip removes it first; this item is defensive-surface convergence and a fail-closed gap-fill).
667
+ - **Approach**: a new pure module `src/client/op-outcome.js` (GLOSSARY term "Mutation Outcome Copy" registered) — a factory `createOpOutcome(lookup)` producing finished copy (the `createMarkdown(h)` injection precedent) with four functions `guardBlockedText`/`buildsSuffix`/`upgradeOkText`/`mutationFailed`; six wiring sites (five projection families) become one-line calls, while flow-specific branches (compat dialog / capability-rejection banner / optimistic note) and the ToolCardRow short variant stay behind with an annotation. D9: the workspace leg of `verifyPatchMapping` now judges key membership via real yaml parsing (parse failure fails closed with "read-back failed"); `restorePatchMapping` parse-dedupes before appending (skip when the key exists in any quoting form; on parse failure it degrades to today's verbatim line check).
668
+ - **Verification**: full suite 1567 green (new client-op-outcome parametrized matrix of 32 cases — guardBlockedText/buildsSuffix/upgradeOkText to×activation 3×3 full cross/mutationFailed × five verbs, expected strings all hand-written literals; five transaction patch-mapping pins — single-quote dedupe / unquoted dedupe / simulated pnpm parse-explosion end-to-end / malformed-YAML false-committed → fail-closed / restore degradation does not throw, with the cross-package fixture premise pinned), typecheck clean; convergence anchors in main.jsx: `join(" | ")` 3→0, `mutationFailed` ≥ 6, and the `upgradeNotify` destructure removed along with its call sites; released after a four-round 15-finding plan review plus a hard-stopped two-round execution review (baseline independently re-run at 1530 for comparison + TDD red→green independently re-enacted); implementation plan in `docs/plans/2026-10-17-op-outcome-and-verify-quote-fix-implementation-plan.md`.
669
+
658
670
  ### Refactor in 0.9.93 — post-install guard wire format converged onto InstallGuardError.toWire; adding a guard field now touches 8 → 2 points (pure refactor, zero wire-format semantic change)
659
671
 
660
672
  - **Need (2026-10-16 architecture walk, friction point 3, the walk's top pick)**: the `InstallGuardError` → wire-format projection was hand-written verbatim in 5 places (host-api 409 payload / tools install+upgrade arms / cli install+upgrade arms) plus one more assembly in the client — adding a guard field meant touching 8 points (class + 5 projections + client read + tests); the later addition of `repairBasis` had already demonstrated the recurrence once.
package/lib/client.js CHANGED
@@ -2005,6 +2005,53 @@ var init_install_cmd = __esm({
2005
2005
  }
2006
2006
  });
2007
2007
 
2008
+ // src/client/op-outcome.js
2009
+ var op_outcome_exports = {};
2010
+ __export(op_outcome_exports, {
2011
+ createOpOutcome: () => createOpOutcome
2012
+ });
2013
+ function createOpOutcome(lookup2) {
2014
+ const guardBlockedText = (guard) => [
2015
+ lookup2("guard.blocked"),
2016
+ `${lookup2("guard.compstatus")}: ${guard.compensation?.status || "\u2014"}\uFF08${guard.compensation?.note || ""}\uFF09`,
2017
+ guard.repairBasis ? `${lookup2("guard.repairbasis")}: ${guard.repairBasis}` : null,
2018
+ lookup2("guard.noforce"),
2019
+ guard.restartSafe ? lookup2("guard.restartsafenow") : lookup2("guard.restartunsafe")
2020
+ ].filter(Boolean).join(" | ");
2021
+ const buildsSuffix = (res) => res.buildApprovals && res.buildApprovals.length ? lookup2("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup2("notify.builds.fallback") : "";
2022
+ const upgradeOkText = (res) => {
2023
+ const note = upgradeNotify(res.activation);
2024
+ return {
2025
+ needsRestart: note.needsRestart,
2026
+ text: lookup2("notify.upgraded", {
2027
+ pkg: res.pkg,
2028
+ from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
2029
+ to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
2030
+ }) + buildsSuffix(res) + (note.suffixKey ? lookup2(note.suffixKey) : "")
2031
+ };
2032
+ };
2033
+ const mutationFailed = (e, { verb, target }) => {
2034
+ if (e && e.opSuperseded) return { kind: "ok", text: lookup2("op.superseded.note", { target }) };
2035
+ if (e && e.guard) return { kind: "err", text: guardBlockedText(e.guard) };
2036
+ return { kind: "err", text: lookup2(PLAIN_FAIL_KEY[verb], { err: e && e.message || e }) };
2037
+ };
2038
+ return { guardBlockedText, buildsSuffix, upgradeOkText, mutationFailed };
2039
+ }
2040
+ var PLAIN_FAIL_KEY;
2041
+ var init_op_outcome = __esm({
2042
+ "src/client/op-outcome.js"() {
2043
+ "use strict";
2044
+ init_operations();
2045
+ PLAIN_FAIL_KEY = {
2046
+ install: "failed.install",
2047
+ upgrade: "failed.upgrade",
2048
+ uninstall: "failed.uninstall",
2049
+ toggle: "toggle.failed",
2050
+ self: "notify.selfupgraded.failed"
2051
+ };
2052
+ }
2053
+ });
2054
+
2008
2055
  // src/client/main.jsx
2009
2056
  var React = require("react");
2010
2057
  var rd = require("react-dom");
@@ -2028,11 +2075,12 @@ var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_vi
2028
2075
  var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_MS2, nextRestartWait: nextRestartWait2, isAmbiguousRestartRequestError: isAmbiguousRestartRequestError2 } = (init_restart_wait(), __toCommonJS(restart_wait_exports));
2029
2076
  var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
2030
2077
  var { applyInstalledUpdates: applyInstalledUpdates2, installedUpdateStats: installedUpdateStats2 } = (init_installed_updates(), __toCommonJS(installed_updates_exports));
2031
- var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2, upgradeNotify: upgradeNotify2, REGISTRY_DEFAULT_TARGET: REGISTRY_DEFAULT_TARGET2 } = (init_operations(), __toCommonJS(operations_exports));
2078
+ var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2, TERMINAL_CLEARABLE: TERMINAL_CLEARABLE2, REGISTRY_DEFAULT_TARGET: REGISTRY_DEFAULT_TARGET2 } = (init_operations(), __toCommonJS(operations_exports));
2032
2079
  var { groupOpsByDay: groupOpsByDay2, opRowVm: opRowVm2, opUpgradeMeta: opUpgradeMeta2, canRemoveRow: canRemoveRow2, channelLabelKey: channelLabelKey2 } = (init_operations_view(), __toCommonJS(operations_view_exports));
2033
2080
  var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
2034
2081
  var { readSelfCheckCache: readSelfCheckCache2, writeSelfCheckCache: writeSelfCheckCache2, clearSelfCheckCache: clearSelfCheckCache2, deriveChipState: deriveChipState2 } = (init_self_check(), __toCommonJS(self_check_exports));
2035
2082
  var { shouldShowInstallCmd: shouldShowInstallCmd2 } = (init_install_cmd(), __toCommonJS(install_cmd_exports));
2083
+ var { createOpOutcome: createOpOutcome2 } = (init_op_outcome(), __toCommonJS(op_outcome_exports));
2036
2084
  var ZH = {
2037
2085
  "market.title": "\u63D2\u4EF6\u5E02\u573A",
2038
2086
  "tab.market": "\u5E02\u573A",
@@ -2645,6 +2693,7 @@ function lookup(key, params) {
2645
2693
  const dict = browserLang() === "en" ? EN : ZH;
2646
2694
  return interpolate(dict[key] ?? ZH[key] ?? key, params);
2647
2695
  }
2696
+ var oc = createOpOutcome2(lookup);
2648
2697
  function communityLabels(data) {
2649
2698
  const c = data && data.community && typeof data.community === "object" ? data.community : {};
2650
2699
  const zh = c.categoryLabels && typeof c.categoryLabels === "object" ? c.categoryLabels : {};
@@ -4079,7 +4128,7 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
4079
4128
  h(SearchBox, { key: zone, placeholder: lookup("search.ph"), initial: query.query, onCommit: (v) => updateQuery(v ? { query: v, category: null } : { query: v }) })
4080
4129
  ) : null;
4081
4130
  const zoneBar = zoneChips3;
4082
- const installResultText = (res) => lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (res.bundleWarning === "no-patch-layer" ? lookup("notify.bundlewarning") : "");
4131
+ const installResultText = (res) => lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + oc.buildsSuffix(res) + (res.bundleWarning === "no-patch-layer" ? lookup("notify.bundlewarning") : "");
4083
4132
  const installDone = (it2, res) => {
4084
4133
  notify({ kind: "ok", needsRestart: true, text: installResultText(res) });
4085
4134
  setInstallNote({ id: it2.id, kind: "ok", text: installResultText(res) });
@@ -4099,25 +4148,12 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
4099
4148
  installDone(it, res);
4100
4149
  await (onMutation ? onMutation() : reload(false));
4101
4150
  } catch (e) {
4102
- if (e && e.opSuperseded) {
4103
- notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.name }) });
4104
- setInstallNote({ id: it.id, kind: "hint", text: lookup("op.superseded.note", { target: it.name }) });
4105
- } else if (e && e.guard) {
4106
- const guardText = [
4107
- lookup("guard.blocked"),
4108
- `${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
4109
- e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
4110
- lookup("guard.noforce"),
4111
- e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
4112
- ].filter(Boolean).join(" | ");
4113
- notify({ kind: "err", text: guardText });
4114
- setInstallNote({ id: it.id, kind: "err", text: guardText });
4115
- } else if (e && e.issue) {
4151
+ if (e && e.issue) {
4116
4152
  setCompatConfirm({ it, version, issue: e.issue, opId: e.opId });
4117
4153
  } else {
4118
- const failText = lookup("failed.install", { err: e && e.message || e });
4119
- notify({ kind: "err", text: failText });
4120
- setInstallNote({ id: it.id, kind: "err", text: failText });
4154
+ const m = oc.mutationFailed(e, { verb: "install", target: it.name });
4155
+ notify({ kind: m.kind, text: m.text });
4156
+ setInstallNote({ id: it.id, kind: m.kind === "ok" ? "hint" : "err", text: m.text });
4121
4157
  }
4122
4158
  }
4123
4159
  };
@@ -4128,31 +4164,14 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind, o
4128
4164
  const m = opUpgradeMeta2(r);
4129
4165
  return m ? { ...r, opMeta: m } : r;
4130
4166
  });
4131
- const note = upgradeNotify2(res.activation);
4132
- const text = lookup("notify.upgraded", { pkg: res.pkg, from: res.fromVersion ? `v${res.fromVersion}` : "\u2014", to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (note.suffixKey ? lookup(note.suffixKey) : "");
4133
- notify({ kind: "ok", needsRestart: note.needsRestart, text });
4134
- setInstallNote({ id: it.id, kind: "ok", text });
4167
+ const ok = oc.upgradeOkText(res);
4168
+ notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
4169
+ setInstallNote({ id: it.id, kind: "ok", text: ok.text });
4135
4170
  await (onMutation ? onMutation() : void 0);
4136
4171
  } catch (e) {
4137
- if (e && e.opSuperseded) {
4138
- const sup = lookup("op.superseded.note", { target: it.installedPkg });
4139
- notify({ kind: "ok", text: sup });
4140
- setInstallNote({ id: it.id, kind: "hint", text: sup });
4141
- } else if (e && e.guard) {
4142
- const guardText = [
4143
- lookup("guard.blocked"),
4144
- `${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
4145
- e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
4146
- lookup("guard.noforce"),
4147
- e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
4148
- ].filter(Boolean).join(" | ");
4149
- notify({ kind: "err", text: guardText });
4150
- setInstallNote({ id: it.id, kind: "err", text: guardText });
4151
- } else {
4152
- const failText = lookup("failed.upgrade", { err: e && e.message || e });
4153
- notify({ kind: "err", text: failText });
4154
- setInstallNote({ id: it.id, kind: "err", text: failText });
4155
- }
4172
+ const m = oc.mutationFailed(e, { verb: "upgrade", target: it.installedPkg });
4173
+ notify({ kind: m.kind, text: m.text });
4174
+ setInstallNote({ id: it.id, kind: m.kind === "ok" ? "hint" : "err", text: m.text });
4156
4175
  }
4157
4176
  };
4158
4177
  const CompatDialog = compatConfirm ? h(
@@ -4510,11 +4529,8 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
4510
4529
  notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
4511
4530
  await reload();
4512
4531
  } catch (e) {
4513
- if (e && e.opSuperseded) {
4514
- notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
4515
- } else {
4516
- notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
4517
- }
4532
+ const m = oc.mutationFailed(e, { verb: "toggle", target: it.pkg });
4533
+ notify({ kind: m.kind, text: m.text });
4518
4534
  } finally {
4519
4535
  setPendingToggles((m) => {
4520
4536
  if (!(it.pkg in m)) return m;
@@ -4533,11 +4549,8 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
4533
4549
  });
4534
4550
  await (onMutation ? onMutation() : reload());
4535
4551
  } catch (e) {
4536
- if (e && e.opSuperseded) {
4537
- notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
4538
- } else {
4539
- notify({ kind: "err", text: lookup("failed.uninstall", { err: e && e.message || e }) });
4540
- }
4552
+ const m = oc.mutationFailed(e, { verb: "uninstall", target: it.pkg });
4553
+ notify({ kind: m.kind, text: m.text });
4541
4554
  }
4542
4555
  };
4543
4556
  const doUpgrade = async (it) => {
@@ -4547,34 +4560,12 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
4547
4560
  const m = opUpgradeMeta2(r);
4548
4561
  return m ? { ...r, opMeta: m } : r;
4549
4562
  });
4550
- const note = upgradeNotify2(res.activation);
4551
- notify({
4552
- kind: "ok",
4553
- needsRestart: note.needsRestart,
4554
- text: lookup("notify.upgraded", {
4555
- pkg: res.pkg,
4556
- from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
4557
- to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
4558
- }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (note.suffixKey ? lookup(note.suffixKey) : "")
4559
- });
4563
+ const ok = oc.upgradeOkText(res);
4564
+ notify({ kind: "ok", needsRestart: ok.needsRestart, text: ok.text });
4560
4565
  await (onMutation ? onMutation() : reload());
4561
4566
  } catch (e) {
4562
- if (e && e.guard) {
4563
- notify({
4564
- kind: "err",
4565
- text: [
4566
- lookup("guard.blocked"),
4567
- `${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
4568
- e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
4569
- lookup("guard.noforce"),
4570
- e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
4571
- ].filter(Boolean).join(" | ")
4572
- });
4573
- } else if (e && e.opSuperseded) {
4574
- notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
4575
- } else {
4576
- notify({ kind: "err", text: lookup("failed.upgrade", { err: e && e.message || e }) });
4577
- }
4567
+ const m = oc.mutationFailed(e, { verb: "upgrade", target: it.pkg });
4568
+ notify({ kind: m.kind, text: m.text });
4578
4569
  }
4579
4570
  };
4580
4571
  if (loading && !data) return h("div", { className: "dshm-empty" }, lookup("installed.loading"), Spin());
@@ -5211,13 +5202,14 @@ function DshmVersionChip({ version, notify, initialCheck, ops }) {
5211
5202
  notify({
5212
5203
  kind: "ok",
5213
5204
  needsRestart: true,
5214
- text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "")
5205
+ text: lookup("notify.selfupgraded", { from: version, to: res.version || "latest" }) + oc.buildsSuffix(res)
5215
5206
  });
5216
5207
  } catch (e) {
5217
5208
  if (e && e.unsupported) {
5218
5209
  notify({ kind: "info", text: e.unsupported.guidance || e && e.message || String(e) });
5219
5210
  } else {
5220
- notify({ kind: "err", text: lookup("notify.selfupgraded.failed", { err: e && e.message || e }) });
5211
+ const m = oc.mutationFailed(e, { verb: "self", target: "dsh-m" });
5212
+ notify({ kind: m.kind, text: m.text });
5221
5213
  }
5222
5214
  } finally {
5223
5215
  setBusy(false);
@@ -40,6 +40,7 @@ import { setLivePluginDisabled } from './live-plugin.js';
40
40
  import { warmPackumentFor } from './packument.js';
41
41
  import { syncNpmmirrorPackage } from './npm-route.js';
42
42
  import { clearAllLatestCache } from './latest-cache.js';
43
+ import { parse as yamlParse } from 'yaml';
43
44
  import { assertNpmIntegrity, atomicWriteFile, readPnpmLockIntegrity, readPnpmLockOverrides, restoreSnapshots, snapshotFiles, verifySnapshots, } from './npm-integrity.js';
44
45
  import { isSafePluginTarget, makeDshRunner, removePatchedDependencyEntries, PNPM_OUTCOME_CODES, } from './dsh-cli.js';
45
46
  export const TX_HEAL_CODES = [
@@ -811,6 +812,25 @@ async function uninstall(req, d, snapshots) {
811
812
  }
812
813
  }
813
814
  // ---------- compensate-install 门(M2 Task 3:专用补偿事务,四边界见 DESIGN §3) ----------
815
+ /** 解析 pnpm-workspace.yaml 的 patchedDependencies 键集合(0.9.94 D9)。
816
+ * 文本不可解析(坏 YAML / 重复键)→ null(保守,不抛)——调用方据此走退化或 fail-closed;
817
+ * 空值/缺省(parse 得 null/undefined)与数组形态一律按空集处理(本机实证 parse('') === null;
818
+ * 防 typeof null === 'object' 朴素实现令 Object.keys(null) 抛错走 INTERNAL_ERROR——评审 N5)。 */
819
+ function workspacePatchedKeys(text) {
820
+ let doc;
821
+ try {
822
+ doc = yamlParse(text);
823
+ }
824
+ catch {
825
+ return null;
826
+ }
827
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
828
+ return new Set();
829
+ const patched = doc.patchedDependencies;
830
+ if (patched === null || typeof patched !== 'object' || Array.isArray(patched))
831
+ return new Set();
832
+ return new Set(Object.keys(patched));
833
+ }
814
834
  /** patchMapping 结构化双落点恢复:workspace(pnpm-workspace.yaml patchedDependencies)与 manifest(pnpm.patchedDependencies)。 */
815
835
  async function restorePatchMapping(profileDir, mapping, heal) {
816
836
  const wsFile = join(profileDir, 'pnpm-workspace.yaml');
@@ -826,9 +846,13 @@ async function restorePatchMapping(profileDir, mapping, heal) {
826
846
  if (!lines.some((l) => l.trim() === 'patchedDependencies:')) {
827
847
  lines.push('patchedDependencies:');
828
848
  }
849
+ // 0.9.94 D9:解析去重——同键任意引号形态(pnpm 的单引号/无引号书写)在场即跳过,杜绝对重复键文件的自造;
850
+ // 解析失败(坏 YAML/重复键)退化为今日逐字判重(保守不收紧)
851
+ const existingKeys = workspacePatchedKeys(text);
829
852
  for (const entry of mapping.workspace) {
830
853
  const line = ` ${JSON.stringify(entry.key)}: ${entry.patchPath}`;
831
- if (!lines.includes(line))
854
+ const present = existingKeys !== null ? existingKeys.has(entry.key) : lines.includes(line);
855
+ if (!present)
832
856
  lines.push(line);
833
857
  }
834
858
  await atomicWriteFile(wsFile, Buffer.from(lines.join('\n'), 'utf8'));
@@ -860,8 +884,13 @@ async function verifyPatchMapping(profileDir, mapping) {
860
884
  if (mapping.workspace.length > 0) {
861
885
  try {
862
886
  const text = await readFile(join(profileDir, 'pnpm-workspace.yaml'), 'utf8');
887
+ // 0.9.94 D9:yaml 键成员判定替代双引号子串扫描——单引号/无引号在场形态不再被误判「复读缺失」;
888
+ // 解析失败(坏 YAML/重复键)fail-closed 报「复读失败」(与 manifest 支 JSON 解析失败同款纪律)
889
+ const keys = workspacePatchedKeys(text);
890
+ if (keys === null)
891
+ return 'pnpm-workspace.yaml 复读失败:YAML 无法解析';
863
892
  for (const entry of mapping.workspace) {
864
- if (!text.includes(JSON.stringify(entry.key)))
893
+ if (!keys.has(entry.key))
865
894
  return `workspace 补丁映射复读缺失:${entry.key}`;
866
895
  }
867
896
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.93",
3
+ "version": "0.9.94",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",