@moonquake2004/dsh-doctor 0.8.2 → 0.8.3
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 +47 -1
- package/dsh-doctor.mjs +41 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,35 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.8.3] — 2026-09-16
|
|
20
|
+
|
|
21
|
+
**把"自造的对抗者"变成默认动作**(用户要求:红队入流程 + 不依赖外部报错也能自我进化)。
|
|
22
|
+
|
|
23
|
+
### Added —— 变异测试:验证"守卫是否真的在守"
|
|
24
|
+
|
|
25
|
+
`scripts/mutation-test.mjs`:**故意破坏每个守卫,要求测试必须变红**;破坏后测试仍绿 = 那个守卫是装饰品。
|
|
26
|
+
|
|
27
|
+
- 首轮结果:**8 个变异中 4 个存活** —— 也就是说**一半守卫此前无人看守**:
|
|
28
|
+
`arity-guard`(没测)、`catalog-skip-as-pass`(没测)、`closure-symbol`(我的变异插错了位置)、
|
|
29
|
+
`coverage-reset-noop`(**守卫本身冗余**:各段都自己设了值,去掉重置没有任何测试变红)。
|
|
30
|
+
- 处置:补了两条缺失的测试(arity 守卫 / 目录检查 skip 语义,且**用例自控环境**:PATH 收窄到只有 node,
|
|
31
|
+
否则 E7 在有 dsh 的机器上合法地 pass,测试就变成对环境的断言);把"覆盖量上下文"改为**绑定求值阶段**
|
|
32
|
+
(`coverageNow()` 只接受属于当前阶段的上下文),守卫才有牙齿;无牙齿的那条如实标为**等价变异**并记录。
|
|
33
|
+
- 终态:**杀死 7 / 存活 0 / 等价 1(已记录)**。
|
|
34
|
+
|
|
35
|
+
### Added —— 发布闸门:红队不跑就不许发版
|
|
36
|
+
|
|
37
|
+
- `docs/redteam-brief.md`:红队行动简报(如何发起、攻击面、产出要求)。
|
|
38
|
+
- `scripts/release-check.sh`:五道闸门 —— 环境泛化审计 · **变异测试无存活** · 全套测试 ·
|
|
39
|
+
**本版本必须留下 `docs/redteam-<版本>.md`** · 待决策清单可见。缺一项即"不许发布"。
|
|
40
|
+
- 接入 CI(ubuntu × node24)。**闸门第一次运行就拦住了 0.8.3 自己**(缺红队档案)。
|
|
41
|
+
|
|
42
|
+
### Added —— 缺陷台账:让"自我进化"可度量
|
|
43
|
+
|
|
44
|
+
`docs/defect-ledger.md`:20 条缺陷按**来源**分类(EXT 外部报告 / RT 红队 / MUT 变异 / AUDIT 自审),
|
|
45
|
+
并给出**预发布发现率**。截至今日:EXT 6 · RT 6 · MUT 1 类 · AUDIT 7 → **预发布发现率 ≈ 70%**。
|
|
46
|
+
台账里如实注明:**红队那 6 条是因为用户要求才跑的**,所以闸门(强制留下档案)才是把比率变成能力的那一步。
|
|
47
|
+
|
|
19
48
|
## [0.8.2] — 2026-09-16
|
|
20
49
|
|
|
21
50
|
**红队审计(fresh-context 证伪者)在"已自审三轮"的版本里找出 6 个真漏洞**,其中 2 个推翻了我刚声称"已验证"的断言。
|
|
@@ -43,7 +72,24 @@
|
|
|
43
72
|
- `scripts/audit.sh` 的畸形-manifest 断言由 `n > 1` 收紧为 `n >= 15`,并把当前不达标**如实计为"已知缺口"**(自证太弱是 R8 的实例)。
|
|
44
73
|
- 新增 `docs/redteam-2026-09-16.md`(逐条反例 + 处置 + 红队未能验证项)。
|
|
45
74
|
|
|
46
|
-
|
|
75
|
+
### 第二轮红队(攻击这批新机制本身)→ 本轮修复
|
|
76
|
+
|
|
77
|
+
完整档案 `docs/redteam-0.8.3.md`。**8 条真反例**,其中 1 条推翻台账里"R1 已修 0.8.2"的结论:
|
|
78
|
+
|
|
79
|
+
- **F1(严重)**:0.8.2 只修了**目录段**的"跳过报成 pass";**profile 段的阶段占位值仍在伪造覆盖量** ——
|
|
80
|
+
3 个**一个都没装**的幽灵 bundle 也能得到 `✓ 16 项通过 / ok=true / verified=16`,且 15 条记录带
|
|
81
|
+
`examined=3, examinedWhat="bundle 条目"`(P21 文案写"扫描 0 个 host 入口"却记 examined=3)。
|
|
82
|
+
**已修**:删除占位回退(`examined` 只能显式申报);未申报者标 `unreported` 并**单独计数、进 JSON**;
|
|
83
|
+
补齐 P11/P15/P17/P19/P20/P21 的数量与单位;删除"数量巧合就继承单位"的回退(它会把 P15 误贴成"3 个 bundle 条目")。
|
|
84
|
+
- **F3/F4/F8(高)**:发布闸门可被空文件或任意版本号旁路;**闸门与变异清单此前根本未入库**;闸门诊断在 C locale 下崩溃且从不核对数量。已修并入库。
|
|
85
|
+
- **F5(高)**:测试**依赖网络**导致不可复现,进而让变异测试产生**假杀死**(环境红 = 记成 kill)。
|
|
86
|
+
已加离线开关 `DSH_DOCTOR_OFFLINE=1`;变异判据收紧为"必须出现断言失败";每条变异先跑基线;超时单列。
|
|
87
|
+
- **F6(中)**:等价变异是自证后门(无预算、不进 `--json`)。已加预算(1)并在 JSON 暴露。
|
|
88
|
+
- **F7(中)**:台账计数与自己的 20 行不自洽 → 已按行重算并写明归属规则。
|
|
89
|
+
- **F2(严重,未修)**:闭集只是**文本律**(`String.fromCharCode` 可绕过;`client.js` 未被扫描)→ **待决策 D6**。
|
|
90
|
+
- 附注:`--no-catalog` 只是 `noRemote`,`checks.local.json` 仍会合并 → **待决策 D7**。
|
|
91
|
+
|
|
92
|
+
测试 144 → **214**。
|
|
47
93
|
|
|
48
94
|
## [0.8.1] — 2026-09-16
|
|
49
95
|
|
package/dsh-doctor.mjs
CHANGED
|
@@ -233,16 +233,21 @@ function hasDshEnvironment(home = HOME) {
|
|
|
233
233
|
* · `examined === 0` → **不得 pass**,自动降为 skip("无可检查对象")——硬不变量,CI 有测试盯着;
|
|
234
234
|
* · `examined` 未报告 → 结果带 `coverage: 'unreported'`(机器可读的欠账标记),可被测试度量并逐项清零。
|
|
235
235
|
*/
|
|
236
|
-
let coverageContext = null; // 当前检查段"检查了多少同类对象"的默认值(由 setCoverage
|
|
237
|
-
|
|
236
|
+
let coverageContext = null; // 当前检查段"检查了多少同类对象"的默认值(由 setCoverage 设置,且**绑定阶段**)
|
|
237
|
+
let currentPhase = null; // 求值阶段:env / profile / session / catalog
|
|
238
|
+
function setCoverage(n, what) { coverageContext = typeof n === 'number' ? { n, what, phase: currentPhase } : null; }
|
|
238
239
|
/** 段边界重置:覆盖量上下文**绝不允许跨段继承**——否则会伪造出一个看起来可信的数字。
|
|
239
240
|
* 2026-09 回归审计实测:会话段的 S6/S14 曾号称"检查了 16 个 bundle 条目",而它们实际展开的是 11352 个事件。 */
|
|
240
241
|
function resetCoverage() { coverageContext = null; }
|
|
241
242
|
/** 求值阶段(env/profile/session/catalog)。覆盖量的**单位属于阶段**,不属于展示用的 section——
|
|
242
243
|
* 目录提供的检查会带 section:'env'/'profile',但它们在 catalog 阶段求值(回归审计实测)。 */
|
|
243
|
-
let currentPhase = null;
|
|
244
244
|
function setPhase(p) { currentPhase = p; }
|
|
245
|
-
function coverageNow() {
|
|
245
|
+
function coverageNow() {
|
|
246
|
+
// 只接受**属于当前阶段**的上下文:这样"忘记在段边界重置"不可能再伪造出跨段的数字
|
|
247
|
+
// (变异测试发现:只靠 resetCoverage 的版本里,去掉重置后没有任何测试变红——守卫是冗余的;
|
|
248
|
+
// 绑定阶段后,守卫才有牙齿。)
|
|
249
|
+
return coverageContext && coverageContext.phase === currentPhase ? coverageContext : null;
|
|
250
|
+
}
|
|
246
251
|
|
|
247
252
|
function report(section, id, ok, detail, fix, src, examined) {
|
|
248
253
|
// R6 接口律:参数放错位置必须**立刻抛**,而不是变成一条静默的错误判定。
|
|
@@ -250,8 +255,8 @@ function report(section, id, ok, detail, fix, src, examined) {
|
|
|
250
255
|
// 回退到上下文 0 → 一条本该 pass 的检查被静默降级。凭记忆拼参数是可以通过机制消灭的。
|
|
251
256
|
// R6 扩展(2026-09 回归审计):**参数个数**守卫。JS 会静默忽略多余参数,而我只校验了类型——
|
|
252
257
|
// 于是"多传一个 undefined"这类错误(我给 S11 补覆盖量时就犯了)能一路走到运行时。
|
|
253
|
-
if (arguments.length >
|
|
254
|
-
throw new Error(`report(${id}): 最多
|
|
258
|
+
if (arguments.length > 8) {
|
|
259
|
+
throw new Error(`report(${id}): 最多 8 个参数 (section, id, ok, detail, fix, src, examined, examinedWhat),收到 ${arguments.length} 个 —— 检查参数位置`);
|
|
255
260
|
}
|
|
256
261
|
if (typeof examined !== 'number' && examined !== undefined) {
|
|
257
262
|
throw new Error(`report(${id}): examined 必须是 number 或 undefined,收到 ${typeof examined}(${JSON.stringify(examined)})——检查参数位置`);
|
|
@@ -262,7 +267,11 @@ function report(section, id, ok, detail, fix, src, examined) {
|
|
|
262
267
|
if (typeof section !== 'string' || typeof id !== 'string' || typeof ok !== 'boolean' || typeof detail !== 'string') {
|
|
263
268
|
throw new Error(`report(): 前四个参数必须是 (section:string, id:string, ok:boolean, detail:string)`);
|
|
264
269
|
}
|
|
265
|
-
|
|
270
|
+
// 红队 F1(2026-09-16):**不得**用阶段占位值顶替真实数量。
|
|
271
|
+
// 实测反例:3 个「幽灵 bundle」(一个都没装)时,15 条通过项带上 `examined=3, examinedWhat="bundle 条目"`,
|
|
272
|
+
// 其中 P21 的文案是"扫描 0 个 host 入口与 0 个 client 产物"——**文案与覆盖量自相矛盾**,
|
|
273
|
+
// 而 verified=16 让聚合层宣称"16 项通过"。占位值把"零对象不变量"整个架空了。
|
|
274
|
+
const zeroCheck = typeof examined === 'number' ? examined : undefined;
|
|
266
275
|
if (ok === true && zeroCheck === 0) {
|
|
267
276
|
// 2026-09 语料发现的措辞缺陷:原实现把原判定文案原样留下 → 出现"结构正常(无可检查对象,未做任何比较)"
|
|
268
277
|
// 这种**自相矛盾**的句子(既宣称正常、又说没比较)。原文案改放 note(机器可读),人读文案只陈述事实。
|
|
@@ -278,9 +287,14 @@ function report(section, id, ok, detail, fix, src, examined) {
|
|
|
278
287
|
const safeDetail = String(detail).replace(VERDICT_GLYPH_RE, '·');
|
|
279
288
|
const rec = { section, id, ok, detail: safeDetail, fix, src: src ?? 'builtin', phase: currentPhase };
|
|
280
289
|
if (ok === true) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
// 只有显式申报才算数;`examinedWhat` 也必须与**同一处**申报配套(由调用点传 what)
|
|
291
|
+
if (typeof examined === 'number') {
|
|
292
|
+
rec.examined = examined;
|
|
293
|
+
// 第 8 个参数是**覆盖量单位**(与 examined 配套申报,避免"数量对不上单位"的旧问题)
|
|
294
|
+
// 单位**只能**由第 8 个参数显式申报。绝不按"数量恰好相等"去继承——
|
|
295
|
+
// 那会让"检查了 3 个文件"的 P15 被贴上"3 个 bundle 条目"的标签(我第一版就这么错了)。
|
|
296
|
+
if (typeof arguments[7] === 'string') rec.examinedWhat = arguments[7];
|
|
297
|
+
}
|
|
284
298
|
else rec.coverage = 'unreported';
|
|
285
299
|
}
|
|
286
300
|
results.push(rec);
|
|
@@ -350,15 +364,19 @@ function aggregateVerdict(records) {
|
|
|
350
364
|
const failed = records.filter((r) => !r.skip && !r.ok);
|
|
351
365
|
const verified = records.filter((r) => !r.skip && r.ok && (r.examined ?? 0) > 0).length;
|
|
352
366
|
const skipped = records.filter((r) => r.skip).length;
|
|
353
|
-
|
|
367
|
+
// 红队 F1:**未申报覆盖量的通过**既不算"已验证",也不能隐形——单独计数并进汇总
|
|
368
|
+
const unreported = records.filter((r) => !r.skip && r.ok && r.coverage === 'unreported').length;
|
|
369
|
+
const totals = { verified, skipped, failed: failed.length, unreported };
|
|
354
370
|
if (failed.length) return makeVerdict('fail', { totals, detail: `${failed.length} 个问题` });
|
|
355
371
|
if (verified === 0) {
|
|
356
372
|
return makeVerdict('skip', { totals, reason: `没有任何检查实际验证过(${skipped} 项全部跳过)` });
|
|
357
373
|
}
|
|
358
374
|
if (skipped > 0) {
|
|
359
|
-
|
|
375
|
+
const unrep = unreported > 0 ? `;另有 ${unreported} 项通过但**未申报检查了多少**(不算已验证)` : '';
|
|
376
|
+
return makeVerdict('pass', { checked: verified, totals, detail: `${verified} 项通过、${skipped} 项未检查(skip)${unrep}` });
|
|
360
377
|
}
|
|
361
|
-
|
|
378
|
+
const unrep2 = unreported > 0 ? `(其中 ${unreported} 项未申报覆盖量)` : '';
|
|
379
|
+
return makeVerdict('pass', { checked: verified, totals, detail: `全部通过(${verified} 项已验证${unrep2})` });
|
|
362
380
|
}
|
|
363
381
|
|
|
364
382
|
/** 解析 --profile 参数:名字(如 web)→ $DSH_HOME/profiles/<name>;含路径分隔符/~/开头 → 直接当 profile 目录(契约 harness 传绝对路径)。 */
|
|
@@ -885,7 +903,7 @@ function checkProfile(name) {
|
|
|
885
903
|
+ `systemd 健康、端口在听、页面永远空白(#6693 实测:整份 journal 里该错误计数为 0)`,
|
|
886
904
|
'把 client 半边改成 `__ModuleLoader__.load({ id, factory })` 的 CJS 工厂形态(参照 dsh-better-sidebar / dsh-dream-skin)');
|
|
887
905
|
} else {
|
|
888
|
-
report('profile', 'P20', true, `client 产物均为 CJS 工厂形态(${p20scanned}
|
|
906
|
+
report('profile', 'P20', true, `client 产物均为 CJS 工厂形态(${p20scanned} 个)`, undefined, undefined, p20scanned, 'client 产物');
|
|
889
907
|
}
|
|
890
908
|
|
|
891
909
|
// P21:**沙箱专属符号在普通插件里不存在**(host 与 client 两侧一律如此,同一根因两个面)
|
|
@@ -937,7 +955,7 @@ function checkProfile(name) {
|
|
|
937
955
|
+ `\n host 侧表现为 \`ReferenceError: harness is not defined\`(在 \`apply()\` 内同步抛出 → cordis 中断装载链 → Web UI 整体不可用)`,
|
|
938
956
|
'移除这些引用或用宿主提供的等价能力;若确需守卫,必须写成 `typeof X !== \"undefined\"` **放在最前**——`X.foo && …` 挡不住(裸标识符先被解析)');
|
|
939
957
|
} else if (notes21.length) {
|
|
940
|
-
report('profile', 'P21', true, `未见裸引用沙箱专属符号(${notes21.length} 处有 typeof
|
|
958
|
+
report('profile', 'P21', true, `未见裸引用沙箱专属符号(${notes21.length} 处有 typeof 守卫,未计入)`, undefined, undefined, hostFiles.length + clientFiles.length, 'host/client 文件');
|
|
941
959
|
} else {
|
|
942
960
|
report('profile', 'P21', true, `扫描 ${hostFiles.length} 个 host 入口与 ${clientFiles.length} 个 client 产物,未见沙箱专属符号引用`);
|
|
943
961
|
}
|
|
@@ -997,7 +1015,7 @@ function checkProfile(name) {
|
|
|
997
1015
|
report('profile', 'P19', true,
|
|
998
1016
|
`核对 ${checked19} 条 host peer 声明,均可接受当前核心版本`
|
|
999
1017
|
+ (unknown19 ? `(${unknown19} 条无法判定,按未知处理未计入)` : '')
|
|
1000
|
-
+ undeclaredNote.replace(/\n\s*/g, ' '));
|
|
1018
|
+
+ undeclaredNote.replace(/\n\s*/g, ' '), undefined, undefined, checked19, 'host peer 声明');
|
|
1001
1019
|
}
|
|
1002
1020
|
}
|
|
1003
1021
|
|
|
@@ -1227,7 +1245,7 @@ function packageNamedExports(pkgDir) {
|
|
|
1227
1245
|
}
|
|
1228
1246
|
}
|
|
1229
1247
|
if (entryIssues.length) report('profile', 'P11', false, `已装 bundle 的 main 入口缺失(#1965:市场装源码不跑构建 → ERR_MODULE_NOT_FOUND → dsh web boot 崩溃): ${entryIssues.join('; ')}`, '在插件目录跑构建(pnpm install && pnpm run build 产出 main 指向的文件),或改用打包好的 npm 包安装;monorepo 插件需装子包(dsh-market #18 同族)');
|
|
1230
|
-
else report('profile', 'P11', true, '已装 bundle 的 main 入口产物均在', undefined);
|
|
1248
|
+
else report('profile', 'P11', true, '已装 bundle 的 main 入口产物均在', undefined, undefined, bundleDirs.size, '已装 bundle');
|
|
1231
1249
|
|
|
1232
1250
|
// P13:client 端服务名抢注核心客户端服务(#2752:ctx.provide("chatFileMentions") 撞核心 dsh-client-ui-deliverables
|
|
1233
1251
|
// → 浏览器端 service already registered → Web UI 白屏,服务端日志无感知、报错无冲突来源)
|
|
@@ -1418,7 +1436,7 @@ function packageNamedExports(pkgDir) {
|
|
|
1418
1436
|
+ '改用 [IO.File]::WriteAllText($p, (Get-Content $p -Raw), (New-Object Text.UTF8Encoding $false));'
|
|
1419
1437
|
+ '或 sed -i "" "1s/^\xEF\xBB\xBF//" <file>', undefined, bomTargets.length);
|
|
1420
1438
|
} else {
|
|
1421
|
-
report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, undefined, bomTargets.length);
|
|
1439
|
+
report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, undefined, bomTargets.length, '关键文件');
|
|
1422
1440
|
}
|
|
1423
1441
|
|
|
1424
1442
|
/* P16:插件命名导入的导出缺失检测(#5864:一个缺失导出 → 整棵插件树 boot 崩溃循环、
|
|
@@ -1522,7 +1540,7 @@ function packageNamedExports(pkgDir) {
|
|
|
1522
1540
|
if (p17Issues.length) {
|
|
1523
1541
|
report('profile', 'P17', false, `client 端 require 的模块不在宿主模块表(#5719:makeRequire 硬 throw → 浏览器白屏且服务端无感知): ${p17Issues.join('; ')}`, `改用宿主提供的模块名;若确由宿主提供,在本包 package.json 的 dsh.client.external/inject 里声明;平台种子当前 ${CLIENT_SEEDS.size} 项、已装图行 ${composedRows.size} 项`);
|
|
1524
1542
|
} else {
|
|
1525
|
-
report('profile', 'P17', true, `client 端 require 的 specifier 均可服务(平台种子 ${CLIENT_SEEDS.size} 项 + 已装图行 ${composedRows.size} 项)`, undefined);
|
|
1543
|
+
report('profile', 'P17', true, `client 端 require 的 specifier 均可服务(平台种子 ${CLIENT_SEEDS.size} 项 + 已装图行 ${composedRows.size} 项)`, undefined, undefined, CLIENT_SEEDS.size + composedRows.size, 'client specifier 来源');
|
|
1526
1544
|
}
|
|
1527
1545
|
}
|
|
1528
1546
|
|
|
@@ -2164,6 +2182,8 @@ function validCatalog(data) {
|
|
|
2164
2182
|
|
|
2165
2183
|
/** 拉取目录:新鲜缓存(≤TTL) → 远程(raw.githubusercontent,3s 超时) → 旧缓存(last-known-good) → 内置副本;末尾合并本地覆盖层。 */
|
|
2166
2184
|
async function loadCatalog({ noRemote = false, fetchImpl, home = HOME, localPath } = {}) {
|
|
2185
|
+
// 离线开关(红队 F5:目录用例曾因取到远程目录而结果不可复现)——DSH_DOCTOR_OFFLINE=1 等价于 noRemote
|
|
2186
|
+
if (process.env.DSH_DOCTOR_OFFLINE === '1') noRemote = true;
|
|
2167
2187
|
const bundled = bundledCatalog();
|
|
2168
2188
|
let base;
|
|
2169
2189
|
if (noRemote || typeof fetchImpl !== 'function') {
|
|
@@ -3355,6 +3375,8 @@ async function run() {
|
|
|
3355
3375
|
verified: aggJson.totals.verified,
|
|
3356
3376
|
skipped: aggJson.totals.skipped,
|
|
3357
3377
|
failed: aggJson.totals.failed,
|
|
3378
|
+
// 红队 F1:未申报覆盖量的通过**单独暴露**(既不算已验证,也不隐形)
|
|
3379
|
+
unreported: aggJson.totals.unreported,
|
|
3358
3380
|
checks: checksWithStatus, catalog: catalogMeta, update: updateInfo, ...(securityMeta.enabled ? { security: securityMeta } : {}),
|
|
3359
3381
|
}, null, 2));
|
|
3360
3382
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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": [
|