dsh-weiwen-law 0.2.2 → 0.2.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/README.md +20 -5
- package/package.json +3 -2
- package/src/core/capability.mjs +16 -4
- package/src/index.js +21 -6
- package/test/build-id.test.mjs +57 -0
- package/test/capability.test.mjs +12 -0
package/README.md
CHANGED
|
@@ -288,16 +288,31 @@ export DEEPSEEK_API_KEY=sk-xxxx # Linux/macOS
|
|
|
288
288
|
本插件已声明 `dsh.bundle.patch` 清单(见 `package.json`),DSH 用户可直接通过官方插件命令安装:
|
|
289
289
|
|
|
290
290
|
```bash
|
|
291
|
-
# 从
|
|
292
|
-
dsh plugin --profile web add "
|
|
293
|
-
|
|
294
|
-
# 从 npm 安装(npm 发布后可用)
|
|
295
|
-
# dsh plugin --profile web add "dsh-weiwen-law"
|
|
291
|
+
# 从 npm 安装 —— **对外唯一安装入口**,钉住版本号(不要用 latest)
|
|
292
|
+
dsh plugin --profile web add "dsh-weiwen-law@0.2.4"
|
|
296
293
|
|
|
297
294
|
# 重启生效
|
|
298
295
|
dsh --profile web
|
|
299
296
|
```
|
|
300
297
|
|
|
298
|
+
<details>
|
|
299
|
+
<summary>⚠️ 从 GitHub 源码直装(<b>仅开发自用,不对外</b>)</summary>
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
dsh plugin --profile web add "github:Shaky77/weiwen-law-dsh"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
这条走的是仓库 `main` 的**当前态**,不是发版态:`main` 随时在动,且动了不会通知任何人。
|
|
306
|
+
两条路装出来可能不是同一份代码,而**版本号长得一样** ⇒ 出了问题无法对账。
|
|
307
|
+
|
|
308
|
+
仓库的职责是"源",npm 的职责是"分发",两者分开:
|
|
309
|
+
- **对外发布一律走 npm**,一个版本号对应一个提交,记在 [`RELEASES.md`](RELEASES.md);
|
|
310
|
+
- 要核对"npm 上此刻那份对应哪个提交",跑 `npm run check:release`(真下载包算指纹,不看自报)。
|
|
311
|
+
|
|
312
|
+
判断自己装的是哪一份,**看 `buildId`(代码哈希),不看版本号** —— 版本号是人写的,能忘改。
|
|
313
|
+
|
|
314
|
+
</details>
|
|
315
|
+
|
|
301
316
|
装完后在 `设置 → 插件 → 插件列表` 可见 `weiwen-law` 状态为"已启用";Agent 自动获得 12 个白箱自查工具 + 3 道硬性闸门(`tools/pre-execute` / `agent/pre-step` / `tools/post-execute`)+ 1 个只读审计钩子(`tools/result`)。
|
|
302
317
|
|
|
303
318
|
### 卸载
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-weiwen-law",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "唯稳律(Weiwen's Law)通用因果引擎(白箱呈现)—— DeepSeek Harness (Cordis) 插件实现",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "node --test \"test/*.test.mjs\"",
|
|
25
25
|
"build:docs": "node scripts/build-docs-engine.mjs",
|
|
26
|
-
"check:docs": "node scripts/build-docs-engine.mjs --check"
|
|
26
|
+
"check:docs": "node scripts/build-docs-engine.mjs --check",
|
|
27
|
+
"check:release": "node scripts/check-release.mjs"
|
|
27
28
|
},
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=22"
|
package/src/core/capability.mjs
CHANGED
|
@@ -258,12 +258,24 @@ export function parseCapability(input) {
|
|
|
258
258
|
const USE_LEDGER_CAP = 20;
|
|
259
259
|
const _uses = new Map(); // key -> {count, log:[{at,action,targets}]}
|
|
260
260
|
|
|
261
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* 证的稳定键:**内容指纹恒参与**,`id` 只作可读前缀。
|
|
263
|
+
* ⇒ 同 id 同内容 ⇒ 同键(就是同一张证,计数当然合并);
|
|
264
|
+
* 同 id 换了内容(改作用域/改次数/改到期)⇒ 新键 ⇒ **计数从 0 起**。
|
|
265
|
+
*
|
|
266
|
+
* 🔴 为什么不能"有 id 就只认 id"(0.2.2 实测踩到,真装探针复现):
|
|
267
|
+
* 宿主习惯用固定 id(`xiaod-001`)重签新证,若键只认 id ⇒ 旧证的用量被新证继承
|
|
268
|
+
* ⇒ **新证一签发就显示"已用尽"** ⇒ 全量转 review。
|
|
269
|
+
* 方向虽然是"更严"(不会误放行),但它把「替换不累积」这条硬约束在计数维度上打穿了:
|
|
270
|
+
* 换掉的授权不该还挂着旧账。⇒ 键必须含内容,替换才是真替换。
|
|
271
|
+
*/
|
|
262
272
|
export function capabilityKey(cap) {
|
|
263
273
|
if (!cap) return '';
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
+ '|' + String(cap.maxUses)
|
|
274
|
+
const body = [...cap.actions].sort().join(',')
|
|
275
|
+
+ '|' + cap.scope.map((s) => s.kind + ':' + s.value).sort().join(',')
|
|
276
|
+
+ '|' + String(cap.maxUses)
|
|
277
|
+
+ '|' + String(cap.expiresAt);
|
|
278
|
+
return (cap.id ? 'id:' + cap.id : 'anon') + '#' + body;
|
|
267
279
|
}
|
|
268
280
|
|
|
269
281
|
/** 已用次数 */
|
package/src/index.js
CHANGED
|
@@ -53,15 +53,30 @@ const LOG = new URL('./runtime.log', import.meta.url);
|
|
|
53
53
|
// 故指纹只由 `src/core/*.mjs` 的内容派生,不含版本号、不含时间。
|
|
54
54
|
// `_engine_base_*` 是打包时排除的基线副本,本地存在会污染哈希 ⇒ 显式排除。
|
|
55
55
|
// 语义:同一 buildId ⇒ 同一份判据代码;不同 ⇒ 读数不可互认(**不是"谁新谁旧"**,新旧要另看提交)。
|
|
56
|
+
// 🔴 换行必须规范化再哈希(2026-10-27 实测踩到,真装探针发现):
|
|
57
|
+
// 原实现直接对**工作区字节**做哈希 ⇒ 同一份代码在不同安装路径下给出不同指纹:
|
|
58
|
+
// npm 包内(老文件被 autocrlf 检出成 CRLF)→ `8e8604efd2cf`
|
|
59
|
+
// git 存储 / Linux 检出(LF) → `91fea30b57f8`
|
|
60
|
+
// 而 buildId 的**唯一用途**是「我说的代码和你测的代码是不是同一份」。
|
|
61
|
+
// 掺进与代码无关的平台换行习惯 ⇒ 该用途当场失效,且失效方式最坏:
|
|
62
|
+
// 比对不上会被读成「你装的是旧包」,而不是「指纹口径不一致」⇒ 直接把人带偏。
|
|
63
|
+
// ⇒ 规范化后再哈希:同一份代码,不论哪条安装路、哪个平台,值唯一。
|
|
64
|
+
export function computeBuildId(entries) {
|
|
65
|
+
const h = createHash('sha256');
|
|
66
|
+
for (const [name, content] of [...entries].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))) {
|
|
67
|
+
h.update(name);
|
|
68
|
+
h.update(String(content).replace(/\r\n/g, '\n'));
|
|
69
|
+
}
|
|
70
|
+
return h.digest('hex').slice(0, 12);
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
const BUILD_ID = (() => {
|
|
57
74
|
try {
|
|
58
75
|
const dir = new URL('./core/', import.meta.url);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
return h.digest('hex').slice(0, 12);
|
|
76
|
+
const entries = readdirSync(dir)
|
|
77
|
+
.filter((n) => n.endsWith('.mjs') && !n.startsWith('_engine_base_'))
|
|
78
|
+
.map((n) => [n, readFileSync(new URL(n, dir)).toString('utf8')]);
|
|
79
|
+
return computeBuildId(entries);
|
|
65
80
|
} catch { return 'unavailable'; }
|
|
66
81
|
})();
|
|
67
82
|
const LOADED_AT = new Date().toISOString();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// build-id.test.mjs — buildId 口径回归锁
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// 锁的是一条性质,不是一张期望值表:
|
|
4
|
+
// **同一份代码 ⇒ 同一个 buildId,与平台换行习惯、安装路径无关。**
|
|
5
|
+
//
|
|
6
|
+
// 病灶(2026-10-27 真装探针实测,0.2.3 现场):
|
|
7
|
+
// 同一份代码,npm 包内算出 `8e8604efd2cf`,git 存储侧算出 `91fea30b57f8`。
|
|
8
|
+
// 差在换行:老文件被 autocrlf 检出成 CRLF 进了包,git 里存的是 LF。
|
|
9
|
+
// 后果不是"数字难看",是**该判据的用途失效**:
|
|
10
|
+
// buildId 存在的唯一理由是回答「你测的和我说的,是不是同一份代码」;
|
|
11
|
+
// 掺进平台习惯之后,比对不上会被读成「你装的是旧包」,而不是「口径不一致」。
|
|
12
|
+
//
|
|
13
|
+
// 为什么用性质而不是列清单:将来文件增减、内容改动,期望值表就全废了;
|
|
14
|
+
// 而「规范化后同内容 ⇒ 同值」这条性质,改多少次代码都成立。
|
|
15
|
+
|
|
16
|
+
import test from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { computeBuildId } from '../src/index.js';
|
|
19
|
+
|
|
20
|
+
const FILES = [
|
|
21
|
+
['engine.mjs', 'line one\nline two\nline three\n'],
|
|
22
|
+
['law.mjs', 'const a = 1;\nconst b = 2;\n'],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
test('同一内容:全 LF 与全 CRLF 必须同值(跨平台可比的根据)', () => {
|
|
26
|
+
const lf = FILES.map(([n, c]) => [n, c]);
|
|
27
|
+
const crlf = FILES.map(([n, c]) => [n, c.replace(/\n/g, '\r\n')]);
|
|
28
|
+
assert.equal(computeBuildId(lf), computeBuildId(crlf));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('同一内容:混合换行(老文件 CRLF + 新文件 LF)与统一换行同值', () => {
|
|
32
|
+
const mixed = [
|
|
33
|
+
['engine.mjs', 'line one\r\nline two\r\nline three\r\n'], // 被 autocrlf 检出的老文件
|
|
34
|
+
['law.mjs', 'const a = 1;\nconst b = 2;\n'], // 工具新写的文件
|
|
35
|
+
];
|
|
36
|
+
assert.equal(computeBuildId(mixed), computeBuildId(FILES));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('内容变一个字符 ⇒ 值必变(指纹得咬得住内容)', () => {
|
|
40
|
+
const changed = [['engine.mjs', 'line one\nline two\nline three!\n'], FILES[1]];
|
|
41
|
+
assert.notEqual(computeBuildId(changed), computeBuildId(FILES));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('文件名参与哈希 ⇒ 改名即变(不能靠"内容一样"蒙混)', () => {
|
|
45
|
+
const renamed = [['engine2.mjs', FILES[0][1]], FILES[1]];
|
|
46
|
+
assert.notEqual(computeBuildId(renamed), computeBuildId(FILES));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('文件顺序不影响结果(调用方给的顺序不该成为口径差)', () => {
|
|
50
|
+
assert.equal(computeBuildId([FILES[1], FILES[0]]), computeBuildId(FILES));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('非 UTF-8 换行不误伤:单独 \\r 不算换行,不该被规范化掉', () => {
|
|
54
|
+
const a = [['x.mjs', 'a\rb\n']];
|
|
55
|
+
const b = [['x.mjs', 'a\nb\n']];
|
|
56
|
+
assert.notEqual(computeBuildId(a), computeBuildId(b), '孤立 \\r 是内容,不是换行标记');
|
|
57
|
+
});
|
package/test/capability.test.mjs
CHANGED
|
@@ -257,4 +257,16 @@ test('证是当前有效量(替换不累积):不同证各自计数,不
|
|
|
257
257
|
const y = cap({ id: null, maxUses: 1 });
|
|
258
258
|
consume(x, { action: 'write', targets: [] });
|
|
259
259
|
assert.equal(usesOf(y), 1);
|
|
260
|
+
// 🔴 同 id 换内容 ⇒ 计数从 0 起(替换不累积在**计数维度**上的落点)
|
|
261
|
+
// 病灶(0.2.2 真装探针实测):键只认 id ⇒ 宿主固定 id 重签 ⇒ 新证一签发就"已用尽"⇒ 全量 review。
|
|
262
|
+
// 方向虽更严,但换掉的授权还挂着旧账 ⇒ 替换语义被打穿,且会让人误判"证不管用"。
|
|
263
|
+
resetCapabilityUsage();
|
|
264
|
+
const oldOne = cap({ id: 'same', maxUses: 1 });
|
|
265
|
+
consume(oldOne, { action: 'write', targets: [] });
|
|
266
|
+
assert.equal(usesOf(oldOne), 1);
|
|
267
|
+
const renewed = cap({ id: 'same', maxUses: 2 }); // 同 id、改了次数
|
|
268
|
+
assert.equal(usesOf(renewed), 0, '同 id 换内容必须是新证,不得继承旧计数');
|
|
269
|
+
assert.notEqual(capabilityKey(oldOne), capabilityKey(renewed));
|
|
270
|
+
// 同 id 同内容仍是同一张证(重复解析/重复下发不得变成新证逃过计数)
|
|
271
|
+
assert.equal(capabilityKey(cap({ id: 'same', maxUses: 2 })), capabilityKey(renewed));
|
|
260
272
|
});
|