dsh-m 0.9.92 → 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,18 @@ 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
+
17
+ ### 0.9.93 重构:装后守卫线格式收敛至 InstallGuardError.toWire——加守卫字段编辑点 8 → 2(纯重构,线格式语义零变化)
18
+
19
+ - **需求(2026-10-16 架构巡查摩擦点 3,巡查首推)**:`InstallGuardError` → 线格式的投影在手写 5 处逐字重复(host-api 409 payload / tools install+upgrade 两臂 / cli install+upgrade 两臂)+ client 再拼一次——加一个守卫字段要碰 8 处(类 + 5 投影 + client 读取 + 测试),`repairBasis` 后加时各点手补已实证过一次复发。
20
+ - **方案**:产地方法 `toWire(): InstallGuardWire`(随类导出接口 + `GuardViolation` re-export)成为 6 字段共享线格式单一事实源;五处投影改一行展开(信封差异留在各端:host-api `ok/error`、tools `ok/guard/message`、cli 裸 JSON),`repairBasis` undefined 时键不存在的语义移入方法本体。加字段编辑点降为 2(类+toWire 一处、client 读取一处——浏览器边界不能 import core,防御性拼装留守,如实注记)。自觉差异注记:tools 信封键序 kind/message 交换(JSON 键序对消费方无语义,全 tests/ 无原始 JSON 键序钉)。
21
+ - **验证**:全量 1530 测试全绿(新增 toWire 三钉例:六字段齐/repairBasis 键缺席/键序;新增 host-api dispatcher 409 钉例补 host-api 投影零测试覆盖的缺口;tools 两臂 execute 钉例零迁移)、typecheck 零错误;收敛锚 `repairBasis ? { repairBasis: … }` 三文件 5 → 0(兼证无第 6 处投影);实施计划经两轮独立评审四项收敛 + 执行评审两轮(含 A1 钉例独立成 describe)记录在 `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`。
22
+
11
23
  ### 0.9.92 修复+重构:三端市场查询文法统一(market-query 单一入口)——CLI 非法 category 吞错修复
12
24
 
13
25
  - **需求(2026-10-16 实测)**:`dshm search --category <非法>` 静默返回「没有匹配的收录条目」+ exit 0,而同输入 GUI 得 400、agent 工具显式报错——「合并市场」词条承诺的「三端同源同语义」被 CLI 打破;根因是分类 slug 正则、source/offset/limit 解析、精选标签表在三端各持一份拷贝(正则 ×3、解析 ×3、标签表 ×2、deadline 常量 ×2),CLI 的拷贝漏了 category 校验。
@@ -649,6 +661,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
649
661
 
650
662
  ## English
651
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
+
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)
671
+
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.
673
+ - **Approach**: a source method `toWire(): InstallGuardWire` (interface exported alongside the class, plus a `GuardViolation` re-export) becomes the single source of the shared 6-field wire format; the five projections become one-line spreads (envelope differences stay per-end: host-api `ok/error`, tools `ok/guard/message`, cli bare JSON), and the "repairBasis key absent when undefined" semantics move into the method body. Adding a field now touches 2 points (class+toWire, and the client read — the browser bundle cannot import core modules, so its defensive assembly stays, honestly noted). Deliberate delta: the tools envelope swaps key order between kind/message (JSON key order carries no consumer semantics; no raw-JSON key-order pins exist across tests/).
674
+ - **Verification**: full suite 1530 green (three new toWire pins: six fields / repairBasis key absence / key order; a new host-api dispatcher 409 pin closing the "host-api projection had zero test coverage" gap; tools-arm execute pins migrated zero), typecheck clean; the convergence anchor `repairBasis ? { repairBasis: … }` went 5 → 0 across the three files (also proving there is no 6th projection); plan review (two rounds, four findings) and execution review (two rounds, including A1 extracting the pin into its own describe) recorded in `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`.
675
+
652
676
  ### Fix + refactor in 0.9.92 — unified three-end market query intake (market-query single entry); CLI invalid-category silent swallow fixed
653
677
 
654
678
  - **Need (2026-10-16, reproduced live)**: `dshm search --category <invalid>` silently printed "no matching entries" and exited 0, while the same input gets a 400 from the panel API and an explicit error from the agent tool — breaking the "same source, same semantics across GUI / tools / CLI" promise. Root cause: the category slug regex, source/offset/limit parsing, and the curated label map each had per-end copies (regex ×3, parsing ×3, labels ×2, deadline constants ×2), and the CLI copy lacked category validation.
