dsh-weiwen-law 0.2.0 → 0.2.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.
Files changed (36) hide show
  1. package/package.json +103 -103
  2. package/src/core/engine.mjs +142 -12
  3. package/src/index.js +32 -3
  4. package/test/alignment.test.mjs +64 -0
  5. package/test/anchor-channel.test.mjs +367 -0
  6. package/test/attribution.test.mjs +95 -0
  7. package/test/blind-spot-20260830.test.mjs +286 -0
  8. package/test/broken-window-heal.test.mjs +78 -0
  9. package/test/bugstop.test.mjs +90 -0
  10. package/test/cases.test.mjs +196 -0
  11. package/test/criterion-wiring.test.mjs +196 -0
  12. package/test/deduce_risk.test.mjs +157 -0
  13. package/test/dict-as-s-gate.test.mjs +204 -0
  14. package/test/docs-engine-sync.test.mjs +73 -0
  15. package/test/engine.test.mjs +211 -0
  16. package/test/fail-closed-gate.test.mjs +65 -0
  17. package/test/inner-h-parked.test.mjs +220 -0
  18. package/test/ledger-api-integration.test.mjs +74 -0
  19. package/test/m-court.test.mjs +171 -0
  20. package/test/mutation-gate.test.mjs +92 -0
  21. package/test/nested-wiring.test.mjs +383 -0
  22. package/test/opaque_exec.test.mjs +213 -0
  23. package/test/police-gate.test.mjs +81 -0
  24. package/test/projection-export.test.mjs +41 -0
  25. package/test/rcapacity-wiring.test.mjs +248 -0
  26. package/test/receipt-gate.test.mjs +185 -0
  27. package/test/residual-rdomain-fractal.test.mjs +142 -0
  28. package/test/s-account-ledger.test.mjs +126 -0
  29. package/test/scar-anchor-gate.test.mjs +150 -0
  30. package/test/sd-effect-fusion.test.mjs +127 -0
  31. package/test/sd-type-gate.test.mjs +123 -0
  32. package/test/session-ledger-buckets.test.mjs +55 -0
  33. package/test/speech-act-publish.test.mjs +82 -0
  34. package/test/strata-wiring.test.mjs +136 -0
  35. package/test/structure-integration.test.mjs +78 -0
  36. package/test/verb-wiring.test.mjs +1235 -0
