@moonquake2004/dsh-doctor 0.7.0 → 0.7.2

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,75 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## [0.7.2] — 2026-09-16
20
+
21
+ ### 用新规则复查旧错误 —— **确认重复犯了同一个错**
22
+
23
+ 按 `docs/check-authoring-rules.md` 的 R1–R9 重新审前面犯过错的案例(尤其审 0.7.0/0.7.1 这批新代码)。
24
+ 结论:**是的,重复犯了,而且是"治症状"的典型**——我修了 #6788 那个**具体形态**,没修**这一类**。
25
+
26
+ **① 同一个缺陷出现在三处输出路径**(R3 覆盖律只在 `report()` 里,而这些路径不走它):
27
+
28
+ | 路径 | 零对象时的行为(修前) |
29
+ |---|---|
30
+ | `--boot-check` | 打印"✓ 所有可探测 entry 均可导入",退出码 0 |
31
+ | `--safe-add` | `ok: true, stage: 'verified'` **并写入"已知良好"快照**——把"未验证"固化成"已验证" |
32
+ | `--post-upgrade` | 打印"✓ 装载模拟通过"并写快照 |
33
+
34
+ 改为**唯一判定入口 `bootVerdict()`**(三处统一):零对象一律 `skip`,**不写快照、不宣称通过**。
35
+ 按 R7 的问法——"如果它完全坏了,会打印什么?"——修前与健康时相同,即**不可证伪**;现在三态可区分。
36
+
37
+ **② 清单机制不够**(R1/R5):`docs/check-inventory.md` 曾**缺 8 个动态构造的 id**
38
+ (`E1-node`/`E1-pnpm`/`E1-zstd`/`E7-dsh-in-path`/`E8-npmrc-workspace-flag`/`E9-storages-json-valid`/
39
+ `E11-settings-writable`/`P6-patch-name-space`),即"查清单"可能查不到它们。现已补齐(**51 项、缺失 0**)。
40
+ **范围重叠仍无自动检测**——见下。
41
+
42
+ **③ 更正我自己的过度声称**(R8 的来历):0.7.1 的 release note 写"P22 与 P15 重复那件事**由此可被拦住**",
43
+ 但该机制只强制一次可见 diff,**并不检测重叠**。已在原处标注更正。
44
+
45
+ **④ R4 隔离律远未落地**:复查 profile/env 段的读取,**14 处疑似未加保护**,抽查 3 处
46
+ (`readFileSync(patchFile)`、`JSON.parse(readFileSync(.../package.json))`、`readFileSync(patchPath)`)
47
+ **全部确认在 try 之外**。此前只修了 manifest 那一处。
48
+
49
+ **⑤ 规则集本身缺两条,已补**(这是复查最有价值的产出):
50
+ - **R8 断言强度律**:断言的强度不得超过证据(复现过 > 读过代码 > 推断;一个样本 ≠ 全体;**机制存在 ≠ 机制足够**)。
51
+ **这是唯一一条没有机制的规则**,已明确标注为待补。
52
+ - **R9 版本漂移律**:宿主收紧前置条件后,先假设 **fixture 失真**而不是代码坏了
53
+ (0.1.6-alpha.1 收紧 loader 后,我们多个 fixture 悄悄变成不合法输入,测试还绿着)。
54
+
55
+ 测试 122 → 124。
56
+
57
+ ## [0.7.1] — 2026-09-16
58
+
59
+ 把"事后总结"变成**规则 + 机制**(`docs/check-authoring-rules.md`)。起因是用户指出:一次次的修复是头疼医头,
60
+ 应当**总结规律、确定规则**。
61
+
62
+ ### 定律(六个错误是它的六个实例)
63
+
64
+ > **一、结论必须来自来源,不能来自模型;够不到来源就报未知。**
65
+ > **二、我们对用户承诺的契约(三态、`unknown` 不折叠、不适用带理由 skip),必须先在工具自身的构建过程中成立。**
66
+
67
+ ### Added — 机制化
68
+
69
+ - **R6 接口律**:`report()` 对自己的参数做类型校验并**立即抛错**(`examined` 非 number、`src` 非 string 等)。
70
+ 来历是我在 `fix` 后多插两个参数、`'builtin'` 落到 `examined` 位置 → 一条本该 pass 的检查被静默降级。
71
+ **落地当天它就抓出了我自己另外 5 处同类错误**——这一类从此可由机制消灭,不必靠小心。
72
+ - **R5 唯一归属律 / R1 来源律**:新增 `scripts/gen-check-inventory.mjs` 生成 `docs/check-inventory.md`
73
+ (43 项,含段、范围、来源),并加 CI 断言"清单与代码一致"。
74
+ 于是"加检查前先查清单"不再是自觉,而是**一次可见的 diff**。
75
+ ⚠️ **更正(同日复查)**:我最初写的是"P22 与 P15 重复那件事**由此可被拦住**"——**这是过度声称**:
76
+ 该机制只强制一次可见 diff,**并不检测范围重叠**;而且清单当时还**缺 8 个动态构造的 id**。
77
+ 现已补齐清单(51 项、缺失 0),并把"机制存在 ≠ 机制足够"写成了规则 R8。
78
+ 清单中**尚有 8 项未标注权威来源(R1 欠账)**,如实列出。
79
+ - **R3 收尾**:环境段补齐覆盖量(每个环境探针各检查一个对象),**全环境覆盖量欠账 18 → 0**;
80
+ CI 断言"空环境下未报告覆盖量的 pass 必须为 0"(此前只是打印数字)。
81
+
82
+ ### 规则集(`docs/check-authoring-rules.md`)
83
+
84
+ R1 来源律 · R2 样本律 · R3 覆盖律 · R4 隔离律 · R5 唯一归属律 · R6 接口律 · R7 可证伪律。
85
+ **每条都必须配一个可执行机制;没有机制的规则一律视为不存在**(这是 §0 第 4 条,也是这一轮最贵的一课)。
86
+ 文件末尾如实列出各规则的落地状态:R3/R6 已落地,R4/R7 部分落地,**R1/R2/R5 的机制刚起步**。
87
+
19
88
  ## [0.7.0] — 2026-09-16
