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/README.md +161 -6
- package/lib/core/audit.js +21 -4
- package/lib/core/authorization.js +18 -29
- package/lib/core/config.js +4 -1
- package/lib/core/dualtrack-markers.js +61 -0
- package/lib/core/guard-core.js +96 -16
- package/lib/core/intent.js +15 -22
- package/lib/core/lang.js +61 -0
- package/lib/core/lexicon.js +177 -72
- package/lib/core/llm-intent.js +2 -2
- package/lib/core/matcher.js +18 -9
- package/lib/core/patterns.js +344 -46
- package/lib/core/quality-ledger.js +124 -0
- package/lib/core/state.js +39 -3
- package/lib/core/text-detect.js +126 -68
- package/lib/core/understander.js +9 -9
- package/lib/index.js +119 -6
- package/lib/messages.js +134 -0
- package/package.json +3 -2
- package/scripts/check-tool-coverage.mjs +3 -1
- package/scripts/dualtrack-check.mjs +335 -0
- package/scripts/dualtrack-whitelist.json +7 -0
- package/scripts/local-residue-scan.mjs +10 -4
- package/scripts/plugins.json +10 -0
- package/scripts/release-plugin.mjs +23 -5
- package/scripts/verify-all.mjs +4 -0
- package/scripts/verify-v474.mjs +8 -8
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)
|
|
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
|
// 持久化失败不阻断主流程(卡片是展示层增强)
|
package/lib/core/text-detect.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
promiseWordsRe,
|
|
7
|
+
sourceMarkRe,
|
|
8
|
+
timeWordsRe,
|
|
9
|
+
historicDateRe,
|
|
10
|
+
evidenceMarkRe,
|
|
10
11
|
URL_RE,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
/(?:怎么(?:又|还|居然|仍然|老是|总)?[^,。!?]{0,14}(?:了|呢|啊|?|!|!)?|你(?:又|还|居然|咋|怎么)[^,。!?]{0,12}(?:了|呢|啊|吧)?|(?:又|还|总是|老是|仍然|居然)[^,。!?]{0,12}(?:了|呢|啊|吧)?|这(?:不|有点|根本|完全|哪)?(?:对|行|合理|应该|像话)(?:吧|么|吗|啊|呢)?|你(?:是不是|难道|难道说|该不会)[^,。!?]{0,10}(?:了|呢|吧)?|(?:搞什么|干什么|什么情况|怎么回事|搞砸|搞错了|犯什么错))/;
|
|
89
|
+
// P8 小批 C:经 patterns.js 的 criticismWeakRe() 取(内置默认=英文最小集;中文形态由本机 patterns 注入)。
|
|
35
90
|
|
|
36
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
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 =
|
|
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 =
|
|
121
|
-
const hasHist =
|
|
174
|
+
const hasNow = timeWordsRe().test(text);
|
|
175
|
+
const hasHist = historicDateRe().test(text);
|
|
122
176
|
if (!timeCfg || (!hasNow && !hasHist)) return [];
|
|
123
|
-
if (isQuoteOrParaphraseContext(text,
|
|
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) && !
|
|
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 && (
|
|
168
|
-
if (!session.turn.getDateSeen &&
|
|
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 (!
|
|
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 &&
|
|
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 && !
|
|
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
|
|
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
|
-
|
|
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: "
|
|
241
|
-
|
|
295
|
+
kind: "self-certify",
|
|
296
|
+
mode: "criticism",
|
|
297
|
+
suspect: "strong",
|
|
298
|
+
reason: getMessage("criticism.strong")
|
|
242
299
|
});
|
|
243
|
-
} else if (
|
|
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
|
-
|
|
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) &&
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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) &&
|
|
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:
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 =
|
|
442
|
+
const hint = selfCertHint(cfg.ruleId);
|
|
385
443
|
if (!hint) continue;
|
|
386
444
|
if (hint.re.test(text)) {
|
|
387
445
|
hits.push({
|
package/lib/core/understander.js
CHANGED
|
@@ -5,13 +5,13 @@ import { extractElements, levelFromTitle } from "./parser.js";
|
|
|
5
5
|
import {
|
|
6
6
|
BOM_WRITE,
|
|
7
7
|
DESTRUCTIVE_CMD,
|
|
8
|
-
|
|
8
|
+
dshKeywordsRe,
|
|
9
9
|
INLINE_CMD,
|
|
10
10
|
SENSITIVE_CMD,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
timeWordsRe,
|
|
12
|
+
promiseWordsRe,
|
|
13
13
|
URL_RE,
|
|
14
|
-
|
|
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
|
};
|