@moonquake2004/dsh-doctor 0.8.0 → 0.8.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 +33 -0
- package/dsh-doctor.mjs +28 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,39 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.8.1] — 2026-09-16
|
|
20
|
+
|
|
21
|
+
### 0.8.0 的回归审计 —— **修一个引入一个,被查出来了四个**
|
|
22
|
+
|
|
23
|
+
对 0.8.0 做回归审计(R12/R13),确认它**引入了新问题**:
|
|
24
|
+
|
|
25
|
+
| 引入的问题 | 现象 | 修法 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| **覆盖量上下文跨段泄漏** | 会话段的 S6/S14 号称"检查了 **16 个 bundle 条目**",而它们实际展开的是 **11352 个事件** —— 伪造的数字比没有数字更危险,因为它看起来可信 | 段边界 `resetCoverage()` + **单位属于求值阶段**(新增 `phase` 字段);回归测试断言"单位不得串台" |
|
|
28
|
+
| **目录段继承会话单位** | 目录检查 `P6-patch-name-space`(section=profile)声称"检查了会话日志" | catalog 阶段独立单位(`目录检查项`) |
|
|
29
|
+
| **R6 只校验类型、不校验个数** | JS 静默忽略多余参数 → 我给 S11 补覆盖量时多传一个 `undefined`,**没人报错**(类型守卫也抓不到) | `report()` 增加**参数个数守卫**(>7 即抛) |
|
|
30
|
+
| **两处测试自伤** | 抽取 `report` 的测试缺 `currentPhase` 桩;清单需重新生成 | 已修 |
|
|
31
|
+
|
|
32
|
+
### Added — 论坛病例语料(`plugin/test/forum-cases.test.mjs`)
|
|
33
|
+
|
|
34
|
+
把社区**实际报告的问题**做成回归用例(R2 样本律),每例都配**健康对照**,同时防漏报与误报:
|
|
35
|
+
|
|
36
|
+
| 病例 | 出处 | 结果 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| 缺 `dsh.bundle` → `--boot-check` 必须失败 | #6788 | ✓ 通过(含对照) |
|
|
39
|
+
| manifest 带 BOM → P15 报出且其余检查仍运行 | #6758 | ✓ 通过(含对照) |
|
|
40
|
+
| 迁移后缺 `data.message` → S14 报出 | #6686 | ✓ 通过(含对照) |
|
|
41
|
+
| 裸引用 `harness`/`styles` → P21 失败 | #6693 规则2/4 | ✓ 通过(含注释/字符串对照) |
|
|
42
|
+
| client 产物写成裸 ESM → P20 报出 | #6693 规则3 | ✓ 通过(含对照) |
|
|
43
|
+
| `apply()` 期抛错 | #6693 规则1 | **已知不能检出**:我们只到 import 层,判定仍是 loadable —— 作为**边界用例**钉在测试里,防止把"P20/P21 通过"误读成"一定能起来" |
|
|
44
|
+
| 损坏的 `.zstd` → 判为损坏而非"读取不稳" | #6739 | ✓ 通过。**仅损坏一侧可验证**:报告者的"失败帧偏移每次不同"(读取路径不稳)无法在测试里忠实复现,已在用例注释里写明 |
|
|
45
|
+
|
|
46
|
+
**写这个语料时我又犯了一次 R9**:病例 G 的 fixture 我写成两层目录,而真实布局是三层
|
|
47
|
+
(`sessions/<项目>/<会话>/<日志>`)→ 定位器找不到 → S11 报"未发现会话日志"。**那是 fixture 失真,不是漏报** ——
|
|
48
|
+
修成真实布局后,S11 正确报出"1 个损坏会话…3 次尝试均失败 → 倾向于文件本身损坏"。
|
|
49
|
+
|
|
50
|
+
测试 129 → 141(+12 论坛病例);审计 **8/8**。
|
|
51
|
+
|
|
19
52
|
## [0.8.0] — 2026-09-16
|
|
20
53
|
|
|
21
54
|
### 结构性修复:假绿灯的**类**被消灭,而不是又一个实例
|
package/dsh-doctor.mjs
CHANGED
|
@@ -235,12 +235,24 @@ function hasDshEnvironment(home = HOME) {
|
|
|
235
235
|
*/
|
|
236
236
|
let coverageContext = null; // 当前检查段"检查了多少同类对象"的默认值(由 setCoverage 设置)
|
|
237
237
|
function setCoverage(n, what) { coverageContext = typeof n === 'number' ? { n, what } : null; }
|
|
238
|
+
/** 段边界重置:覆盖量上下文**绝不允许跨段继承**——否则会伪造出一个看起来可信的数字。
|
|
239
|
+
* 2026-09 回归审计实测:会话段的 S6/S14 曾号称"检查了 16 个 bundle 条目",而它们实际展开的是 11352 个事件。 */
|
|
240
|
+
function resetCoverage() { coverageContext = null; }
|
|
241
|
+
/** 求值阶段(env/profile/session/catalog)。覆盖量的**单位属于阶段**,不属于展示用的 section——
|
|
242
|
+
* 目录提供的检查会带 section:'env'/'profile',但它们在 catalog 阶段求值(回归审计实测)。 */
|
|
243
|
+
let currentPhase = null;
|
|
244
|
+
function setPhase(p) { currentPhase = p; }
|
|
238
245
|
function coverageNow() { return coverageContext; }
|
|
239
246
|
|
|
240
247
|
function report(section, id, ok, detail, fix, src, examined) {
|
|
241
248
|
// R6 接口律:参数放错位置必须**立刻抛**,而不是变成一条静默的错误判定。
|
|
242
249
|
// 来历:我曾在 fix 之后多插了两个参数,结果 'builtin' 落到了 examined 的位置(字符串)→
|
|
243
250
|
// 回退到上下文 0 → 一条本该 pass 的检查被静默降级。凭记忆拼参数是可以通过机制消灭的。
|
|
251
|
+
// R6 扩展(2026-09 回归审计):**参数个数**守卫。JS 会静默忽略多余参数,而我只校验了类型——
|
|
252
|
+
// 于是"多传一个 undefined"这类错误(我给 S11 补覆盖量时就犯了)能一路走到运行时。
|
|
253
|
+
if (arguments.length > 7) {
|
|
254
|
+
throw new Error(`report(${id}): 最多 7 个参数 (section, id, ok, detail, fix, src, examined),收到 ${arguments.length} 个 —— 检查参数位置`);
|
|
255
|
+
}
|
|
244
256
|
if (typeof examined !== 'number' && examined !== undefined) {
|
|
245
257
|
throw new Error(`report(${id}): examined 必须是 number 或 undefined,收到 ${typeof examined}(${JSON.stringify(examined)})——检查参数位置`);
|
|
246
258
|
}
|
|
@@ -255,7 +267,7 @@ function report(section, id, ok, detail, fix, src, examined) {
|
|
|
255
267
|
results.push({ section, id, ok: true, skip: true, coverage: 'none', detail: `${detail}(无可检查对象,未做任何比较)`, fix, src: src ?? 'builtin' });
|
|
256
268
|
return;
|
|
257
269
|
}
|
|
258
|
-
const rec = { section, id, ok, detail, fix, src: src ?? 'builtin' };
|
|
270
|
+
const rec = { section, id, ok, detail, fix, src: src ?? 'builtin', phase: currentPhase };
|
|
259
271
|
if (ok === true) {
|
|
260
272
|
const ctx = coverageNow();
|
|
261
273
|
const n = typeof examined === 'number' ? examined : ctx?.n;
|
|
@@ -378,6 +390,8 @@ function nodeInSupportedRange(v, range = NODE_RANGE_FALLBACK) {
|
|
|
378
390
|
return peerRangeState(String(v).replace(/^v/, ''), range).state === 'satisfied';
|
|
379
391
|
}
|
|
380
392
|
function checkEnv() {
|
|
393
|
+
setPhase('env');
|
|
394
|
+
resetCoverage();
|
|
381
395
|
if (!wants('env')) return;
|
|
382
396
|
// R3 覆盖律:环境段的每个探针各检查**一个**对象(某个二进制/版本/端口)。
|
|
383
397
|
// 单位不同的检查(如 E1 系列各自查一个可执行文件)如需别的数量应显式传入。
|
|
@@ -667,6 +681,8 @@ function checkProfile(name) {
|
|
|
667
681
|
}
|
|
668
682
|
const bundles = manifest.dsh?.profile?.bundles ?? [];
|
|
669
683
|
const deps = manifest.dependencies ?? {};
|
|
684
|
+
setPhase('profile');
|
|
685
|
+
resetCoverage(); // 段边界:不得继承上一段的覆盖量
|
|
670
686
|
setCoverage(bundles.length, 'bundle 条目');
|
|
671
687
|
|
|
672
688
|
const installAnchor = (() => {
|
|
@@ -1565,6 +1581,9 @@ function latestSessionLog(root = join(HOME, 'sessions')) {
|
|
|
1565
1581
|
|
|
1566
1582
|
/* ================= session ================= */
|
|
1567
1583
|
function checkSession(targetPath) {
|
|
1584
|
+
setPhase('session');
|
|
1585
|
+
resetCoverage();
|
|
1586
|
+
setCoverage(1, '会话日志'); // 单位是这一份会话日志;事件数由各检查在 detail 里给出
|
|
1568
1587
|
if (!wants('session')) return;
|
|
1569
1588
|
const target = targetPath || latestSessionLog();
|
|
1570
1589
|
if (!target || !existsSync(target)) { report('session', 'S0', true, '无会话日志,跳过单会话检查(可用 --session <path> 指定)', undefined); return; }
|
|
@@ -2073,7 +2092,7 @@ function scanAllSessions() {
|
|
|
2073
2092
|
const heapLimit = Number(process.env.DSH_DOCTOR_HEAP_MB || 1024);
|
|
2074
2093
|
const totalRisk = estHeapMB > heapLimit;
|
|
2075
2094
|
if (quars.length) {
|
|
2076
|
-
report('session', 'S11', false, `全会话扫描:${corrupt.length} 个损坏会话(#1550:冷打开会拖垮服务器): ${quars.join(' | ')}`, `隔离:把这些会话目录移出 ${join(HOME, 'sessions')}(如 mv
|
|
2095
|
+
report('session', 'S11', false, `全会话扫描:${corrupt.length} 个损坏会话(#1550:冷打开会拖垮服务器): ${quars.join(' | ')}`, `隔离:把这些会话目录移出 ${join(HOME, 'sessions')}(如 mv 到备份目录)`, undefined, files.length);
|
|
2077
2096
|
} else if (oversized.length || totalRisk || unstableReads.length) {
|
|
2078
2097
|
const parts = [];
|
|
2079
2098
|
// 间歇性读取失败:**不是文件损坏**(重试即成功、失败帧位置每次不同,见 #6739),
|
|
@@ -2085,9 +2104,9 @@ function scanAllSessions() {
|
|
|
2085
2104
|
report('session', 'S11', true, `⚠ 全会话扫描:${parts.join(';')}(未损坏,可接受或归档)`,
|
|
2086
2105
|
unstableReads.length
|
|
2087
2106
|
? '间歇性读取失败不是日志问题:先重试读取;持续出现则排查存储健康(SMART)、内存与磁盘驱动(#6739 的证据是失败帧位置每次不同)'
|
|
2088
|
-
: '冷启动会明显变慢;必要时压缩/归档历史会话');
|
|
2107
|
+
: '冷启动会明显变慢;必要时压缩/归档历史会话', undefined, files.length);
|
|
2089
2108
|
} else {
|
|
2090
|
-
report('session', 'S11', true, `全会话扫描:${clean.length} 个会话均健康(损坏 0 / 超大 0 / 估算物化堆 ${estHeapMB}MB)`, undefined);
|
|
2109
|
+
report('session', 'S11', true, `全会话扫描:${clean.length} 个会话均健康(损坏 0 / 超大 0 / 估算物化堆 ${estHeapMB}MB)`, undefined, undefined, files.length);
|
|
2091
2110
|
}
|
|
2092
2111
|
}
|
|
2093
2112
|
|
|
@@ -2318,6 +2337,11 @@ export function runCatalogCheck(check, ctx) {
|
|
|
2318
2337
|
|
|
2319
2338
|
/** 逐条执行目录检查,汇入统一 results 管线(src='catalog')。尊重 --profile/--env/--session 收窄。 */
|
|
2320
2339
|
function checkCatalog(ctx, catalog) {
|
|
2340
|
+
setPhase('catalog');
|
|
2341
|
+
// 段边界:目录检查在会话段之后求值,若不清空上下文会**继承会话的单位**——
|
|
2342
|
+
// 回归审计实测到 `P6-patch-name-space`(profile) 声称"检查了会话日志"。目录检查各查一个目标,单位独立。
|
|
2343
|
+
resetCoverage();
|
|
2344
|
+
setCoverage(1, '目录检查项');
|
|
2321
2345
|
const platform = process.platform;
|
|
2322
2346
|
for (const check of catalog.checks ?? []) {
|
|
2323
2347
|
if (!wants(check.section)) continue; // 与内置检查一致的 section 收窄
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.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": [
|