package/lib/cli.js CHANGED
@@ -193,7 +193,7 @@ async function runCliDispatch(argv, deps, io) {
193
193
  // metadata-only:不构造全量 latest;0.9.92 三端查询文法:category/source/limit/offset 经
194
194
  // market-query 单一入口归一(与 host-api/工具同源同语义);文法违规 throw 直飞 dispatch 统一
195
195
  // catch(`错误:${msg}` + exit 1,与此前 source 内联报错输出逐字节同款)——非法 category
196
- // 自此显式报错(0.9.91 前静默空结果吞错,回归钉例在 cli-search.test.mjs)。
196
+ // 自此显式报错(≤0.9.91 静默空结果吞错,回归钉例在 cli-search.test.mjs)。
197
197
  // 双源判定:主清单 unavailable 但社区有条目 → 照常出页(Q42);两层皆不可用 → exit 1
198
198
  const intake = parseMarketQueryInput({ category: flags.category, source: flags.source, limit: flags.limit, offset: flags.offset }, { limitMax: 80, limitDefault: 10 });
199
199
  const result = await d.listMarket(cfg, {
@@ -332,7 +332,7 @@ async function runCliDispatch(argv, deps, io) {
332
332
  }
333
333
  catch (e) {
334
334
  if (e instanceof InstallGuardError) {
335
- err('[⛔ 装后守卫拦截] ' + JSON.stringify({ kind: e.kind, violations: e.violations, compensation: e.compensation, needsRestart: e.needsRestart, restartSafe: e.restartSafe, ...(e.repairBasis ? { repairBasis: e.repairBasis } : {}) }));
335
+ err('[⛔ 装后守卫拦截] ' + JSON.stringify(e.toWire()));
336
336
  err(e.message);
337
337
  err(e.restartSafe ? '可以重启 DSH Web:dshm restart --yes' : '修复后再重启(不要现在重启)。');
338
338
  return 1;
@@ -357,7 +357,7 @@ async function runCliDispatch(argv, deps, io) {
357
357
  }
358
358
  catch (e) {
359
359
  if (e instanceof InstallGuardError) {
360
- err('[⛔ 装后守卫拦截] ' + JSON.stringify({ kind: e.kind, violations: e.violations, compensation: e.compensation, needsRestart: e.needsRestart, restartSafe: e.restartSafe, ...(e.repairBasis ? { repairBasis: e.repairBasis } : {}) }));
360
+ err('[⛔ 装后守卫拦截] ' + JSON.stringify(e.toWire()));
361
361
  err(e.message);
362
362
  err(e.restartSafe ? '可以重启 DSH Web:dshm restart --yes' : '修复后再重启(不要现在重启)。');
363
363
  return 1;
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);
@@ -166,18 +166,14 @@ function errorStatus(err) {
166
166
  return { status, payload: { ok: false, error: err.message, code: err.code } };
167
167
  }
168
168
  if (err instanceof InstallGuardError) {
169
- // 装后守卫拦截(M2 Task 3):结构化投影含 needsRestart/restartSafe/kind——GUI 一键重启只读 restartSafe
169
+ // 装后守卫拦截(M2 Task 3):结构化投影含 needsRestart/restartSafe/kind——GUI 一键重启只读 restartSafe;
170
+ // 0.9.93 起线格式单源于 toWire(信封 ok/error 本端自拼)
170
171
  return {
171
172
  status: 409,
172
173
  payload: {
173
174
  ok: false,
174
175
  error: err.message,
175
- kind: err.kind,
176
- violations: err.violations,
177
- compensation: err.compensation,
178
- needsRestart: err.needsRestart,
179
- restartSafe: err.restartSafe,
180
- ...(err.repairBasis ? { repairBasis: err.repairBasis } : {}),
176
+ ...err.toWire(),
181
177
  },
182
178
  };
183
179
  }
@@ -822,6 +822,21 @@ export class InstallGuardError extends Error {
822
822
  this.compensation = fields.compensation;
823
823
  this.repairBasis = fields.repairBasis;
824
824
  }
825
+ /**
826
+ * 线格式单一事实源(0.9.93):三端共享的 6 字段守卫投影。各端信封自拼(host-api 409 payload 的
827
+ * ok/error、tools 的 ok/guard/message、cli 的裸 JSON),共享部分一律经此方法——加守卫字段只改
828
+ * 本方法 + client 读取一处。repairBasis undefined 时键不存在(今日 5 处投影同语义)。
829
+ */
830
+ toWire() {
831
+ return {
832
+ kind: this.kind,
833
+ violations: this.violations,
834
+ compensation: this.compensation,
835
+ needsRestart: this.needsRestart,
836
+ restartSafe: this.restartSafe,
837
+ ...(this.repairBasis ? { repairBasis: this.repairBasis } : {}),
838
+ };
839
+ }
825
840
  }
826
841
  /** lockfile packages 段的目标包 resolution 提取(行扫描:键行 `pkg@ver:` → 收集至下一键行;引号/嵌套键兼容)。 */
827
842
  function lockResolutionOf(lockText, pkg) {
@@ -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/lib/tools.js CHANGED
@@ -241,7 +241,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
241
241
  catch (err) {
242
242
  if (err instanceof InstallGuardError) {
243
243
  // 结构化 error result(M2 Task 3 ㉑-㉜):不渲染「已安装」,无 force 通道;restartSafe 随结果携带
244
- return { ok: false, guard: true, kind: err.kind, message: err.message, violations: err.violations, compensation: err.compensation, needsRestart: err.needsRestart, restartSafe: err.restartSafe, ...(err.repairBasis ? { repairBasis: err.repairBasis } : {}) };
244
+ return { ok: false, guard: true, message: err.message, ...err.toWire() };
245
245
  }
246
246
  throw err;
247
247
  }
@@ -403,7 +403,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
403
403
  }
404
404
  catch (err) {
405
405
  if (err instanceof InstallGuardError) {
406
- return { ok: false, guard: true, kind: err.kind, message: err.message, violations: err.violations, compensation: err.compensation, needsRestart: err.needsRestart, restartSafe: err.restartSafe, ...(err.repairBasis ? { repairBasis: err.repairBasis } : {}) };
406
+ return { ok: false, guard: true, message: err.message, ...err.toWire() };
407
407
  }
408
408
  throw err;
409
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.92",
3
+ "version": "0.9.94",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",