dsh-rule-engine 0.6.2 → 0.6.3

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.
@@ -18,33 +18,30 @@
18
18
  // 消灭“两套疑问词判定不一致”(如 authorization 的“影响吗”与 intent 的“怎么”)。
19
19
  // - 新增 STATUS_SIGNAL_RE / isStatusSignal():状态信号分类(宽松版,危险词排除,
20
20
  // 12A/13A 兜底),与“无用户消息回合”同等待遇。
21
- // 0.5.11(用户拍板):词表唯一源 = lib/core/lexicon.js——下面 5 张表全部从那里 import,
22
- // 本文件不再自持定义(原定义删除);对外 export 名保持不变(QUESTION_RE/STATUS_SIGNAL_RE
23
- // 仍对外可见,/guard 与测试引用不受影响)。
21
+ // 0.5.11(用户拍板):词表唯一源 = lib/core/lexicon.js——下面 5 张表全部从那里取,
22
+ // 本文件不再自持定义。
23
+ // P8 小批 A(2026-09-08):改为函数取词(questionWordsRe() 等)——配置层注入后立即生效;
24
+ // 旧导出名 QUESTION_RE / STATUS_SIGNAL_RE 已删除(正则常量会锁死内置值,是本次改造的根因)。
25
+ // 对外仍以函数形态提供:questionWordsRe / statusSignalRe(lexicon.js)。
24
26
 
25
27
  import {
26
- QUESTION_WORDS_RE,
27
- ACTION_WORDS_RE,
28
- STRONG_EXEC_RE as STRONG_EXEC_LEX,
29
- STATUS_SIGNAL_WORDS_RE,
30
- DANGEROUS_ACTION_WORDS_RE
28
+ questionWordsRe,
29
+ actionWordsRe,
30
+ strongExecRe,
31
+ statusSignalRe,
32
+ dangerousActionRe
31
33
  } from "./lexicon.js";
32
34
  import { hasHotwordAction } from "./hotwords.js";
33
35
 
34
36
  export const TAG_RE = /【(执行|方案|问询|信息)】/;
35
37
 
36
- // 统一疑问词表(唯一源:lexicon.js;此处再导出保持外部引用兼容)
37
- export const QUESTION_RE = QUESTION_WORDS_RE;
38
-
39
38
  const DEFER_RE = /待确认|确认后再|等我确认|先方案|先别|不要执行|之后再说|确认后|再确认|先确认|不要直接/;
40
39
  // 2026-08-31(柱子 C:条件句识别——"推送完成后…/等 X 了…/待…后…"型分点):
41
40
  // 条件句分点 = 依赖前序分点完成后的后续动作,**本条不产生任何授权/scope**(安全方向:宁拦不放)。
42
41
  // 词面拒绝"推送/上传"等动作词被当作本条独立动作(3 号分点"推送完成后发地址"实证:引擎曾把
43
42
  // "推送"当本条动作致 git 全局授权弥漫——分点失效根因之一)。
44
43
  const CONDITIONAL_RE = /(?:完成|结束|做完|弄完|搞定|好了|恢复|重启?)\s*(?:了|之后|后|以后|再)|(?:等|待|等\s*[\u4e00-\u9fff]{0,6}?)\s*[\u4e00-\u9fff]{0,8}?(?:后|之后|了)\s*|(?:推送|上传|提交|发布|执行)\s*(?:完成|结束|完|好)\s*(?:了|之?后)?/;
45
- const ACTION_RE = ACTION_WORDS_RE;
46
44
  const PLAN_RE = /方案|建议|评估|分析|设计|规划|计划|给出|提供/;
47
- const STRONG_EXEC_RE = STRONG_EXEC_LEX;
48
45
  const NUMBERED_ITEM_RE = /^\s*(?:\(?(\d+)[\)、..)]\s*)/;
49
46
  // A1(2026-08-28 阶段一):具体动作词(区别于"执行/开始/进行"等宽泛词)。
50
47
  // 用途:hasAction+hasPlan 时区分"给出执行方案"(方案请求)与"将建议书转化到业务通告"(执行指令)。
