dsh-rule-engine 0.5.10 → 0.5.12

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.
@@ -5,15 +5,21 @@ import {
5
5
  PROMISE_WORDS,
6
6
  SOURCE_MARK,
7
7
  TIME_WORDS,
8
+ EVIDENCE_MARK_RE,
8
9
  URL_RE,
9
10
  TECH_TERM_RE,
10
11
  TERM_EXPLANATION_RE,
11
12
  SUGGEST_RE,
12
13
  isNegatingSuggestion,
13
- isPromiseQuoteContext
14
+ isQuoteOrParaphraseContext
14
15
  } from "./patterns.js";
15
16
  import { detectOverengineeringText } from "./overengineering.js";
16
17
 
18
+ // F2(2026-08-28 阶段三):交付声明强模式(规则 23④ verify-gap 词面)——
19
+ // 裸"完成"太宽("完成社区检索/尚未完成/正在完成"误触)→ 强完成声明 + 否定/进行态排除。
20
+ // 模块级导出(纯函数模块,可测试锁定);LLM 裁决层(deliverSuspects)兜底不变。
21
+ export const DELIVERY_RE = /(?:已完成|全部(?:[^\s,。;!?]{0,12})完成|已[^\s,。;!?]{0,8}完成|修复完成|落盘完成|验证[^\s,。;!?]{0,6}(?:通过|成功)|全部通过|全部[^\s,。;!?]{0,10}通过|已通过|已修复|搞定)(?:\s*了|!|!|,[^。]*)?(?![^。]*(?:尚未|没有|未|没|还没|未完|待做|待完成))/;
22
+
17
23
  /** 从 assistant message 内容中提取纯文本 */
