@moonquake2004/dsh-doctor 0.7.0 → 0.7.1

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,34 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## [0.7.1] — 2026-09-16
20
+
21
+ 把"事后总结"变成**规则 + 机制**(`docs/check-authoring-rules.md`)。起因是用户指出:一次次的修复是头疼医头,
22
+ 应当**总结规律、确定规则**。
23
+
24
+ ### 定律(六个错误是它的六个实例)
25
+
26
+ > **一、结论必须来自来源,不能来自模型;够不到来源就报未知。**
27
+ > **二、我们对用户承诺的契约(三态、`unknown` 不折叠、不适用带理由 skip),必须先在工具自身的构建过程中成立。**
28
+
29
+ ### Added — 机制化
30
+
31
+ - **R6 接口律**:`report()` 对自己的参数做类型校验并**立即抛错**(`examined` 非 number、`src` 非 string 等)。
32
+ 来历是我在 `fix` 后多插两个参数、`'builtin'` 落到 `examined` 位置 → 一条本该 pass 的检查被静默降级。
33
+ **落地当天它就抓出了我自己另外 5 处同类错误**——这一类从此可由机制消灭,不必靠小心。
34
+ - **R5 唯一归属律 / R1 来源律**:新增 `scripts/gen-check-inventory.mjs` 生成 `docs/check-inventory.md`
35
+ (43 项,含段、范围、来源),并加 CI 断言"清单与代码一致"。
36
+ 于是"加检查前先查清单"不再是自觉,而是**一次可见的 diff**——P22 与 P15 重复那件事由此可被拦住。
37
+ 清单中**尚有 8 项未标注权威来源(R1 欠账)**,如实列出。
38
+ - **R3 收尾**:环境段补齐覆盖量(每个环境探针各检查一个对象),**全环境覆盖量欠账 18 → 0**;
39
+ CI 断言"空环境下未报告覆盖量的 pass 必须为 0"(此前只是打印数字)。
40
+
41
+ ### 规则集(`docs/check-authoring-rules.md`)
42
+
43
+ R1 来源律 · R2 样本律 · R3 覆盖律 · R4 隔离律 · R5 唯一归属律 · R6 接口律 · R7 可证伪律。
44
+ **每条都必须配一个可执行机制;没有机制的规则一律视为不存在**(这是 §0 第 4 条,也是这一轮最贵的一课)。
45
+ 文件末尾如实列出各规则的落地状态:R3/R6 已落地,R4/R7 部分落地,**R1/R2/R5 的机制刚起步**。
46
+
19
47
  ## [0.7.0] — 2026-09-16
20
48
 
21
49
  一次**针对工具自身可信度**的加固,起因是连续四轮社区报告都照出了同一个病:**我们的结论比证据更自信**。
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 崩溃循环、
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.1",
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": [