@moonquake2004/dsh-doctor 0.8.1 → 0.8.2

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,35 @@
16
16
 
17
17
  ---
18
18
 
19
+ ## [0.8.2] — 2026-09-16
20
+
21
+ **红队审计(fresh-context 证伪者)在"已自审三轮"的版本里找出 6 个真漏洞**,其中 2 个推翻了我刚声称"已验证"的断言。
22
+ 完整档案:`docs/redteam-2026-09-16.md`;未修项见 `docs/check-authoring-rules.md` §5。
23
+
24
+ ### Fixed
25
+
26
+ - **目录检查把"跳过"报成 pass,并伪造覆盖量**(最严重):`runCatalogCheck` 自报 `skipped` 被记成 `ok:true`;
27
+ `report()` 的 `examined` 回退到段级占位值 1,零对象永不降级。实测 `✓ 未发现 DSH 环境…跳过 dsh 的 PATH 检查`;
28
+ profile 目录**整个不存在**时 P6 仍 pass。
29
+ - 自报 `skipped` → `reportSkip`;探测原语的"目标不存在"分支补 `skipped: true`(根因是**分支里没设标记、文案却写着"跳过"**);
30
+ 保留"文案含跳过 ⇒ skip"作兜底。空环境下被伪造的 `verified` 由 11 降到 8。
31
+ - **闭集检测有盲点**:`⚠` 也是判定符号却裸写在收口点外,而闭集正则只有 `/[✓⊖✗]/`。纳入 `⚠` 后**立刻又暴露 3 处**,已全部收口;
32
+ 判定符号正则集中为 `VERDICT_GLYPH_RE`;**收口处剥掉数据里的判定符号**(远程目录数据不得伪造判定上屏)。
33
+ - **`reportSkip` 缺 `phase`** → 补上("单位属于求值阶段"这条断言此前在 skip 上无法核对)。
34
+
35
+ ### Changed — 契约级决策,**未擅改**
36
+
37
+ - **退出码随输出格式分叉**(`--json` 1 / `--envelope` 2 / 文本 1):契约 `doctor-contract.md:32` 规定
38
+ `0 全通过 / 1 任何 WARN / 2 任何 FAIL` → **envelope 是对的一边,主路径违反契约**。
39
+ 我一度把 envelope 改成 1(**统一到违反契约的一边**),已回滚;分歧现由测试钉住并列为待决策 D1。
40
+
41
+ ### Infra
42
+
43
+ - `scripts/audit.sh` 的畸形-manifest 断言由 `n > 1` 收紧为 `n >= 15`,并把当前不达标**如实计为"已知缺口"**(自证太弱是 R8 的实例)。
44
+ - 新增 `docs/redteam-2026-09-16.md`(逐条反例 + 处置 + 红队未能验证项)。
45
+
46
+ 测试 141 → 144。
47
+
19
48
  ## [0.8.1] — 2026-09-16
20
49
 
21
50
  ### 0.8.0 的回归审计 —— **修一个引入一个,被查出来了四个**
package/dsh-doctor.mjs CHANGED
@@ -264,10 +264,19 @@ function report(section, id, ok, detail, fix, src, examined) {
264
264
  }
265
265
  const zeroCheck = typeof examined === 'number' ? examined : coverageNow()?.n;
266
266
  if (ok === true && zeroCheck === 0) {
267
- results.push({ section, id, ok: true, skip: true, coverage: 'none', detail: `${detail}(无可检查对象,未做任何比较)`, fix, src: src ?? 'builtin' });
267
+ // 2026-09 语料发现的措辞缺陷:原实现把原判定文案原样留下 → 出现"结构正常(无可检查对象,未做任何比较)"
268
+ // 这种**自相矛盾**的句子(既宣称正常、又说没比较)。原文案改放 note(机器可读),人读文案只陈述事实。
269
+ results.push({ section, id, ok: true, skip: true, coverage: 'none',
270
+ // 措辞要求:**不得自相矛盾**(2026-09 语料曾产出"结构正常(无可检查对象,未做任何比较)"),
271
+ // 同时保留具体原因(否则用户与测试都只能看到一句通用话)。
272
+ detail: `无可检查对象、未做任何比较——不作为「通过」;原判定:${String(detail).replace(VERDICT_GLYPH_RE, '·')}`,
273
+ fix, src: src ?? 'builtin' });
268
274
  return;
269
275
  }
