@moonquake2004/dsh-doctor 0.6.1 → 0.7.0

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
@@ -16,6 +16,48 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## [0.7.0] — 2026-09-16
20
+
21
+ 一次**针对工具自身可信度**的加固,起因是连续四轮社区报告都照出了同一个病:**我们的结论比证据更自信**。
22
+
23
+ ### Added — 覆盖不变量("没看" ≠ "通过")
24
+
25
+ 实测:空环境(零 bundle、无会话库)下曾有 **18 项 pass,其中 14 项连一个数量都没给** —— "我什么都没看"与
26
+ "我全看过、很干净"打印出来完全一样。`--boot-check` 对"bundle 缺 `dsh.bundle`"(零 entry 可探)报
27
+ "✓ 全部可导入"(#6788 正是这一类)只是同一 bug 的一个实例。
28
+
29
+ 现在:
30
+ - **`examined === 0` 的 `pass` 一律自动降为 `skip`**("无可检查对象,未做任何比较")——硬不变量,CI 有测试盯着;
31
+ - `pass` 需报告**检查了多少对象**(含 `examinedWhat`);未报告者带 `coverage: 'unreported'` 欠账标记,
32
+ 可被测试度量。**profile 段的欠账已从 18 项清零**(上下文默认值 + 逐项复核单位不同的检查,如
33
+ `P12` 的对象是"已装候选包"、`P18` 是"manifest",都不是 bundle 列表长度)。
34
+ **env 段仍有 10 项未报告覆盖量**(`E1-*` / `E3-node` / `E4` / `E5` / `E6` / `E10` / `E12` / `E13`)——
35
+ 这些探针各查一个对象(某个二进制/版本/端口),补起来是机械的,但**尚未补**,故在此如实列出,
36
+ 而不是让 release note 显得比实际干净。
37
+
38
+ ### Fixed — 故障隔离:一条正确的检查曾被无关崩溃静默
39
+
40
+ #6758 的 BOM 场景里,**P15 本来就能查、也查对了**;但在修复前,主读取点先抛错 →
41
+ `P0: profile 检查异常` → **P15 根本没机会执行**。真实失败不是"缺检查",而是**"正确的检查被上游故障静默了"**。
42
+ 今天同类问题出现两次(另一处是变量作用域错误掐掉整个 profile 段)。现在外部读取一律不抛(带 BOM 感知的
43
+ 读取已覆盖 manifest),并有两条回归测试钉住:**BOM 时 P15 仍报出、manifest 无法解析时其余检查仍给结论**。
44
+
45
+ ### Removed — 撤销 `P22`(与 `P15` 重复)
46
+
47
+ `P15`(关键文件 BOM,源自 #5176)**早就扫描 profile 的 `package.json`** 并报出同一结论;我加 `P22` 之前
48
+ **没有查现有清单**。#6758 特有的信息(PowerShell 5.1 的 `-Encoding UTF8` 会写 BOM、`JSON.parse` 硬失败、
49
+ 报错被 GBK 渲染成乱码)已并入 `P15`。**同一事实只应有一个归属。**
50
+
51
+ ### 为什么这些错误能发生(写下来,供以后对照)
52
+
53
+ 六处错误(S14 判错视图、SR5 成片误报、`--boot-check` 假绿灯、P22 重复、P18 矛盾结论、P15 被静默)**根因相同**:
54
+ **用一个"看起来合理"的模型,代替了权威来源**——来源分别是宿主代码路径、整库样本、loader 前置条件、
55
+ 自家检查清单、宿主的解析代码、检查的执行顺序。
56
+
57
+ 我们的原则(`unknown` 不得折叠、不适用就带理由 skip、"只在一个平台验证过 ≈ 没验证")此前只写在文档与
58
+ 评审意见里,**没有变成引擎里的不变量**;而唯一一次我把原则写成机制(peer-range 一致性测试从**已发布源码**
59
+ 抽函数来跑语料)恰恰没出问题。**原则不落成机制,就只剩运气。**
60
+
19
61
  ## [0.6.1] — 2026-09-15
20
62
 
21
63
  ### Fixed — `--boot-check` 在"bundle 缺 `dsh.bundle`"这一类上给假绿灯(社区 #6788)
package/dsh-doctor.mjs CHANGED
@@ -221,8 +221,36 @@ function hasDshEnvironment(home = HOME) {
221
221
  return existsSync(join(home, 'sessions')) || existsSync(join(home, 'settings.yaml'));
222
222
  }
223
223
 
224
- function report(section, id, ok, detail, fix, src) {
225
- results.push({ section, id, ok, detail, fix, src: src ?? 'builtin' });
224
+ /**
225
+ * 记录一条检查结论。
226
+ *
227
+ * **覆盖不变量(2026-09 反思后加入)**:一条检查说"通过"时,必须能说出**它检查了多少东西**。
228
+ * 起因是一组实测:空环境(零 bundle、无会话库)下曾出现 **18 项 pass,其中 14 项连一个数量都没有** ——
229
+ * 也就是说"我什么都没看"与"我全看过、很干净"打印出来完全一样。`--boot-check` 对
230
+ * "bundle 缺 dsh.bundle"(零 entry 可探)给出"✓ 全部可导入"就是这个 bug 的一个实例(社区 #6788)。
231
+ *
232
+ * 规则:
233
+ * · `examined === 0` → **不得 pass**,自动降为 skip("无可检查对象")——硬不变量,CI 有测试盯着;
234
+ * · `examined` 未报告 → 结果带 `coverage: 'unreported'`(机器可读的欠账标记),可被测试度量并逐项清零。
235
+ */
236
+ let coverageContext = null; // 当前检查段"检查了多少同类对象"的默认值(由 setCoverage 设置)
237
+ function setCoverage(n, what) { coverageContext = typeof n === 'number' ? { n, what } : null; }
238
+ function coverageNow() { return coverageContext; }
239
+
240
+ function report(section, id, ok, detail, fix, src, examined) {
241
+ const zeroCheck = typeof examined === 'number' ? examined : coverageNow()?.n;
242
+ if (ok === true && zeroCheck === 0) {
243
+ results.push({ section, id, ok: true, skip: true, coverage: 'none', detail: `${detail}(无可检查对象,未做任何比较)`, fix, src: src ?? 'builtin' });
244
+ return;
245
+ }
246
+ const rec = { section, id, ok, detail, fix, src: src ?? 'builtin' };
247
+ if (ok === true) {
248
+ const ctx = coverageNow();
249
+ const n = typeof examined === 'number' ? examined : ctx?.n;
250
+ if (typeof n === 'number') { rec.examined = n; if (ctx?.what) rec.examinedWhat = ctx.what; }
251
+ else rec.coverage = 'unreported';
252
+ }
253
+ results.push(rec);
226
254
  }
227
255
 
228
256
  /** skip 状态(v1 词汇表 r5:#1719)——"不适用"而非"通过",必须带 reason(detail)。不计入 pass/fail,不翻退出码。 */
@@ -559,6 +587,7 @@ function checkProfile(name) {
559
587
  }
560
588
  const bundles = manifest.dsh?.profile?.bundles ?? [];
561
589
  const deps = manifest.dependencies ?? {};
590
+ setCoverage(bundles.length, 'bundle 条目');
562
591
 
563
592
  const installAnchor = (() => {
564
593
  // 从 PATH 找 dsh 的安装目录(node_modules),用于 bundle 双锚点解析
@@ -692,7 +721,7 @@ function checkProfile(name) {
692
721
  if (!profManifest || !(profManifest.dsh && profManifest.dsh.profile)) {
693
722
  reportSkip('profile', 'P18', '未找到 profile manifest(无 dsh.profile),跳过 version 检查');
694
723
  } else if (typeof profManifest.version === 'string' && profManifest.version.length > 0) {
695
- report('profile', 'P18', true, `profile manifest 声明了 version(${profManifest.version}),不触发 #6667`);
724
+ report('profile', 'P18', true, `profile manifest 声明了 version(${profManifest.version}),不触发 #6667`, undefined, 'builtin', 1);
696
725
  } else {
697
726
  report('profile', 'P18', false,
698
727
  `profile manifest 有 name(${profManifest.name})但**没有 version**——与 #6667 的条件一致:package inventory 在解析**游离本地模块**时会把该 manifest 当包处理并抛 "must declare non-empty name and version"(dsh-plugin-package-inventory-deepseek:34;其 allowAnonymous 只容忍缺 name),表现为 DeepSeek 请求 REQUEST_EXTENSION 失败`,
@@ -804,26 +833,8 @@ function checkProfile(name) {
804
833
  }
805
834
  }
806
835
 
807
- // P22:profile manifest 带 UTF-8 BOM(#6758)—— **硬失败且报错不指向病因**
808
- // DSH 直接 `JSON.parse` 该文件:带 BOM 时抛 `Unexpected token '...' is not valid JSON` 并**起不来**;
809
- // 而 GBK 控制台会把 BOM 三个字节渲染成乱码,用户完全看不出是编码问题。
810
- // 这条判据零误报,且正是"报错不指向病因"要补的那一句:**是 BOM,不是 JSON 语法**。
811
- {
812
- const profRead22 = readJsonReportingBom(join(dir, 'package.json'));
813
- if (!profRead22.exists) {
814
- reportSkip('profile', 'P22', '无 profile manifest,跳过 BOM 检查');
815
- } else if (profRead22.hadBom) {
816
- report('profile', 'P22', false,
817
- `profile manifest 带 **UTF-8 BOM**(EF BB BF)—— DSH 会直接 \`JSON.parse\` 该文件:`
818
- + `BOM 会让它抛 \`SyntaxError: Unexpected token '…' is not valid JSON\`(#6758)并**启动硬失败**;`
819
- + `而在 GBK 控制台上那三个字节会显示成乱码,报错里看不出是编码问题`,
820
- '去掉 BOM(保留 UTF-8 无 BOM):PowerShell 5.1 的 `Set-Content -Encoding UTF8` 默认会写 BOM,改用 '
821
- + '`[IO.File]::WriteAllText($p, (Get-Content $p -Raw), (New-Object Text.UTF8Encoding $false))`,'
822
- + '或任何「UTF-8(无 BOM)」保存方式;本工具的其余检查已忽略 BOM 继续工作');
823
- } else {
824
- report('profile', 'P22', true, 'profile manifest 无 UTF-8 BOM(不会触发 #6758 的启动硬失败)');
825
- }
826
- }
836
+ // P22 已撤销:profile manifest 的 BOM 由 P15 覆盖(同一事实只应有一个归属)。
837
+ // 2026-09 反思的产物:加检查前必须先查清单——我当初没查,于是加了一条与 P15 重复的检查。
827
838
 
828
839
  // P19:插件声明的 host peer 范围 vs 实际提供的 host 版本(社区 #6678 @ciceroyang 提案)
829
840
  // 这是"升级后起不来"的常见原因之一:插件声明只支持某段 core 版本,而实际装的核心已在区间外,
@@ -1254,9 +1265,11 @@ function packageNamedExports(pkgDir) {
1254
1265
  const bundleVersion = JSON.parse(readFileSync(bundlePkg, 'utf8')).version;
1255
1266
  const cliVersion = localVersion();
1256
1267
  const same = bundleVersion === cliVersion;
1268
+ // 显式覆盖量:P12 的比较对象是"已装的候选包"(1 个),不是 bundle 列表长度——
1269
+ // 这正是上下文默认值需要被逐项复核的地方(否则 0 个 bundle 的 profile 会被误降为 skip)。
1257
1270
  report('profile', 'installed_bundle', same,
1258
1271
  same ? `profile 内 bundle 版本 ${bundleVersion} 与运行 CLI ${cliVersion} 一致` : `profile 内 bundle 版本 ${bundleVersion} ≠ 运行 CLI ${cliVersion}(web 面板/API 跑的是 bundle,两边行为可能不一致;若刚发布过新版本,升级可能被 pnpm-workspace.yaml 的 minimumReleaseAgeExclude 年龄门暂缓,可次日重试)`,
1259
- same ? undefined : `同步安装版本:dsh plugin --profile ${name} update ${selfName}(或让 CLI 与 bundle 走同一安装方式)`);
1272
+ same ? undefined : `同步安装版本:dsh plugin --profile ${name} update ${selfName}(或让 CLI 与 bundle 走同一安装方式)`, undefined, 1);
1260
1273
  }
1261
1274
  } catch (e) {
1262
1275
  report('profile', 'installed_bundle', false, `bundle 版本对比异常: ${e.message.slice(0, 60)}`, undefined);
@@ -1287,9 +1300,15 @@ function packageNamedExports(pkgDir) {
1287
1300
  } catch { /* skip */ }
1288
1301
  }
1289
1302
  if (bomFiles.length > 0) {
1290
- report('profile', 'P15', false, `检测到 BOM 头(#5176:JSON/YAML 解析将失败): ${bomFiles.join(', ')}`, '用文本编辑器打开文件,删除首字符(BOM/U+FEFF)后保存;或运行: sed -i "" "1s/^\xEF\xBB\xBF//" <file>');
1303
+ report('profile', 'P15', false,
1304
+ `检测到 BOM 头(#5176 / #6758:JSON/YAML 解析将失败): ${bomFiles.join(', ')}`
1305
+ + `—— 若命中 profile 的 package.json,DSH 会直接 \`JSON.parse\` 它并抛 \`SyntaxError: Unexpected token '…' is not valid JSON\`,`
1306
+ + `**启动硬失败**;而 GBK 控制台会把 BOM 三个字节渲染成乱码,报错里看不出是编码问题`,
1307
+ '删除首字符(BOM/U+FEFF)后保存;PowerShell 5.1 的 `Set-Content -Encoding UTF8` **默认会写 BOM**,'
1308
+ + '改用 [IO.File]::WriteAllText($p, (Get-Content $p -Raw), (New-Object Text.UTF8Encoding $false));'
1309
+ + '或 sed -i "" "1s/^\xEF\xBB\xBF//" <file>', undefined, 'builtin', bomTargets.length);
1291
1310
  } else {
1292
- report('profile', 'P15', true, '关键文件无 BOM 头', undefined);
1311
+ report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, 'builtin', bomTargets.length);
1293
1312
  }
1294
1313
 
1295
1314
  /* P16:插件命名导入的导出缺失检测(#5864:一个缺失导出 → 整棵插件树 boot 崩溃循环、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
5
5
  "main": "lib/index.js",
6
6
  "files": [