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.
package/lib/core/state.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // state.js - 插件运行时状态(内存态)。
2
2
  // 所有会话级状态以 sessionId 为 key;turn 级状态在 turn/start 重置。
3
- import { readFileSync, writeFileSync, statSync } from "node:fs";
3
+ import { appendFileSync, readFileSync, writeFileSync, statSync } from "node:fs";
4
4
  import { loadRules } from "./parser.js";
5
5
  import { agentsFilePath, disabledRulesFilePath, turnCardsFilePath } from "./paths.js";
6
6
  import { understandAll } from "./understander.js";
@@ -351,11 +351,47 @@ export function loadTurnCardsFromDisk() {
351
351
  }
352
352
  }
353
353
 
354
- /** 保存卡片索引到磁盘(数组化 + 上限裁剪;失败静默——卡片是增强层) */
354
+ /** 判例保护(2026-09-08 第二批 §1.1-①):带标卡判定——blocks[].label 或顶层 label */
355
+ export function hasCardLabel(card) {
356
+ if (!card || typeof card !== "object") return false;
357
+ if (card.label) return true;
358
+ return Array.isArray(card.blocks) && card.blocks.some((b) => b && typeof b === "object" && b.label);
359
+ }
360
+
361
+ /** 被裁卡片归档文件(判例=教学数据;归档文件不参与轮转,只追加) */
362
+ export function turnCardsArchiveFilePath() {
363
+ return join(dshHome(), "rule-engine-turn-cards-archive.jsonl");
364
+ }
365
+
366
+ function archiveTurnCards(dropped) {
367
+ try {
368
+ const lines = dropped
369
+ .map(([messageId, card]) => JSON.stringify({ ts: new Date().toISOString(), messageId, card }))
370
+ .join("\n");
371
+ if (lines) appendFileSync(turnCardsArchiveFilePath(), lines + "\n", "utf8");
372
+ } catch {
373
+ // 归档失败不阻断(卡片持久化本身容错)
374
+ }
375
+ }
376
+
377
+ /** 保存卡片索引到磁盘(数组化 + 上限裁剪;失败静默——卡片是增强层)
378
+ * 裁剪保护(2026-09-08):候选集先排除带标卡——先裁最旧无标卡;无标耗尽后才按最旧裁带标卡;
379
+ * 被裁卡裁剪前完整 JSON 追加写入归档 jsonl(不参与轮转)。 */
355
380
  export function saveTurnCardsToDisk(state, max = TURN_CARDS_MAX) {
356
381
  try {
357
382
  let entries = [...(state.cardByMessage || new Map()).entries()];
358
- if (entries.length > max) entries = entries.slice(entries.length - max);
383
+ if (entries.length > max) {
384
+ const overflow = entries.length - max;
385
+ const plain = entries.filter(([, c]) => !hasCardLabel(c));
386
+ const labeled = entries.filter(([, c]) => hasCardLabel(c));
387
+ const dropPlain = plain.slice(0, Math.min(overflow, plain.length));
388
+ const rest = overflow - dropPlain.length;
389
+ const dropLabeled = rest > 0 ? labeled.slice(0, rest) : [];
390
+ const dropped = [...dropPlain, ...dropLabeled];
391
+ if (dropped.length) archiveTurnCards(dropped);
392
+ const dropSet = new Set(dropped.map(([id]) => id));
393
+ entries = entries.filter(([id]) => !dropSet.has(id));
394
+ }
359
395
  writeFileSync(turnCardsFilePath(), JSON.stringify(entries.map(([messageId, card]) => ({ messageId, card })), null, 2) + "\n", "utf8");
360
396
  } catch {
361
397
  // 持久化失败不阻断主流程(卡片是展示层增强)
@@ -1,16 +1,37 @@
1
1
  // text-detect.js - 输出文本检测(B 级纠察 + 部分 D 级自证触发)。
2
2
  // 官方架构下 assistant/message 无法拦下不发,因此这里做「必发现、必记账、可注入纠正」。
3
+ import { getMessage, hasMessage } from "../messages.js";
3
4
  import {
4
5
  CJK_RE,
5
- PROMISE_WORDS,
6
- SOURCE_MARK,
7
- TIME_WORDS,
8
- HISTORIC_DATE_RE,
9
- EVIDENCE_MARK_RE,
6
+ promiseWordsRe,
7
+ sourceMarkRe,
8
+ timeWordsRe,
9
+ historicDateRe,
10
+ evidenceMarkRe,
10
11
  URL_RE,
11
- TECH_TERM_RE,
12
- TERM_EXPLANATION_RE,
13
- SUGGEST_RE,
12
+ techTermRe,
13
+ termExplanationRe,
14
+ suggestRe,
15
+ criticismShapeRe,
16
+ criticismWeakRe,
17
+ execDirectiveRe,
18
+ deliveryClaimRe,
19
+ internalRefRe,
20
+ verifyIntentRe,
21
+ emptyTalkRe,
22
+ deliveryNoVerifyRe,
23
+ verifyEvidenceRe,
24
+ mountMentionRe,
25
+ mountAuditOkRe,
26
+ scopeOverreachRe,
27
+ scopeBoundedRe,
28
+ apologyOnlyRe,
29
+ apologyWithCauseRe,
30
+ versionRecordMentionRe,
31
+ versionRecordOnlyRe,
32
+ versionSyncOkRe,
33
+ patMap,
34
+ patNum,
14
35
  isNegatingSuggestion,
15
36
  isQuoteOrParaphraseContext,
16
37
  isReadOnlyTool
@@ -20,20 +41,63 @@ import { detectOverengineeringText } from "./overengineering.js";
20
41
  // F2(2026-08-28 阶段三):交付声明强模式(规则 23④ verify-gap 词面)——
21
42
  // 裸"完成"太宽("完成社区检索/尚未完成/正在完成"误触)→ 强完成声明 + 否定/进行态排除。
22
43
  // 模块级导出(纯函数模块,可测试锁定);LLM 裁决层(deliverSuspects)兜底不变。
23
- // 用户批评形态(规则 22② 机器提示,2026-09-022026-09-02 实弹修正分层):
24
- // CRITICISM_RE(STRONG)= 辱骂词/连续问叹符 → 确定性批评,直接 B 级提醒(零成本零延迟);
25
- // CRITICISM_WEAK_RE(WEAK) = 反问/责问形态 → 只产嫌疑(awaitingJudge),交 judge 裁决,
26
- // 确认批评才提醒、拿不准不打扰——词表只产嫌疑、判定权归模型(兑现 v4.107/v4.113 拍板)。
27
- // 检测对象=用户消息(session.lastUserText),命中=审计+注入"批评≠授权"提醒(B 级,不拦截)
28
- export const CRITICISM_RE =
29
- /(?:草泥马|你妈的|你妈|傻[逼B Xx]|煞笔|智障|神经病|你聋了|你瞎了|你疯了|你傻了|蠢货|废物|滚蛋|[??]{3,}|[!!]{4,})/;
44
+ // 用户批评形态(规则 22②;2026-09-02 建、2026-09-08 第二批 §1.3 重构为 A″):
45
+ // A″ = 语义主判、无通用词表、宽松触发、行为闸:
46
+ // - 通用层只留**语言无关形态**:连续问号/叹号(CRITICISM_SHAPE_RE)+ 英文全大写比率(hasHighCapsRatio);
47
+ // - 辱骂词枚举**迁出**到本机 rule-engine.json 的 criticismPersonal(发布面不含个人话术),经 setCriticismPersonal 注入;
48
+ // - 机器层只产"疑似"信号(不再直接产 correct),裁决归 LLM 层(judge/deliverSuspects 兜底);
49
+ // - 行为闸在 index.js:命中疑似 → 冻结本回合写类工具 + 注入四段模板(停止/归因四问/三件套/等指令)。
50
+ // A″ 强形态正则:P8 小批 C 起经 patterns.js 的 criticismShapeRe() 取(内置默认含全角/半角问号叹号形态,可配置)
51
+
52
+
53
+ /** 本机辱骂词表(criticismPersonal 注入;空 = 不启用——通用发布面恒为空) */
54
+ let personalCriticism = [];
55
+ export function setCriticismPersonal(words) {
56
+ personalCriticism = Array.isArray(words)
57
+ ? words.filter((w) => typeof w === "string" && w.trim().length > 0)
58
+ : [];
59
+ }
60
+ export function getCriticismPersonal() {
61
+ return [...personalCriticism];
62
+ }
63
+ function personalCriticismRe() {
64
+ if (!personalCriticism.length) return null;
65
+ const alts = personalCriticism.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
66
+ return new RegExp(`(?:${alts.join("|")})`);
67
+ }
68
+
69
+ /** 情绪强度信号:英文全大写比率 ≥ 阈值(默认 0.6,经 patterns.criticism_caps_ratio 可配置)且字母数 ≥6(语言无关的"喊叫"形态) */
70
+ export function hasHighCapsRatio(text) {
71
+ const t = String(text || "");
72
+ const letters = t.replace(/[^A-Za-z]/g, "");
73
+ if (letters.length < 6) return false;
74
+ const caps = letters.replace(/[^A-Z]/g, "");
75
+ return caps.length / letters.length >= patNum("criticism_caps_ratio");
76
+ }
77
+
78
+ /** 批评疑似检测(A″):{shape, personal, weak, suspect}——机器只产疑似,判定权归模型 */
79
+ export function criticismSignals(text) {
80
+ const t = String(text || "");
81
+ const shape = criticismShapeRe().test(t) || hasHighCapsRatio(t);
82
+ const personal = (personalCriticismRe() || /$^/).test(t);
83
+ const weak = criticismWeakRe().test(t);
84
+ return { shape, personal, weak, suspect: shape || personal ? "strong" : weak ? "weak" : null };
85
+ }
30
86
 
31
87
  // WEAK 嫌疑集:反问/责问/否定比较形态("怎么还在做/又错了/这不对吧/你是不是又…")。
32
88
  // 允许偏宽(普通技术疑问如"怎么用"也会进入嫌疑)——判定权在 judge:模型确认才是提醒,拿不准=不打扰。
33
- export const CRITICISM_WEAK_RE =
34
- /(?:怎么(?:又|还|居然|仍然|老是|总)?[^,。!?]{0,14}(?:了|呢|啊|?|!|!)?|你(?:又|还|居然|咋|怎么)[^,。!?]{0,12}(?:了|呢|啊|吧)?|(?:又|还|总是|老是|仍然|居然)[^,。!?]{0,12}(?:了|呢|啊|吧)?|这(?:不|有点|根本|完全|哪)?(?:对|行|合理|应该|像话)(?:吧|么|吗|啊|呢)?|你(?:是不是|难道|难道说|该不会)[^,。!?]{0,10}(?:了|呢|吧)?|(?:搞什么|干什么|什么情况|怎么回事|搞砸|搞错了|犯什么错))/;
89
+ // P8 小批 C:经 patterns.js 的 criticismWeakRe() 取(内置默认=英文最小集;中文形态由本机 patterns 注入)。
35
90
 
36
- export const DELIVERY_RE = /(?:已完成|全部(?:[^\s,。;!?]{0,12})完成|已[^\s,。;!?]{0,8}完成|修复完成|落盘完成|验证[^\s,。;!?]{0,6}(?:通过|成功)|全部通过|全部[^\s,。;!?]{0,10}通过|已通过|已修复|搞定)(?:\s*了|!|!|,[^。]*)?(?![^。]*(?:尚未|没有|未|没|还没|未完|待做|待完成))/;
91
+ /**
92
+ * A″ 反向检查(2026-09-09 第三批,A″ 后果分级):用户消息是否含明确执行指令词。
93
+ * 用途:弱信号(偏宽形态)遇执行指令 → 只留痕、不注入提示
94
+ *(避免把"…是更新还是写…"这类正常指令读成责问)。强信号不受本检查影响,仍按行为闸冻结。
95
+ */
96
+ export function hasExplicitExecWord(text) {
97
+ return execDirectiveRe().test(String(text || ""));
98
+ }
99
+
100
+ export const DELIVERY_RE = () => deliveryClaimRe();
37
101
 
38
102
  // 2026-09-06 0.6.x(tsk_527d222c 第一期):路径缩写 B 级检测——回复含缩写路径(reports\ / ~/.dsh / %USERPROFILE%)
39
103
  // 且非完整盘符形态 → 提醒写完整绝对路径(规则 14⑤ 机器化第一层;WEAK 形态:留痕+注入,不拦截)
@@ -57,32 +121,22 @@ export function extractAssistantText(message) {
57
121
  return "";
58
122
  }
59
123
 
60
- const SELF_CERT_HINTS = {
61
- "14": { re: /总结|汇报|完成/, reason: "请按规则 14 一次性完整汇报,区分事实与推断" },
62
- // "22" 词表(text-detect):检测**我的回复文本**里的空话(B/D 级语义嫌疑),判定对象 = assistant 输出;
63
- // 与 lexicon.js 的 ACTION/QUESTION/STATUS 等词表**不同层**(lexicon 判**用户消息**意图、服务规则 22 时序判定)。
64
- // 二者用途不同、无重复:本表 = 输出侧"我说了空话";lexicon = 输入侧"用户说了什么"。改动需两处各自成对。
65
- "22": { re: /我记下了|记住了|收到,我记下了|放心,我记住了/i, reason: "检测到「我记下了」类空话;正确动作是落盘执行并汇报" },
66
- "23": { re: /完成|交付/, reason: "交付/完成声明未附运行时验证证据" },
67
- "16": { re: /建议|优化|更优方案|推理档位|档位/, reason: "请按规则 16 用绑定检查格式提建议(或给档位建议并说明理由),避免频繁打断" },
68
- "12E": { re: /重启|GUI|登录|界面|窗口/, reason: "请按规则 12E 先确认界面真实存在并声明能力边界" },
69
- "26": { re: /(?=发布|Release|附件)(?!(?:Attach binaries|正式 Asset|release\.assets))/i, reason: "请按规则 26 确认正式 Release Asset(release.assets)并附 tgz/zip" },
70
- "10": { re: /新会话|旧会话|旧项目|上下文恢复/, reason: "请按规则 10 自证:如实说明默认不自动继承,按优先级恢复并标注来源" },
71
- "12C": { re: /下载|网络|curl|Clash|被墙/, reason: "请按规则 12C 自证:需代理先告知、失败即停、下载后校验、排查先测连通性" },
72
- "13B": { re: /会话文件|会话修复|fromRestore|Inbox 回放|孤儿扫描/, reason: "请按规则 13B 自证:会话替换前用户完全退出、三层验证一次跑完、mtime 判断" },
73
- "15": { re: /文件版本|文件用途|新旧|版本.*处置|保留文件/, reason: "请按规则 15 自证:不凭 mtime/文件名判断新旧,无法确定版本/用途时保留并询问" },
74
- "19": { re: /学到新.*DSH|踩到新坑|踩坑.*沉淀|手册有误|知识必沉淀/, reason: "请按规则 19 自证:按①-⑧沉淀并当次汇报正文同步位置" },
75
- "28": { re: /新建.*文件|放入.*目录|工作区.*归类/, reason: "请按规则 28 自证:新文件按工作区目录索引归类,不确定则查 README 或放 _inbox" },
76
- "29": { re: /依赖闭包|node_modules.*验证|动过.*node_modules/, reason: "请按规则 29 自证:重启前验证依赖闭包(关键包非空/顶层 junction 完整/无 dangling)" },
77
- "30": { re: /变更验证|验证证据|不破坏.*依赖/, reason: "请按规则 30 自证:执行前证明不破坏依赖+已授权,完成后附验证证据" },
78
- // 规则 31(2026-09-01 用户拍板):提到"撞墙/盲试"类表述 → D 级自证:写明验证目标→撞了什么/为什么撞→结论来源
79
- "31": { re: /撞(?:了)?南墙|撞墙|盲试|乱撞|连续盲试/, reason: "规则 31 自证:请写明「验证目标→撞了什么/为什么撞→结论来源(手册位置/源码行号)」;不连续盲试" }
80
- };
124
+ /** 条款自证触发词的中文提示文案(reason 留代码;正则在配置层 self_cert_hints) */
125
+ // 判据 A(2026-09-09):规则号清单**不再硬编码**——提示 key 由约定推导(self-cert.<ruleId>),
126
+ // 是否存在由 messages 层决定。规则号怎么变,改本机配置即可。
127
+
128
+ /** 取某条款的自证提示(正则经 patMap 从配置层取;无配置/无文案 = null) */
129
+ function selfCertHint(ruleId) {
130
+ const src = patMap("self_cert_hints")[String(ruleId)];
131
+ const reasonKey = `self-cert.${ruleId}`;
132
+ if (typeof src !== "string" || !src || !hasMessage(reasonKey)) return null;
133
+ return { re: new RegExp(src, "i"), reason: getMessage(reasonKey) };
134
+ }
81
135
 
82
136
  // 规则 31(2026-09-01 用户拍板):内部文档引用锚点词("文档"仅在后随"写了/记载…"语境命中,防泛化误报)
83
- const INTERNAL_REF_RE = /(?:手册|踩坑\s*\d+|条款\s*\d+|第\s*\d+\s*章|SKILL\.md|AGENTS\.md|源码|README|文档[^\n]{0,12}(?:写了|记载|记录|写明|说明))/i;
137
+ const INTERNAL_REF_RE = () => internalRefRe();
84
138
  // 验证目标启发式:思维链含这些词 → 大概率是有序查证(防规则 31③误报)
85
- const VERIFY_INTENT_RE = /(?:验证|查证|核对|目标|交叉|对照|确认)/;
139
+ const VERIFY_INTENT_RE = () => verifyIntentRe();
86
140
 
87
141
  /** 简单判断一段文本是否以英文为主 */
88
142
  function isMostlyEnglish(text) {
@@ -117,10 +171,10 @@ export function isSelfCertified(text, ruleId) {
117
171
  * @returns {Array<{ruleId,title,kind,reason}>}
118
172
  */
119
173
  export function detectTimeRule(session, text, timeCfg) {
120
- const hasNow = TIME_WORDS.test(text);
121
- const hasHist = HISTORIC_DATE_RE.test(text);
174
+ const hasNow = timeWordsRe().test(text);
175
+ const hasHist = historicDateRe().test(text);
122
176
  if (!timeCfg || (!hasNow && !hasHist)) return [];
123
- if (isQuoteOrParaphraseContext(text, TIME_WORDS) || isQuoteOrParaphraseContext(text, HISTORIC_DATE_RE)) return [];
177
+ if (isQuoteOrParaphraseContext(text, timeWordsRe()) || isQuoteOrParaphraseContext(text, historicDateRe())) return [];
124
178
  // A1(2026-09-03 拆组):当下时间词才要求 Get-Date 核对(①);历史日期只走证据锚(②),
125
179
  // 不再要求 Get-Date——消除"引用历史日期必判未核对"的误报(规则 2②:每时间点绑定自己证据)。
126
180
  if (hasNow && !session?.turn?.getDateSeen) {
@@ -131,7 +185,7 @@ export function detectTimeRule(session, text, timeCfg) {
131
185
  reason: "回答出现具体时间词/日期,但本回合未先调用 Get-Date 核对"
132
186
  }];
133
187
  }
134
- if ((hasNow || hasHist) && !EVIDENCE_MARK_RE.test(text)) {
188
+ if ((hasNow || hasHist) && !evidenceMarkRe().test(text)) {
135
189
  return [{
136
190
  ruleId: "2",
137
191
  title: timeCfg.title,
@@ -164,15 +218,15 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
164
218
  // F1(2026-08-28 阶段三):本检测仅作"判定",投递时机由调用方在 turn/end 复核(见 detectTimeRule 注释)。
165
219
  // B3(2026-08-29):引述/转述语境的时间词不触发("你昨天说…"是转述,不是我的时间表述);
166
220
  // 第一人称"我说昨天…"仍触发(转述不了自己)。
167
- if (timeCfg && (TIME_WORDS.test(text) || HISTORIC_DATE_RE.test(text)) && !isQuoteOrParaphraseContext(text, TIME_WORDS) && !isQuoteOrParaphraseContext(text, HISTORIC_DATE_RE)) {
168
- if (!session.turn.getDateSeen && TIME_WORDS.test(text)) {
221
+ if (timeCfg && (timeWordsRe().test(text) || historicDateRe().test(text)) && !isQuoteOrParaphraseContext(text, timeWordsRe()) && !isQuoteOrParaphraseContext(text, historicDateRe())) {
222
+ if (!session.turn.getDateSeen && timeWordsRe().test(text)) {
169
223
  hits.push({
170
224
  ruleId: "2",
171
225
  title: timeCfg.title,
172
226
  kind: "correct",
173
227
  reason: "回答出现具体时间词/日期,但本回合未先调用 Get-Date 核对"
174
228
  });
175
- } else if (!EVIDENCE_MARK_RE.test(text)) {
229
+ } else if (!evidenceMarkRe().test(text)) {
176
230
  hits.push({
177
231
  ruleId: "2",
178
232
  title: timeCfg.title,
@@ -184,7 +238,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
184
238
 
185
239
  const promiseCfg = byId.get("7");
186
240
  // v0.5.7 P0.5-5:承诺词处于引述/改写语境("把'保证'改成…")→ 引述不是承诺,不触发
187
- if (promiseCfg && PROMISE_WORDS.test(text) && !isQuoteOrParaphraseContext(text, PROMISE_WORDS)) {
241
+ if (promiseCfg && promiseWordsRe().test(text) && !isQuoteOrParaphraseContext(text, promiseWordsRe())) {
188
242
  hits.push({
189
243
  ruleId: "7",
190
244
  title: promiseCfg.title,
@@ -204,7 +258,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
204
258
  }
205
259
 
206
260
  const sourceCfg = byId.get("5");
207
- if (sourceCfg && !SOURCE_MARK.test(text)) {
261
+ if (sourceCfg && !sourceMarkRe().test(text)) {
208
262
  if (URL_RE.test(text)) {
209
263
  hits.push({
210
264
  ruleId: "5",
@@ -212,7 +266,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
212
266
  kind: "correct",
213
267
  reason: "回答包含 URL 但未标注出处/来源"
214
268
  });
215
- } else if (INTERNAL_REF_RE.test(text) && !isQuoteOrParaphraseContext(text, INTERNAL_REF_RE)) {
269
+ } else if (INTERNAL_REF_RE().test(text) && !isQuoteOrParaphraseContext(text, INTERNAL_REF_RE())) {
216
270
  // 规则 5 扩展(2026-09-01 用户拍板):内部文档引用(手册/踩坑/条款/源码…)须有依据——
217
271
  // 近 rule5Window 回合(默认 3;配置层 rule-engine.json `rule5SourceWindow` 可覆盖,2026-09-03
218
272
  // 通用化修正:本机偏好走配置、通用默认保持 3)无对应 read/grep 时提示(B 级:留痕+注入,不拦截)。
@@ -229,24 +283,28 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
229
283
  }
230
284
  }
231
285
 
232
- // 规则 22② 机器提示(2026-09-02;实弹修正:STRONG 直接提醒 / WEAK 只产嫌疑交 judge 裁决)
286
+ // 规则 22② 机器提示(2026-09-02 建;2026-09-08 第二批 §1.3 重构 A″:机器只产疑似、裁决归 LLM;行为闸在 index.js)
233
287
  const cfg22 = byId.get("22");
234
288
  if (cfg22 && session.lastUserText) {
235
289
  const critText = session.lastUserText;
236
- if (CRITICISM_RE.test(critText) && !isQuoteOrParaphraseContext(critText, CRITICISM_RE)) {
290
+ const sig = criticismSignals(critText);
291
+ if (sig.suspect === "strong" && !isQuoteOrParaphraseContext(critText, criticismShapeRe())) {
237
292
  hits.push({
238
293
  ruleId: "22",
239
294
  title: cfg22.title,
240
- kind: "correct",
241
- reason: "用户消息含确定性批评形态(规则 22②/手册 5b-2):用户批评不构成任何授权——被批评后应停、按归因四问产出三件套、等指令;禁止因批评启动任何变更"
295
+ kind: "self-certify",
296
+ mode: "criticism",
297
+ suspect: "strong",
298
+ reason: getMessage("criticism.strong")
242
299
  });
243
- } else if (CRITICISM_WEAK_RE.test(critText) && !isQuoteOrParaphraseContext(critText, CRITICISM_WEAK_RE)) {
300
+ } else if (sig.suspect === "weak" && !isQuoteOrParaphraseContext(critText, criticismWeakRe())) {
244
301
  hits.push({
245
302
  ruleId: "22",
246
303
  title: cfg22.title,
247
304
  kind: "self-certify",
248
305
  mode: "criticism",
249
- reason: "用户消息疑似批评/质问形态(规则 22②/手册 5b-2,待裁决):若确属批评——不构成任何授权,被批评后应停、按归因四问产出三件套、等指令;禁止因批评启动任何变更"
306
+ suspect: "weak",
307
+ reason: getMessage("criticism.weak")
250
308
  });
251
309
  }
252
310
  }
@@ -263,7 +321,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
263
321
  }
264
322
  }
265
323
  const repeated = [...counts.entries()].find(([, c]) => c >= 3);
266
- if (repeated && !VERIFY_INTENT_RE.test(reasoningText)) {
324
+ if (repeated && !VERIFY_INTENT_RE().test(reasoningText)) {
267
325
  hits.push({
268
326
  ruleId: "31",
269
327
  title: rule31Cfg.title,
@@ -288,7 +346,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
288
346
 
289
347
  // 批次 5:术语密度(规则 11③——零基础表达:中文提问 + 术语 + 无解释伴随 → 自证)
290
348
  const termCfg = byId.get("11");
291
- if (termCfg && session.lastUserText && CJK_RE.test(session.lastUserText) && TECH_TERM_RE.test(text) && !TERM_EXPLANATION_RE.test(text)) {
349
+ if (termCfg && session.lastUserText && CJK_RE.test(session.lastUserText) && techTermRe().test(text) && !termExplanationRe().test(text)) {
292
350
  hits.push({
293
351
  ruleId: "11",
294
352
  title: termCfg.title,
@@ -301,7 +359,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
301
359
  // v0.5.7 P0.5-6:否定/合规声明语境("不再/无 XX")不计数——那不是重复推销(粗筛省钱,
302
360
  // 语义判断权仍归裁决器)
303
361
  const suggestCfg = byId.get("16");
304
- if (suggestCfg && SUGGEST_RE.test(text) && !isNegatingSuggestion(text)) {
362
+ if (suggestCfg && suggestRe().test(text) && !isNegatingSuggestion(text)) {
305
363
  session.suggestionCounts = session.suggestionCounts || {};
306
364
  session.suggestionCounts.general = (session.suggestionCounts.general || 0) + 1;
307
365
  if (session.suggestionCounts.general >= 2) {
@@ -315,7 +373,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
315
373
  }
316
374
 
317
375
  const directCfg = byId.get("22");
318
- if (directCfg && /我记下了|记住了|收到,我记下了|放心,我记住了/i.test(text)) {
376
+ if (directCfg && emptyTalkRe().test(text)) {
319
377
  hits.push({
320
378
  ruleId: "22",
321
379
  title: directCfg.title,
@@ -325,7 +383,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
325
383
  }
326
384
 
327
385
  const verifyCfg = byId.get("23");
328
- if (verifyCfg && /完成|交付/.test(text) && !/运行时验证|mock|启动|实测|测试|验证/.test(text)) {
386
+ if (verifyCfg && deliveryNoVerifyRe().test(text) && !verifyEvidenceRe().test(text)) {
329
387
  hits.push({
330
388
  ruleId: "23",
331
389
  title: verifyCfg.title,
@@ -335,18 +393,18 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
335
393
  }
336
394
 
337
395
  const mountCfg = byId.get("27");
338
- if (mountCfg && mountRevision > (session.mountAuditRevision || 0) && /重启|装配|挂载/.test(text) && !/审计通过|全量审计|MOUNT CONSISTENT|DUPLICATES FOUND|audit-mount-consistency/.test(text)) {
396
+ if (mountCfg && mountRevision > (session.mountAuditRevision || 0) && mountMentionRe().test(text) && !mountAuditOkRe().test(text)) {
339
397
  hits.push({
340
398
  ruleId: "27",
341
399
  title: mountCfg.title,
342
400
  kind: "self-certify",
343
- reason: `规则 27:插件装配已变更(mountRevision=${mountRevision})且本会话未通过全量审计,重启/继续前请先运行 audit-mount-consistency.mjs`
401
+ reason: `Rule 27: plugin assembly changed (mountRevision=${mountRevision}) and this session has not passed a full audit. ${getMessage("pitfall.mount-audit")}`
344
402
  });
345
403
  }
346
404
 
347
405
  // 规则 21:选项即边界——检测“补充/增加”类越界表述,未声明仅按勾选时触发自证
348
406
  const scopeCfg = byId.get("21");
349
- if (scopeCfg && /(?:我补充|顺便补充|额外补充|我增加|顺便加|额外加|多加|自行纳入)/.test(text) && !/仅按勾选|只实现被选项|未选项|单独确认|再次征求/.test(text)) {
407
+ if (scopeCfg && scopeOverreachRe().test(text) && !scopeBoundedRe().test(text)) {
350
408
  hits.push({
351
409
  ruleId: "21",
352
410
  title: scopeCfg.title,
@@ -357,7 +415,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
357
415
 
358
416
  // 规则 22:被指出错误后需主动给出原因/改正/防再犯,不能只道歉
359
417
  const errorCfg = byId.get("22");
360
- if (errorCfg && /(?:抱歉|对不起|我错了|这是我的错|失误)/.test(text) && !/(?:原因|改正|防再犯|避免|机制|解法|修正)/.test(text)) {
418
+ if (errorCfg && apologyOnlyRe().test(text) && !apologyWithCauseRe().test(text)) {
361
419
  hits.push({
362
420
  ruleId: "22",
363
421
  title: errorCfg.title,
@@ -368,7 +426,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
368
426
 
369
427
  // 规则 19⑥:版本记录≠完成——只报版本记录未列正文同步时触发自证
370
428
  const rule19Cfg = byId.get("19");
371
- if (rule19Cfg && /版本记录|v\d+\.\d+/.test(text) && /已记入版本记录|版本记录已|只加了版本记录|只加版本记录/.test(text) && !/正文|同步|无需同步|无需改正文/.test(text)) {
429
+ if (rule19Cfg && versionRecordMentionRe().test(text) && versionRecordOnlyRe().test(text) && !versionSyncOkRe().test(text)) {
372
430
  hits.push({
373
431
  ruleId: "19",
374
432
  title: rule19Cfg.title,
@@ -381,7 +439,7 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
381
439
  for (const cfg of configs) {
382
440
  if (cfg.confidence === "low") continue;
383
441
  if (!(cfg.actions || []).includes("self-certify")) continue;
384
- const hint = SELF_CERT_HINTS[String(cfg.ruleId)];
442
+ const hint = selfCertHint(cfg.ruleId);
385
443
  if (!hint) continue;
386
444
  if (hint.re.test(text)) {
387
445
  hits.push({
@@ -5,13 +5,13 @@ import { extractElements, levelFromTitle } from "./parser.js";
5
5
  import {
6
6
  BOM_WRITE,
7
7
  DESTRUCTIVE_CMD,
8
- DSH_KEYWORDS_RE,
8
+ dshKeywordsRe,
9
9
  INLINE_CMD,
10
10
  SENSITIVE_CMD,
11
- TIME_WORDS,
12
- PROMISE_WORDS,
11
+ timeWordsRe,
12
+ promiseWordsRe,
13
13
  URL_RE,
14
- SOURCE_MARK
14
+ sourceMarkRe
15
15
  } from "./patterns.js";
16
16
 
17
17
  // 2026-08-31(残余1 剥离,用户定稿):本机默认偏好表从代码下沉到本机配置——
@@ -207,15 +207,15 @@ export function understandAll(rules, opts = {}) {
207
207
  return configs;
208
208
  }
209
209
 
210
- /** 导出常用正则供测试/调试 */
210
+ /** 导出常用正则供测试/调试(P8 小批 B:可配置项经访问器取当前生效值) */
211
211
  export const REGEX = {
212
212
  INLINE_CMD,
213
213
  BOM_WRITE,
214
214
  DESTRUCTIVE_CMD,
215
215
  SENSITIVE_CMD,
216
- TIME_WORDS,
217
- PROMISE_WORDS,
216
+ get TIME_WORDS() { return timeWordsRe(); },
217
+ get PROMISE_WORDS() { return promiseWordsRe(); },
218
218
  URL_RE,
219
- SOURCE_MARK,
220
- DSH_KEYWORDS_RE
219
+ get SOURCE_MARK() { return sourceMarkRe(); },
220
+ get DSH_KEYWORDS_RE() { return dshKeywordsRe(); }
221
221
  };