@moonquake2004/dsh-doctor 0.8.2 → 0.8.4
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 +65 -1
- package/dsh-doctor.mjs +120 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,53 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.8.4] — 2026-09-16
|
|
20
|
+
|
|
21
|
+
**巡查产出的新检查**:社区 #6789 报告的形状,用新机制(语料优先 + 变体分离)验证后发现是**真实漏检**。
|
|
22
|
+
|
|
23
|
+
### Added — P23:插件把宿主机包声明为普通依赖(社区 #6789)
|
|
24
|
+
|
|
25
|
+
- **报告的现象**:`@deepseek-ai/dsh-experimental-browser-use-runtime` 把 `@deepseek-ai/dsh-scope` 写成
|
|
26
|
+
**dependency**(而非 peer)→ 该包自带**第二份副本** → 两份实例 Symbol/协议不匹配 → **第一个会话能用、之后每个会话都失败**。
|
|
27
|
+
- **为什么此前抓不到**:`P5` 只扫 profile **顶层** `node_modules/@deepseek-ai`;未 hoist 时副本**嵌套在插件自己的 node_modules 里**,
|
|
28
|
+
顶层什么也看不到。**用两个变体分别实测确认**:有顶层副本 → P5 命中;**只有嵌套副本 → 没有任何检查提及**
|
|
29
|
+
(这一步很关键:若只看"顶层也有一份"的那个变体,就会误判为"我们已覆盖"——正是本会话反复犯的"用巧合冒充覆盖")。
|
|
30
|
+
- **新检查判据**:已装 bundle 的 manifest 里 `dependencies` 出现 `@deepseek-ai/*` 即 fail(宿主包应由宿主共享根提供),
|
|
31
|
+
并申报 `examined`/`examinedWhat`。对照实测:改用 `peerDependencies` 的插件 **不误报**。
|
|
32
|
+
- **配套(W4/W6)**:语料新增两条(病态 + **健康对照**),共 16 条;变异清单新增 `P23-host-dep-detection`
|
|
33
|
+
(把检出判据改成恒假,语料必须变红)。
|
|
34
|
+
|
|
35
|
+
测试 214 → **218**;变异 **9 个 → 8 杀死 / 0 存活 / 1 等价**。
|
|
36
|
+
|
|
37
|
+
## [0.8.3] — 2026-09-16
|
|
38
|
+
|
|
39
|
+
**把"自造的对抗者"变成默认动作**(用户要求:红队入流程 + 不依赖外部报错也能自我进化)。
|
|
40
|
+
|
|
41
|
+
### Added —— 变异测试:验证"守卫是否真的在守"
|
|
42
|
+
|
|
43
|
+
`scripts/mutation-test.mjs`:**故意破坏每个守卫,要求测试必须变红**;破坏后测试仍绿 = 那个守卫是装饰品。
|
|
44
|
+
|
|
45
|
+
- 首轮结果:**8 个变异中 4 个存活** —— 也就是说**一半守卫此前无人看守**:
|
|
46
|
+
`arity-guard`(没测)、`catalog-skip-as-pass`(没测)、`closure-symbol`(我的变异插错了位置)、
|
|
47
|
+
`coverage-reset-noop`(**守卫本身冗余**:各段都自己设了值,去掉重置没有任何测试变红)。
|
|
48
|
+
- 处置:补了两条缺失的测试(arity 守卫 / 目录检查 skip 语义,且**用例自控环境**:PATH 收窄到只有 node,
|
|
49
|
+
否则 E7 在有 dsh 的机器上合法地 pass,测试就变成对环境的断言);把"覆盖量上下文"改为**绑定求值阶段**
|
|
50
|
+
(`coverageNow()` 只接受属于当前阶段的上下文),守卫才有牙齿;无牙齿的那条如实标为**等价变异**并记录。
|
|
51
|
+
- 终态:**杀死 7 / 存活 0 / 等价 1(已记录)**。
|
|
52
|
+
|
|
53
|
+
### Added —— 发布闸门:红队不跑就不许发版
|
|
54
|
+
|
|
55
|
+
- `docs/redteam-brief.md`:红队行动简报(如何发起、攻击面、产出要求)。
|
|
56
|
+
- `scripts/release-check.sh`:五道闸门 —— 环境泛化审计 · **变异测试无存活** · 全套测试 ·
|
|
57
|
+
**本版本必须留下 `docs/redteam-<版本>.md`** · 待决策清单可见。缺一项即"不许发布"。
|
|
58
|
+
- 接入 CI(ubuntu × node24)。**闸门第一次运行就拦住了 0.8.3 自己**(缺红队档案)。
|
|
59
|
+
|
|
60
|
+
### Added —— 缺陷台账:让"自我进化"可度量
|
|
61
|
+
|
|
62
|
+
`docs/defect-ledger.md`:20 条缺陷按**来源**分类(EXT 外部报告 / RT 红队 / MUT 变异 / AUDIT 自审),
|
|
63
|
+
并给出**预发布发现率**。截至今日:EXT 6 · RT 6 · MUT 1 类 · AUDIT 7 → **预发布发现率 ≈ 70%**。
|
|
64
|
+
台账里如实注明:**红队那 6 条是因为用户要求才跑的**,所以闸门(强制留下档案)才是把比率变成能力的那一步。
|
|
65
|
+
|
|
19
66
|
## [0.8.2] — 2026-09-16
|
|
20
67
|
|
|
21
68
|
**红队审计(fresh-context 证伪者)在"已自审三轮"的版本里找出 6 个真漏洞**,其中 2 个推翻了我刚声称"已验证"的断言。
|
|
@@ -43,7 +90,24 @@
|
|
|
43
90
|
- `scripts/audit.sh` 的畸形-manifest 断言由 `n > 1` 收紧为 `n >= 15`,并把当前不达标**如实计为"已知缺口"**(自证太弱是 R8 的实例)。
|
|
44
91
|
- 新增 `docs/redteam-2026-09-16.md`(逐条反例 + 处置 + 红队未能验证项)。
|
|
45
92
|
|
|
46
|
-
|
|
93
|
+
### 第二轮红队(攻击这批新机制本身)→ 本轮修复
|
|
94
|
+
|
|
95
|
+
完整档案 `docs/redteam-0.8.3.md`。**8 条真反例**,其中 1 条推翻台账里"R1 已修 0.8.2"的结论:
|
|
96
|
+
|
|
97
|
+
- **F1(严重)**:0.8.2 只修了**目录段**的"跳过报成 pass";**profile 段的阶段占位值仍在伪造覆盖量** ——
|
|
98
|
+
3 个**一个都没装**的幽灵 bundle 也能得到 `✓ 16 项通过 / ok=true / verified=16`,且 15 条记录带
|
|
99
|
+
`examined=3, examinedWhat="bundle 条目"`(P21 文案写"扫描 0 个 host 入口"却记 examined=3)。
|
|
100
|
+
**已修**:删除占位回退(`examined` 只能显式申报);未申报者标 `unreported` 并**单独计数、进 JSON**;
|
|
101
|
+
补齐 P11/P15/P17/P19/P20/P21 的数量与单位;删除"数量巧合就继承单位"的回退(它会把 P15 误贴成"3 个 bundle 条目")。
|
|
102
|
+
- **F3/F4/F8(高)**:发布闸门可被空文件或任意版本号旁路;**闸门与变异清单此前根本未入库**;闸门诊断在 C locale 下崩溃且从不核对数量。已修并入库。
|
|
103
|
+
- **F5(高)**:测试**依赖网络**导致不可复现,进而让变异测试产生**假杀死**(环境红 = 记成 kill)。
|
|
104
|
+
已加离线开关 `DSH_DOCTOR_OFFLINE=1`;变异判据收紧为"必须出现断言失败";每条变异先跑基线;超时单列。
|
|
105
|
+
- **F6(中)**:等价变异是自证后门(无预算、不进 `--json`)。已加预算(1)并在 JSON 暴露。
|
|
106
|
+
- **F7(中)**:台账计数与自己的 20 行不自洽 → 已按行重算并写明归属规则。
|
|
107
|
+
- **F2(严重,未修)**:闭集只是**文本律**(`String.fromCharCode` 可绕过;`client.js` 未被扫描)→ **待决策 D6**。
|
|
108
|
+
- 附注:`--no-catalog` 只是 `noRemote`,`checks.local.json` 仍会合并 → **待决策 D7**。
|
|
109
|
+
|
|
110
|
+
测试 144 → **214**。
|
|
47
111
|
|
|
48
112
|
## [0.8.1] — 2026-09-16
|
|
49
113
|
|
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 传绝对路径)。 */
|
|
@@ -839,6 +857,83 @@ function checkProfile(name) {
|
|
|
839
857
|
}
|
|
840
858
|
}
|
|
841
859
|
|
|
860
|
+
// P23:bundle **真的自带了一份宿主机包的副本**(社区 #6789)
|
|
861
|
+
//
|
|
862
|
+
// 报告的现象:`@deepseek-ai/dsh-experimental-browser-use-runtime` 把 `@deepseek-ai/dsh-scope` 写成普通 dependency,
|
|
863
|
+
// 于是 profile 里出现**第二份实例** → 两份的 Symbol/协议不匹配 → "第一个会话能用、之后每个会话都失败"。
|
|
864
|
+
//
|
|
865
|
+
// 三轮红队把这条检查打穿过三次,教训是**判"代理"而不是判"条件"**:
|
|
866
|
+
// · 第一版只看 manifest 键名(`dependencies` 里有 `@deepseek-ai/*`)→
|
|
867
|
+
// 副本**真的躺在盘上**但 manifest 没写时完全失明(红队 F1);`optionalDependencies`(F2)、
|
|
868
|
+
// `npm:`/`file:` 别名(F3)全漏;同 scope 的**插件依赖**被误报(F5);
|
|
869
|
+
// bundle 装在父层 `profiles/node_modules` 时整条跳过(F4)。
|
|
870
|
+
// 现在改为**判条件本身**:
|
|
871
|
+
// ① 扫 bundle 自己的 `node_modules/@deepseek-ai/*`(真实存在的副本才报——这才是 #6789 的状态);
|
|
872
|
+
// ② 再看 `dependencies`/`optionalDependencies` 里指向宿主包的**别名**(`npm:` 前缀或键名本身就是宿主包名);
|
|
873
|
+
// ③ "是不是宿主机包"用 `resolveHostVersion`(profile nm + 父层两根)判定,**不用裸前缀**(修 F5);
|
|
874
|
+
// ④ 解析根与 `resolveHostVersion` 一致(含父层,修 F4);排除 `_quarantined` 与本身就是 bundle 的插件包(F5/F6)。
|
|
875
|
+
{
|
|
876
|
+
const roots23 = [join(dir, 'node_modules'), join(dirname(dir), 'node_modules')];
|
|
877
|
+
const bundleSet = new Set(bundles.map(String));
|
|
878
|
+
const quarantined23 = new Set((manifest._quarantined ?? []).map((q) => String(q.name ?? q)));
|
|
879
|
+
const resolveBundleDir = (b) => roots23.map((r) => join(r, String(b))).find((d) => existsSync(join(d, 'package.json'))) ?? null;
|
|
880
|
+
// 判定"这是宿主库还是插件"用**内容**,不用锚点也不用裸前缀——
|
|
881
|
+
// 教训链:① 只看 manifest 键名 → 副本躺在盘上也失明(红队 F1);
|
|
882
|
+
// ② 改用 CLI 安装树(installAnchor)→ 它依赖 PATH 里的 node_modules/.bin,
|
|
883
|
+
// 实测**本机也是 null**,于是永远判不出宿主包(等于静默失效);
|
|
884
|
+
// ③ 现在:读**副本自己的 manifest** —— 带 `dsh.bundle`/`dsh.client` 的是**插件**(依赖它是正确写法,红队 F5),
|
|
885
|
+
// 不带的才是**宿主库**(出现第二份副本即 #6789 的条件)。
|
|
886
|
+
const isPluginPkg = (mf) => Boolean(mf && mf.dsh && (mf.dsh.bundle || mf.dsh.client));
|
|
887
|
+
const readPkg = (d) => { try { return readJsonReportingBom(join(d, 'package.json')).data; } catch { return null; } };
|
|
888
|
+
let scanned23 = 0;
|
|
889
|
+
const issues23 = [];
|
|
890
|
+
for (const b of bundles) {
|
|
891
|
+
if (quarantined23.has(String(b))) continue;
|
|
892
|
+
const bdir = resolveBundleDir(b);
|
|
893
|
+
if (!bdir) continue; // 解析不到 → 由 P1 负责
|
|
894
|
+
scanned23++;
|
|
895
|
+
// ① 真实存在的第二份副本
|
|
896
|
+
let nested = [];
|
|
897
|
+
try { nested = readdirSync(join(bdir, 'node_modules', '@deepseek-ai')); } catch { /* 无嵌套目录 */ }
|
|
898
|
+
for (const n of nested) {
|
|
899
|
+
const full = `@deepseek-ai/${n}`;
|
|
900
|
+
const nestedDir = join(bdir, 'node_modules', '@deepseek-ai', n);
|
|
901
|
+
const nmf = readPkg(nestedDir);
|
|
902
|
+
if (!isPluginPkg(nmf)) {
|
|
903
|
+
issues23.push(`${b} 自带**宿主库**副本 ${full}${nmf?.version ? `@${nmf.version}` : ''}(嵌套在它自己的 node_modules 里,P5 只看顶层所以看不到)`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// ② 别名 / optional 指向宿主包
|
|
907
|
+
let mf23 = null;
|
|
908
|
+
try { mf23 = readJsonReportingBom(join(bdir, 'package.json')).data; } catch { /* 忽略 */ }
|
|
909
|
+
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
910
|
+
for (const [k, v] of Object.entries(mf23?.[field] ?? {})) {
|
|
911
|
+
const target = /^npm:(@deepseek-ai\/[^@]+)@/.exec(String(v));
|
|
912
|
+
const name = target ? target[1] : (k.startsWith('@deepseek-ai/') ? k : null);
|
|
913
|
+
if (!name || bundleSet.has(name)) continue; // 同 scope 的插件依赖插件 = 正确写法(F5)
|
|
914
|
+
// 目标落在 profile 两根里且**不是插件** → 是宿主库被当普通依赖引入(会带来第二份副本)
|
|
915
|
+
const dirs = roots23.map((r) => join(r, name)).concat([join(bdir, 'node_modules', name)]);
|
|
916
|
+
const found = dirs.map(readPkg).find((m) => m && !isPluginPkg(m));
|
|
917
|
+
if (found) issues23.push(`${b} 用 ${field} 引入宿主库 ${name}${target ? `(别名 ${k} → ${v})` : ''}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (issues23.length) {
|
|
922
|
+
report('profile', 'P23', false,
|
|
923
|
+
`已装 bundle 自带**宿主包的第二份副本**(${issues23.length} 处 / 扫过 ${scanned23} 个 bundle)——`
|
|
924
|
+
+ `两份实例的 Symbol/协议不匹配时,典型症状是"第一个会话能用、之后每个会话都失败"(社区 #6789:\`dsh-scope\`):\n `
|
|
925
|
+
+ issues23.slice(0, 5).join('\n ')
|
|
926
|
+
+ (issues23.length > 5 ? `\n …另有 ${issues23.length - 5} 处` : ''),
|
|
927
|
+
'在该插件的 package.json 里把宿主机包从 dependencies/optionalDependencies 改为 **peerDependencies**(宿主机包由宿主共享根提供);'
|
|
928
|
+
+ '若副本是构建产物带进来的,清理其 node_modules 后重装;并向上游报告该发布缺陷',
|
|
929
|
+
undefined, scanned23, '已装 bundle');
|
|
930
|
+
} else {
|
|
931
|
+
report('profile', 'P23', true,
|
|
932
|
+
`已装 bundle 均未自带宿主包副本(扫过 ${scanned23} 个 bundle 的 node_modules 与依赖声明)`,
|
|
933
|
+
undefined, undefined, scanned23, '已装 bundle');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
842
937
|
// P20 / P21:第三方插件把整棵插件树拖垮的两种**静态可判**形态(社区 #6693 实测)
|
|
843
938
|
// 报告者在 host 侧 `apply()` 里连续撞了两个独立致命错、client 侧撞了两个静默白屏错;
|
|
844
939
|
// 其核心抱怨是"唯一的诊断入口被故障本身摧毁"。这两条都不需要执行插件代码即可判定。
|
|
@@ -885,7 +980,7 @@ function checkProfile(name) {
|
|
|
885
980
|
+ `systemd 健康、端口在听、页面永远空白(#6693 实测:整份 journal 里该错误计数为 0)`,
|
|
886
981
|
'把 client 半边改成 `__ModuleLoader__.load({ id, factory })` 的 CJS 工厂形态(参照 dsh-better-sidebar / dsh-dream-skin)');
|
|
887
982
|
} else {
|
|
888
|
-
report('profile', 'P20', true, `client 产物均为 CJS 工厂形态(${p20scanned}
|
|
983
|
+
report('profile', 'P20', true, `client 产物均为 CJS 工厂形态(${p20scanned} 个)`, undefined, undefined, p20scanned, 'client 产物');
|
|
889
984
|
}
|
|
890
985
|
|
|
891
986
|
// P21:**沙箱专属符号在普通插件里不存在**(host 与 client 两侧一律如此,同一根因两个面)
|
|
@@ -937,9 +1032,9 @@ function checkProfile(name) {
|
|
|
937
1032
|
+ `\n host 侧表现为 \`ReferenceError: harness is not defined\`(在 \`apply()\` 内同步抛出 → cordis 中断装载链 → Web UI 整体不可用)`,
|
|
938
1033
|
'移除这些引用或用宿主提供的等价能力;若确需守卫,必须写成 `typeof X !== \"undefined\"` **放在最前**——`X.foo && …` 挡不住(裸标识符先被解析)');
|
|
939
1034
|
} else if (notes21.length) {
|
|
940
|
-
report('profile', 'P21', true, `未见裸引用沙箱专属符号(${notes21.length} 处有 typeof
|
|
1035
|
+
report('profile', 'P21', true, `未见裸引用沙箱专属符号(${notes21.length} 处有 typeof 守卫,未计入)`, undefined, undefined, hostFiles.length + clientFiles.length, 'host/client 文件');
|
|
941
1036
|
} else {
|
|
942
|
-
report('profile', 'P21', true, `扫描 ${hostFiles.length} 个 host 入口与 ${clientFiles.length} 个 client
|
|
1037
|
+
report('profile', 'P21', true, `扫描 ${hostFiles.length} 个 host 入口与 ${clientFiles.length} 个 client 产物,未见沙箱专属符号引用`, undefined, undefined, hostFiles.length + clientFiles.length, 'host/client 文件');
|
|
943
1038
|
}
|
|
944
1039
|
}
|
|
945
1040
|
|
|
@@ -997,7 +1092,7 @@ function checkProfile(name) {
|
|
|
997
1092
|
report('profile', 'P19', true,
|
|
998
1093
|
`核对 ${checked19} 条 host peer 声明,均可接受当前核心版本`
|
|
999
1094
|
+ (unknown19 ? `(${unknown19} 条无法判定,按未知处理未计入)` : '')
|
|
1000
|
-
+ undeclaredNote.replace(/\n\s*/g, ' '));
|
|
1095
|
+
+ undeclaredNote.replace(/\n\s*/g, ' '), undefined, undefined, checked19, 'host peer 声明');
|
|
1001
1096
|
}
|
|
1002
1097
|
}
|
|
1003
1098
|
|
|
@@ -1227,7 +1322,7 @@ function packageNamedExports(pkgDir) {
|
|
|
1227
1322
|
}
|
|
1228
1323
|
}
|
|
1229
1324
|
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);
|
|
1325
|
+
else report('profile', 'P11', true, '已装 bundle 的 main 入口产物均在', undefined, undefined, bundleDirs.size, '已装 bundle');
|
|
1231
1326
|
|
|
1232
1327
|
// P13:client 端服务名抢注核心客户端服务(#2752:ctx.provide("chatFileMentions") 撞核心 dsh-client-ui-deliverables
|
|
1233
1328
|
// → 浏览器端 service already registered → Web UI 白屏,服务端日志无感知、报错无冲突来源)
|
|
@@ -1418,7 +1513,7 @@ function packageNamedExports(pkgDir) {
|
|
|
1418
1513
|
+ '改用 [IO.File]::WriteAllText($p, (Get-Content $p -Raw), (New-Object Text.UTF8Encoding $false));'
|
|
1419
1514
|
+ '或 sed -i "" "1s/^\xEF\xBB\xBF//" <file>', undefined, bomTargets.length);
|
|
1420
1515
|
} else {
|
|
1421
|
-
report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, undefined, bomTargets.length);
|
|
1516
|
+
report('profile', 'P15', true, `关键文件无 BOM 头(检查 ${bomTargets.length} 个)`, undefined, undefined, bomTargets.length, '关键文件');
|
|
1422
1517
|
}
|
|
1423
1518
|
|
|
1424
1519
|
/* P16:插件命名导入的导出缺失检测(#5864:一个缺失导出 → 整棵插件树 boot 崩溃循环、
|
|
@@ -1522,7 +1617,7 @@ function packageNamedExports(pkgDir) {
|
|
|
1522
1617
|
if (p17Issues.length) {
|
|
1523
1618
|
report('profile', 'P17', false, `client 端 require 的模块不在宿主模块表(#5719:makeRequire 硬 throw → 浏览器白屏且服务端无感知): ${p17Issues.join('; ')}`, `改用宿主提供的模块名;若确由宿主提供,在本包 package.json 的 dsh.client.external/inject 里声明;平台种子当前 ${CLIENT_SEEDS.size} 项、已装图行 ${composedRows.size} 项`);
|
|
1524
1619
|
} else {
|
|
1525
|
-
report('profile', 'P17', true, `client 端 require 的 specifier 均可服务(平台种子 ${CLIENT_SEEDS.size} 项 + 已装图行 ${composedRows.size} 项)`, undefined);
|
|
1620
|
+
report('profile', 'P17', true, `client 端 require 的 specifier 均可服务(平台种子 ${CLIENT_SEEDS.size} 项 + 已装图行 ${composedRows.size} 项)`, undefined, undefined, CLIENT_SEEDS.size + composedRows.size, 'client specifier 来源');
|
|
1526
1621
|
}
|
|
1527
1622
|
}
|
|
1528
1623
|
|
|
@@ -2164,6 +2259,8 @@ function validCatalog(data) {
|
|
|
2164
2259
|
|
|
2165
2260
|
/** 拉取目录:新鲜缓存(≤TTL) → 远程(raw.githubusercontent,3s 超时) → 旧缓存(last-known-good) → 内置副本;末尾合并本地覆盖层。 */
|
|
2166
2261
|
async function loadCatalog({ noRemote = false, fetchImpl, home = HOME, localPath } = {}) {
|
|
2262
|
+
// 离线开关(红队 F5:目录用例曾因取到远程目录而结果不可复现)——DSH_DOCTOR_OFFLINE=1 等价于 noRemote
|
|
2263
|
+
if (process.env.DSH_DOCTOR_OFFLINE === '1') noRemote = true;
|
|
2167
2264
|
const bundled = bundledCatalog();
|
|
2168
2265
|
let base;
|
|
2169
2266
|
if (noRemote || typeof fetchImpl !== 'function') {
|
|
@@ -2371,6 +2468,7 @@ function checkCatalog(ctx, catalog) {
|
|
|
2371
2468
|
// 于是"没有目标可查"被记成**通过**(实测:无 profile 目录时 P6 仍 pass)。判据用探测自己的话:
|
|
2372
2469
|
// **凡自述跳过的一律是 skip** —— 这类"文案与状态不一致"只能从结构上消灭,不能靠逐个改分支。
|
|
2373
2470
|
if (!r.skipped && r.ok === true && /跳过|不适用/.test(String(r.detail))) r.skipped = true;
|
|
2471
|
+
// R1(红队,2026-09-16;**曾被后续编辑覆盖回去,由离线目录用例抓回**):自报 skipped 必须是 skip。
|
|
2374
2472
|
if (r.skipped) { reportSkip(check.section, check.id, r.detail, 'catalog'); continue; }
|
|
2375
2473
|
const severity = check.severity ?? 'error';
|
|
2376
2474
|
catalogSeverity.set(check.id, severity);
|
|
@@ -3355,6 +3453,8 @@ async function run() {
|
|
|
3355
3453
|
verified: aggJson.totals.verified,
|
|
3356
3454
|
skipped: aggJson.totals.skipped,
|
|
3357
3455
|
failed: aggJson.totals.failed,
|
|
3456
|
+
// 红队 F1:未申报覆盖量的通过**单独暴露**(既不算已验证,也不隐形)
|
|
3457
|
+
unreported: aggJson.totals.unreported,
|
|
3358
3458
|
checks: checksWithStatus, catalog: catalogMeta, update: updateInfo, ...(securityMeta.enabled ? { security: securityMeta } : {}),
|
|
3359
3459
|
}, null, 2));
|
|
3360
3460
|
} 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.4",
|
|
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": [
|