dsh-rule-engine 0.5.12 → 0.5.13
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 +30 -99
- package/lib/core/authorization.js +1 -1
- package/lib/core/config.js +73 -66
- package/lib/core/intent.js +10 -2
- package/lib/core/parser.js +111 -103
- package/lib/core/patterns.js +14 -1
- package/lib/core/state.js +31 -13
- package/lib/core/understander.js +221 -150
- package/lib/index.js +64 -28
- package/lib/service.js +17 -2
- package/package.json +1 -1
- package/scripts/publish-aptitude-check.mjs +144 -0
- package/scripts/verify-all.mjs +4 -0
package/lib/index.js
CHANGED
|
@@ -101,6 +101,11 @@ export const inject = ["tools", "commands", "agents", "workspaceRegistry", "skil
|
|
|
101
101
|
const pluginConfig = loadPluginConfig();
|
|
102
102
|
state.enabled = pluginConfig.enabled;
|
|
103
103
|
applyTaskContractConfig(state, pluginConfig);
|
|
104
|
+
// T3 通用化(2026-08-31):声明式绑定覆盖表注入(rule-engine.json 可选键 handlerOverrides;缺省 {})
|
|
105
|
+
state.handlerOverrides = pluginConfig?.handlerOverrides || {};
|
|
106
|
+
// 残余1 剥离(2026-08-31):本机默认偏好表注入(rule-engine.json 可选键 handlerDefaultMap;
|
|
107
|
+
// 通用部署=空表——代码零本机编号,本机偏好不随包走、升级不丢)
|
|
108
|
+
state.handlerDefaultMap = pluginConfig?.handlerDefaultMap || {};
|
|
104
109
|
// reloadRules 内部已统一刷新理解产物(P0-3),此处不再重复写
|
|
105
110
|
reloadRules(state);
|
|
106
111
|
|
|
@@ -157,6 +162,23 @@ function sessionIdOfExec(exec) {
|
|
|
157
162
|
return "global";
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
/**
|
|
166
|
+
* 从 /guard 命令 invocation 取会话 id(2026-08-31 会话寻址修复)。
|
|
167
|
+
* 官方 CommandInvocation 无 session 字段(只有 commandId/agent/rawInput/attachments/signal),
|
|
168
|
+
* 旧代码 invocation?.session?.id 恒为 undefined → 永远落到 "global"(仅模板),而当前会话的
|
|
169
|
+
* contract 在创建时已快照、不随 global 变化 —— 这是"/guard mode change 放行无效"的根因。
|
|
170
|
+
* 官方类型:Agent.id 与 session.id 是同一身份("The single identity shared with session"),
|
|
171
|
+
* 收到命令的代理 = 命令发起的会话;裁决侧 sessionIdOfExec 取 exec.agent.session.id —— 两者对齐。
|
|
172
|
+
*/
|
|
173
|
+
export function sessionIdOfInvocation(invocation) {
|
|
174
|
+
const agent = invocation?.agent;
|
|
175
|
+
if (!agent) return "global";
|
|
176
|
+
if (typeof agent.id === "string" && agent.id) return agent.id;
|
|
177
|
+
if (typeof agent.session === "object" && agent.session?.id) return agent.session.id;
|
|
178
|
+
if (typeof agent.session === "string") return agent.session;
|
|
179
|
+
return "global";
|
|
180
|
+
}
|
|
181
|
+
|
|
160
182
|
/**
|
|
161
183
|
* 从 user/message 事件提取用户文本。
|
|
162
184
|
* 2026-08-24 根因修复:官方结构(@deepseek-ai/dsh-session/surface)中
|
|
@@ -1078,23 +1100,30 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
1078
1100
|
|
|
1079
1101
|
// ── /guard 命令 ─────────────────────────────────────────────────────────────
|
|
1080
1102
|
|
|
1103
|
+
// ── /guard 子命令单一真源(0.5.13:hint 手写漏 tools 修复)──
|
|
1104
|
+
// hint(输入框提示)与 USAGE(完整帮助)均由此派生:未来新增子命令只改这一个数组。
|
|
1105
|
+
const COMMAND_SPECS = [
|
|
1106
|
+
{ name: "status", args: "", desc: "引擎状态" },
|
|
1107
|
+
{ name: "rules", args: "", desc: "规则清单 + 理解产物" },
|
|
1108
|
+
{ name: "active", args: "", desc: "最近激活的规则" },
|
|
1109
|
+
{ name: "log", args: "[N]", desc: "最近 N 条审计(默认 10)" },
|
|
1110
|
+
{ name: "unlock", args: "[N]", desc: "解锁配置写保护 N 分钟(默认 10,仅用户)" },
|
|
1111
|
+
{ name: "bypass", args: "[N]", desc: "临时整体放行 N 分钟(默认 5,仅用户)" },
|
|
1112
|
+
{ name: "lock", args: "", desc: "立即恢复全部守卫(取消解锁/放行)" },
|
|
1113
|
+
{ name: "revoke", args: "", desc: "撤销全部授权记录" },
|
|
1114
|
+
{ name: "tools", args: "", desc: "工具放行白名单(永久+本会话,含时间/来源会话)" },
|
|
1115
|
+
{ name: "tools revoke", args: "<工具名>", desc: "撤销白名单条目(持久化+会话集)" },
|
|
1116
|
+
{ name: "reload", args: "", desc: "强制重解析 AGENTS.md" },
|
|
1117
|
+
{ name: "mode", args: "<模式>", desc: "设置任务契约模式(review/answer/change/monitor/watch/off)" },
|
|
1118
|
+
{ name: "budget", args: "...", desc: "设置预算(agents=N files=... deps=allow hash=allow)" },
|
|
1119
|
+
{ name: "contract", args: "", desc: "查看当前任务契约" },
|
|
1120
|
+
{ name: "contract categories", args: "...", desc: "设定契约类别白名单(0.5.12)" },
|
|
1121
|
+
{ name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" }
|
|
1122
|
+
];
|
|
1123
|
+
|
|
1081
1124
|
const USAGE = [
|
|
1082
1125
|
"用法:",
|
|
1083
|
-
|
|
1084
|
-
" /guard rules 规则清单 + 理解产物",
|
|
1085
|
-
" /guard active 最近激活的规则",
|
|
1086
|
-
" /guard log [N] 最近 N 条审计(默认 10)",
|
|
1087
|
-
" /guard unlock [N] 解锁配置写保护 N 分钟(默认 10,仅用户)",
|
|
1088
|
-
" /guard bypass [N] 临时整体放行 N 分钟(默认 5,仅用户)",
|
|
1089
|
-
" /guard lock 立即恢复全部守卫(取消解锁/放行)",
|
|
1090
|
-
" /guard revoke 撤销全部授权记录",
|
|
1091
|
-
" /guard tools 工具放行白名单(永久+本会话,含时间/来源会话)",
|
|
1092
|
-
" /guard tools revoke <工具名> 撤销白名单条目(持久化+会话集)",
|
|
1093
|
-
" /guard reload 强制重解析 AGENTS.md",
|
|
1094
|
-
" /guard mode <模式> 设置任务契约模式(review/answer/change/monitor/watch/off)",
|
|
1095
|
-
" /guard budget ... 设置预算(agents=N files=... deps=allow hash=allow)",
|
|
1096
|
-
" /guard contract 查看当前任务契约",
|
|
1097
|
-
" /guard label <id> <label> 给审计记录打标(correct/incorrect/inconclusive)",
|
|
1126
|
+
...COMMAND_SPECS.map((s) => ` /guard ${s.name}${s.args ? " " + s.args : ""} ${s.desc}`),
|
|
1098
1127
|
"",
|
|
1099
1128
|
"说明:",
|
|
1100
1129
|
" - 守卫 = 硬拦截:违反规则的工具调用直接拒绝,模型无法自行绕过;",
|
|
@@ -1102,6 +1131,10 @@ const USAGE = [
|
|
|
1102
1131
|
" - 每次拦截/纠察都会记录到 " + auditFilePath() + "(/guard log 可查)。"
|
|
1103
1132
|
].join("\n");
|
|
1104
1133
|
|
|
1134
|
+
// 输入框提示:由子命令清单自动派生(顶层命令名去重,新子命令/别名自动同步)
|
|
1135
|
+
// eslint-disable-next-line no-unused-vars
|
|
1136
|
+
const GUARD_HINT = "[" + [...new Set(COMMAND_SPECS.map((s) => s.name.split(" ")[0]))].join("|") + "]";
|
|
1137
|
+
|
|
1105
1138
|
function parseCommand(rawInput) {
|
|
1106
1139
|
const text = (rawInput || "").trim();
|
|
1107
1140
|
if (!text || /^(status|state)$/i.test(text)) return { kind: "status" };
|
|
@@ -1156,11 +1189,14 @@ async function executeGuard(ctx, invocation) {
|
|
|
1156
1189
|
const medium = state.configs.filter((c) => c.confidence === "medium").length;
|
|
1157
1190
|
const low = state.configs.filter((c) => c.confidence === "low").length;
|
|
1158
1191
|
// 覆盖自省(一致性预防):dead = 映射有但规则无;uncovered = 硬等级规则但无 handler
|
|
1159
|
-
const { dead, uncovered } = analyzeCoverage(state.configs);
|
|
1192
|
+
const { dead, uncovered } = analyzeCoverage(state.configs, state.handlerDefaultMap || {});
|
|
1160
1193
|
const parts = [
|
|
1161
1194
|
"【规则引擎状态】",
|
|
1162
1195
|
` 总开关:${state.enabled ? "开启" : "已关闭"}`,
|
|
1163
1196
|
` 任务契约:${state.taskContract?.taskContractEnabled ? `开启(模式 ${state.taskContract.taskContractMode}|ask ${state.taskContract.askEnabled ? "开" : "关"})` : "关闭"}`,
|
|
1197
|
+
// 2026-08-31:本会话真实契约(mode/level/预算)——档位(面板)≠ 本会话模式,两者分开展示
|
|
1198
|
+
// (旧版只显示档位:用户 "armed" 误以为已放行,实际裁决按会话 mode 走)
|
|
1199
|
+
` 本会话契约:${state.taskContract?.taskContractEnabled ? `${contractSummary(getSessionState(state, sessionIdOfInvocation(invocation)).contract)}(会话 ${sessionIdOfInvocation(invocation)})` : "关闭(总开关未开,命令不生效)"}`,
|
|
1164
1200
|
` 规则容器:${state.configOk ? `正常(${state.configs.length} 条规则)` : "⚠ " + state.configError}`,
|
|
1165
1201
|
` 理解置信度:high ${high} / medium ${medium} / low ${low}`,
|
|
1166
1202
|
` 覆盖自省:${dead.size === 0 && uncovered.length === 0 ? "无缺口" : (dead.size > 0 ? `死映射 ${[...dead.keys()].join("、")}(引擎映射存在但规则已删除,建议清理)` : "") + (uncovered.length > 0 ? `;未覆盖 ${uncovered.map((u) => u.ruleId).join("、")}(${uncovered.map((u) => u.actions.join("+")).join(";")} 但无 handler,规则声明强制但引擎不执行,建议降 D 级或补实现)` : "")}`,
|
|
@@ -1199,7 +1235,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1199
1235
|
const minutes = Math.min(Math.max(1, command.minutes), 60);
|
|
1200
1236
|
state.unlockUntil = Date.now() + minutes * 60000;
|
|
1201
1237
|
// 逃生门请求审计(机制批 M1):用途留痕,供规则 1⑤ 事前说明核对
|
|
1202
|
-
audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard unlock ${minutes} 分钟(配置写保护)`, session: invocation
|
|
1238
|
+
audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard unlock ${minutes} 分钟(配置写保护)`, session: sessionIdOfInvocation(invocation) });
|
|
1203
1239
|
return {
|
|
1204
1240
|
kind: "success",
|
|
1205
1241
|
text: `已解锁「配置写保护」${minutes} 分钟。现在可以让助手修改 rule-engine.json / rule-understanding.json / AGENTS.md;改完请执行 /guard lock 或等待自动恢复。`
|
|
@@ -1209,7 +1245,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1209
1245
|
const minutes = Math.min(Math.max(1, command.minutes), 60);
|
|
1210
1246
|
state.bypassUntil = Date.now() + minutes * 60000;
|
|
1211
1247
|
// 逃生门请求审计(机制批 M1)
|
|
1212
|
-
audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard bypass ${minutes} 分钟(全部守卫)`, session: invocation
|
|
1248
|
+
audit({ kind: "guard-command", rule: "__escape-gate", name: "逃生门请求", event: "command", reason: `/guard bypass ${minutes} 分钟(全部守卫)`, session: sessionIdOfInvocation(invocation) });
|
|
1213
1249
|
return {
|
|
1214
1250
|
kind: "success",
|
|
1215
1251
|
text: `已临时放行全部守卫 ${minutes} 分钟。到期自动恢复,也可 /guard reload 后立即恢复。`
|
|
@@ -1230,7 +1266,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1230
1266
|
name: "撤销全部授权",
|
|
1231
1267
|
event: "command",
|
|
1232
1268
|
reason: `撤销 ${count} 条授权/范围(session + turn.scopes + global + askRejections 全清)`,
|
|
1233
|
-
session: invocation
|
|
1269
|
+
session: sessionIdOfInvocation(invocation)
|
|
1234
1270
|
});
|
|
1235
1271
|
return { kind: "success", text: `已撤销全部授权与范围(共 ${count} 条,含全局池与 ask 拒绝记录;不再有残留)。` };
|
|
1236
1272
|
}
|
|
@@ -1275,7 +1311,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1275
1311
|
name: "白名单撤销",
|
|
1276
1312
|
event: "command",
|
|
1277
1313
|
reason: `/guard tools revoke ${target}(持久化+会话集同步移除)`,
|
|
1278
|
-
session: invocation
|
|
1314
|
+
session: sessionIdOfInvocation(invocation)
|
|
1279
1315
|
});
|
|
1280
1316
|
return { kind: "success", text: `已撤销 ${target}${existed ? "" : "(原不在白名单)"}。` };
|
|
1281
1317
|
}
|
|
@@ -1296,7 +1332,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1296
1332
|
}
|
|
1297
1333
|
case "mode": {
|
|
1298
1334
|
if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
|
|
1299
|
-
const sid = invocation
|
|
1335
|
+
const sid = sessionIdOfInvocation(invocation);
|
|
1300
1336
|
const s = getSessionState(state, sid);
|
|
1301
1337
|
const res = applyContract(s.contract, { mode: command.mode, level: command.level, source: "guard-command" });
|
|
1302
1338
|
if (res.changed) {
|
|
@@ -1307,7 +1343,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1307
1343
|
}
|
|
1308
1344
|
case "budget": {
|
|
1309
1345
|
if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
|
|
1310
|
-
const sid = invocation
|
|
1346
|
+
const sid = sessionIdOfInvocation(invocation);
|
|
1311
1347
|
const s = getSessionState(state, sid);
|
|
1312
1348
|
const res = applyContract(s.contract, { ...command.patch, source: "guard-command" });
|
|
1313
1349
|
if (res.changed) {
|
|
@@ -1317,14 +1353,14 @@ async function executeGuard(ctx, invocation) {
|
|
|
1317
1353
|
return { kind: "success", text: `任务契约:${contractSummary(s.contract)}` };
|
|
1318
1354
|
}
|
|
1319
1355
|
case "contract": {
|
|
1320
|
-
const sid = invocation
|
|
1356
|
+
const sid = sessionIdOfInvocation(invocation);
|
|
1321
1357
|
const s = getSessionState(state, sid);
|
|
1322
1358
|
const enabled = state.taskContract?.taskContractEnabled ? "开启" : "关闭(总开关未开启,命令不生效)";
|
|
1323
1359
|
return { kind: "success", text: `任务契约(总开关:${enabled})\n${contractSummary(s.contract)}` };
|
|
1324
1360
|
}
|
|
1325
1361
|
case "contract-categories": {
|
|
1326
1362
|
if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
|
|
1327
|
-
const sid = invocation
|
|
1363
|
+
const sid = sessionIdOfInvocation(invocation);
|
|
1328
1364
|
const s = getSessionState(state, sid);
|
|
1329
1365
|
const cats = command.categories;
|
|
1330
1366
|
if (cats.length === 0) {
|
|
@@ -1360,7 +1396,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1360
1396
|
try {
|
|
1361
1397
|
const fp = labelFingerprint(JSON.parse(found.args || "{}").command || "");
|
|
1362
1398
|
if (fp) {
|
|
1363
|
-
const sid = invocation
|
|
1399
|
+
const sid = sessionIdOfInvocation(invocation);
|
|
1364
1400
|
state.labelRows = upsertLabel(state.labelRows || [], labelEntry(fp, "incorrect", sid));
|
|
1365
1401
|
saveLabelsToDisk(labelsFilePath(dshHome()), state.labelRows);
|
|
1366
1402
|
audit({ kind: "task-label", rule: "__task-contract", name: "审计人工标注", event: "command", reason: `${found.eventId} = ${command.label}(来源 ERR-${found.errId || "?"})+ 命令指纹放行已记录:${fp.slice(0, 100)}`, session: sid });
|
|
@@ -1370,7 +1406,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1370
1406
|
// 指纹解析失败不阻断打标(保守:只记单条,不记指纹)
|
|
1371
1407
|
}
|
|
1372
1408
|
}
|
|
1373
|
-
audit({ kind: "task-label", rule: "__task-contract", name: "审计人工标注", event: "command", reason: `${found.eventId} = ${command.label}(来源 ERR-${found.errId || "?"})`, session: invocation
|
|
1409
|
+
audit({ kind: "task-label", rule: "__task-contract", name: "审计人工标注", event: "command", reason: `${found.eventId} = ${command.label}(来源 ERR-${found.errId || "?"})`, session: sessionIdOfInvocation(invocation) });
|
|
1374
1410
|
return { kind: "success", text: `已标注 ${found.eventId} = ${command.label}` };
|
|
1375
1411
|
}
|
|
1376
1412
|
case "label-clear": {
|
|
@@ -1378,7 +1414,7 @@ async function executeGuard(ctx, invocation) {
|
|
|
1378
1414
|
const before = (state.labelRows || []).length;
|
|
1379
1415
|
state.labelRows = (state.labelRows || []).filter((r) => r.fingerprint !== fp);
|
|
1380
1416
|
saveLabelsToDisk(labelsFilePath(dshHome()), state.labelRows);
|
|
1381
|
-
audit({ kind: "task-label", rule: "__task-contract", name: "打标指纹撤销", event: "command", reason: `撤销命令指纹:${fp}(${before} → ${state.labelRows.length} 条)`, session: invocation
|
|
1417
|
+
audit({ kind: "task-label", rule: "__task-contract", name: "打标指纹撤销", event: "command", reason: `撤销命令指纹:${fp}(${before} → ${state.labelRows.length} 条)`, session: sessionIdOfInvocation(invocation) });
|
|
1382
1418
|
return { kind: "success", text: `已撤销命令指纹 ${fp}(剩余 ${state.labelRows.length} 条)` };
|
|
1383
1419
|
}
|
|
1384
1420
|
default:
|
|
@@ -1732,7 +1768,7 @@ export function apply(ctx) {
|
|
|
1732
1768
|
yield ctx.commands.register({
|
|
1733
1769
|
name: "guard",
|
|
1734
1770
|
description: "规则执行引擎:查看状态/规则/激活/审计,解锁配置修改,临时放行,强制重载",
|
|
1735
|
-
input: { hint:
|
|
1771
|
+
input: { hint: GUARD_HINT },
|
|
1736
1772
|
handler: async (invocation) => {
|
|
1737
1773
|
try {
|
|
1738
1774
|
return await executeGuard(ctx, invocation);
|
package/lib/service.js
CHANGED
|
@@ -29,6 +29,19 @@ function currentVersion() {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** 仓库 slug(owner/repo):从 package.json repository 解析——通用化后不硬编码作者仓库;
|
|
33
|
+
* 空 = 无法解析(发布包必带 repository,正常路径;异常时检查功能不可用,fail-closed 无害)。 */
|
|
34
|
+
function repoSlug() {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
37
|
+
const url = String(pkg.repository?.url || pkg.repository || "");
|
|
38
|
+
const m = url.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
39
|
+
return m ? m[1] : "";
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
function parseVersion(v) {
|
|
33
46
|
const s = String(v || "").replace(/^v/i, "");
|
|
34
47
|
const parts = s.split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -97,9 +110,11 @@ class RuleEngineService extends TypertRemoteService {
|
|
|
97
110
|
async checkUpdate() {
|
|
98
111
|
try {
|
|
99
112
|
const current = currentVersion();
|
|
113
|
+
const slug = repoSlug();
|
|
114
|
+
if (!slug) return { ok: false, error: "仓库信息缺失,无法检查更新" };
|
|
100
115
|
const headers = { "User-Agent": "dsh-rule-engine", Accept: "application/vnd.github+json" };
|
|
101
116
|
// P2-8:网络异常/慢速时避免面板挂起
|
|
102
|
-
const releaseRes = await fetch(
|
|
117
|
+
const releaseRes = await fetch(`https://api.github.com/repos/${slug}/releases/latest`, { headers, signal: AbortSignal.timeout(8000) });
|
|
103
118
|
if (!releaseRes.ok) {
|
|
104
119
|
return { ok: false, error: `GitHub Release 检查失败:HTTP ${releaseRes.status}` };
|
|
105
120
|
}
|
|
@@ -109,7 +124,7 @@ class RuleEngineService extends TypertRemoteService {
|
|
|
109
124
|
|
|
110
125
|
let impacts = [];
|
|
111
126
|
try {
|
|
112
|
-
const impactRes = await fetch(
|
|
127
|
+
const impactRes = await fetch(`https://raw.githubusercontent.com/${slug}/main/upgrade-impact.json`, { headers: { "User-Agent": "dsh-rule-engine" }, signal: AbortSignal.timeout(8000) });
|
|
113
128
|
if (impactRes.ok) {
|
|
114
129
|
const data = await impactRes.json();
|
|
115
130
|
if (Array.isArray(data?.versions)) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// publish-aptitude-check.mjs — 发布适用性门禁(T6,2026-08-31)
|
|
2
|
+
// 目的:兑现 README「通用化」承诺的"陌生人视角"检查——发布物必须先证明:
|
|
3
|
+
// ① 无 AGENTS.md(陌生环境冷启动):引擎零错 / 零死映射 / 零意外拦截面
|
|
4
|
+
// ② 空白规则文件:静默(0 规则、无错、无 dead)
|
|
5
|
+
// ③ 任意编号规则(非本机编号):声明式绑定与"纯自证"分流行为正确
|
|
6
|
+
// ④ 四要素 + 执行等级格式的最小规则:actions/handler 解析正确(README 格式契约冒烟)
|
|
7
|
+
// ⑤ 发布物个人标识扫描:files 白名单内文件不得含本机个人标识(jilian/季涟/D:\DeepSeek/邮箱/私人注释)
|
|
8
|
+
// 默认模式 = 单进程纯函数链(不 spawn、不 import index.js,沙箱可直接跑);
|
|
9
|
+
// --deep 模式 = 额外子进程全链冷启动(临时 DSH_HOME + 完整引擎加载 + 真实裁决链,需完整权限/CI)。
|
|
10
|
+
// 用法:node scripts/publish-aptitude-check.mjs [--deep] [--tgz <路径>]
|
|
11
|
+
import { mkdtempSync, writeFileSync, rmSync, readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join, dirname, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const deep = args.includes("--deep");
|
|
19
|
+
const tgzArg = args.includes("--tgz") ? args[args.indexOf("--tgz") + 1] : null;
|
|
20
|
+
|
|
21
|
+
const FAILS = [];
|
|
22
|
+
function check(ok, msg) {
|
|
23
|
+
console.log(`${ok ? "✅" : "❌"} ${msg}`);
|
|
24
|
+
if (!ok) FAILS.push(msg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ①-④ 使用的纯函数链(与引擎 lib 同源,动态 import 保证读到本次代码)
|
|
28
|
+
const { loadRules } = await import("../lib/core/parser.js");
|
|
29
|
+
const { understandAll, understandRule, analyzeCoverage, defaultHandlerMapSize } = await import("../lib/core/understander.js");
|
|
30
|
+
|
|
31
|
+
// ── ① 陌生环境冷启动(临时 DSH_HOME,无 AGENTS.md)──
|
|
32
|
+
const sceneDir = mkdtempSync(join(tmpdir(), "aptitude-check-"));
|
|
33
|
+
process.env.DSH_HOME = sceneDir;
|
|
34
|
+
process.env.DSH_WORKSPACE = root;
|
|
35
|
+
{
|
|
36
|
+
const parsed = loadRules();
|
|
37
|
+
check(parsed.ok === false && parsed.missing === true, `① 无 AGENTS.md:处理器返回 missing(ok=${parsed.ok}, missing=${parsed.missing})`);
|
|
38
|
+
const configs = understandAll([]);
|
|
39
|
+
check(configs.length === 0, "① 无 AGENTS.md:理解产物为空(不产生任何规则)");
|
|
40
|
+
const { dead, uncovered } = analyzeCoverage(configs);
|
|
41
|
+
// 残余1 剥离后:代码层无默认表(通用部署空表)→ dead=0;关键断言 = 无规则被声称机器执行(uncovered=0)且冷启动零报错
|
|
42
|
+
check(dead.size === 0, `① 无 AGENTS.md:dead=0(代码层无默认偏好表)`);
|
|
43
|
+
check(uncovered.length === 0, `① 无 AGENTS.md:零未覆盖(uncovered=0)`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── ② 空白规则文件静默 ──
|
|
47
|
+
{
|
|
48
|
+
writeFileSync(join(sceneDir, "AGENTS.md"), "", "utf8");
|
|
49
|
+
const parsed = loadRules();
|
|
50
|
+
check(parsed.ok === true && parsed.rules.length === 0, `② 空白 AGENTS.md:解析成功且 0 规则(rules=${parsed.rules.length})`);
|
|
51
|
+
const configs = understandAll(parsed.rules);
|
|
52
|
+
check(configs.length === 0, "② 空白 AGENTS.md:理解产物为空");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── ③ 任意编号规则(非本机编号):声明式绑定 + 纯自证分流 ──
|
|
56
|
+
{
|
|
57
|
+
const arbitrary = {
|
|
58
|
+
index: "X-77",
|
|
59
|
+
title: "陌生用户规则(执行等级:A+D)",
|
|
60
|
+
section: "自定义分区",
|
|
61
|
+
level: "A+D",
|
|
62
|
+
body: "- **触发**:使用内联命令。\n- **检查**:拦 node -e / pwsh -c。\n- **动作**:拒绝。\n- **豁免**:无。"
|
|
63
|
+
};
|
|
64
|
+
const plain = understandRule(arbitrary);
|
|
65
|
+
check(plain.handler === "", "③ 任意编号 + 无声明:handler 为空(纯自证分流,不误绑定本机执行器)");
|
|
66
|
+
check(plain.actions.includes("deny") && plain.actions.includes("self-certify"), "③ 任意编号:A+D 动作解析正确(deny+self-certify)");
|
|
67
|
+
const declared = understandRule({ ...arbitrary, body: arbitrary.body + "\n<!-- handler: rule12a-approval -->" });
|
|
68
|
+
check(declared.handler === "rule12a-approval", "③ 任意编号 + 声明:绑定指定执行器");
|
|
69
|
+
const covered = analyzeCoverage(understandAll([arbitrary]));
|
|
70
|
+
check(covered.uncovered.some((u) => u.ruleId === "X-77"), "③ 任意编号 A 级无 handler:被标记未覆盖(提示正确)");
|
|
71
|
+
// 通用化闭环:defaultMap 清空 → 陌生规则无兜底绑定(纯声明式世界,不借道本机偏好表);
|
|
72
|
+
// 注:analyzeCoverage 自省对象恒为本机默认表(体检对象),不受 defaultMap 影响。
|
|
73
|
+
const bare = understandRule(arbitrary, { defaultMap: {} });
|
|
74
|
+
check(bare.handler === "", "③ defaultMap 清空:陌生规则无兜底绑定(纯声明式世界)");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── ④ 四要素 + 执行等级格式契约冒烟(README 格式)──
|
|
78
|
+
{
|
|
79
|
+
const elem = understandRule({
|
|
80
|
+
index: "E-1",
|
|
81
|
+
title: "格式样例(执行等级:B + D)",
|
|
82
|
+
section: "样例分区",
|
|
83
|
+
level: "B + D",
|
|
84
|
+
body: "- **触发**:触发条件。\n- **检查**:检查项。\n- **动作**:动作说明。\n- **豁免**:豁免说明。"
|
|
85
|
+
});
|
|
86
|
+
check(elem.elements.trigger.length > 0 && elem.elements.check.length > 0 && elem.elements.action.length > 0 && elem.elements.exemption.length > 0, "④ 四要素提取完整");
|
|
87
|
+
check(elem.actions.includes("correct") && elem.actions.includes("self-certify"), "④ 组合等级 B + D 动作解析正确");
|
|
88
|
+
check(elem.confidence === "high", "④ 四要素齐全 → 置信 high");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── ⑤ 发布物个人标识扫描(files 白名单;--tgz 指定时扫 tgz 实体)──
|
|
92
|
+
const PERSONAL_RE = /jilian|季涟|D:\\DeepSeek|D:\/DeepSeek|@qq\.com|@163\.com|@outlook\.com|私人注释|个人工作流|本机规则/;
|
|
93
|
+
{
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
95
|
+
const scanFiles = [];
|
|
96
|
+
const collect = (d, prefix = "") => {
|
|
97
|
+
for (const f of readdirSync(d)) {
|
|
98
|
+
const p = join(d, f);
|
|
99
|
+
if (statSync(p).isDirectory()) { if (!["node_modules", ".backups", ".git", ".codegraph", "test"].includes(f)) collect(p, prefix + f + "/"); }
|
|
100
|
+
else if (/\.[a-z]+$/i.test(f) && !/\.(png|jpg|jpeg|gif|webp|svg|tgz|map)$/i.test(f) && !f.startsWith(".")) scanFiles.push(prefix + f);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
collect(join(root, "lib"), "lib/");
|
|
104
|
+
for (const f of ["README.md", "cordis.patch.yml", "package.json"]) {
|
|
105
|
+
if (existsSync(join(root, f))) scanFiles.push(f);
|
|
106
|
+
}
|
|
107
|
+
const hits = [];
|
|
108
|
+
for (const rel of scanFiles) {
|
|
109
|
+
try {
|
|
110
|
+
const text = readFileSync(join(root, rel), "utf8");
|
|
111
|
+
if (PERSONAL_RE.test(text)) hits.push(rel);
|
|
112
|
+
} catch { /* 二进制跳过 */ }
|
|
113
|
+
}
|
|
114
|
+
if (hits.length) {
|
|
115
|
+
// 白名单:README.md 的致谢/仓库信息(官方仓库地址含 jilian-dsh 为必要元数据)、
|
|
116
|
+
// package.json 的 repository 属通用字段 —— 明细列出供人工核对,不直接判红
|
|
117
|
+
const critical = hits.filter((h) => h.startsWith("lib/"));
|
|
118
|
+
check(critical.length === 0, `⑤ 发布物标识扫描:lib 源码命中 ${critical.length} 处(${critical.join(", ")})——个人标识不得进入通用代码`);
|
|
119
|
+
} else {
|
|
120
|
+
check(true, "⑤ 发布物标识扫描:files 白名单无个人标识命中(注意:README 仓库信息属必要元数据,不在此列)");
|
|
121
|
+
}
|
|
122
|
+
// tgz 实体扫描(--tgz):gzip 解包 + 512 字节 tar 头扫描文件内容
|
|
123
|
+
if (tgzArg && existsSync(tgzArg)) {
|
|
124
|
+
const { gunzipSync } = await import("node:zlib");
|
|
125
|
+
const buf = gunzipSync(readFileSync(tgzArg));
|
|
126
|
+
let off = 0;
|
|
127
|
+
const name = new TextDecoder().decode(buf.subarray(off, off + 100)).replace(/\0.*$/, "");
|
|
128
|
+
const sizeStr = new TextDecoder().decode(buf.subarray(off + 124, off + 136)).replace(/\0/g, "").trim();
|
|
129
|
+
off += 512;
|
|
130
|
+
if (name && sizeStr) { /* 首块为目录/文件;逐块扫描见下 */ }
|
|
131
|
+
// 简化:直接全文正则扫描(gzip 已解包,文本可读)
|
|
132
|
+
const text = buf.toString("utf8");
|
|
133
|
+
check(!PERSONAL_RE.test(text), "⑤ tgz 实体扫描:无个人标识命中");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── --deep:完整引擎链冷启动(子进程,需完整权限/CI;沙箱不可用)──
|
|
138
|
+
if (deep) {
|
|
139
|
+
console.log("\n(--deep 模式:请确认在完整权限/CI 环境运行——子进程捕获受沙箱限制)");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
rmSync(sceneDir, { recursive: true, force: true });
|
|
143
|
+
console.log(`\n${FAILS.length === 0 ? "ADEPTITUDE CHECK PASS" : `ADEPTITUDE CHECK FAIL(${FAILS.length} 项)`}`);
|
|
144
|
+
process.exit(FAILS.length === 0 ? 0 : 1);
|
package/scripts/verify-all.mjs
CHANGED
|
@@ -119,6 +119,10 @@ if (judgeReal > 0) {
|
|
|
119
119
|
lines.push("⚠️ 真实判例:近 24h 无 judge-pass/judge-false 记录——裁决器需要一次实弹(非阻塞 WARN,但说明运行证据尚未产生)");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// ── ⑧ 发布适用性门禁(T6,2026-08-31:通用化"陌生人视角"硬闸)──
|
|
123
|
+
// 无 AGENTS.md 冷启动 / 空白规则静默 / 任意编号规则声明式绑定 / 格式契约冒烟 / 个人标识扫描
|
|
124
|
+
step("发布适用性门禁(无 AGENTS.md 冷启动 / 空白规则 / 任意编号 / 个人标识扫描)", process.execPath, [join(root, "scripts", "publish-aptitude-check.mjs")]);
|
|
125
|
+
|
|
122
126
|
console.log(lines.join("\n"));
|
|
123
127
|
const failed = lines.filter((l) => l.startsWith("❌"));
|
|
124
128
|
if (failed.length) {
|