dsh-rule-engine 0.5.11 → 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.
@@ -44,7 +44,14 @@ const NUMBERED_ITEM_RE = /^\s*(?:\(?(\d+)[\)、..)]\s*)/;
44
44
  // A1(2026-08-28 阶段一):具体动作词(区别于"执行/开始/进行"等宽泛词)。
45
45
  // 用途:hasAction+hasPlan 时区分"给出执行方案"(方案请求)与"将建议书转化到业务通告"(执行指令)。
46
46
  const SPECIFIC_ACTION_RE =
47
- /写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:完|出|一下|一遍|出来)?|展示|打开|提取|还原|导入|导出|采纳|选择|选定|落盘|落地|清理|移除|清空/i;
47
+ /写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:完|出|一下|一遍|出来)?|展示|打开|提取|还原|导入|导出|采纳|选择|选定|落盘|落地|清理|移除|清空|合入|合并|并入|融合|整合|起草|开展|搭建|重构|兼容|投产|校验|核验|过一遍|跑一遍|复验/i;
48
+
49
+ // 2026-08-31("方案+执行"误判修复,用户定稿/通用语义):
50
+ // "按已有方案顺序依次执行/上述方案执行" —— 用户已指出既有方案并让执行(执行语+方案词)
51
+ // 区别于"给出执行方案/提供方案/设计方案"——产出请求(要方案,不是执行)。
52
+ // 判定:方案词+强执行语同句,且无产出请求词 → 执行分点(不再被 PLAN_RE 降档)。
53
+ const PLAN_EXECUTING_RE = /(?:方案|计划|步骤|清单|安排)[^\n]{0,16}?(?:执行|开始|继续|开工|去做|照做|落地)|(?:执行|开始|继续|做)[^\n]{0,12}(?:该|此|这个|上述|已有|给出(?:的)?|拟定(?:的)?)?方案/;
54
+ const PLAN_REQUEST_RE = /(?:给出|提供|出|设计|评估|分析|规划|计划|整理|拟定|撰写|编写|看看|讲解|说说|如何|怎么|哪里|要(?:一|个|份)|需?(?:要|求)(?:一|个|份)?)/;
48
55
 
49
56
  // A3(2026-08-28,阶段一):@文件引用信号——用户消息中 @ 引用路径/文件(DSH 官方
50
57
  // file-reference 语法,@path 或 @"path with spaces"/@file.docx),且含处理动词 → 视为执行分点。