@@ -67,12 +64,8 @@ const AT_ACTION_RE = /看|读|改|写|转|查|处理|提取|打开|展示|审阅
67
64
  /**
68
65
  * 状态信号:对先前指令的就绪/完成确认(“我已重启 / 已输入 / 重启完成 / 好了 / done”)。
69
66
  * 语义 = 与“无用户消息回合”同等待遇:不触发规则 22 的“无执行分点”判定(规则 22⑩)。
70
- * 唯一源:lexicon.js STATUS_SIGNAL_WORDS_RE;此处再导出保持外部引用兼容。
67
+ * 唯一源:lexicon.js(P8 小批 A 起经 statusSignalRe() 取词,配置注入后生效)。
71
68
  */
72
- export const STATUS_SIGNAL_RE = STATUS_SIGNAL_WORDS_RE;
73
-
74
- // 危险动作词:命中则不当状态信号(防“我已删除了某文件”被当成就绪确认)
75
- const DANGEROUS_ACTION_RE = DANGEROUS_ACTION_WORDS_RE;
76
69
 
77
70
  /** DSH 在会话上下文里注入的系统文本(工作区指令/技能目录等),不是用户的真实消息 */
78
71
  export function isInjectedContextMessage(text) {
@@ -128,7 +121,7 @@ export function isStatusSignal(text) {
128
121
  //(“完成 A 和 C 两项”/“重启服务”),不是对先前指令的就绪确认;状态信号须带“已/我已完成”前缀
129
122
  // 或“了/完成/完毕/好”后缀(根因:STATUS_SIGNAL_RE 的“裸词 + 可选后缀”分支把命令式误判为状态)。
130
123
  if (/^(?:请|帮我)?\s*(?:先|接着|然后)?\s*(?:完成|重启|执行|搞定|做完|弄好)\s+\S/.test(t.trim())) return false;
131
- return STATUS_SIGNAL_RE.test(t) && !DANGEROUS_ACTION_RE.test(t);
124
+ return statusSignalRe().test(t) && !dangerousActionRe().test(t);
132
125
  }
133
126
 
134
127
  function normalizeTag(tag) {
@@ -214,12 +207,12 @@ function classifyClause(clause) {
214
207
  const text = String(clause.raw || "").trim();
215
208
  const tag = extractTag(text);
216
209
  const tagType = normalizeTag(tag);
217
- const hasQuestion = QUESTION_RE.test(text);
210
+ const hasQuestion = questionWordsRe().test(text);
218
211
  const hasDefer = DEFER_RE.test(text);
219
212
  // B1(2026-08-29):词表命中 ∪ 热词命中(热词=LLM 兜底已确认过的动作词,见 hotwords.js)
220
- const hasAction = ACTION_RE.test(text) || hasHotwordAction(text);
213
+ const hasAction = actionWordsRe().test(text) || hasHotwordAction(text);
221
214
  const hasPlan = PLAN_RE.test(text);
222
- const hasStrongExec = STRONG_EXEC_RE.test(text);
215
+ const hasStrongExec = strongExecRe().test(text);
223
216
  // A3(阶段一):@文件引用信号 + 处理动词 → 执行分点(用户用 @ 引用文件即"处理它")
224
217
  const hasAtRef = AT_REF_RE.test(text) && AT_ACTION_RE.test(text);
225
218
 
@@ -0,0 +1,61 @@
1
+ // lang.js - 首启语言探测(第三批第 1 波 §4)
2
+ //
3
+ // 设计(DSH 方案 §五③ 选项 (b)):引擎启动时探测规则集语言——
4
+ // 读 AGENTS.md(经调用方传入其解析结果或原文),含 CJK → zh-CN;否则 en。
5
+ // 结果只缓存在**内存**,每次启动重探,**不落盘**(无配置文件、无副作用)。
6
+ // 用途:报错/提示/自由面/盲区等面向用户的展示语言。
7
+ //
8
+ // 注意:本模块是**通用层**——它只判断"用什么语言展示",不携带任何语言内容。
9
+ // 文案本身由 messages 层/个人层提供(第 1 批清淤时落地)。
10
+ export const LANGS = ["zh-CN", "en"];
11
+ const DEFAULT_LANG = "en";
12
+ const CJK = /[\u4e00-\u9fff]/;
13
+
14
+ let current = DEFAULT_LANG;
15
+ let detectedFrom = "default";
16
+
17
+ /**
18
+ * 探测语言:传规则集文本(AGENTS.md 原文或解析结果拼接),含 CJK → zh-CN。
19
+ * @param {string} text 规则集文本
20
+ * @returns {string} 生效语言
21
+ */
22
+ export function detectLang(text) {
23
+ const t = String(text || "");
24
+ if (!t.trim()) {
25
+ current = DEFAULT_LANG;
26
+ detectedFrom = "empty";
27
+ return current;
28
+ }
29
+ current = CJK.test(t) ? "zh-CN" : "en";
30
+ detectedFrom = "agents-md";
31
+ return current;
32
+ }
33
+
34
+ /** 当前生效语言 */
35
+ export function getLang() {
36
+ return current;
37
+ }
38
+
39
+ /** 探测来源(诊断用:default | empty | agents-md | manual) */
40
+ export function getLangSource() {
41
+ return detectedFrom;
42
+ }
43
+
44
+ /** 显式设置(测试/用户覆盖用;不落盘) */
45
+ export function setLang(lang) {
46
+ if (!LANGS.includes(lang)) throw new Error(`Unknown language: ${lang} (supported: ${LANGS.join(" / ")})`);
47
+ current = lang;
48
+ detectedFrom = "manual";
49
+ return current;
50
+ }
51
+
52
+ /** 双语选择:pickLang(zh, en) → 按当前语言取 */
53
+ export function pickLang(zh, en) {
54
+ return current === "zh-CN" ? zh : en;
55
+ }
56
+
57
+ /** 重置为默认(测试用) */
58
+ export function resetLang() {
59
+ current = DEFAULT_LANG;
60
+ detectedFrom = "default";
61
+ }
@@ -1,72 +1,177 @@
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
- /执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|建(?:设|立|好|一下|个|一个)?|安装|发布|下载|提交|推送|push|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留|合入|合并|并入|融合|整合|起草|开展|搭建|重构|兼容|投产|校验|核验|过一遍|跑一遍|复验/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 - 规则引擎行为词表的唯一源(0.5.11"唯一词表源";P8 小批 A 起双层化)。
2
+ //
3
+ // 双层模型(P8 小批 A,2026-09-08):
4
+ // 通用层(内置默认 BUILTIN_LEXICONS)——语言无关最小集,随包发布,不含任何中文/本机行为词;
5
+ // 个人层(rule-engine.json lexicons 键)——null/缺省 = 用内置默认;非空 = 按键完全替换。
6
+ // 机制同 setTypeHints(Override 模式):不做合并,配置即真相,删键即回退。
7
+ //
8
+ // 为什么取词必须走函数(lexRe / xxxRe())而不能缓存正则对象:
9
+ // 配置注入发生在模块求值之后(index.js 启动时 setLexicons)。若调用方在模块顶层
10
+ // `const X = SOME_RE` 缓存引用,注入后旧引用永不更新——这正是本批要根除的形态。
11
+ //
12
+ // 词表键(LEXICON_KEYS,11 张)与语义见 README「词表配置」章节。
13
+
14
+ /** 词表键名(顺序即文档顺序;approval_exec 为派生键,可显式覆盖) */
15
+ export const LEXICON_KEYS = [
16
+ "approval", // 执行许可词("确认/同意/可以…")
17
+ "exec_follow", // 执行语(许可词 + 执行语 = 执行许可)
18
+ "approval_exec", // 派生:approval + 0..16 字符 + exec_follow(可显式覆盖)
19
+ "plan_only", // ① 纯方案/理解确认(不构成执行授权)
20
+ "action_words", // ② 执行动作词(意图判定与授权判定共用)
21
+ "strong_exec", // ② 强执行语(直接执行指令,不等授权确认)
22
+ "directive", // ② 指令式动作词(命令式指令 = 视为授权执行)
23
+ "rejection", // ② 拒绝词
24
+ "question", // ③ 疑问词(问询分点判定)
25
+ "status_signal", // 状态信号词(就绪确认,不判"无执行分点")
26
+ "dangerous_action" // ④ 危险动作词(命中则不当作状态信号)
27
+ ];
28
+
29
+ // ── 通用层:内置默认(语言无关最小集)────────────────────────────────────────
30
+ // 约束:本表随 npm 包发布 → 不得含中文或任何本机专属行为词;
31
+ // 中文词表由本机 rule-engine.json.lexicons 注入(缺省时引擎按此最小集工作)。
32
+ const BUILTIN_LEXICONS = {
33
+ approval: "(?:ok|yes|y|approve|approved|agree|agreed|allow|allowed|permission granted)",
34
+ exec_follow: "(?:now|go ahead|proceed|continue|start|execute|run|apply|do it|write|save|delete|modify|create|install|publish|download|commit|push|fix|replace|rebuild|restart|stop|uninstall|update|deploy)",
35
+ plan_only: "confirm (?:the )?plan|understood|got it|acknowledged|noted|received",
36
+ action_words: "(?:execute|run|apply|write|save|delete|modify|edit|create|install|publish|download|commit|push|fix|replace|rebuild|start|restart|stop|uninstall|improve|optimize|enhance|upgrade|migrate|tidy|adjust|implement|add|append|rewrite|refactor|debug|investigate|resolve|continue|advance|update|deploy|verify|comb|diagnose|align|follow up|design and implement|survey|kick off|proceed|complete|choose|adopt|select|produce|author|write up|generate|review|read|inspect|check|audit|recheck|consult|validate|compare|research|study|convert|transform|fetch|read out|display|open|extract|restore|import|export|clean|remove|purge|merge|integrate|draft|carry out|build|make compatible|validate|re-verify)",
37
+ strong_exec: "(?:execute now|start now|do it|go ahead|proceed now|apply now|start writing|start editing|carry it out|adopt|selected)",
38
+ directive: "(?:please\\s+)?(?:delete|remove|modify|edit|replace|write|save|create|copy|move|execute|run|download|commit|push|backup|install|uninstall|clean|start|stop|rebuild|fix|implement|advance|continue|handle|resolve)",
39
+ rejection: "(?:do not|don'?t|reject|rejected|cancel|no(?!tes?|thing|pe)\\b|\\bno\\b)",
40
+ question: "[?]|why|whether|can (?:you|i)|could (?:you|i)|should (?:you|i)|how|what about|is it|are there",
41
+ status_signal: "(?:^|\\s)(?:done|restarted|finished|entered|completed|ready|input done)(?:\\s|$)",
42
+ dangerous_action: "(?:remove-item|rm\\s+-r|del\\s+|overwrite|drop\\s+table)"
43
+ };
44
+
45
+ let lexiconsOverride = null; // null = 内置默认;非 null = 按键完全替换(Override)
46
+ let reCache = new Map(); // "key\0source" -> RegExp(setLexicons 时清空)
47
+
48
+ function compileRe(source, flags) {
49
+ if (typeof source !== "string" || source.length === 0) return null;
50
+ try {
51
+ return new RegExp(source, flags);
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /** 解析单个键的生效 source(个人层优先 派生键 → 内置默认) */
58
+ function resolveSource(key) {
59
+ const ov = lexiconsOverride?.[key];
60
+ if (typeof ov === "string" && ov.length > 0) return ov;
61
+ if (key === "approval_exec") {
62
+ // 派生:许可词 [0,16 字符内] 执行语(与 0.5.11 内置组合语义一致)
63
+ return `${resolveSource("approval")}[^\\n]{0,16}${resolveSource("exec_follow")}`;
64
+ }
65
+ return BUILTIN_LEXICONS[key] ?? "";
66
+ }
67
+
68
+ /**
69
+ * 注入个人层词表(rule-engine.json 的 lexicons 键)。
70
+ *
71
+ * 语义边界(P8 小批 A):
72
+ * · cfg 为 undefined/null(配置里没这个键)→ **不改变当前状态**(不干预词表层)。
73
+ * 进程首次启动时模块态本就是内置默认,故"不干预" == "用内置默认";而这一条同时让
74
+ * 测试夹具(先注入、后加载 index.js)不被插件启动流程清掉。
75
+ * · cfg 为对象 → 配置即真相:按键完全替换(Override);`{}` = 无有效键 = 回退内置默认。
76
+ * 非法/未知键被拒绝并返回明细(不静默半套生效)。
77
+ * @returns {{applied: string[], rejected: Array<{key: string, reason: string}>, noop?: boolean}}
78
+ */
79
+ export function setLexicons(cfg) {
80
+ const applied = [];
81
+ const rejected = [];
82
+ if (cfg === undefined || cfg === null) {
83
+ return { applied, rejected, noop: true }; // 配置缺省:不干预
84
+ }
85
+ if (typeof cfg !== "object" || Array.isArray(cfg)) {
86
+ lexiconsOverride = null;
87
+ reCache = new Map();
88
+ return { applied, rejected };
89
+ }
90
+ const next = {};
91
+ for (const [k, v] of Object.entries(cfg)) {
92
+ if (!LEXICON_KEYS.includes(k)) {
93
+ rejected.push({ key: k, reason: "unknown-key" });
94
+ continue;
95
+ }
96
+ if (typeof v !== "string" || v.length === 0) {
97
+ rejected.push({ key: k, reason: "empty-or-not-string" });
98
+ continue;
99
+ }
100
+ if (!compileRe(v, "i")) {
101
+ rejected.push({ key: k, reason: "invalid-regex" });
102
+ continue;
103
+ }
104
+ next[k] = v;
105
+ applied.push(k);
106
+ }
107
+ lexiconsOverride = Object.keys(next).length > 0 ? next : null;
108
+ reCache = new Map();
109
+ return { applied, rejected };
110
+ }
111
+
112
+ /** 显式回退内置默认(测试用;生产环境改配置后由插件重载生效) */
113
+ export function resetLexicons() {
114
+ lexiconsOverride = null;
115
+ reCache = new Map();
116
+ }
117
+
118
+ /** 当前生效的词表快照(键 → source;供 /guard 展示与测试断言,只读) */
119
+ export function effectiveLexicons() {
120
+ const out = {};
121
+ for (const k of LEXICON_KEYS) out[k] = resolveSource(k);
122
+ return out;
123
+ }
124
+
125
+ /** 当前是否处于个人层覆盖状态(true = 至少一个键来自配置) */
126
+ export function hasLexiconOverride() {
127
+ return lexiconsOverride !== null;
128
+ }
129
+
130
+ /**
131
+ * 取当前生效正则(唯一取词入口;调用方禁止缓存返回对象)。
132
+ * 非法/空 source → 返回永不命中的占位正则(守卫无对象,自然静默)。
133
+ */
134
+ export function lexRe(key) {
135
+ const source = resolveSource(key);
136
+ const ck = `${key}\u0000${source}`;
137
+ let hit = reCache.get(ck);
138
+ if (!hit) {
139
+ hit = compileRe(source, "i") || /(?!)/;
140
+ reCache.set(ck, hit);
141
+ }
142
+ return hit;
143
+ }
144
+
145
+ // ── 语义命名访问器(调用方优先用这些,避免手写键名字符串)──
146
+ export const approvalWordsRe = () => lexRe("approval");
147
+ export const execFollowRe = () => lexRe("exec_follow");
148
+ export const approvalExecRe = () => lexRe("approval_exec");
149
+ export const planOnlyRe = () => lexRe("plan_only");
150
+ export const actionWordsRe = () => lexRe("action_words");
151
+ export const strongExecRe = () => lexRe("strong_exec");
152
+ export const directiveWordsRe = () => lexRe("directive");
153
+ export const rejectionWordsRe = () => lexRe("rejection");
154
+ export const questionWordsRe = () => lexRe("question");
155
+ export const statusSignalRe = () => lexRe("status_signal");
156
+ export const dangerousActionRe = () => lexRe("dangerous_action");
157
+
158
+ // ── ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"明细)──
159
+ // 注:工具名清单不是行为词表(语言无关),不进 lexicons 配置层。
160
+ // 只读/展示/询问/产物类工具在"无执行分点"回合也可调用(引擎 tool-catalog 的 analysis/artifact 分类
161
+ // 为总开关;本清单 = 规则正文"等豁免"所指的可枚举集,与 tool-catalog 一致,此处集中注释成对)。
162
+ export const EXEMPT_TOOL_NAMES = new Set([
163
+ // 只读/查询
164
+ "read", "grep", "glob", "read_image",
165
+ // 视觉分析
166
+ "vision_describe", "vision_detect", "vision_ground", "vision_colors", "vision_ocr", "vision_bootstrap",
167
+ // 状态查询
168
+ "job_list", "job_output", "list_agents", "get_goal", "schedule_list",
169
+ "engram_recall", "engram_detail", "engram_model",
170
+ "esr_status", "esr_ready", "esr_model",
171
+ // 网络只读
172
+ "web_fetch", "web_search",
173
+ // 询问/展示/产物
174
+ "ask_user_question", "todo_write", "visualize", "report",
175
+ // inspect 只读
176
+ "cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self"
177
+ ]);
@@ -10,7 +10,7 @@ import { parseUserIntents, shouldDenyMutation } from "./intent.js";
10
10
  import { resolveRoute, callLlm } from "./llm-understander.js";
11
11
  import { audit } from "./audit.js";
12
12
  import { learnHotword } from "./hotwords.js";
13
- import { ACTION_WORDS_RE } from "./lexicon.js";
13
+ import { actionWordsRe } from "./lexicon.js";
14
14
 
15
15
  const LLM_PROMPT = [
16
16
  "你是 DSH 规则引擎的意图判定器。输出严格 JSON,不要输出其他内容:",
@@ -106,7 +106,7 @@ export async function enrichIntentWithLlm(ctx, state, sid, text, cfg) {
106
106
  };
107
107
  // B1(2026-08-29 方案甲):LLM 明确判 execute 且词表未命中 → 学动作词入热词表
108
108
  //(下次同词词表直接命中,不再依赖 LLM;学习失败静默,不影响主流程)
109
- if (verdict.hasExecute && verdict.actionWord && !ACTION_WORDS_RE.test(text)) {
109
+ if (verdict.hasExecute && verdict.actionWord && !actionWordsRe().test(text)) {
110
110
  learnHotword(verdict.actionWord);
111
111
  }
112
112
  const ms = Date.now() - started;
@@ -1,18 +1,27 @@
1
1
  // matcher.js - 情境匹配机。
2
2
  // 每轮只激活相关规则子集,供 /guard active 展示,也用于控制执行成本。
3
- import { DSH_KEYWORDS_RE } from "./patterns.js";
3
+ import {
4
+ activateApprovalRe,
5
+ activateBackupRe,
6
+ activateNetworkRe,
7
+ activatePromiseRe,
8
+ activateSkillRe,
9
+ activateSourceRe,
10
+ activateTimeRe,
11
+ dshKeywordsRe
12
+ } from "./patterns.js";
4
13
 
5
14
  export function activateForUserMessage(configs, userText) {
6
15
  const text = userText || "";
7
16
  return configs.filter((cfg) => {
8
- if (cfg.handler === "rule18-manual-first" && DSH_KEYWORDS_RE.test(text)) return true;
9
- if (cfg.handler === "rule12b-skill" && /技能|skill/i.test(text)) return true;
10
- if (cfg.handler === "rule12a-approval" && /执行|创建|删除|覆盖|移动|下载|提交|配置/i.test(text)) return true;
11
- if (cfg.handler === "rule12c-network" && /下载|网络|curl|境外|clash/i.test(text)) return true;
12
- if (cfg.handler === "rule13a-backup" && /删除|覆盖|迁移|备份/i.test(text)) return true;
13
- if (cfg.handler === "rule2-time" && /时间|今天|昨天|日期/i.test(text)) return true;
14
- if (cfg.handler === "rule7-promise" && /保证|肯定|承诺/i.test(text)) return true;
15
- if (cfg.handler === "rule5-source" && /引用|来源|URL|链接/i.test(text)) return true;
17
+ if (cfg.handler === "rule18-manual-first" && dshKeywordsRe().test(text)) return true;
18
+ if (cfg.handler === "rule12b-skill" && activateSkillRe().test(text)) return true;
19
+ if (cfg.handler === "rule12a-approval" && activateApprovalRe().test(text)) return true;
20
+ if (cfg.handler === "rule12c-network" && activateNetworkRe().test(text)) return true;
21
+ if (cfg.handler === "rule13a-backup" && activateBackupRe().test(text)) return true;
22
+ if (cfg.handler === "rule2-time" && activateTimeRe().test(text)) return true;
23
+ if (cfg.handler === "rule7-promise" && activatePromiseRe().test(text)) return true;
24
+ if (cfg.handler === "rule5-source" && activateSourceRe().test(text)) return true;
16
25
  if (cfg.handler === "rule11-language" && CJK_PRESENT.test(text)) return true;
17
26
  return false;
18
27
  });