@moonquake2004/dsh-doctor 0.6.1 → 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 +70 -0
- package/dsh-doctor.mjs +60 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,76 @@
|
|
|
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
|
+
|
|
47
|
+
## [0.7.0] — 2026-09-16
|
|
48
|
+
|
|
49
|
+
一次**针对工具自身可信度**的加固,起因是连续四轮社区报告都照出了同一个病:**我们的结论比证据更自信**。
|
|
50
|
+
|
|
51
|
+
### Added — 覆盖不变量("没看" ≠ "通过")
|
|
52
|
+
|
|
53
|
+
实测:空环境(零 bundle、无会话库)下曾有 **18 项 pass,其中 14 项连一个数量都没给** —— "我什么都没看"与
|
|
54
|
+
"我全看过、很干净"打印出来完全一样。`--boot-check` 对"bundle 缺 `dsh.bundle`"(零 entry 可探)报
|
|
55
|
+
"✓ 全部可导入"(#6788 正是这一类)只是同一 bug 的一个实例。
|
|
56
|
+
|
|
57
|
+
现在:
|
|
58
|
+
- **`examined === 0` 的 `pass` 一律自动降为 `skip`**("无可检查对象,未做任何比较")——硬不变量,CI 有测试盯着;
|
|
59
|
+
- `pass` 需报告**检查了多少对象**(含 `examinedWhat`);未报告者带 `coverage: 'unreported'` 欠账标记,
|
|
60
|
+
可被测试度量。**profile 段的欠账已从 18 项清零**(上下文默认值 + 逐项复核单位不同的检查,如
|
|
61
|
+
`P12` 的对象是"已装候选包"、`P18` 是"manifest",都不是 bundle 列表长度)。
|
|
62
|
+
**env 段仍有 10 项未报告覆盖量**(`E1-*` / `E3-node` / `E4` / `E5` / `E6` / `E10` / `E12` / `E13`)——
|
|
63
|
+
这些探针各查一个对象(某个二进制/版本/端口),补起来是机械的,但**尚未补**,故在此如实列出,
|
|
64
|
+
而不是让 release note 显得比实际干净。
|
|
65
|
+
|
|
66
|
+
### Fixed — 故障隔离:一条正确的检查曾被无关崩溃静默
|
|
67
|
+
|
|
68
|
+
#6758 的 BOM 场景里,**P15 本来就能查、也查对了**;但在修复前,主读取点先抛错 →
|
|
69
|
+
`P0: profile 检查异常` → **P15 根本没机会执行**。真实失败不是"缺检查",而是**"正确的检查被上游故障静默了"**。
|
|
70
|
+
今天同类问题出现两次(另一处是变量作用域错误掐掉整个 profile 段)。现在外部读取一律不抛(带 BOM 感知的
|
|
71
|
+
读取已覆盖 manifest),并有两条回归测试钉住:**BOM 时 P15 仍报出、manifest 无法解析时其余检查仍给结论**。
|
|
72
|
+
|
|
73
|
+
### Removed — 撤销 `P22`(与 `P15` 重复)
|
|
74
|
+
|
|
75
|
+
`P15`(关键文件 BOM,源自 #5176)**早就扫描 profile 的 `package.json`** 并报出同一结论;我加 `P22` 之前
|
|
76
|
+
**没有查现有清单**。#6758 特有的信息(PowerShell 5.1 的 `-Encoding UTF8` 会写 BOM、`JSON.parse` 硬失败、
|
|
77
|
+
报错被 GBK 渲染成乱码)已并入 `P15`。**同一事实只应有一个归属。**
|
|
78
|
+
|
|
79
|
+
### 为什么这些错误能发生(写下来,供以后对照)
|
|
80
|
+
|
|
81
|
+
六处错误(S14 判错视图、SR5 成片误报、`--boot-check` 假绿灯、P22 重复、P18 矛盾结论、P15 被静默)**根因相同**:
|
|
82
|
+
**用一个"看起来合理"的模型,代替了权威来源**——来源分别是宿主代码路径、整库样本、loader 前置条件、
|
|
83
|
+
自家检查清单、宿主的解析代码、检查的执行顺序。
|
|
84
|
+
|
|
85
|
+
我们的原则(`unknown` 不得折叠、不适用就带理由 skip、"只在一个平台验证过 ≈ 没验证")此前只写在文档与
|
|
86
|
+
评审意见里,**没有变成引擎里的不变量**;而唯一一次我把原则写成机制(peer-range 一致性测试从**已发布源码**
|
|
87
|
+
抽函数来跑语料)恰恰没出问题。**原则不落成机制,就只剩运气。**
|
|
88
|
+
|
|
19
89
|
## [0.6.1] — 2026-09-15
|
|
20
90
|
|
|
21
91
|
### Fixed — `--boot-check` 在"bundle 缺 `dsh.bundle`"这一类上给假绿灯(社区 #6788)
|
package/dsh-doctor.mjs
CHANGED
|
@@ -221,8 +221,48 @@ function hasDshEnvironment(home = HOME) {
|
|
|
221
221
|
return existsSync(join(home, 'sessions')) || existsSync(join(home, 'settings.yaml'));
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
// 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
|
+
}
|
|
253
|
+
const zeroCheck = typeof examined === 'number' ? examined : coverageNow()?.n;
|
|
254
|
+
if (ok === true && zeroCheck === 0) {
|
|
255
|
+
results.push({ section, id, ok: true, skip: true, coverage: 'none', detail: `${detail}(无可检查对象,未做任何比较)`, fix, src: src ?? 'builtin' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const rec = { section, id, ok, detail, fix, src: src ?? 'builtin' };
|
|
259
|
+
if (ok === true) {
|
|
260
|
+
const ctx = coverageNow();
|
|
261
|
+
const n = typeof examined === 'number' ? examined : ctx?.n;
|
|
262
|
+
if (typeof n === 'number') { rec.examined = n; if (ctx?.what) rec.examinedWhat = ctx.what; }
|
|
263
|
+
else rec.coverage = 'unreported';
|
|
264
|
+
}
|
|
265
|
+
results.push(rec);
|
|
226
266
|
}
|
|
227
267
|
|
|
228
268
|
/** skip 状态(v1 词汇表 r5:#1719)——"不适用"而非"通过",必须带 reason(detail)。不计入 pass/fail,不翻退出码。 */
|
|
@@ -274,6 +314,9 @@ function nodeInSupportedRange(v, range = NODE_RANGE_FALLBACK) {
|
|
|
274
314
|
}
|
|
275
315
|
function checkEnv() {
|
|
276
316
|
if (!wants('env')) return;
|
|
317
|
+
// R3 覆盖律:环境段的每个探针各检查**一个**对象(某个二进制/版本/端口)。
|
|
318
|
+
// 单位不同的检查(如 E1 系列各自查一个可执行文件)如需别的数量应显式传入。
|
|
319
|
+
setCoverage(1, '环境对象');
|
|
277
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; };
|
|
278
321
|
for (const cmd of ['node', 'pnpm', 'zstd']) {
|
|
279
322
|
const p = find(cmd);
|
|
@@ -559,6 +602,7 @@ function checkProfile(name) {
|
|
|
559
602
|
}
|
|
560
603
|
const bundles = manifest.dsh?.profile?.bundles ?? [];
|
|
561
604
|
const deps = manifest.dependencies ?? {};
|
|
605
|
+
setCoverage(bundles.length, 'bundle 条目');
|
|
562
606
|
|
|
563
607
|
const installAnchor = (() => {
|
|
564
608
|
// 从 PATH 找 dsh 的安装目录(node_modules),用于 bundle 双锚点解析
|
|
@@ -692,7 +736,7 @@ function checkProfile(name) {
|
|
|
692
736
|
if (!profManifest || !(profManifest.dsh && profManifest.dsh.profile)) {
|
|
693
737
|
reportSkip('profile', 'P18', '未找到 profile manifest(无 dsh.profile),跳过 version 检查');
|
|
694
738
|
} else if (typeof profManifest.version === 'string' && profManifest.version.length > 0) {
|
|
695
|
-
report('profile', 'P18', true, `profile manifest 声明了 version(${profManifest.version}),不触发 #6667
|
|
739
|
+
report('profile', 'P18', true, `profile manifest 声明了 version(${profManifest.version}),不触发 #6667`, undefined, undefined, 1);
|
|
696
740
|
} else {
|
|
697
741
|
report('profile', 'P18', false,
|
|
698
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 失败`,
|
|
@@ -804,26 +848,8 @@ function checkProfile(name) {
|
|
|
804
848
|
}
|
|
805
849
|
}
|
|
806
850
|
|
|
807
|
-
// P22
|
|
808
|
-
//
|
|
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
|
-
}
|
|
851
|
+
// P22 已撤销:profile manifest 的 BOM 由 P15 覆盖(同一事实只应有一个归属)。
|
|
852
|
+
// 2026-09 反思的产物:加检查前必须先查清单——我当初没查,于是加了一条与 P15 重复的检查。
|
|
827
853
|
|
|
828
854
|
// P19:插件声明的 host peer 范围 vs 实际提供的 host 版本(社区 #6678 @ciceroyang 提案)
|
|
829
855
|
// 这是"升级后起不来"的常见原因之一:插件声明只支持某段 core 版本,而实际装的核心已在区间外,
|
|
@@ -1254,9 +1280,11 @@ function packageNamedExports(pkgDir) {
|
|
|
1254
1280
|
const bundleVersion = JSON.parse(readFileSync(bundlePkg, 'utf8')).version;
|
|
1255
1281
|
const cliVersion = localVersion();
|
|
1256
1282
|
const same = bundleVersion === cliVersion;
|
|
1283
|
+
// 显式覆盖量:P12 的比较对象是"已装的候选包"(1 个),不是 bundle 列表长度——
|
|
1284
|
+
// 这正是上下文默认值需要被逐项复核的地方(否则 0 个 bundle 的 profile 会被误降为 skip)。
|
|
1257
1285
|
report('profile', 'installed_bundle', same,
|
|
1258
1286
|
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
|
|
1287
|
+
same ? undefined : `同步安装版本:dsh plugin --profile ${name} update ${selfName}(或让 CLI 与 bundle 走同一安装方式)`, undefined, 1);
|
|
1260
1288
|
}
|
|
1261
1289
|
} catch (e) {
|
|
1262
1290
|
report('profile', 'installed_bundle', false, `bundle 版本对比异常: ${e.message.slice(0, 60)}`, undefined);
|
|
@@ -1287,9 +1315,15 @@ function packageNamedExports(pkgDir) {
|
|
|
1287
1315
|
} catch { /* skip */ }
|
|
1288
1316
|
}
|
|
1289
1317
|
if (bomFiles.length > 0) {
|
|
1290
|
-
report('profile', 'P15', false,
|
|
1318
|
+
report('profile', 'P15', false,
|
|
1319
|
+
`检测到 BOM 头(#5176 / #6758:JSON/YAML 解析将失败): ${bomFiles.join(', ')}`
|
|
1320
|
+
+ `—— 若命中 profile 的 package.json,DSH 会直接 \`JSON.parse\` 它并抛 \`SyntaxError: Unexpected token '…' is not valid JSON\`,`
|
|
1321
|
+
+ `**启动硬失败**;而 GBK 控制台会把 BOM 三个字节渲染成乱码,报错里看不出是编码问题`,
|
|
1322
|
+
'删除首字符(BOM/U+FEFF)后保存;PowerShell 5.1 的 `Set-Content -Encoding UTF8` **默认会写 BOM**,'
|
|
1323
|
+
+ '改用 [IO.File]::WriteAllText($p, (Get-Content $p -Raw), (New-Object Text.UTF8Encoding $false));'
|
|
1324
|
+
+ '或 sed -i "" "1s/^\xEF\xBB\xBF//" <file>', undefined, bomTargets.length);
|
|
1291
1325
|
} else {
|
|
1292
|
-
report('profile', 'P15', true,
|
|
1326
|
+
report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, undefined, bomTargets.length);
|
|
1293
1327
|
}
|
|
1294
1328
|
|
|
1295
1329
|
/* P16:插件命名导入的导出缺失检测(#5864:一个缺失导出 → 整棵插件树 boot 崩溃循环、
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "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": [
|