dsh-rule-engine 0.5.12 → 0.5.14
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 +31 -100
- package/lib/core/authorization.js +29 -5
- package/lib/core/config.js +73 -66
- package/lib/core/intent.js +21 -2
- package/lib/core/lexicon.js +1 -1
- package/lib/core/parser.js +111 -103
- package/lib/core/patterns.js +14 -1
- package/lib/core/state.js +32 -13
- package/lib/core/text-detect.js +56 -11
- package/lib/core/understander.js +221 -150
- package/lib/index.js +66 -28
- package/lib/service.js +17 -2
- package/package.json +1 -1
- package/scripts/publish-aptitude-check.mjs +144 -0
- package/scripts/release-plugin.mjs +277 -0
- package/scripts/verify-all.mjs +4 -0
package/lib/core/parser.js
CHANGED
|
@@ -1,103 +1,111 @@
|
|
|
1
|
-
// parser.js - 解析 AGENTS.md 规则容器。
|
|
2
|
-
// 纯 Node,可独立测试。把 AGENTS.md 解析为规则列表,规则正文保留四要素原文。
|
|
3
|
-
import { readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { agentsFilePath } from "./paths.js";
|
|
5
|
-
|
|
6
|
-
// 与 rules-manager 解析口径一致:标题剥离「(来源:…)」后缀(P1-4),避免 /guard rules 显示带尾巴
|
|
7
|
-
const RULE_HEADER_RE = /^###\s*\[规则\s*([0-9A-Za-z]+)\]\s*(.+?)\s*(?:(来源[^)]*))?\s*$/;
|
|
8
|
-
const SECTION_RE = /^##\s+(.+?)\s*$/;
|
|
9
|
-
const FREE_ZONE_START_RE = /^\s*<!--\s*free-zone:start\s*-->\s*$/;
|
|
10
|
-
const FREE_ZONE_END_RE = /^\s*<!--\s*free-zone:end\s*-->\s*$/;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 从 AGENTS.md 解析全部规则。
|
|
14
|
-
* @returns {{ok: boolean, missing: boolean, error?: string, rules: Array, raw: string, mtimeMs: number}}
|
|
15
|
-
*/
|
|
16
|
-
export function loadRules() {
|
|
17
|
-
const file = agentsFilePath();
|
|
18
|
-
let raw;
|
|
19
|
-
let mtimeMs = 0;
|
|
20
|
-
try {
|
|
21
|
-
raw = readFileSync(file, "utf8");
|
|
22
|
-
mtimeMs = statSync(file).mtimeMs;
|
|
23
|
-
} catch (error) {
|
|
24
|
-
if (error && error.code === "ENOENT") {
|
|
25
|
-
return { ok: false, missing: true, error: `未找到 ${file}`, rules: [], raw: "", mtimeMs: 0 };
|
|
26
|
-
}
|
|
27
|
-
return { ok: false, missing: false, error: String(error), rules: [], raw: "", mtimeMs: 0 };
|
|
28
|
-
}
|
|
29
|
-
const bom = raw.charCodeAt(0) === 0xfeff;
|
|
30
|
-
const text = bom ? raw.slice(1) : raw;
|
|
31
|
-
const lines = text.split("\n");
|
|
32
|
-
const rules = [];
|
|
33
|
-
let currentSection = "未分区";
|
|
34
|
-
let current = null;
|
|
35
|
-
let inFreeZone = false;
|
|
36
|
-
for (let i = 0; i < lines.length; i++) {
|
|
37
|
-
const line = lines[i];
|
|
38
|
-
// 自由区域边界:区内内容完全不解析(不产生规则、不切换分区),对引擎透明
|
|
39
|
-
if (FREE_ZONE_START_RE.test(line)) {
|
|
40
|
-
if (current) rules.push(finalize(current, lines));
|
|
41
|
-
current = null;
|
|
42
|
-
inFreeZone = true;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (FREE_ZONE_END_RE.test(line)) {
|
|
46
|
-
inFreeZone = false;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (inFreeZone) continue;
|
|
50
|
-
const sec = line.match(SECTION_RE);
|
|
51
|
-
if (sec) {
|
|
52
|
-
if (current) rules.push(finalize(current, lines));
|
|
53
|
-
currentSection = sec[1].trim();
|
|
54
|
-
current = null;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const rule = line.match(RULE_HEADER_RE);
|
|
58
|
-
if (rule) {
|
|
59
|
-
if (current) rules.push(finalize(current, lines));
|
|
60
|
-
current = {
|
|
61
|
-
index: rule[1],
|
|
62
|
-
title: rule[2].trim(),
|
|
63
|
-
section: currentSection,
|
|
64
|
-
startLine: i,
|
|
65
|
-
endLine: i + 1
|
|
66
|
-
};
|
|
67
|
-
} else if (current) {
|
|
68
|
-
current.endLine = i + 1;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (current) rules.push(finalize(current, lines));
|
|
72
|
-
return { ok: true, missing: false, error: null, rules, raw: text, mtimeMs };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
out
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
// parser.js - 解析 AGENTS.md 规则容器。
|
|
2
|
+
// 纯 Node,可独立测试。把 AGENTS.md 解析为规则列表,规则正文保留四要素原文。
|
|
3
|
+
import { readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { agentsFilePath } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
// 与 rules-manager 解析口径一致:标题剥离「(来源:…)」后缀(P1-4),避免 /guard rules 显示带尾巴
|
|
7
|
+
const RULE_HEADER_RE = /^###\s*\[规则\s*([0-9A-Za-z]+)\]\s*(.+?)\s*(?:(来源[^)]*))?\s*$/;
|
|
8
|
+
const SECTION_RE = /^##\s+(.+?)\s*$/;
|
|
9
|
+
const FREE_ZONE_START_RE = /^\s*<!--\s*free-zone:start\s*-->\s*$/;
|
|
10
|
+
const FREE_ZONE_END_RE = /^\s*<!--\s*free-zone:end\s*-->\s*$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 从 AGENTS.md 解析全部规则。
|
|
14
|
+
* @returns {{ok: boolean, missing: boolean, error?: string, rules: Array, raw: string, mtimeMs: number}}
|
|
15
|
+
*/
|
|
16
|
+
export function loadRules() {
|
|
17
|
+
const file = agentsFilePath();
|
|
18
|
+
let raw;
|
|
19
|
+
let mtimeMs = 0;
|
|
20
|
+
try {
|
|
21
|
+
raw = readFileSync(file, "utf8");
|
|
22
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error && error.code === "ENOENT") {
|
|
25
|
+
return { ok: false, missing: true, error: `未找到 ${file}`, rules: [], raw: "", mtimeMs: 0 };
|
|
26
|
+
}
|
|
27
|
+
return { ok: false, missing: false, error: String(error), rules: [], raw: "", mtimeMs: 0 };
|
|
28
|
+
}
|
|
29
|
+
const bom = raw.charCodeAt(0) === 0xfeff;
|
|
30
|
+
const text = bom ? raw.slice(1) : raw;
|
|
31
|
+
const lines = text.split("\n");
|
|
32
|
+
const rules = [];
|
|
33
|
+
let currentSection = "未分区";
|
|
34
|
+
let current = null;
|
|
35
|
+
let inFreeZone = false;
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
const line = lines[i];
|
|
38
|
+
// 自由区域边界:区内内容完全不解析(不产生规则、不切换分区),对引擎透明
|
|
39
|
+
if (FREE_ZONE_START_RE.test(line)) {
|
|
40
|
+
if (current) rules.push(finalize(current, lines));
|
|
41
|
+
current = null;
|
|
42
|
+
inFreeZone = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (FREE_ZONE_END_RE.test(line)) {
|
|
46
|
+
inFreeZone = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (inFreeZone) continue;
|
|
50
|
+
const sec = line.match(SECTION_RE);
|
|
51
|
+
if (sec) {
|
|
52
|
+
if (current) rules.push(finalize(current, lines));
|
|
53
|
+
currentSection = sec[1].trim();
|
|
54
|
+
current = null;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const rule = line.match(RULE_HEADER_RE);
|
|
58
|
+
if (rule) {
|
|
59
|
+
if (current) rules.push(finalize(current, lines));
|
|
60
|
+
current = {
|
|
61
|
+
index: rule[1],
|
|
62
|
+
title: rule[2].trim(),
|
|
63
|
+
section: currentSection,
|
|
64
|
+
startLine: i,
|
|
65
|
+
endLine: i + 1
|
|
66
|
+
};
|
|
67
|
+
} else if (current) {
|
|
68
|
+
current.endLine = i + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (current) rules.push(finalize(current, lines));
|
|
72
|
+
return { ok: true, missing: false, error: null, rules, raw: text, mtimeMs };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 从标题/表头提取执行等级(支持空格/顿号连接的组合等级,如 "B + D"、"A 弱 + D")。
|
|
77
|
+
* 单一真源:AGENTS.md 解析(finalize)与 disabled-rules.json 存档重建(state.js)共用,
|
|
78
|
+
* 保证禁用占位规则的 level/actions 与在场规则口径一致。
|
|
79
|
+
*/
|
|
80
|
+
export function levelFromTitle(title) {
|
|
81
|
+
const m = String(title || "").match(/执行等级[::]\s*([A-DM+](?:\s*[强弱])?(?:\s*[++、]\s*[A-DM+](?:\s*[强弱])?)*)/);
|
|
82
|
+
return m ? m[1].replace(/\s+/g, "") : "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function finalize(rule, lines) {
|
|
86
|
+
const body = lines.slice(rule.startLine + 1, rule.endLine).join("\n").trim();
|
|
87
|
+
return {
|
|
88
|
+
index: rule.index,
|
|
89
|
+
title: rule.title,
|
|
90
|
+
section: rule.section,
|
|
91
|
+
startLine: rule.startLine,
|
|
92
|
+
endLine: rule.endLine,
|
|
93
|
+
level: levelFromTitle(rule.title),
|
|
94
|
+
body
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 从规则正文提取四要素 */
|
|
99
|
+
export function extractElements(body) {
|
|
100
|
+
const out = { trigger: "", check: "", action: "", exemption: "" };
|
|
101
|
+
const grab = (label) => {
|
|
102
|
+
const re = new RegExp(`\\*\\*${label}(?:([^)]*))?\\*\\*[::]\\s*([^\\n]*(?:\\n(?!\\s*\\*\\*)[^\\n]*)*)`, "i");
|
|
103
|
+
const m = body.match(re);
|
|
104
|
+
return m ? m[1].trim() : "";
|
|
105
|
+
};
|
|
106
|
+
out.trigger = grab("触发");
|
|
107
|
+
out.check = grab("检查");
|
|
108
|
+
out.action = grab("动作");
|
|
109
|
+
out.exemption = grab("豁免");
|
|
110
|
+
return out;
|
|
111
|
+
}
|
package/lib/core/patterns.js
CHANGED
|
@@ -388,6 +388,9 @@ export function isAnalysisOp(toolName, args) {
|
|
|
388
388
|
}
|
|
389
389
|
}
|
|
390
390
|
if (name === "node" || name === "pwsh" || name === "bash") {
|
|
391
|
+
// ㊄ 验证通道(2026-08-31,用户定稿):验证类命令(测试/体检/探针/审计/dry-run/只读查询)
|
|
392
|
+
// 独立放行——调研/验证灵活性与"执行类"严格授权分层;清单见 isVerificationCommand(执行类不匹配)。
|
|
393
|
+
if (isVerificationCommand(cmd)) return true;
|
|
391
394
|
// ③ 临时区写:写命令(不含删除/移动——删除/移动属 13A 红线,不走分析豁免)+ 提取路径全部命中临时区 + 无受保护名
|
|
392
395
|
if (isMutationCommand(cmd) && !/(?:Remove-Item|Move-Item|Rename-Item|del\s|rm\s|git\s+push|git\s+commit)/i.test(cmd)) {
|
|
393
396
|
const paths = extractAnalysisScratchPaths(cmd);
|
|
@@ -576,11 +579,21 @@ export function isAuditCommand(command) {
|
|
|
576
579
|
* v0.5.7 P0-1(用户拍板 2026-08-26):验证类命令判定——运行测试/冷加载探针/挂载审计/语法检查。
|
|
577
580
|
* 与"修/改/写"授权伴生放行使用:用户授权修改后,验证是变更的正常闭环,不再被
|
|
578
581
|
* write vs command 类型不匹配误拦(今天实弹连卡 4 次)。
|
|
582
|
+
* 2026-08-31(验证通道放行面,用户定稿):清单扩展 + 由 isAnalysisOp 独立调用——
|
|
583
|
+
* ① 单测通配(任意 *.test.mjs)、② 验证/门禁脚本(check-tool-coverage / publish-aptitude-check)、
|
|
584
|
+
* ③ --dry-run(发布脚本 dry-run=只读推导,发布链无 dry-run 不匹配)、④ gh api 只读 GET、npm whoami。
|
|
585
|
+
* 安全边界:执行类脚本(release-plugin 无 --dry-run / inject / uninject / install / publish / commit /
|
|
586
|
+
* push / Remove-Item 等)一律不匹配——执行类仍走授权语义严格版(0.5.11 防借道回归不放松)。
|
|
579
587
|
*/
|
|
580
588
|
export function isVerificationCommand(command) {
|
|
581
589
|
if (typeof command !== "string") return false;
|
|
582
|
-
if (/\b(?:node|npm|npx)\s+[^\n]*(?:run-all\.mjs|check-plugin-load\.mjs|audit-mount-consistency\.mjs|loader-smoke\.e2e\.mjs|verify-all\.mjs|health-audit\.mjs|--check|--test)\b/i.test(command)) return true;
|
|
590
|
+
if (/\b(?:node|npm|npx)\s+[^\n]*(?:run-all\.mjs|check-plugin-load\.mjs|audit-mount-consistency\.mjs|loader-smoke\.e2e\.mjs|verify-all\.mjs|health-audit\.mjs|check-tool-coverage\.mjs|publish-aptitude-check\.mjs|\.test\.mjs\b|--check|--test|--dry-run)\b/i.test(command)) return true;
|
|
583
591
|
if (/\b(?:npm|pnpm)\s+(?:test|run\s+test)\b/i.test(command)) return true;
|
|
592
|
+
if (/\bgh\s+api\b/i.test(command)) {
|
|
593
|
+
// 只读 GET 形态;写形态(--method/-X 显式方法、-F/--field 表单)不豁免(gg api 写=执行类)
|
|
594
|
+
return !/(?:--method|-X)\s+(?:POST|PUT|PATCH|DELETE)|\s-(?:F|f)\s|--field\s/i.test(command);
|
|
595
|
+
}
|
|
596
|
+
if (/\bnpm\s+whoami\b/i.test(command)) return true;
|
|
584
597
|
return false;
|
|
585
598
|
}
|
|
586
599
|
|
package/lib/core/state.js
CHANGED
|
@@ -61,7 +61,13 @@ export function createState() {
|
|
|
61
61
|
deniedKeys: new Set(), // 引擎 deny 的调用 key(规则 1 计数解耦)
|
|
62
62
|
unknownToolApproved: new Set(), // 机制 B:已 ask 过的未归类工具(本会话内批准后不再重复询问)
|
|
63
63
|
unknownToolApprovedMeta: new Map(), // v0.5.9:name → {name,time,session}(持久化元数据,/guard tools 可视化)
|
|
64
|
-
unknownToolSessionAdded: new Set() // v0.5.9:本会话新增的批准(与永久加载区分,/guard tools 展示)
|
|
64
|
+
unknownToolSessionAdded: new Set(), // v0.5.9:本会话新增的批准(与永久加载区分,/guard tools 展示)
|
|
65
|
+
// T3 通用化(2026-08-31):声明式 handler 绑定覆盖表 { ruleId: handlerName }
|
|
66
|
+
// 由 apply() 从插件配置(rule-engine.json 可选键 handlerOverrides)注入;重载时经理解层生效
|
|
67
|
+
handlerOverrides: {},
|
|
68
|
+
// 残余1 剥离(2026-08-31):本机默认偏好表(rule-engine.json 可选键 handlerDefaultMap)
|
|
69
|
+
// 由 apply() 注入;重载时经理解层生效(通用部署=空表,零本机编号)
|
|
70
|
+
handlerDefaultMap: {}
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -141,6 +147,7 @@ export function getSessionState(state, sessionId) {
|
|
|
141
147
|
id: key,
|
|
142
148
|
ruleStats: {}, // C2:每规则 detected/suppressed/injected 统计(会话级,跨回合累计)
|
|
143
149
|
manualReadSeen: false,
|
|
150
|
+
lastQueryTurn: -1, // 规则 5/31(2026-09-01):最近一次查询类工具调用所在回合号(判定"近 3 回合有据")
|
|
144
151
|
lastUserText: "",
|
|
145
152
|
selfCertCount: new Map(),
|
|
146
153
|
authorizations: [],
|
|
@@ -268,28 +275,40 @@ export function resetTurn(state, sessionId, turnNumber) {
|
|
|
268
275
|
return s.turn;
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
/** 读取 dsh-rules-manager 的 disabled-rules.json
|
|
272
|
-
export function
|
|
278
|
+
/** 读取 dsh-rules-manager 的 disabled-rules.json 全部存档条目(禁用=正文移走+原样存档) */
|
|
279
|
+
export function loadDisabledRuleEntries() {
|
|
273
280
|
try {
|
|
274
281
|
const raw = readFileSync(disabledRulesFilePath(), "utf8");
|
|
275
282
|
const list = JSON.parse(raw);
|
|
276
|
-
if (!Array.isArray(list)) return
|
|
277
|
-
return
|
|
283
|
+
if (!Array.isArray(list)) return [];
|
|
284
|
+
return list.filter((d) => d && d.index !== undefined && d.index !== null);
|
|
278
285
|
} catch {
|
|
279
|
-
return
|
|
286
|
+
return [];
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
/**
|
|
290
|
+
/** 已禁用规则 id 集合(P0-2;加载失败=空集,保守不误拦) */
|
|
291
|
+
export function loadDisabledRuleIds() {
|
|
292
|
+
return new Set(loadDisabledRuleEntries().map((d) => String(d.index)).filter(Boolean));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 重新加载并理解 AGENTS.md;统一在 reload 后刷新理解产物(P0-3)。
|
|
297
|
+
* 禁用语义(2026-08-31 修复):禁用规则以 disabled=true 占位存在——不只是标记"在场规则"。
|
|
298
|
+
* 正文已移走的禁用规则由 understandAll(opts.disabledEntries)用存档正文重建占位:
|
|
299
|
+
* ① 禁用不再"消失"(/guard rules 显示(已禁用),恢复=写回 AGENTS.md 后自动重新生效);
|
|
300
|
+
* ② 死映射消除(HANDLER_BY_RULE 中的 12B 等因规则在场而不再被判 dead,consistency-live 转绿)。
|
|
301
|
+
*/
|
|
284
302
|
export function reloadRules(state) {
|
|
285
303
|
const parsed = loadRules();
|
|
286
304
|
state.rules = parsed.ok ? parsed.rules : [];
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
305
|
+
const archived = loadDisabledRuleEntries();
|
|
306
|
+
// 禁用语义由理解层完整封装:在场标 disabled + 缺场存档占位重建(单真源,见 understandAll)
|
|
307
|
+
state.configs = understandAll(state.rules, {
|
|
308
|
+
disabledEntries: archived,
|
|
309
|
+
handlerOverrides: state.handlerOverrides || {},
|
|
310
|
+
defaultMap: state.handlerDefaultMap || {}
|
|
311
|
+
});
|
|
293
312
|
state.configOk = parsed.ok;
|
|
294
313
|
state.configError = parsed.error || null;
|
|
295
314
|
state.mtimeMs = parsed.mtimeMs || 0;
|
package/lib/core/text-detect.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
TERM_EXPLANATION_RE,
|
|
12
12
|
SUGGEST_RE,
|
|
13
13
|
isNegatingSuggestion,
|
|
14
|
-
isQuoteOrParaphraseContext
|
|
14
|
+
isQuoteOrParaphraseContext,
|
|
15
|
+
isReadOnlyTool
|
|
15
16
|
} from "./patterns.js";
|
|
16
17
|
import { detectOverengineeringText } from "./overengineering.js";
|
|
17
18
|
|
|
@@ -50,9 +51,16 @@ const SELF_CERT_HINTS = {
|
|
|
50
51
|
"19": { re: /学到新.*DSH|踩到新坑|踩坑.*沉淀|手册有误|知识必沉淀/, reason: "请按规则 19 自证:按①-⑧沉淀并当次汇报正文同步位置" },
|
|
51
52
|
"28": { re: /新建.*文件|放入.*目录|工作区.*归类/, reason: "请按规则 28 自证:新文件按工作区目录索引归类,不确定则查 README 或放 _inbox" },
|
|
52
53
|
"29": { re: /依赖闭包|node_modules.*验证|动过.*node_modules/, reason: "请按规则 29 自证:重启前验证依赖闭包(关键包非空/顶层 junction 完整/无 dangling)" },
|
|
53
|
-
"30": { re: /变更验证|验证证据|不破坏.*依赖/, reason: "请按规则 30 自证:执行前证明不破坏依赖+已授权,完成后附验证证据" }
|
|
54
|
+
"30": { re: /变更验证|验证证据|不破坏.*依赖/, reason: "请按规则 30 自证:执行前证明不破坏依赖+已授权,完成后附验证证据" },
|
|
55
|
+
// 规则 31(2026-09-01 用户拍板):提到"撞墙/盲试"类表述 → D 级自证:写明验证目标→撞了什么/为什么撞→结论来源
|
|
56
|
+
"31": { re: /撞(?:了)?南墙|撞墙|盲试|乱撞|连续盲试/, reason: "规则 31 自证:请写明「验证目标→撞了什么/为什么撞→结论来源(手册位置/源码行号)」;不连续盲试" }
|
|
54
57
|
};
|
|
55
58
|
|
|
59
|
+
// 规则 31(2026-09-01 用户拍板):内部文档引用锚点词("文档"仅在后随"写了/记载…"语境命中,防泛化误报)
|
|
60
|
+
const INTERNAL_REF_RE = /(?:手册|踩坑\s*\d+|条款\s*\d+|第\s*\d+\s*章|SKILL\.md|AGENTS\.md|源码|README|文档[^\n]{0,12}(?:写了|记载|记录|写明|说明))/i;
|
|
61
|
+
// 验证目标启发式:思维链含这些词 → 大概率是有序查证(防规则 31③误报)
|
|
62
|
+
const VERIFY_INTENT_RE = /(?:验证|查证|核对|目标|交叉|对照|确认)/;
|
|
63
|
+
|
|
56
64
|
/** 简单判断一段文本是否以英文为主 */
|
|
57
65
|
function isMostlyEnglish(text) {
|
|
58
66
|
if (!text) return false;
|
|
@@ -157,19 +165,56 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
|
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
const sourceCfg = byId.get("5");
|
|
160
|
-
if (sourceCfg &&
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
if (sourceCfg && !SOURCE_MARK.test(text)) {
|
|
169
|
+
if (URL_RE.test(text)) {
|
|
170
|
+
hits.push({
|
|
171
|
+
ruleId: "5",
|
|
172
|
+
title: sourceCfg.title,
|
|
173
|
+
kind: "correct",
|
|
174
|
+
reason: "回答包含 URL 但未标注出处/来源"
|
|
175
|
+
});
|
|
176
|
+
} else if (INTERNAL_REF_RE.test(text) && !isQuoteOrParaphraseContext(text, INTERNAL_REF_RE)) {
|
|
177
|
+
// 规则 5 扩展(2026-09-01 用户拍板):内部文档引用(手册/踩坑/条款/源码…)须有依据——
|
|
178
|
+
// 近 3 回合无对应 read/grep 时提示(B 级:留痕+注入,不拦截)。
|
|
179
|
+
const curTurn = session.turn.number || 0;
|
|
180
|
+
const lastQuery = session.lastQueryTurn ?? -1;
|
|
181
|
+
if (lastQuery < 0 || curTurn - lastQuery > 3) {
|
|
182
|
+
hits.push({
|
|
183
|
+
ruleId: "5",
|
|
184
|
+
title: sourceCfg.title,
|
|
185
|
+
kind: "correct",
|
|
186
|
+
reason: "回答引用内部文档(手册/踩坑/条款/源码等)但近 3 回合无对应 read/grep——请标注手册位置或删去断言"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 规则 31(2026-09-01 用户拍板):同回合同一只读工具 ≥3 次且思维链无验证目标语 → 提示(B 级:留痕+注入)
|
|
193
|
+
const rule31Cfg = byId.get("31");
|
|
194
|
+
if (rule31Cfg) {
|
|
195
|
+
const counts = new Map();
|
|
196
|
+
const pending = session.turn?.pendingToolCalls;
|
|
197
|
+
if (pending && typeof pending.values === "function") {
|
|
198
|
+
for (const pc of pending.values()) {
|
|
199
|
+
if (pc && pc.name && isReadOnlyTool(pc.name, pc.args)) {
|
|
200
|
+
counts.set(pc.name, (counts.get(pc.name) || 0) + 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const repeated = [...counts.entries()].find(([, c]) => c >= 3);
|
|
205
|
+
if (repeated && !VERIFY_INTENT_RE.test(reasoningText)) {
|
|
206
|
+
hits.push({
|
|
207
|
+
ruleId: "31",
|
|
208
|
+
title: rule31Cfg.title,
|
|
209
|
+
kind: "correct",
|
|
210
|
+
reason: `规则 31:回合同一只读工具(${repeated[0]})调用 ${repeated[1]} 次且思维链无验证目标语——有序交叉验证请忽略,连续盲试请先写明「要验证什么」`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
167
213
|
}
|
|
168
214
|
|
|
169
215
|
const langCfg = byId.get("11");
|
|
170
216
|
const visibleEnglish = isMostlyEnglish(text);
|
|
171
|
-
const reasoningEnglish = isMostlyEnglish(reasoningText);
|
|
172
|
-
if (langCfg && session.lastUserText && CJK_RE.test(session.lastUserText) && (visibleEnglish || reasoningEnglish)) {
|
|
217
|
+
const reasoningEnglish = isMostlyEnglish(reasoningText); if (langCfg && session.lastUserText && CJK_RE.test(session.lastUserText) && (visibleEnglish || reasoningEnglish)) {
|
|
173
218
|
hits.push({
|
|
174
219
|
ruleId: "11",
|
|
175
220
|
title: langCfg.title,
|