@@ -227,8 +234,9 @@ function classifyClause(clause) {
227
234
  type = "status";
228
235
  } else if (hasDefer && hasAction) {
229
236
  type = "plan";
230
- } else if (hasAction && hasPlan && !SPECIFIC_ACTION_RE.test(text)) {
231
- // 只有宽泛动作词(执行/开始/进行…)+ 方案词 → 方案请求("给出执行方案"≠执行指令)
237
+ } else if (hasAction && hasPlan && !SPECIFIC_ACTION_RE.test(text) && !(PLAN_EXECUTING_RE.test(text) && !PLAN_REQUEST_RE.test(text))) {
238
+ // 只有宽泛动作词(执行/开始/进行…)+ 方案词 → 方案请求("给出执行方案"≠执行指令);
239
+ // 例外(2026-08-31):"按已有方案执行/上述方案执行"——执行语+方案词且无产出请求词 → 执行分点
232
240
  type = "plan";
233
241
  } else if (hasAction) {
234
242
  // A1(2026-08-28 阶段一):动作词优先于方案词——"将建议书内容转化到业务通告中"
@@ -0,0 +1,145 @@
1
+ // label-fingerprint.js - 打标命令指纹记忆(F2,0.5.12)
2
+ // 目标:`/guard label ERR-xxx incorrect` 后,同指纹命令直接放行并记 label-hits
3
+ // 审计;持久化到 ~/.dsh/rule-engine-labels.json。
4
+ // 安全规格(重证报告 §3.2 补充):
5
+ // - 开关/写参数(-Recurse/-Force 等)**必须保留**在指纹中——只归一化无害部分;
6
+ // - 破坏类命令(rm/move/Remove-Item/irm/iwr 等)**永不进指纹放行**;
7
+ // - 持久化带 TTL(默认 7 天)+ 可撤销(/guard label clear <fingerprint>)。
8
+ // 纯函数,可独立测试;不依赖 Cordis。
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { normalizePath } from "./authorization.js";
11
+
12
+ /**
13
+ * 命令指纹归一化规则。
14
+ * 保留:命令名(首个 token 的小写规范形式)、开关类参数(-x/--xx)、位置参数中的"参数名前缀"(= 左侧)。
15
+ * 归一化(无害化):路径前缀(盘符/工作区路径)、时间戳、随机串、引号、大小写、空格压缩。
16
+ */
17
+ export function fingerprintCommand(command) {
18
+ const s = String(command || "").trim();
19
+ if (!s) return "";
20
+ // 只取主命令链的第一段(; | & 换行之后的副作用不在本命令指纹范围内——由守卫整体判定)
21
+ const head = s.split(/[;|&\r\n]/)[0].trim();
22
+ // 去除路径前缀(盘符路径/引号包裹路径 → 只保留最后一段 basename 与扩展名;目录变化不影响指纹)
23
+ const depathed = head
24
+ .replace(/(?:\\?['"]?[A-Za-z]:[\\/][^'"\s]*|['"][^'"]+|\\\/?) /g, " ")
25
+ .replace(/\b(path|pathprefix|workdir)\s*[=:]\s*(?:['"]?[^'"\s]+['"]?)/gi, "")
26
+ .replace(/[\\/](?:node_modules|projects|workspace)[\\/][^'"\s]*/gi, " ")
27
+ .replace(/[\\/]+/g, "/");
28
+ // 去除时间戳/随机串(8-16 位十六进制/数字串)
29
+ const denormalized = depathed.replace(/\b[0-9a-f]{8,16}\b/gi, "<TS>").replace(/\b\d{6,}\b/g, "<N>");
30
+ // 规范形式:小写 + 空白压缩
31
+ const canonical = denormalized
32
+ .toLowerCase()
33
+ .replace(/\s+/g, " ")
34
+ .replace(/\s*([=&])\s*/g, "$1")
35
+ .trim();
36
+ return canonical || "";
37
+ }
38
+
39
+ /** 是否破坏类命令(永不进指纹放行)——写/删除/移动/网络下载类 */
40
+ export function isDangerousCommand(command) {
41
+ const s = String(command || "").toLowerCase();
42
+ return /(?:remove-item|rm(?:dir)?\b|del(?:ete)?\b|move-item|move\b|copy-item|cp\b|mv\b|irm|iwr|\bcurl\b|\bwget\b|invoke-exp(?:ression)?|iex\b|new-item|set-content|add-content|out-file|rename-item|clear-content|git\s+(?:push|commit|reset|checkout|clean))/i.test(s);
43
+ }
44
+
45
+ /** 从命令文本提取可保留的"写/开关参数"(保留在指纹里,防止 -Recurse 被归一化放行) */
46
+ export function extractSignificantParams(command) {
47
+ const s = String(command || "");
48
+ // 开关:-x / --xx / -Force / -Recurse 等(含 = 值)
49
+ const flags = [];
50
+ const flagRe = /(?:^|\s)(--?[A-Za-z][\w-]*)(?:(?:=([^\s]+))|(\s+[^\s-][^\s]*\s*))?/g;
51
+ let m;
52
+ while ((m = flagRe.exec(s))) {
53
+ if (m[1]) flags.push(m[1].toLowerCase());
54
+ }
55
+ return flags;
56
+ }
57
+
58
+ /**
59
+ * 计算放行指纹 = 命令骨架 + 保留参数(开关原样 + 位置参数的"名称=值"对)。
60
+ * @returns {string} 指纹字符串(稳定、可比较);空串表示不可指纹化
61
+ */
62
+ export function labelFingerprint(command) {
63
+ const cmd = String(command || "").trim();
64
+ if (!cmd) return "";
65
+ if (isDangerousCommand(cmd)) return ""; // 危险命令永不进指纹(返回空 = 不可放行)
66
+ const canonical = fingerprintCommand(cmd);
67
+ if (!canonical) return "";
68
+ const params = extractSignificantParams(cmd).sort();
69
+ return `${canonical}|flags=${params.join(",")}`;
70
+ }
71
+
72
+ /** 打标台账记录结构 */
73
+ export function labelEntry(fingerprint, label, sid, now = Date.now(), ttlMs = 7 * 24 * 60 * 60 * 1000) {
74
+ return { fingerprint, label, session: sid || "global", at: now, expiresAt: now + ttlMs };
75
+ }
76
+
77
+ /** 从原始 JSON 解析台账(容错:损坏返回空数组 + 旧格式兼容) */
78
+ export function parseLabels(raw) {
79
+ try {
80
+ const arr = JSON.parse(raw || "[]");
81
+ if (!Array.isArray(arr)) return [];
82
+ return arr
83
+ .map((it) => {
84
+ if (typeof it === "string") return { fingerprint: it, label: "incorrect", session: null, at: null, expiresAt: null };
85
+ if (it && typeof it.fingerprint === "string") {
86
+ return {
87
+ fingerprint: it.fingerprint,
88
+ label: typeof it.label === "string" ? it.label : "incorrect",
89
+ session: typeof it.session === "string" ? it.session : null,
90
+ at: typeof it.at === "number" ? it.at : null,
91
+ expiresAt: typeof it.expiresAt === "number" ? it.expiresAt : null
92
+ };
93
+ }
94
+ return null;
95
+ })
96
+ .filter(Boolean);
97
+ } catch {
98
+ return [];
99
+ }
100
+ }
101
+
102
+ /** 持枪裁判:一份台账中某指纹是否命中"incorrect 放行"(且未过期) */
103
+ export function labelsAllowFingerprint(rows, fingerprint, now = Date.now()) {
104
+ if (!fingerprint) return false;
105
+ return rows.some((r) => r && r.fingerprint === fingerprint && r.label === "incorrect" && (!r.expiresAt || r.expiresAt > now));
106
+ }
107
+
108
+ /** 写台账(保留未过期 + 去重:同指纹同标签更新 at/expiresAt) */
109
+ export function serializeLabels(rows, now = Date.now()) {
110
+ const keep = rows.filter((r) => r && r.fingerprint && (!r.expiresAt || r.expiresAt > now));
111
+ // 同指纹同 label 只保留最新(去重)
112
+ const map = new Map();
113
+ for (const r of keep) map.set(`${r.fingerprint}|${r.label}`, r);
114
+ return JSON.stringify([...map.values()], null, 2);
115
+ }
116
+
117
+ /** 持久化路径(与 toolsWhitelistFilePath 同模式) */
118
+ export function labelsFilePath(dshHome) {
119
+ return `${dshHome}/rule-engine-labels.json`;
120
+ }
121
+
122
+ /** 从磁盘加载台账(纯函数输入输出:读取失败 → 空数组) */
123
+ export function loadLabelsFromDisk(filePath) {
124
+ try {
125
+ return parseLabels(readFileSync(filePath, "utf8"));
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ /** 写磁盘(失败静默——审计链路不因台账读写失败而阻断) */
132
+ export function saveLabelsToDisk(filePath, rows) {
133
+ try {
134
+ writeFileSync(filePath, serializeLabels(rows), "utf8");
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /** 去重复存:同指纹同 label 记录只留最新一条(新增/更新均用) */
142
+ export function upsertLabel(rows, entry) {
143
+ const others = rows.filter((r) => !(r.fingerprint === entry.fingerprint && r.label === entry.label));
144
+ return [...others, entry];
145
+ }
@@ -1,72 +1,72 @@
1
- // lexicon.js - 规则 22 语义判定的唯一词表源(0.5.11,用户拍板"唯一词表源由引擎维护")。
2
- // 背景(2026-08-28 全量审查):正文曾手抄词表(L144/148),引擎内部 intent.js 与 authorization.js
3
- // 又各写一份(许可词在 authorization 出现 3 次:AUTH_WORDS_RE/APPROVAL_RE/APPROVAL_EXEC_RE)——
4
- // 同语义多处定义 = 漂移根源。本文件 = 五表唯一源:
5
- // ① 执行许可词表(许可词 + 执行语,两者缺一才不构成许可)
6
- // ② 执行动作词表(意图/授权判定共用)
7
- // ③ 疑问词表(问询分点判定)
8
- // ④ 状态信号词表("我已重启/好了/done"等,状态信号不触发无执行分点)
9
- // ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"的明细)
10
- // 调用方(intent.js/authorization.js)一律从这里 import,禁止再定义第二份。
11
- // 设计原则:词表可以在,但只能有一份;语义变更改这里,正文措辞改 AGENTS.md,二者成对。
12
-
13
- // ── ① 执行许可词表 ──
14
- // 许可词:用户确认/同意/批准/授权/可以/允许/好/行/ok/yes 等 + 执行语(即可/现在/马上/…)
15
- // 组合规则:许可词 [0,16 字符内] 执行语 → 构成执行许可;纯"确认方案/理解/方向"不构成。
16
- export const APPROVAL_WORDS_RE = /(?:确认|同意|批准|授权|可以|允许|好|行|ok|yes)/i;
17
- export const EXEC_FOLLOW_RE = /(?:即可|现在|马上|继续|开始|执行|落盘|去做|实施|进行|删除|修改|改|补|修|做|写入|创建|安装|发布|下载|提交|运行)/i;
18
- export const APPROVAL_EXEC_RE = new RegExp(
19
- APPROVAL_WORDS_RE.source.replace(/\\\//g, "/") +
20
- "[^\\n]{0,16}" +
21
- EXEC_FOLLOW_RE.source.replace(/\\\//g, "/"),
22
- "i"
23
- );
24
- // 纯方案/理解/方向确认(不构成执行授权)
25
- export const PLAN_ONLY_RE = /确认方案|确认理解|确认方向|确认无误|理解了|明白方向|了解|没问题|收到/i;
26
-
27
- // ── ② 执行动作词表(intent.ACTION_RE / authorization.EXEC_ACTION_RE 的合并唯一源)──
28
- // 注意:单一表同时服务"意图判定"(这句话是不是执行分点)与"ask 结果批准判定"(选择=批准),
29
- // 语义一致,不另分裂;新增动作词只改这里。
30
- export const ACTION_WORDS_RE =
31
- /执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留/i;
32
-
33
- // 强执行语(直接执行指令,不等授权确认——用于意图置信提升)
34
- export const STRONG_EXEC_RE =
35
- /执行吧|开始执行|直接执行|立即执行|现在执行|马上执行|请落盘|开始写|开始改|执行以下|执行第|执行这个|执行它|落盘|开始蒸馏|开始迁移|开始修复|开始改进|开始优化|采纳|选定/i;
36
-
37
- // 指令式动作词(用户命令式指令 = 视为授权执行;与批准词不同源、用途不同)
38
- export const DIRECTIVE_WORDS_RE =
39
- /(?:请\s*)?(?:删除|移除|修改|编辑|替换|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|创建|复制|移动|执行|运行|下载|提交|推送|备份|安装|卸载|清理|启动|停止|重建|修复|改(?:为|成|动|一下)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|实施|推进|继续|处理|解决)/i;
40
-
41
- // 拒绝词(授权/批准判定用)
42
- export const REJECTION_WORDS_RE = /(?:不允许|拒绝|不要|不同意|取消(?:这次|并停)?|(?:^|[^是])否(?:[^则]|$)|no(?!tes?|thing|pe)\b|\bno\b|No)/i;
43
-
44
- // ── ③ 疑问词表(问询分点判定;authorization.isQuestionMessage 同源)──
45
- export const QUESTION_WORDS_RE =
46
- /[??]|吗|为什么|是否|能不能|行不行|难道|是不是|能否|怎么|为何|是否要|影响吗|可以吗|要不要|需不需要/;
47
-
48
- // ── ④ 状态信号词表("我已重启/已输入/好了/done";触发时不判"无执行分点")──
49
- export const STATUS_SIGNAL_WORDS_RE =
50
- /(?:我已|我已经|我(?:们)?(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好))(?:了|完毕|完成|好)?|(?:^|\s)(?:已|已经)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)(?:了|完毕|完成|好)?(?:\s|$)|(?:^|\s)已经(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)[^\s]*(?:\s|$)|(?:^|\s)(?:重启|输入|命令|操作)(?:完成|完毕|好了|已输入)(?:\s|$)|(?:^|\s)好了(?:\s|$)|^(?:done|restarted|finished|entered)$/i;
51
- // 危险动作词:命中则不当作状态信号(防"我已删除了某文件"被当成就绪确认)
52
- export const DANGEROUS_ACTION_WORDS_RE = /删除|覆盖|移除|清空|卸载|remove-item|rm\s+-r|del\s+/i;
53
-
54
- // ── ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"明细)──
55
- // 只读/展示/询问/产物类工具在"无执行分点"回合也可调用(引擎 tool-catalog 的 analysis/artifact 分类
56
- // 为总开关;本清单 = 规则正文"等豁免"所指的可枚举集,与 tool-catalog 一致,此处集中注释成对)。
57
- export const EXEMPT_TOOL_NAMES = new Set([
58
- // 只读/查询
59
- "read", "grep", "glob", "read_image",
60
- // 视觉分析
61
- "vision_describe", "vision_detect", "vision_ground", "vision_colors", "vision_ocr", "vision_bootstrap",
62
- // 状态查询
63
- "job_list", "job_output", "list_agents", "get_goal", "schedule_list",
64
- "engram_recall", "engram_detail", "engram_model",
65
- "esr_status", "esr_ready", "esr_model",
66
- // 网络只读
67
- "web_fetch", "web_search",
68
- // 询问/展示/产物
69
- "ask_user_question", "todo_write", "visualize", "report",
70
- // inspect 只读
71
- "cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self"
72
- ]);
1
+ // lexicon.js - 规则 22 语义判定的唯一词表源(0.5.11,用户拍板"唯一词表源由引擎维护")。
2
+ // 背景(2026-08-28 全量审查):正文曾手抄词表(L144/148),引擎内部 intent.js 与 authorization.js
3
+ // 又各写一份(许可词在 authorization 出现 3 次:AUTH_WORDS_RE/APPROVAL_RE/APPROVAL_EXEC_RE)——
4
+ // 同语义多处定义 = 漂移根源。本文件 = 五表唯一源:
5
+ // ① 执行许可词表(许可词 + 执行语,两者缺一才不构成许可)
6
+ // ② 执行动作词表(意图/授权判定共用)
7
+ // ③ 疑问词表(问询分点判定)
8
+ // ④ 状态信号词表("我已重启/好了/done"等,状态信号不触发无执行分点)
9
+ // ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"的明细)
10
+ // 调用方(intent.js/authorization.js)一律从这里 import,禁止再定义第二份。
11
+ // 设计原则:词表可以在,但只能有一份;语义变更改这里,正文措辞改 AGENTS.md,二者成对。
12
+
13
+ // ── ① 执行许可词表 ──
14
+ // 许可词:用户确认/同意/批准/授权/可以/允许/好/行/ok/yes 等 + 执行语(即可/现在/马上/…)
15
+ // 组合规则:许可词 [0,16 字符内] 执行语 → 构成执行许可;纯"确认方案/理解/方向"不构成。
16
+ export const APPROVAL_WORDS_RE = /(?:确认|同意|批准|授权|可以|允许|好|行|ok|yes)/i;
17
+ export const EXEC_FOLLOW_RE = /(?:即可|现在|马上|继续|开始|执行|落盘|去做|实施|进行|删除|修改|改|补|修|做|写入|创建|安装|发布|下载|提交|运行)/i;
18
+ export const APPROVAL_EXEC_RE = new RegExp(
19
+ APPROVAL_WORDS_RE.source.replace(/\\\//g, "/") +
20
+ "[^\\n]{0,16}" +
21
+ EXEC_FOLLOW_RE.source.replace(/\\\//g, "/"),
22
+ "i"
23
+ );
24
+ // 纯方案/理解/方向确认(不构成执行授权)
25
+ export const PLAN_ONLY_RE = /确认方案|确认理解|确认方向|确认无误|理解了|明白方向|了解|没问题|收到/i;
26
+
27
+ // ── ② 执行动作词表(intent.ACTION_RE / authorization.EXEC_ACTION_RE 的合并唯一源)──
28
+ // 注意:单一表同时服务"意图判定"(这句话是不是执行分点)与"ask 结果批准判定"(选择=批准),
29
+ // 语义一致,不另分裂;新增动作词只改这里。
30
+ export const ACTION_WORDS_RE =
31
+ /执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留|合入|合并|并入|融合|整合|起草|开展|搭建|重构|兼容|投产|校验|核验|过一遍|跑一遍|复验/i;
32
+
33
+ // 强执行语(直接执行指令,不等授权确认——用于意图置信提升)
34
+ export const STRONG_EXEC_RE =
35
+ /执行吧|开始执行|直接执行|立即执行|现在执行|马上执行|请落盘|开始写|开始改|执行以下|执行第|执行这个|执行它|落盘|开始蒸馏|开始迁移|开始修复|开始改进|开始优化|采纳|选定/i;
36
+
37
+ // 指令式动作词(用户命令式指令 = 视为授权执行;与批准词不同源、用途不同)
38
+ export const DIRECTIVE_WORDS_RE =
39
+ /(?:请\s*)?(?:删除|移除|修改|编辑|替换|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|创建|复制|移动|执行|运行|下载|提交|推送|备份|安装|卸载|清理|启动|停止|重建|修复|改(?:为|成|动|一下)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|实施|推进|继续|处理|解决)/i;
40
+
41
+ // 拒绝词(授权/批准判定用)
42
+ export const REJECTION_WORDS_RE = /(?:不允许|拒绝|不要|不同意|取消(?:这次|并停)?|(?:^|[^是])否(?:[^则]|$)|no(?!tes?|thing|pe)\b|\bno\b|No)/i;
43
+
44
+ // ── ③ 疑问词表(问询分点判定;authorization.isQuestionMessage 同源)──
45
+ export const QUESTION_WORDS_RE =
46
+ /[??]|吗|为什么|是否|能不能|行不行|难道|是不是|能否|怎么|为何|是否要|影响吗|可以吗|要不要|需不需要/;
47
+
48
+ // ── ④ 状态信号词表("我已重启/已输入/好了/done";触发时不判"无执行分点")──
49
+ export const STATUS_SIGNAL_WORDS_RE =
50
+ /(?:我已|我已经|我(?:们)?(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好))(?:了|完毕|完成|好)?|(?:^|\s)(?:已|已经)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)(?:了|完毕|完成|好)?(?:\s|$)|(?:^|\s)已经(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)[^\s]*(?:\s|$)|(?:^|\s)(?:重启|输入|命令|操作)(?:完成|完毕|好了|已输入)(?:\s|$)|(?:^|\s)好了(?:\s|$)|^(?:done|restarted|finished|entered)$/i;
51
+ // 危险动作词:命中则不当作状态信号(防"我已删除了某文件"被当成就绪确认)
52
+ export const DANGEROUS_ACTION_WORDS_RE = /删除|覆盖|移除|清空|卸载|remove-item|rm\s+-r|del\s+/i;
53
+
54
+ // ── ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"明细)──
55
+ // 只读/展示/询问/产物类工具在"无执行分点"回合也可调用(引擎 tool-catalog 的 analysis/artifact 分类
56
+ // 为总开关;本清单 = 规则正文"等豁免"所指的可枚举集,与 tool-catalog 一致,此处集中注释成对)。
57
+ export const EXEMPT_TOOL_NAMES = new Set([
58
+ // 只读/查询
59
+ "read", "grep", "glob", "read_image",
60
+ // 视觉分析
61
+ "vision_describe", "vision_detect", "vision_ground", "vision_colors", "vision_ocr", "vision_bootstrap",
62
+ // 状态查询
63
+ "job_list", "job_output", "list_agents", "get_goal", "schedule_list",
64
+ "engram_recall", "engram_detail", "engram_model",
65
+ "esr_status", "esr_ready", "esr_model",
66
+ // 网络只读
67
+ "web_fetch", "web_search",
68
+ // 询问/展示/产物
69
+ "ask_user_question", "todo_write", "visualize", "report",
70
+ // inspect 只读
71
+ "cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self"
72
+ ]);
@@ -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
- function finalize(rule, lines) {
76
- const body = lines.slice(rule.startLine + 1, rule.endLine).join("\n").trim();
77
- // 等级正则:支持空格/顿号连接的组合等级(如 "B + D"、"A 弱 + D"),解析结果去空白
78
- const levelMatch = rule.title.match(/执行等级[::]\s*([A-DM+](?:\s*[强弱])?(?:\s*[++、]\s*[A-DM+](?:\s*[强弱])?)*)/);
79
- return {
80
- index: rule.index,
81
- title: rule.title,
82
- section: rule.section,
83
- startLine: rule.startLine,
84
- endLine: rule.endLine,
85
- level: levelMatch ? levelMatch[1].replace(/\s+/g, "") : "",
86
- body
87
- };
88
- }
89
-
90
- /** 从规则正文提取四要素 */
91
- export function extractElements(body) {
92
- const out = { trigger: "", check: "", action: "", exemption: "" };
93
- const grab = (label) => {
94
- const re = new RegExp(`\\*\\*${label}(?:([^)]*))?\\*\\*[::]\\s*([^\\n]*(?:\\n(?!\\s*\\*\\*)[^\\n]*)*)`, "i");
95
- const m = body.match(re);
96
- return m ? m[1].trim() : "";
97
- };
98
- out.trigger = grab("触发");
99
- out.check = grab("检查");
100
- out.action = grab("动作");
101
- out.exemption = grab("豁免");
102
- return out;
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
+ }