20
89
 
21
90
  一次**针对工具自身可信度**的加固,起因是连续四轮社区报告都照出了同一个病:**我们的结论比证据更自信**。
package/dsh-doctor.mjs CHANGED
@@ -238,6 +238,18 @@ function setCoverage(n, what) { coverageContext = typeof n === 'number' ? { n, w
238
238
  function coverageNow() { return coverageContext; }
239
239
 
240
240
  function report(section, id, ok, detail, fix, src, examined) {
241
+ // R6 接口律:参数放错位置必须**立刻抛**,而不是变成一条静默的错误判定。
242
+ // 来历:我曾在 fix 之后多插了两个参数,结果 'builtin' 落到了 examined 的位置(字符串)→
243
+ // 回退到上下文 0 → 一条本该 pass 的检查被静默降级。凭记忆拼参数是可以通过机制消灭的。
244
+ if (typeof examined !== 'number' && examined !== undefined) {
245
+ throw new Error(`report(${id}): examined 必须是 number 或 undefined,收到 ${typeof examined}(${JSON.stringify(examined)})——检查参数位置`);
246
+ }
247
+ if (typeof src !== 'string' && src !== undefined) {
248
+ throw new Error(`report(${id}): src 必须是 string 或 undefined,收到 ${typeof src}(${JSON.stringify(src)})——检查参数位置`);
249
+ }
250
+ if (typeof section !== 'string' || typeof id !== 'string' || typeof ok !== 'boolean' || typeof detail !== 'string') {
251
+ throw new Error(`report(): 前四个参数必须是 (section:string, id:string, ok:boolean, detail:string)`);
252
+ }
241
253
  const zeroCheck = typeof examined === 'number' ? examined : coverageNow()?.n;
242
254
  if (ok === true && zeroCheck === 0) {
243
255
  results.push({ section, id, ok: true, skip: true, coverage: 'none', detail: `${detail}(无可检查对象,未做任何比较)`, fix, src: src ?? 'builtin' });
@@ -302,6 +314,9 @@ function nodeInSupportedRange(v, range = NODE_RANGE_FALLBACK) {
302
314
  }
303
315
  function checkEnv() {
304
316
  if (!wants('env')) return;
317
+ // R3 覆盖律:环境段的每个探针各检查**一个**对象(某个二进制/版本/端口)。
318
+ // 单位不同的检查(如 E1 系列各自查一个可执行文件)如需别的数量应显式传入。
319
+ setCoverage(1, '环境对象');
305
320
  const find = (cmd) => { for (const w of process.platform === 'win32' ? ['where'] : ['which']) { const r = spawnSync(w, [cmd]); if (r.status === 0) { const p = String(r.stdout).split(/\r?\n/)[0].trim(); if (p) return p; } } return null; };
306
321
  for (const cmd of ['node', 'pnpm', 'zstd']) {
307
322
  const p = find(cmd);
@@ -721,7 +736,7 @@ function checkProfile(name) {
721
736
  if (!profManifest || !(profManifest.dsh && profManifest.dsh.profile)) {
722
737
  reportSkip('profile', 'P18', '未找到 profile manifest(无 dsh.profile),跳过 version 检查');
723
738
  } else if (typeof profManifest.version === 'string' && profManifest.version.length > 0) {
724
- report('profile', 'P18', true, `profile manifest 声明了 version(${profManifest.version}),不触发 #6667`, undefined, 'builtin', 1);
739
+ report('profile', 'P18', true, `profile manifest 声明了 version(${profManifest.version}),不触发 #6667`, undefined, undefined, 1);
725
740
  } else {
726
741
  report('profile', 'P18', false,
727
742
  `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 失败`,
@@ -1306,9 +1321,9 @@ function packageNamedExports(pkgDir) {
1306
1321
  + `**启动硬失败**;而 GBK 控制台会把 BOM 三个字节渲染成乱码,报错里看不出是编码问题`,
1307
1322
  '删除首字符(BOM/U+FEFF)后保存;PowerShell 5.1 的 `Set-Content -Encoding UTF8` **默认会写 BOM**,'
1308
1323
  + '改用 [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);
1324
+ + '或 sed -i "" "1s/^\xEF\xBB\xBF//" <file>', undefined, bomTargets.length);
1310
1325
  } else {
1311
- report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, 'builtin', bomTargets.length);
1326
+ report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, undefined, bomTargets.length);
1312
1327
  }
1313
1328
 
1314
1329
  /* P16:插件命名导入的导出缺失检测(#5864:一个缺失导出 → 整棵插件树 boot 崩溃循环、
@@ -2697,8 +2712,13 @@ function safeAdd(profileArg, pkg) {
2697
2712
  return { ok: false, stage: 'install', restored: true, exit: install.status };
2698
2713
  }
2699
2714
  const results = runBootCheckSync(profDir);
2700
- const failed = results.filter((r) => r.status === 'failed');
2701
- if (!failed.length) {
2715
+ const verdictAdd = bootVerdict(results);
2716
+ const failed = verdictAdd.failed;
2717
+ if (verdictAdd.state === 'skip') {
2718
+ // 零对象:**不写"已知良好"快照、不宣称已验证**——否则"未验证"会被固化成"已知良好"
2719
+ return { ok: true, stage: 'unchecked', checked: 0, failed: 0, reason: verdictAdd.reason };
2720
+ }
2721
+ if (verdictAdd.state === 'pass') {
2702
2722
  writeSnapshot(profDir, bootSnapshot(profDir));
2703
2723
  return { ok: true, stage: 'verified', checked: results.length, failed: 0 };
2704
2724
  }
@@ -2718,6 +2738,25 @@ function safeAdd(profileArg, pkg) {
2718
2738
  return { ok: true, stage: 'quarantined', quarantined, failures: failed.map((f) => `${f.bundle}/${f.id}: ${f.kind} ${f.error}`) };
2719
2739
  }
2720
2740
 
2741
+
2742
+ /**
2743
+ * **装载模拟的唯一判定入口**(R3 覆盖律的中央落实)。
2744
+ *
2745
+ * 来历:同一个缺陷曾出现在**三处**输出路径(--boot-check / --safe-add / --post-upgrade)——
2746
+ * "零 entry 可探"被当成"通过",其中 --safe-add 还会写"已知良好"快照。
2747
+ * 我最初只修了 --boot-check 那一个实例(#6788 的具体形态),没修这一类。
2748
+ * 所以判定必须只在一个地方:任何"探测了 0 个对象"的运行都是 **skip**,不是 pass;
2749
+ * 也不得据此写快照(否则"未验证"会被固化成"已知良好")。
2750
+ */
2751
+ function bootVerdict(results) {
2752
+ const failed = results.filter((r) => r.status === 'failed');
2753
+ if (failed.length) return { state: 'fail', failed, checked: results.length };
2754
+ if (results.length === 0) {
2755
+ return { state: 'skip', failed: [], checked: 0, reason: '启动列表里没有任何 entry 可探测,未做装载模拟(这不等于通过)' };
2756
+ }
2757
+ return { state: 'pass', failed: [], checked: results.length };
2758
+ }
2759
+
2721
2760
  /** 同步版装载模拟(--safe-add 内部用;与 --boot-check 同一逻辑) */
2722
2761
  function runBootCheckSync(profileDir) {
2723
2762
  const out = [];
@@ -2879,7 +2918,10 @@ async function run() {
2879
2918
  }
2880
2919
  console.log(`升级后复检(基线取自 ${String(base.at).slice(0, 16)}):`);
2881
2920
  for (const l of lines) console.log(` · ${l}`);
2882
- if (!failed.length) {
2921
+ const verdictPost = bootVerdict(results);
2922
+ if (verdictPost.state === 'skip') {
2923
+ console.log(` ⊖ ${verdictPost.reason}——**不改写已知良好基线**`);
2924
+ } else if (verdictPost.state === 'pass') {
2883
2925
  console.log(' ✓ 装载模拟通过——升级未破坏任何可探测 entry');
2884
2926
  writeSnapshot(profDir, nowSnap);
2885
2927
  } else {
@@ -2953,7 +2995,22 @@ async function run() {
2953
2995
  console.log(' ——若本次启动失败,上面这些就是首要嫌疑。');
2954
2996
  }
2955
2997
  }
2956
- if (!failed.length) writeSnapshot(profDir, curSnap); // 只有通过时才更新"已知良好"快照
2998
+ // R3 覆盖律(2026-09 复查):**"零检查对象"不得等于"通过"**。
2999
+ // 这个输出路径不走 report(),所以 report() 里的不变量盖不到它——实测它曾对空启动列表打印
3000
+ // "✓ 所有可探测 entry 均可导入"。这正是 R7 要问的那句:"如果它完全坏了,会打印什么?"
3001
+ // 答案与健康时相同 ⇒ 不可证伪。故这里把"检查了 0 个对象"单列为 skip(不更新快照、不宣称通过)。
3002
+ const verdict = bootVerdict(results);
3003
+ const vacuous = verdict.state === 'skip';
3004
+ if (vacuous) {
3005
+ if (jsonOut) {
3006
+ console.log(JSON.stringify({ ok: true, skip: true, reason: '启动列表为空:没有任何 entry 可探测,未做装载模拟', profile: profDir, checked: 0, failed: 0, drift, results }, null, 2));
3007
+ } else {
3008
+ console.log(`装载模拟(${profDir}):⊖ 无可检查对象——启动列表为空,**未做任何装载模拟**(这不等于"通过")`);
3009
+ console.log(' 若你预期这里有插件:检查 profile 的 dsh.profile.bundles 是否为空,或插件是否装到了别的 profile。');
3010
+ }
3011
+ process.exit(0);
3012
+ }
3013
+ if (!failed.length) writeSnapshot(profDir, curSnap); // 只有**实际检查过且通过**时才更新"已知良好"快照
2957
3014
  if (jsonOut) {
2958
3015
  console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, checked: results.length, failed: failed.length, drift, results }, null, 2));
2959
3016
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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": [