package/package.json CHANGED
@@ -1,103 +1,103 @@
1
- {
2
- "name": "dsh-weiwen-law",
3
- "version": "0.2.0",
4
- "description": "唯稳律(Weiwen's Law)通用因果引擎(白箱呈现)—— DeepSeek Harness (Cordis) 插件实现",
5
- "type": "module",
6
- "main": "./src/index.js",
7
- "dsh": {
8
- "bundle": {
9
- "patch": "./weiwen-law.patch.yml"
10
- }
11
- },
12
- "files": [
13
- "src",
14
- "!src/runtime.log",
15
- "!src/core/_engine_base_*",
16
- "weiwen-law.patch.yml",
17
- "README.md",
18
- "DESIGN.md",
19
- "LICENSE",
20
- "examples",
21
- "test"
22
- ],
23
- "scripts": {
24
- "test": "node --test \"test/*.test.mjs\"",
25
- "build:docs": "node scripts/build-docs-engine.mjs",
26
- "check:docs": "node scripts/build-docs-engine.mjs --check"
27
- },
28
- "engines": {
29
- "node": ">=22"
30
- },
31
- "license": "AGPL-3.0",
32
- "author": {
33
- "name": "Shaky77",
34
- "email": "563003@qq.com"
35
- },
36
- "repository": {
37
- "type": "git",
38
- "url": "https://github.com/Shaky77/weiwen-law-dsh.git"
39
- },
40
- "bugs": {
41
- "url": "https://github.com/Shaky77/weiwen-law-dsh/issues"
42
- },
43
- "homepage": "https://github.com/Shaky77/weiwen-law-dsh#readme",
44
- "keywords": [
45
- "ai",
46
- "causal",
47
- "causality",
48
- "causal-inference",
49
- "causal-reasoning",
50
- "causal-engine",
51
- "guardrail",
52
- "guardrails",
53
- "white-box",
54
- "whitebox",
55
- "deepseek",
56
- "harness",
57
- "agent",
58
- "safety",
59
- "ai-safety",
60
- "ai-security",
61
- "agi-alignment",
62
- "audit",
63
- "auditable",
64
- "interpretability",
65
- "endogenous-risk-control",
66
- "token-efficient",
67
- "weiwens-law",
68
- "weiwen-law",
69
- "kiss-law",
70
- "steady-state",
71
- "rigid-anchor",
72
- "agent-safety",
73
- "llm-guardrail",
74
- "dsh-plugin",
75
- "causal-logic",
76
- "ai-alignment",
77
- "因果律",
78
- "因果推理",
79
- "因果引擎",
80
- "白箱",
81
- "护栏",
82
- "人工智能安全",
83
- "AI安全",
84
- "人工智能对齐",
85
- "可审计",
86
- "稳态",
87
- "刚性锚点",
88
- "内生风险控制",
89
- "智能体安全",
90
- "大模型护栏",
91
- "令牌高效",
92
- "唯稳律",
93
- "守真稳态"
94
- ],
95
- "peerDependencies": {
96
- "@deepseek-ai/dsh-tools": "*"
97
- },
98
- "peerDependenciesMeta": {
99
- "@deepseek-ai/dsh-tools": {
100
- "optional": true
101
- }
102
- }
103
- }
1
+ {
2
+ "name": "dsh-weiwen-law",
3
+ "version": "0.2.1",
4
+ "description": "唯稳律(Weiwen's Law)通用因果引擎(白箱呈现)—— DeepSeek Harness (Cordis) 插件实现",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "dsh": {
8
+ "bundle": {
9
+ "patch": "./weiwen-law.patch.yml"
10
+ }
11
+ },
12
+ "files": [
13
+ "src",
14
+ "!src/runtime.log",
15
+ "!src/core/_engine_base_*",
16
+ "weiwen-law.patch.yml",
17
+ "README.md",
18
+ "DESIGN.md",
19
+ "LICENSE",
20
+ "examples",
21
+ "test"
22
+ ],
23
+ "scripts": {
24
+ "test": "node --test \"test/*.test.mjs\"",
25
+ "build:docs": "node scripts/build-docs-engine.mjs",
26
+ "check:docs": "node scripts/build-docs-engine.mjs --check"
27
+ },
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "license": "AGPL-3.0",
32
+ "author": {
33
+ "name": "Shaky77",
34
+ "email": "563003@qq.com"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Shaky77/weiwen-law-dsh.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Shaky77/weiwen-law-dsh/issues"
42
+ },
43
+ "homepage": "https://github.com/Shaky77/weiwen-law-dsh#readme",
44
+ "keywords": [
45
+ "ai",
46
+ "causal",
47
+ "causality",
48
+ "causal-inference",
49
+ "causal-reasoning",
50
+ "causal-engine",
51
+ "guardrail",
52
+ "guardrails",
53
+ "white-box",
54
+ "whitebox",
55
+ "deepseek",
56
+ "harness",
57
+ "agent",
58
+ "safety",
59
+ "ai-safety",
60
+ "ai-security",
61
+ "agi-alignment",
62
+ "audit",
63
+ "auditable",
64
+ "interpretability",
65
+ "endogenous-risk-control",
66
+ "token-efficient",
67
+ "weiwens-law",
68
+ "weiwen-law",
69
+ "kiss-law",
70
+ "steady-state",
71
+ "rigid-anchor",
72
+ "agent-safety",
73
+ "llm-guardrail",
74
+ "dsh-plugin",
75
+ "causal-logic",
76
+ "ai-alignment",
77
+ "因果律",
78
+ "因果推理",
79
+ "因果引擎",
80
+ "白箱",
81
+ "护栏",
82
+ "人工智能安全",
83
+ "AI安全",
84
+ "人工智能对齐",
85
+ "可审计",
86
+ "稳态",
87
+ "刚性锚点",
88
+ "内生风险控制",
89
+ "智能体安全",
90
+ "大模型护栏",
91
+ "令牌高效",
92
+ "唯稳律",
93
+ "守真稳态"
94
+ ],
95
+ "peerDependencies": {
96
+ "@deepseek-ai/dsh-tools": "*"
97
+ },
98
+ "peerDependenciesMeta": {
99
+ "@deepseek-ai/dsh-tools": {
100
+ "optional": true
101
+ }
102
+ }
103
+ }
@@ -156,7 +156,14 @@ function isCredWriteSignal(category, path, sh) {
156
156
  if (/(>>?|tee)\s+[^\s'"]+/.test(sh) && (CREDENTIAL_PATH.test(sh) || CREDENTIAL_SH.test(sh))) return true;
157
157
  return false;
158
158
  }
159
+ // 🔴 [2026-10-26] 系统路径判据:补 OS 保留区(Windows 侧原为**完全空白**)。
160
+ // 实测:`C:/Windows/System32/x.dll` 判 allow,而 `C:/Windows/System32/drivers/etc/hosts` 判 review ——
161
+ // 两者同为系统保留区,差别只是路径里**恰好含 `/etc/`** 这个 Unix 目录名 ⇒ 判据读的是"字符串含哪个目录名",
162
+ // 不是"是否落在 OS 保留区"。此处补齐 Windows 保留区(来源=OS 自身定义,非我们列举危险文件)。
163
+ // ⚠️ Unix 侧暂只保留原有四目录:加 bin/sbin/lib/usr 会命中用户自建的 `.../usr/...` 目录(误伤),
164
+ // 留作待裁(扩表=覆盖更全但误伤面更大,需对照实测后才可动)。
159
165
  const SYSTEM_PATH = /\/(etc|sys|proc|boot)\//i;
166
+ const SYSTEM_PATH_WIN = /(?:^|[^a-z0-9])[a-z]:[\/\\](?:windows|program files(?: \(x86\))?|programdata|\$recycle\.bin|system volume information)(?:[\/\\]|$)/i;
160
167
  const EXTERNAL_TARGET = /https?:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[^\s'"]+/i;
161
168
  // [A3 · 2026-10-02 · 文本层止血·不自称完备] 外联判据从「HTTP 词形」升为「网络命名空间」:
162
169
  // DNS 盲区根因=解析命令(dig/nslookup/host/getaddrinfo…)含域名/IP 却非 http(s),被放行。
@@ -272,7 +279,34 @@ function _pathByKeyShape(args) {
272
279
  walk(args, 0);
273
280
  return found;
274
281
  }
282
+ // 🔴 [2026-10-26 · 判据读真实落点,不读字符串形态] 路径先规范化再交给任何判据。
283
+ // 实测病灶:`write{file_path:'C:/work/lab/../../Windows/System32/x.dll'}` 判 allow,
284
+ // 而同落点的 `C:/Windows/System32/drivers/etc/hosts` 判 review —— 差别只在**字符串开头**,
285
+ // 不在真实落点。⇒ "系统路径"判据读到的是字符串前缀,不是它最终写到哪里。
286
+ // 归一放在**唯一出口**(extractPath 返回处),下游 SYSTEM_PATH / CREDENTIAL_PATH /
287
+ // isCredWrite / 锚匹配 / 日志全部同步 —— 不在各判据处各写一份(那又会分裂成两套)。
288
+ function normalizeLoc(p) {
289
+ if (typeof p !== 'string' || !p) return p;
290
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(p)) return p; // URL 不是文件落点,不做文件系统归一
291
+ let s = p.replace(/\\/g, '/');
292
+ const tilde = s.startsWith('~') ? '~' : '';
293
+ if (tilde) s = s.slice(1);
294
+ const drive = (s.match(/^[A-Za-z]:/) || [''])[0];
295
+ if (drive) s = s.slice(drive.length);
296
+ const abs = s.startsWith('/');
297
+ const out = [];
298
+ for (const seg of s.split('/')) {
299
+ if (!seg || seg === '.') continue;
300
+ // `..` 上溯:能消则消;已到根外则**保留**(超根上溯无法确定真实落点,不得当成普通路径放过去)
301
+ if (seg === '..') { if (out.length) out.pop(); else out.push('..'); continue; }
302
+ out.push(seg);
303
+ }
304
+ return tilde + drive + (abs ? '/' : '') + out.join('/');
305
+ }
275
306
  function extractPath(call) {
307
+ return normalizeLoc(_extractPathRaw(call));
308
+ }
309
+ function _extractPathRaw(call) {
276
310
  const c = call?.args ?? {};
277
311
  if (typeof c.path === 'string' && c.path.trim()) return c.path;
278
312
  if (typeof c.file === 'string' && c.file.trim()) return c.file;
@@ -359,7 +393,7 @@ function inferCallSemantics(call, ctx = {}) {
359
393
  // write_file 的 args.path 属写类工具,由 isCredWrite 判写方向,不受此过滤影响(sh 为空)。
360
394
  isCred: (CREDENTIAL_PATH.test(path) && !credNonReadHead(sh)) || (CREDENTIAL_SH.test(sh) && !credNonReadHead(sh)) || hasEnvSecretRef(sh) || hasEnvSecretRef(packed),
361
395
  isCredWrite,
362
- isSystem: SYSTEM_PATH.test(path) || SYSTEM_PATH.test(sh),
396
+ isSystem: SYSTEM_PATH.test(path) || SYSTEM_PATH.test(sh) || SYSTEM_PATH_WIN.test(path) || SYSTEM_PATH_WIN.test(sh),
363
397
  isExternal,
364
398
  hitsInnerH: hitsInnerH(packed),
365
399
  // 方向信号(非内容信号):执行内容在执行前是否可审计。
@@ -382,8 +416,65 @@ function inferCallSemantics(call, ctx = {}) {
382
416
  // H₀ → S₀(S₀+1) ← S 增路径(稳态增量,纯白箱)
383
417
  // H₀ → D₀(D₀+1) ← D 增路径(=|S-1| 稳态绝对侵蚀)
384
418
  // 两路同时跑、都汇入 M₀(M+1)(独立事件沉淀),终态 S 再分叉 {S₀(S₀+1)/S₀(S₀-1)} 对比区分。
419
+ // ────────────────────────────────────────────────────────────────────
420
+ // 🔴 推演前提(2026-10-26 · 接 M_TWO_TENSES/FORWARD_INFERENCE 的锚在场判据)
421
+ //
422
+ // 安定(law.mjs `M_TWO_TENSES.adjudicationVsUse`):
423
+ // 「前提 = 参照锚必须在场(R / 恒定项在位…)—— 锚不在则参照线不存在、Δ 无从测。」
424
+ //
425
+ // 实测病灶(`_tmp/dia-premise-20261026.mjs`):`category === 'other'`(动作类别归不出)
426
+ // 时,本层所有信号恒为 false ⇒
427
+ // · `simulateBranch(s,'S+1')` 走 `purelyBenign` 默认真值 ⇒ `finalS: +1`
428
+ // · `simulateBranch(s,'D-1')` 落末尾无 note 的 `finalS: 0`
429
+ // ⇒ `deduceRisk` 读成「S 增路径成立、D 路径无实际侵蚀」⇒ 输出 allow。
430
+ // 但这两句**都不是推演结论**,是空转的默认真值 ⇒ 四象格「真的假话」:
431
+ // 逐字核得真(确实没命中任何信号)、整体是假(它并没有推过)。
432
+ // ⇒ 病根不是"判错",是**"我推不出"被写成了"我推过、没风险"**(与「未测 ≠ 零」同族)。
433
+ //
434
+ // 🔴 三次修正(同日):第一版写成**两态**(锚在场 / 不在场)⇒ 影子实验把"真空"一起打成 review,
435
+ // 误伤 3 条正常样本(查询类空 args / 写操作缺省 / 委托人授权删除)⇒ 当场还原。
436
+ // 病根是**一桶两义**(本轮第三次撞见同一个形状):`!path && !sh` 既可能是「真空」
437
+ // 也可能是「有载荷但本层抽不到」,两者缺省方向相反,不能共用一个桶。
438
+ // ⇒ 定三态,且**真空不需要锚**(安 09-26:「空没有任何上下浮动…没有风险」⇒ 结论独立成立):
439
+ // VACUUM 无载荷 ⇒ 无需推演,S 持平、M 未变(判决合法,理由不得写成"证明了增益")
440
+ // ANCHORED 载荷在 ∧ 类别可归 ⇒ 推演可跑
441
+ // NO_ANCHOR 载荷在 ∧ 类别归不出 ⇒ 无参照线 ⇒ 本层未产生判据(未测 ≠ 零)
442
+ const PREMISE = { VACUUM: 'vacuum', ANCHORED: 'anchored', NO_ANCHOR: 'no-anchor' };
443
+ /** 载荷是否为空(真空判据)。`bash{command:''}` 属真空:有键、内容为空 —— 只看键数会判错。 */
444
+ function isVacuous(call) {
445
+ const isEmpty = (v) => v === null || v === undefined || v === ''
446
+ || (Array.isArray(v) && v.length === 0)
447
+ || (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0);
448
+ return Object.values(call?.args ?? {}).every(isEmpty);
449
+ }
450
+ function inferencePremise(s, call) {
451
+ if (call && isVacuous(call)) {
452
+ return { ok: true, code: PREMISE.VACUUM,
453
+ note: '真空(无载荷):无作用对象 ⇒ 无需推演,S 持平、M 未变' };
454
+ }
455
+ // 🔴 [2026-10-26 · 路径2 接路径1 的锚] 锚的判据源 = **归因层 `attributeCall`**,不是 `inferCategory`。
456
+ // 来由(attribution.mjs 文件头原文):
457
+ // 「路径1(分形属性查归因):先把"这是什么动作"归因清楚,以暴露的归因为**锚点**;
458
+ // 路径2(以锚点推演预测):**拿锚点**跑推演(引擎既有 deduceRisk / simulateBranch)。」
459
+ // 实测(2026-10-26):两条路径各有一套归因源,对同一工具结论相反 ——
460
+ // `fs_write` / `delete_file`:inferCategory ⇒ 'other'(推演层空转)
461
+ // attributeCall ⇒ file-write / file-delete(锚在场)
462
+ // ⇒ 推演层一直在用**自己那张表**当锚,而不是路径1 给出的锚 ⇒ 这是"判据分裂"的第三处
463
+ // (前两处:extractPath 白名单 vs 词素;inferCategory vs attributeCall 此处)。
464
+ // 为何必须接归因层:孤立的 `inferCategory='other'` 既可能是"名不在表里"、也可能是"真中性",
465
+ // 两者缺省方向相反;而归因层已经把这件事判完了(`ok` / `layer`)⇒ 单一来源。
466
+ const anchored = call
467
+ ? (() => { try { return attributeCall(call).ok === true; } catch { return false; } })()
468
+ : !!(s?.category && s.category !== 'other');
469
+ if (!anchored) {
470
+ return { ok: false, code: PREMISE.NO_ANCHOR,
471
+ note: '锚不在场(路径1 归不出动作类别)⇒ 推演无参照线:本层未产生判据,S/M 变化未测(≠ 无风险)' };
472
+ }
473
+ return { ok: true, code: PREMISE.ANCHORED, note: null };
474
+ }
475
+
385
476
  // simulateBranch:给定语义,模拟"单条分支"的终态 S。kind='S+1' 即 S 增路径,'D-1' 即 D 增(蚀)路径。
386
- function simulateBranch(s, kind) {
477
+ function simulateBranch(s, kind, premise = inferencePremise(s)) {
387
478
  if (kind === 'S+1') {
388
479
  // S 增路径:假定 call 作为稳态增量被接受。仅当 call 纯稳态贡献(无任何风险信号)才真 +1。
389
480
  // 前提:可审计性是 S 成立的前提——
@@ -394,8 +485,11 @@ function simulateBranch(s, kind) {
394
485
  const auditable = !opaque;
395
486
  const purelyBenign = auditable && !s.isCred && !s.isSystem && !s.isExternal && !s.hitsInnerH && !s.isAuthSink;
396
487
  return {
397
- path: 'S+1', finalS: purelyBenign ? +1 : 0,
488
+ // 🔴 前提三态:真空(无载荷)⇒ 结论独立成立,可给 +1;锚不在场 ⇒ 空转,不得给 +1
489
+ path: 'S+1', finalS: (purelyBenign && premise.ok) ? +1 : 0,
490
+ premise,
398
491
  ...(auditable ? {} : { note: '执行内容不可审计(不透明源/未知变量/引用本会话写入文件/跨调用敏感源→汇组合)→ 无法证明稳态增量,S+1 不成立' }),
492
+ ...(premise.ok ? {} : { note: `${premise.note} ⇒ S+1 的 +1 未成立(空转,非增益)` }),
399
493
  };
400
494
  }
401
495
  // D-1 路径(=|S-1|):假定 call 触发损伤。侵蚀量按风险信号严重度分级(客观结构,非枚举具体值)。
@@ -419,7 +513,16 @@ function simulateBranch(s, kind) {
419
513
  if (s.isCredWrite) return { path: 'D-1', finalS: -2, note: '写凭据存放位(篡改/植入身份凭据),需人工确认' };
420
514
  if (s.isSystem && ['read', 'write', 'exec', 'exec-destructive'].includes(s.category)) return { path: 'D-1', finalS: -1, note: '系统信息泄露/完整性受损' };
421
515
  if (s.hasOpaqueVar) return { path: 'D-1', finalS: -1, note: '执行内容含未知变量引用,值不可审计(无法证明无风险)' };
422
- return { path: 'D-1', finalS: 0 };
516
+ // 🔴 [2026-10-26] 末尾这一格是「假定有损伤」分支的**缺省**。原来写 `finalS: 0` 且无 note
517
+ // ⇒ 与"推演过了、确认无侵蚀"在读数上完全同形(阿澄 81 的判据精确命中此格,只是他猜的位置在
518
+ // 另一个出口)。缺省值本身没错(0=未命中任何侵蚀条目),错在**没交代它是不是推出来的**:
519
+ // · 锚在场(category 可归)⇒ 0 是结论:查过侵蚀条目、无一命中 ⇒ 无侵蚀。
520
+ // · 锚不在场(category='other')⇒ 0 是空转:侵蚀条目一条都没被问过 ⇒ **未测**。
521
+ return {
522
+ path: 'D-1', finalS: 0,
523
+ premise,
524
+ ...(premise.ok ? { note: '侵蚀条目已逐条比对,无一命中 ⇒ 无侵蚀(结论)' } : { note: `${premise.note} ⇒ 0 是空转缺省,非"无侵蚀"结论` }),
525
+ };
423
526
  }
424
527
 
425
528
  // ---------------- 默认 R 刚性锚点策略(具象判据示例) ----------------
@@ -1655,8 +1758,10 @@ export class WeiwenLawEngine {
1655
1758
  // ⇒ 判据自否定:越敏感 ⇒ 越早退 ⇒ 越到不了登记点 ⇒ refsSessionSensitiveRead「源→汇」通道结构上不可达。
1656
1759
  // 为何测试全绿却未暴露:既有用例直调 deduceRisk(绕过判定层)⇒ 只测了后半段,前段断路未被覆盖。
1657
1760
  // H 分叉:S 增路径 + D 增(蚀)路径 同时模拟(并行,非二选一)
1658
- const bS = simulateBranch(s, 'S+1');
1659
- const bD = simulateBranch(s, 'D-1');
1761
+ // 🔴 [2026-10-26] 前提先算、再喂两路:锚不在场时两路的缺省值都不是结论(见 inferencePremise)。
1762
+ const premise = inferencePremise(s, call);
1763
+ const bS = simulateBranch(s, 'S+1', premise);
1764
+ const bD = simulateBranch(s, 'D-1', premise);
1660
1765
  // 两路都汇入 M(独立事件沉淀,手稿:M₀(M₀+1))
1661
1766
  const m = { m: 1, branches: { bS, bD } };
1662
1767
  // 终态 S 对比区分(手稿:{S₀(S₀+1) / S₀(S₀-1)})。
@@ -1670,8 +1775,13 @@ export class WeiwenLawEngine {
1670
1775
  reason: `推演判定高风险(D 路径确定性侵蚀 S):${bD.note || '凭据/内H/外传'}` };
1671
1776
  }
1672
1777
  if (erosion < 0 || !sOk) {
1673
- return { verdict: 'review', m, branches: { bS, bD }, deduced: true,
1674
- reason: `推演判定中风险(${!sOk ? 'S+1 不成立:无法证明稳态增量' : `D 路径轻度侵蚀 S:${bD.note || '灰区'}`}):建议限权/二次确认` };
1778
+ // 🔴 [2026-10-26] 前提缺失时**点名是前提缺失**,不再笼统说"S+1 不成立":
1779
+ // 后者会让外部读成"推演跑了、结论是中等风险",而真相是"推演没跑起来"。
1780
+ // (实测反馈:外部审计 agent 无法从这类回执反推规则 ⇒ 读数必须可归因到具体一层。)
1781
+ return { verdict: 'review', m, branches: { bS, bD }, deduced: true, premise,
1782
+ reason: !premise.ok
1783
+ ? `推演未产生判据(${premise.note}):交还人工,建议限权/二次确认`
1784
+ : `推演判定中风险(${!sOk ? 'S+1 不成立:无法证明稳态增量' : `D 路径轻度侵蚀 S:${bD.note || '灰区'}`}):建议限权/二次确认` };
1675
1785
  }
1676
1786
  // [2026-09-26 · reason 正位(安 空输入判据 · 根因级)] 无动作文本 ⇒ **如实陈述**,不套"S 增路径成立"。
1677
1787
  // 安的原话:「空,没有上下文,仅仅只是空,按照唯稳律推演,空没有任何上下浮动,持平稳定,
@@ -1694,12 +1804,32 @@ export class WeiwenLawEngine {
1694
1804
  // 🔴 **根因(一桶两义)**:该收口把 `null`(=**本层不接**)也改成了 review。⇒ 正确修法必须**带门**:
1695
1805
  // 先判"该对象是否落在本层职责域内"(够不着 vs 不涉及),**只有"受理事由成立且够不着"才 review**;
1696
1806
  // 不能只做"穷尽 ⇒ review"。⇒ 已在 `deployment.md` 更正 `XSUB-10` 登记。
1697
- return { verdict: 'allow', m, branches: { bS, bD }, deduced: true,
1698
- reason: '无扰动入基线(未抽到动作文本):S 持平、M 未变 ⇒ 无风险,放行' };
1807
+ // ── 🔴 [2026-10-26 · 推演前提收口] 本出口必须交代"这一格是推出来的、还是空转的"。──
1808
+ // 见 simulateBranch 上方 premise 注释与实测 `_tmp/dia-premise-20261026.mjs`:
1809
+ // `glob{pattern}` / `grep{pattern,path}` 都落在这里或其下一格,原文案一律给
1810
+ // 「无扰动 ⇒ 无风险」/「S 增路径成立」,而实际是 **category='other' ⇒ 信号恒 false**。
1811
+ // ⚠️ 仍**不改 verdict**:锚在场的终局判定在归因层(2100+ `!attrib.ok ∧ hasPayload ⇒ review`),
1812
+ // 此处重复扣会误伤真空样本;只改**读数**,让「未测」不再显示成「零风险」(铁律:未测 ≠ 零)。
1813
+ // 「真空」的判据是**载荷为空**,不是"键数为零":`bash{command:''}` 有一个键、内容为空,
1814
+ // 安 09-26 那次的样本正是它(原话「空,没有上下文,仅仅只是空」)⇒ 只看键数会把这类判成"有对象"。
1815
+ const isEmptyPayload = (v) => v === null || v === undefined || v === ''
1816
+ || (Array.isArray(v) && v.length === 0)
1817
+ || (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0);
1818
+ const noObject = Object.values(call?.args ?? {}).every(isEmptyPayload);
1819
+ if (noObject) {
1820
+ return { verdict: 'allow', m, branches: { bS, bD }, deduced: true, premise,
1821
+ reason: '无扰动入基线(真空:无作用对象可审):S 持平、M 未变 ⇒ 无风险,放行' };
1822
+ }
1823
+ return { verdict: 'allow', m, branches: { bS, bD }, deduced: true, premise,
1824
+ reason: '本层未取得作用对象(未抽到 shell 命令 / 文件落点)⇒ 推演未产生判据:S/M 变化未测(≠ 无风险)' };
1699
1825
  }
1700
1826
  // 双成立(S+1=+1 且 D 侵蚀=0)→ 风险=0 < 唯稳律 < 稳态 严格成立 → allow
1701
- return { verdict: 'allow', m, branches: { bS, bD }, deduced: true,
1702
- reason: '推演判定低风险(S 增路径成立、D 路径无实际侵蚀):放行并累积 S' };
1827
+ // 🔴 同上:`premise.ok === false` 时,S+1 的 +1 与 D-1 的 0 都是空转缺省,
1828
+ // 不得陈述为"S 增路径成立" —— 那句话是**真的假话**(核得真、整体假)。
1829
+ return { verdict: 'allow', m, branches: { bS, bD }, deduced: true, premise,
1830
+ reason: premise.ok
1831
+ ? '推演判定低风险(S 增路径成立、D 路径无实际侵蚀):放行并累积 S'
1832
+ : `推演未产生判据(${premise.note})⇒ 本格放行依据不来自推演层` };
1703
1833
  }
1704
1834
 
1705
1835
  // ---------- M 沉淀:推演层两分支都汇入 M(独立事件,append-only 历史刻痕) ----------
package/src/index.js CHANGED
@@ -18,7 +18,8 @@
18
18
  //
19
19
  // 仍属 RC 预览,官方提示后续存在破坏性 API 变更;接入真机前以官方 docs 当前版本复核。
20
20
 
21
- import { writeFileSync, appendFileSync } from 'node:fs';
21
+ import { writeFileSync, appendFileSync, readFileSync, readdirSync } from 'node:fs';
22
+ import { createHash } from 'node:crypto';
22
23
  import { WeiwenLawEngine, DEFAULT_RIGID_ANCHORS, inferCategory, isIrreversible } from './core/engine.mjs';
23
24
  import { R_DOMAIN, THREE_IRON_LAWS } from './core/law.mjs';
24
25
  // [2026-10-20] 判据接线读数(只读面):让「库里哪几条判据真的进了计算」可自查。
@@ -42,8 +43,30 @@ import { bugKeyOf } from './core/bugstop.mjs';
42
43
  import { defineTool } from '@deepseek-ai/dsh-tools';
43
44
 
44
45
  const LOG = new URL('./runtime.log', import.meta.url);
46
+
47
+ // ---------- 版本指纹 buildId(2026-10-27 · 外部审计节点小D 请求④) ----------
48
+ // 来历:小D 在 `0.2.0` 上报了三处缺口,其中两处在本地已被后续提交修掉。他没错,是**包上没有指纹**
49
+ // ——引用的行号 `L2189/L2218` 在 2643 行的当前版本里早已不在原处。行号是唯一定位手段时,
50
+ // 「我说的代码和你测的代码是不是同一份」只能靠自报,而自报不可审。
51
+ // 判据(为什么不用 package.json 的 version):**版本号是人写的,可以忘改;代码哈希改了一定变**。
52
+ // 故指纹只由 `src/core/*.mjs` 的内容派生,不含版本号、不含时间。
53
+ // `_engine_base_*` 是打包时排除的基线副本,本地存在会污染哈希 ⇒ 显式排除。
54
+ // 语义:同一 buildId ⇒ 同一份判据代码;不同 ⇒ 读数不可互认(**不是"谁新谁旧"**,新旧要另看提交)。
55
+ const BUILD_ID = (() => {
56
+ try {
57
+ const dir = new URL('./core/', import.meta.url);
58
+ const h = createHash('sha256');
59
+ for (const f of readdirSync(dir).filter((n) => n.endsWith('.mjs') && !n.startsWith('_engine_base_')).sort()) {
60
+ h.update(f);
61
+ h.update(readFileSync(new URL(f, dir)));
62
+ }
63
+ return h.digest('hex').slice(0, 12);
64
+ } catch { return 'unavailable'; }
65
+ })();
66
+ const LOADED_AT = new Date().toISOString();
67
+
45
68
  function logline(s) {
46
- try { appendFileSync(LOG, `[${new Date().toISOString()}] ${s}\n`); } catch { /* 日志失败不阻断护栏 */ }
69
+ try { appendFileSync(LOG, `[${new Date().toISOString()}] {b:${BUILD_ID}} ${s}\n`); } catch { /* 日志失败不阻断护栏 */ }
47
70
  }
48
71
 
49
72
  // ---------- review 档六步规格(2026-09-02 定稿,见 docs/review-flow-spec.md)----------
@@ -437,6 +460,10 @@ function apply(ctx) {
437
460
  // 实测:18 条裁决的引擎产出**全部可 JSON 序列化、无循环、无函数**(样例 2456 B)⇒ 当前无一项落入排除。
438
461
  const INTERNAL_ONLY = []; // 仅内部实现位;事实位一律放行
439
462
  const out = { ...decision, kind: 'deny', law: decision.law, reason: `[唯稳律·${decision.law}] ${decision.reason}` };
463
+ // 版本指纹进回执(小D 请求④):让**审计方**能判断「这份读数出自哪份判据代码」,
464
+ // 不必依赖被测方自报包版本。与 law/reason 同级,属事实位,不是判据位 —— 不参与任何判决。
465
+ out.buildId = BUILD_ID;
466
+ out.loadedAt = LOADED_AT;
440
467
  for (const k of INTERNAL_ONLY) delete out[k];
441
468
  if (isReview) {
442
469
  // ③ 搁置 + 标注留证:引擎某些 review 出口未挂 bugKey,此处补稳定 BUG 身份供追溯。
@@ -710,8 +737,10 @@ function apply(ctx) {
710
737
  output: { schema: { type: 'object', additionalProperties: true }, render: renderObj },
711
738
  async execute() {
712
739
  return {
740
+ buildId: BUILD_ID,
741
+ loadedAt: LOADED_AT,
713
742
  ...criterionReadout(engine),
714
- note: '只读面:不产生 verdict、不写状态、不参与闸门。gaps 是如实登记的缺口(不是免责通道);unmapped 是欠账,不视为通过。',
743
+ note: '只读面:不产生 verdict、不写状态、不参与闸门。gaps 是如实登记的缺口(不是免责通道);unmapped 是欠账,不视为通过。buildId 由 src/core/*.mjs 内容哈希派生(不含版本号)⇒ 外部审计方据此判断读数出自哪份判据代码,不必依赖被测方自报包版本。',
715
744
  };
716
745
  },
717
746
  }));
@@ -0,0 +1,64 @@
1
+ // 唯稳律定义对齐回归测试 —— ,防止日后升级再次写歪:
2
+ // ① 逻辑反推 与 第一 Bug 停机 分开并行(非合体、非 isomorphicWith 等同)
3
+ // ② 反推沿 R 尺度包含轴反溯(细分规则层→微观→宏观→地球→宇宙),两层轴不与时间演进轴(母链/子链 R)混淆
4
+ //
5
+ //
6
+
7
+ import test from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { CALIBRATION, R_DOMAIN, THREE_IRON_LAWS } from '../src/core/law.mjs';
10
+
11
+ test('对齐① CALIBRATION 存在且三字段齐全,已无 isomorphicWith 等同标注', () => {
12
+ assert.ok(CALIBRATION, 'CALIBRATION 应存在');
13
+ assert.ok(CALIBRATION.rule, '应有 rule');
14
+ assert.ok(CALIBRATION.parallelWith, '应有 parallelWith');
15
+ assert.ok(CALIBRATION.rLayerVerification, '应有 rLayerVerification');
16
+ assert.ok(!('isomorphicWith' in CALIBRATION), '不得再出现 isomorphicWith 等同标注(此前误将两机制合体)');
17
+ });
18
+
19
+ test('对齐① 逻辑反推 与 第一 Bug 停机 明确为分开并行,而非"同精神/等同"', () => {
20
+ const both = (CALIBRATION.rule + ' ' + CALIBRATION.parallelWith).toLowerCase();
21
+ assert.match(CALIBRATION.parallelWith, /分开并行|并行/, '应声明二者并行');
22
+ assert.ok(!both.includes('同精神'), '不得再写"同精神"式合体');
23
+ assert.ok(!both.includes('等同'), '不得再写"等同"式合体');
24
+ assert.match(CALIBRATION.parallelWith, /停机.*断|切链保活/, '停机=管断/切链保活');
25
+ assert.match(CALIBRATION.parallelWith, /反推.*溯|溯源归因/, '反推=管溯/归因');
26
+ });
27
+
28
+ test('对齐② 反推路径沿 R 尺度包含轴(细分规则层→微观→宏观→地球→宇宙),且不含时间演进轴措辞', () => {
29
+ const rule = CALIBRATION.rule;
30
+ for (const layer of ['微观', '宏观', '地球', '宇宙']) {
31
+ assert.ok(rule.includes(layer), `反推路径应包含 ${layer}`);
32
+ }
33
+ assert.ok(!rule.includes('母链') && !rule.includes('子链'), '反推路径不得混入母链/子链(时间演进轴)');
34
+ });
35
+
36
+ test('对齐② R_DOMAIN 嵌套层级顺序为 宇宙⊃地球⊃宏观⊃微观,且含 fractalSubdivision 分形套嵌', () => {
37
+ const h = R_DOMAIN.hierarchy;
38
+ assert.equal(h.length, 4, '应为四层代表层级');
39
+ assert.equal(h[0].name, '宇宙客观规则');
40
+ assert.equal(h[1].name, '地球客观规则');
41
+ assert.equal(h[2].name, '宏观客观规则');
42
+ assert.equal(h[3].name, '微观客观规则');
43
+ assert.equal(h[0].contains, '地球客观规则');
44
+ assert.equal(h[1].contains, '宏观客观规则');
45
+ assert.equal(h[2].contains, '微观客观规则');
46
+ assert.ok(R_DOMAIN.fractalSubdivision, '应新增 fractalSubdivision');
47
+ assert.match(R_DOMAIN.fractalSubdivision, /分形套嵌|同构递归/, '每层细分规则应分形套嵌、同构递归');
48
+ });
49
+
50
+ test('对齐② 尺度包含轴 与 时间演进轴 不混淆:母链/子链概念不得出现在反推定义中', () => {
51
+ const blob = JSON.stringify(CALIBRATION);
52
+ assert.ok(!blob.includes('母链') && !blob.includes('子链'), 'CALIBRATION 不得混入母链/子链时间演进轴');
53
+ });
54
+
55
+ test('对齐③ R 客观规则层可复验:rLayerVerification 声明宣称与复验不符即前提失真', () => {
56
+ assert.match(CALIBRATION.rLayerVerification, /复验/, '应提及复验');
57
+ assert.match(CALIBRATION.rLayerVerification, /前提失真|赋值不可信/, '应落到前提失真/赋值不可信');
58
+ });
59
+
60
+ test('一致性的前提:第一 Bug 停机铁律与 R 层级定义均仍在线(无回退)', () => {
61
+ assert.equal(THREE_IRON_LAWS.length, 3, '三大铁律仍应为三条');
62
+ assert.ok(THREE_IRON_LAWS[1].includes('第一 Bug 停机'), '铁律② 仍为第一 Bug 停机');
63
+ assert.equal(R_DOMAIN.hierarchy[3].name, '微观客观规则', 'R 层级末层应为微观');
64
+ });