270
- const rec = { section, id, ok, detail, fix, src: src ?? 'builtin', phase: currentPhase };
276
+ // R6(红队):detail 可来自远程目录/checks.local.json,**数据不得能伪造判定符号**上屏——
277
+ // 源码里没有 ✓ 也能让 stdout 出现 ✓。故在收口处剥掉数据里的判定符号(含 ⚠)。
278
+ const safeDetail = String(detail).replace(VERDICT_GLYPH_RE, '·');
279
+ const rec = { section, id, ok, detail: safeDetail, fix, src: src ?? 'builtin', phase: currentPhase };
271
280
  if (ok === true) {
272
281
  const ctx = coverageNow();
273
282
  const n = typeof examined === 'number' ? examined : ctx?.n;
@@ -279,7 +288,8 @@ function report(section, id, ok, detail, fix, src, examined) {
279
288
 
280
289
  /** skip 状态(v1 词汇表 r5:#1719)——"不适用"而非"通过",必须带 reason(detail)。不计入 pass/fail,不翻退出码。 */
281
290
  function reportSkip(section, id, detail, src) {
282
- results.push({ section, id, ok: true, skip: true, detail, src: src ?? 'builtin' });
291
+ // R2(红队):skip 记录此前**没有 phase 字段**,于是"单位属于求值阶段"这条断言在 skip 上无法核对。
292
+ results.push({ section, id, ok: true, skip: true, detail, src: src ?? 'builtin', phase: currentPhase });
283
293
  }
284
294
 
285
295
 
@@ -298,6 +308,7 @@ function reportSkip(section, id, detail, src) {
298
308
  * @param {{checked?: number, detail?: string, reason?: string, totals?: {verified:number, skipped:number, failed:number}}} opts
299
309
  */
300
310
  function makeVerdict(state, opts = {}) {
311
+ if (!['pass', 'fail', 'skip', 'warn', 'action'].includes(state)) throw new Error(`makeVerdict: 未知状态 ${state}`);
301
312
  if (state === 'pass') {
302
313
  const n = opts.checked;
303
314
  if (typeof n !== 'number' || n <= 0) {
@@ -313,6 +324,9 @@ function makeVerdict(state, opts = {}) {
313
324
  * 两者构成本工具的**闭集**——闭集测试断言:源码里任何 ✓/⊖/✗ 都必须落在这两个函数体内。
314
325
  */
315
326
  const VERDICT_MARKS = { pass: '✓', fail: '✗', skip: '⊖', warn: '⚠', action: '✓' };
327
+ /** 判定符号集合(含 ⚠)——**唯一**用于"从数据里剥掉符号"的正则。
328
+ * 红队 R6:数据(远程目录/checks.local.json)不得能伪造判定符号上屏;符号只许收口点产出。 */
329
+ const VERDICT_GLYPH_RE = /[✓✔⊖✗✘⚠]/g;
316
330
 
317
331
  function printItem(state, text, indent = 2) {
318
332
  console.log(`${' '.repeat(indent)}${VERDICT_MARKS[state] ?? '?'} ${text}`);
@@ -655,7 +669,7 @@ function checkPort3080() {
655
669
  `端口 ${port} 被 dsh web 实例占用(PID ${info.pid})${httpAlive === true ? '且 HTTP 有应答' : ''}——宿主自身或另一实例,正常`, undefined);
656
670
  }
657
671
  else if (info) report('env', 'E10-port-3080', false, `端口 ${port} 被其他程序占用(PID ${info.pid}: ${info.cmd}),dsh web 启动会 address in use(#1719)`, `关掉占用进程,或让 dsh web 用别的端口`);
658
- else report('env', 'E10-port-3080', true, `⚠ 端口 ${port} 被占用但无法识别占用者`, undefined);
672
+ else report('env', 'E10-port-3080', true, `端口 ${port} 被占用但无法识别占用者`, undefined);
659
673
  } else {
660
674
  report('env', 'E10-port-3080', false, `端口 ${port} 探测异常: ${e.message.slice(0, 60)}`, undefined);
661
675
  }
@@ -2101,7 +2115,7 @@ function scanAllSessions() {
2101
2115
  if (unstableReads.length) parts.push(`${unstableReads.length} 个会话**读取间歇性失败**(重试即成功 → 非损坏,指向存储/内存/驱动的偶发读错误,社区 #6739): ${unstableReads.slice(0, 3).map((u) => `${u.id}(${u.attempts} 次尝试)`).join(' | ')}`);
2102
2116
  if (oversized.length) parts.push(`${oversized.length} 个超大会话: ${oversized.map((o) => `${o.id}(${o.dsMB}MB/${o.events}事件)`).join(' | ')}`);
2103
2117
  if (totalRisk) parts.push(`工作区估算物化堆 ~${estHeapMB}MB(估算= max(${totalEvents}事件×600B, ${totalMB}MB×6),跨 ${files.length} 会话累积,#1550 场景;阈值 ${heapLimit}MB,可设 DSH_DOCTOR_HEAP_MB)`);
2104
- report('session', 'S11', true, `⚠ 全会话扫描:${parts.join(';')}(未损坏,可接受或归档)`,
2118
+ report('session', 'S11', true, `全会话扫描:${parts.join(';')}(未损坏,可接受或归档)`,
2105
2119
  unstableReads.length
2106
2120
  ? '间歇性读取失败不是日志问题:先重试读取;持续出现则排查存储健康(SMART)、内存与磁盘驱动(#6739 的证据是失败帧位置每次不同)'
2107
2121
  : '冷启动会明显变慢;必要时压缩/归档历史会话', undefined, files.length);
@@ -2274,13 +2288,13 @@ export function runCatalogCheck(check, ctx) {
2274
2288
  let ok = existsSync(fp);
2275
2289
  if (ok && probe.type === 'path-is-dir') ok = lstatSync(fp).isDirectory();
2276
2290
  if (ok && probe.type === 'path-is-file') ok = lstatSync(fp).isFile();
2277
- return ok ? { ok: true, detail: check.detailOk ?? `${fp} 存在` }
2291
+ return ok ? { ok: true, checked: 1, detail: check.detailOk ?? `${fp} 存在` }
2278
2292
  : { ok: false, detail: check.detailFail ?? `${fp} 不存在/类型不符` };
2279
2293
  }
2280
2294
  case 'json-valid': {
2281
2295
  const fp = p(probe.path);
2282
2296
  if (!existsSync(fp)) return probe.required === false
2283
- ? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
2297
+ ? { ok: true, skipped: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
2284
2298
  : { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
2285
2299
  let utf8ok = true, jsonok = false;
2286
2300
  try { new TextDecoder('utf-8', { fatal: true }).decode(readFileSync(fp)); } catch { utf8ok = false; }
@@ -2292,7 +2306,7 @@ export function runCatalogCheck(check, ctx) {
2292
2306
  case 'text-not-contains': {
2293
2307
  const fp = p(probe.path);
2294
2308
  if (!existsSync(fp)) return probe.required === false
2295
- ? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
2309
+ ? { ok: true, skipped: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
2296
2310
  : { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
2297
2311
  let re;
2298
2312
  try { re = new RegExp(probe.pattern, probe.flags ?? ''); } catch (e) { return { ok: false, detail: `目录规则正则非法: ${e.message.slice(0, 60)}` }; }
@@ -2323,7 +2337,7 @@ export function runCatalogCheck(check, ctx) {
2323
2337
  case 'file-writable': {
2324
2338
  const fp = p(probe.path);
2325
2339
  if (!existsSync(fp)) return probe.required === false
2326
- ? { ok: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
2340
+ ? { ok: true, skipped: true, detail: check.detailOk ?? `${fp} 不存在(跳过)` }
2327
2341
  : { ok: false, detail: check.detailFail ?? `${fp} 缺失` };
2328
2342
  let writable = false;
2329
2343
  try { const fd = openSync(fp, 'a'); closeSync(fd); writable = true; } catch { /* 只读/属主问题 */ }
@@ -2350,7 +2364,14 @@ function checkCatalog(ctx, catalog) {
2350
2364
  if (check.section === 'profile' && !ctx.profileDir) continue; // profile 无效时跳过 profile 段
2351
2365
  let r;
2352
2366
  try { r = runCatalogCheck(check, ctx); } catch (e) { r = { ok: false, detail: `catalog 检查异常: ${e.message.slice(0, 80)}` }; }
2353
- if (r.skipped) { report(check.section, check.id, true, r.detail, undefined, 'catalog'); continue; }
2367
+ // 红队 R1(2026-09-16):`runCatalogCheck` 自报 `skipped`(原语不支持 / 无 DSH 环境 / 目标文件不存在)
2368
+ // 曾被当成 `ok:true`(pass)—— 于是出现"✓ 未发现 DSH 环境…跳过 dsh 的 PATH 检查"这种自相矛盾的行,
2369
+ // 而且它继承目录段的占位覆盖量 1,零对象永不降级。自报 skipped 就是 skip,覆盖量为 0。
2370
+ // 归一化(红队 R1 的第二半):探测原语里有多处文案写着"…不存在(跳过)"却返回 `ok: true`,
2371
+ // 于是"没有目标可查"被记成**通过**(实测:无 profile 目录时 P6 仍 pass)。判据用探测自己的话:
2372
+ // **凡自述跳过的一律是 skip** —— 这类"文案与状态不一致"只能从结构上消灭,不能靠逐个改分支。
2373
+ if (!r.skipped && r.ok === true && /跳过|不适用/.test(String(r.detail))) r.skipped = true;
2374
+ if (r.skipped) { reportSkip(check.section, check.id, r.detail, 'catalog'); continue; }
2354
2375
  const severity = check.severity ?? 'error';
2355
2376
  catalogSeverity.set(check.id, severity);
2356
2377
  report(check.section, check.id, r.ok, r.detail, r.ok ? undefined : check.fix, 'catalog');
@@ -3039,7 +3060,7 @@ async function run() {
3039
3060
  if (jsonOut) console.log(JSON.stringify(r, null, 2));
3040
3061
  else if (r.ok && r.stage === 'verified') printVerdict(makeVerdict('pass', { checked: r.checked, detail: `已安装并预检通过(${r.checked} 条 entry 均可导入)——重启 dsh 即可` }));
3041
3062
  else if (r.ok && r.stage === 'quarantined') {
3042
- console.log(`⚠ 已安装,但该插件的 entry 导入失败,已自动隔离以避免 dsh 起不来:`);
3063
+ printVerdict(makeVerdict('warn', { detail: '已安装,但该插件的 entry 导入失败,已自动隔离以避免 dsh 起不来:' }));
3043
3064
  for (const f of r.failures || []) console.log(` ${f}`);
3044
3065
  console.log(` 隔离项:${r.quarantined.join(', ')}(用 --unquarantine <包名> 放回,修好版本后再重试)`);
3045
3066
  console.log(` dsh 现在可以正常启动。`);
@@ -3155,6 +3176,13 @@ async function run() {
3155
3176
  try { await checkPort3080(); } catch (e) { report('env', 'E10-port-3080', false, `端口检查异常: ${e.message.slice(0, 60)}`); }
3156
3177
  try { checkProfile(profileArg); } catch (e) { report('profile', 'P0', false, `profile 检查异常: ${e.message.slice(0, 100)}`); }
3157
3178
  try { checkSession(sessionArg); } catch (e) { report('session', 'S0', false, `session 检查异常: ${e.message.slice(0, 100)}`); }
3179
+ // 段级覆盖(2026-09 语料发现):整段没有产出任何检查时,报告里会出现"整段消失"——
3180
+ // 用户看不出"会话检查根本没跑"。R3 的精神在**段**这一级同样成立:没看就必须说出来。
3181
+ for (const [sec, why] of [['session', '未选中任何会话(活跃会话在保护窗口内,或被 --session 过滤)']]) {
3182
+ if (wants(sec) && !results.some((r) => r.section === sec)) {
3183
+ reportSkip(sec, 'S0', `${why}——本段检查未运行,**这不代表通过**`);
3184
+ }
3185
+ }
3158
3186
  try { scanAllSessions(); } catch (e) { report('session', 'S11', false, `全会话扫描异常: ${e.message.slice(0, 100)}`); }
3159
3187
  try { scanMigrationRefusals(); } catch (e) { report('session', 'S12', false, `迁移拒载预检异常: ${e.message.slice(0, 100)}`); }
3160
3188
  }
@@ -3286,6 +3314,9 @@ async function run() {
3286
3314
  if (s === 'fail') baseFail++;
3287
3315
  else if (s === 'warn') baseWarn++;
3288
3316
  }
3317
+ // 契约(docs/doctor-contract.md:32):0 = 全通过 · **1 = 任何 WARN** · **2 = 任何 FAIL**。
3318
+ // 红队 R3 指出 envelope 与 json/文本路径退出码不一致——**方向是 json/文本错**,不是这里错。
3319
+ // 我一度把它改成 1,等于把一致性统一到违反契约的一边;已回滚。见 §"待决策"。
3289
3320
  const baseExit = baseFail > 0 ? 2 : baseWarn > 0 ? 1 : 0;
3290
3321
  const exitCode = Math.max(baseExit, secExit);
3291
3322
  // v1.1 remediation(#1719 ADOPTED:ciceroyang 提名、两位 reviewer +1):opt-in --remediation,
@@ -3342,7 +3373,7 @@ async function run() {
3342
3373
  if (!r.ok && r.fix) console.log(` ↳ 修复: ${r.fix}`);
3343
3374
  }
3344
3375
  if (updateInfo.available && !updateInfo.applied) {
3345
- console.log(`\n⚠ 新版本 ${updateInfo.latest} 可用(当前 ${updateInfo.current})→ 运行 \`dsh-doctor --update\` 或 \`dsh plugin update\``);
3376
+ console.log(''); printVerdict(makeVerdict('warn', { detail: `新版本 ${updateInfo.latest} 可用(当前 ${updateInfo.current})→ 运行 \`dsh-doctor --update\` 或 \`dsh plugin update\`` }));
3346
3377
  } else if (updateInfo.applied) {
3347
3378
  console.log(''); printVerdict(makeVerdict('action', { detail: updateInfo.applied }));
3348
3379
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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": [