18
24
  export function extractAssistantText(message) {
19
25
  if (!message) return "";
@@ -21,8 +27,7 @@ export function extractAssistantText(message) {
21
27
  if (typeof content === "string") return content;
22
28
  if (Array.isArray(content)) {
23
29
  return content
24
- .map((b) => (b && typeof b === "object" && b.type === "text" ? b.text : ""))
25
- .filter(Boolean)
30
+ .map((b) => (b && typeof b === "object" && b.type === "text" ? b.text : "")) .filter(Boolean)
26
31
  .join("\n");
27
32
  }
28
33
  return "";
@@ -30,6 +35,9 @@ export function extractAssistantText(message) {
30
35
 
31
36
  const SELF_CERT_HINTS = {
32
37
  "14": { re: /总结|汇报|完成/, reason: "请按规则 14 一次性完整汇报,区分事实与推断" },
38
+ // "22" 词表(text-detect):检测**我的回复文本**里的空话(B/D 级语义嫌疑),判定对象 = assistant 输出;
39
+ // 与 lexicon.js 的 ACTION/QUESTION/STATUS 等词表**不同层**(lexicon 判**用户消息**意图、服务规则 22 时序判定)。
40
+ // 二者用途不同、无重复:本表 = 输出侧"我说了空话";lexicon = 输入侧"用户说了什么"。改动需两处各自成对。
33
41
  "22": { re: /我记下了|记住了|收到,我记下了|放心,我记住了/i, reason: "检测到「我记下了」类空话;正确动作是落盘执行并汇报" },
34
42
  "23": { re: /完成|交付/, reason: "交付/完成声明未附运行时验证证据" },
35
43
  "16": { re: /建议|优化|更优方案|推理档位|档位/, reason: "请按规则 16 用绑定检查格式提建议(或给档位建议并说明理由),避免频繁打断" },
@@ -67,8 +75,42 @@ export function isSelfCertified(text, ruleId) {
67
75
  return re.test(text);
68
76
  }
69
77
 
78
+ /**
79
+ * 规则 2 时间核对的独立判定(F1,2026-08-28 阶段三):
80
+ * 原实现嵌在 detectViolations(assistant/message 时调)——但 Get-Date 工具常在本回合后续步骤
81
+ * 才执行(事故实弹:correct 04:52:23.061 早于 Get-Date 放行 .068)→ 真调了也判"未先核对"。
82
+ * 修复:判定时机延迟到 turn/end(getDateSeen 已定案),本判定函数只做判定、不依赖调用时机。
83
+ * @param {object} session 会话状态(turn.getDateSeen 已由 tool/call 置位)
84
+ * @param {string} text assistant 纯文本
85
+ * @param {object} timeCfg 规则 2 配置(byId.get("2"))
86
+ * @returns {Array<{ruleId,title,kind,reason}>}
87
+ */
88
+ export function detectTimeRule(session, text, timeCfg) {
89
+ if (!timeCfg || !TIME_WORDS.test(text) || isQuoteOrParaphraseContext(text, TIME_WORDS)) return [];
90
+ if (!session?.turn?.getDateSeen) {
91
+ return [{
92
+ ruleId: "2",
93
+ title: timeCfg.title,
94
+ kind: "correct",
95
+ reason: "回答出现具体时间词/日期,但本回合未先调用 Get-Date 核对"
96
+ }];
97
+ }
98
+ if (!EVIDENCE_MARK_RE.test(text)) {
99
+ return [{
100
+ ruleId: "2",
101
+ title: timeCfg.title,
102
+ kind: "correct",
103
+ reason: "回答含具体时间词但未附事件证据标注(日志 ts/文件 mtime/进程启动时间等)——Get-Date 当前时间不算过去事件证据(规则 2②)。正确动作:查该事件证据补(来源:…)或删时间词/标【时间未核实】,勿以当前时间替代"
104
+ }];
105
+ }
106
+ return [];
107
+ }
108
+
70
109
  /**
71
110
  * 检测一次 assistant/message 的 B/D 级违规。
111
+ * 规则 2 时间核对不在本函数内(F1,2026-08-28):原实现在此判定,但 Get-Date 工具常在本回合
112
+ * 后续步骤才执行(事故实弹:correct 早于 Get-Date 放行)→ 真调了也判"未先核对"。
113
+ * 现由 index.js 在 turn/end 时机调用 detectTimeRule(getDateSeen 定案后)。
72
114
  * @param {object} options
73
115
  * @param {Array} options.configs 理解配置
74
116
  * @param {object} options.session 会话状态(getSessionState 返回)
@@ -80,18 +122,32 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
80
122
  const byId = new Map(configs.filter((c) => c.confidence !== "low").map((c) => [String(c.ruleId), c]));
81
123
 
82
124
  const timeCfg = byId.get("2");
83
- if (timeCfg && TIME_WORDS.test(text) && !session.turn.getDateSeen) {
84
- hits.push({
85
- ruleId: "2",
86
- title: timeCfg.title,
87
- kind: "correct",
88
- reason: "回答出现时间词/日期,但本回合未先调用 Get-Date 核对"
89
- });
125
+ // v0.5.11(用户定稿):① 只命中具体时间词(TIME_WORDS 为具体词表——"之前/当时"等模糊词不命中,不新增);
126
+ // ② 具体时间词 + 无事件证据标注(日志 ts/文件 mtime/进程启动时间等)→ 违规,Get-Date 当前时间不算事件证据。
127
+ // F1(2026-08-28 阶段三):本检测仅作"判定",投递时机由调用方在 turn/end 复核(见 detectTimeRule 注释)。
128
+ // B3(2026-08-29):引述/转述语境的时间词不触发("你昨天说…"是转述,不是我的时间表述);
129
+ // 第一人称"我说昨天…"仍触发(转述不了自己)。
130
+ if (timeCfg && TIME_WORDS.test(text) && !isQuoteOrParaphraseContext(text, TIME_WORDS)) {
131
+ if (!session.turn.getDateSeen) {
132
+ hits.push({
133
+ ruleId: "2",
134
+ title: timeCfg.title,
135
+ kind: "correct",
136
+ reason: "回答出现具体时间词/日期,但本回合未先调用 Get-Date 核对"
137
+ });
138
+ } else if (TIME_WORDS.test(text) && !EVIDENCE_MARK_RE.test(text)) {
139
+ hits.push({
140
+ ruleId: "2",
141
+ title: timeCfg.title,
142
+ kind: "correct",
143
+ reason: "回答含具体时间词但未附事件证据标注(日志 ts/文件 mtime/进程启动时间等)——Get-Date 当前时间不算过去事件证据(规则 2②)"
144
+ });
145
+ }
90
146
  }
91
147
 
92
148
  const promiseCfg = byId.get("7");
93
149
  // v0.5.7 P0.5-5:承诺词处于引述/改写语境("把'保证'改成…")→ 引述不是承诺,不触发
94
- if (promiseCfg && PROMISE_WORDS.test(text) && !isPromiseQuoteContext(text)) {
150
+ if (promiseCfg && PROMISE_WORDS.test(text) && !isQuoteOrParaphraseContext(text, PROMISE_WORDS)) {
95
151
  hits.push({
96
152
  ruleId: "7",
97
153
  title: promiseCfg.title,
@@ -51,6 +51,9 @@ const ARTIFACT_TOOLS = new Set([
51
51
  // 官方 plan-mode 交互工具(参数为 markdown 计划,批准/继续规划,无文件副作用)
52
52
  "exit_plan_mode",
53
53
  // 开发侧 staging(写 staging 区,dev_* 工具的自然行为)
54
+ // 0.5.11 修正(手册 v3.39 口径):dev_stage 四件套(add/call/promote/demote)= 纳入**敏感
55
+ // 授权检查**(守卫链覆盖),**不是改分类**——统一保持 artifact;授权检查由 guard-core
56
+ // (12A isSensitiveToolCall / 13A / 24 装配判定)链上覆盖,不再各自归类。
54
57
  "dev_stage_add", "dev_stage_call", "dev_stage_promote", "dev_stage_demote",
55
58
  // ESR 工程状态写操作(写工作区记忆存储,engram_store 同族)
56
59
  "esr_task", "esr_node", "esr_link", "esr_claim", "esr_close", "esr_unclaim", "esr_dep", "esr_gc"
@@ -26,7 +26,7 @@ const HANDLER_BY_RULE = {
26
26
  "12C": "rule12c-network",
27
27
  "13A": "rule13a-backup",
28
28
  // 13B 已于 2026-08-24 外移至手册(规则正文删除,映射一并清理,防死映射)
29
- 14: "rule14-report",
29
+ // 14 为纯 D 级自证规则,无对应 handler(0.5.11:删除空转映射 registry)
30
30
  18: "rule18-manual-first",
31
31
  21: "rule21-meta",
32
32
  22: "rule22-7-direct",
@@ -129,7 +129,7 @@ function isOrderedLineInsertion(oldString, newString) {
129
129
  }
130
130
 
131
131
  /** 校验 append/删除式编辑:新增需包含 old_string;删除/缩短时 new_string 可为 old_string 的子串;版本行重编号放行;非表格单行修改放行 */
132
- export function validateEditAppend(oldString, newString) {
132
+ export function validateEditAppend(oldString, newString, uniqueMatch = false) {
133
133
  if (typeof oldString !== "string" || typeof newString !== "string") return { ok: true, errors: [] };
134
134
  if (oldString.length === 0) return { ok: true, errors: [] };
135
135
  if (isVersionRenumber(oldString, newString)) return { ok: true, errors: [] };
@@ -139,6 +139,9 @@ export function validateEditAppend(oldString, newString) {
139
139
  if (isSameLineReplacement(oldString, newString)) return { ok: true, errors: [] };
140
140
  if (isTableRowReplacement(oldString, newString)) return { ok: true, errors: [] };
141
141
  if (isOrderedLineInsertion(oldString, newString)) return { ok: true, errors: [] };
142
+ // 0.5.11(用户定稿):单行整句重写放行——**前提 = old 在原文件唯一匹配**(调用方实测传入);
143
+ // edit 工具语义已保证唯一才替换、位置正确;唯一下单行重写 = 正常语义编辑,不再是"覆盖"。
144
+ if (uniqueMatch && isSingleLine(oldString) && isSingleLine(newString)) return { ok: true, errors: [] };
142
145
  return {
143
146
  ok: false,
144
147
  errors: ["new_string 未包含 old_string,疑似覆盖上一行"]
@@ -146,11 +149,11 @@ export function validateEditAppend(oldString, newString) {
146
149
  }
147
150
 
148
151
  /** 综合校验一次编辑后的文件 */
149
- export function validateEditedFile(originalText, currentText, oldString, newString) {
152
+ export function validateEditedFile(originalText, currentText, oldString, newString, uniqueMatch = false) {
150
153
  const errors = [];
151
154
  const continuity = validateVersionContinuity(currentText);
152
155
  if (!continuity.ok) errors.push(...continuity.errors);
153
- const append = validateEditAppend(oldString, newString);
156
+ const append = validateEditAppend(oldString, newString, uniqueMatch);
154
157
  if (!append.ok) errors.push(...append.errors);
155
158
  return { ok: errors.length === 0, errors };
156
159
  }