dsh-rule-engine 0.6.2 → 0.6.4
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 +178 -7
- package/lib/core/audit.js +21 -4
- package/lib/core/authorization.js +30 -33
- package/lib/core/config.js +4 -1
- package/lib/core/dualtrack-markers.js +61 -0
- package/lib/core/guard-core.js +151 -34
- 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 +29 -17
- package/lib/core/measure-kinds.js +127 -0
- package/lib/core/patterns.js +382 -51
- package/lib/core/quality-ledger.js +124 -0
- package/lib/core/state.js +47 -4
- package/lib/core/text-detect.js +126 -68
- package/lib/core/tool-catalog.js +11 -3
- package/lib/core/understander.js +32 -43
- package/lib/index.js +133 -11
- package/lib/messages.js +134 -0
- package/package.json +3 -2
- package/scripts/check-tool-coverage.mjs +3 -1
- package/scripts/dualtrack-check.mjs +481 -0
- package/scripts/dualtrack-whitelist.json +7 -0
- package/scripts/local-residue-scan.mjs +10 -4
- package/scripts/plugins.json +10 -0
- package/scripts/publish-aptitude-check.mjs +5 -2
- package/scripts/release-plugin.mjs +54 -5
- package/scripts/verify-all.mjs +4 -0
- package/scripts/verify-v474.mjs +8 -8
package/lib/core/patterns.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { isAbsolute } from "node:path";
|
|
6
|
+
import { getMessage } from "../messages.js";
|
|
6
7
|
|
|
7
8
|
export const INLINE_CMD =
|
|
8
9
|
/\b(?:node|pwsh|powershell)\s+(?:-[ep]|--eval|--print|-Command|-c)\b/i;
|
|
@@ -28,21 +29,18 @@ export const PROTECTED_FILENAME_RE =
|
|
|
28
29
|
export const DATA_DIR_RE =
|
|
29
30
|
/(?:^|[\\/])\.dsh[\\/](?:sessions|storages|\.backups)[\\/]/i;
|
|
30
31
|
|
|
31
|
-
/** 当下时间词(规则 2①:写当下相对时间前必须先 Get-Date 核对——A1 拆组,2026-09-03)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/** 当下时间词(规则 2①:写当下相对时间前必须先 Get-Date 核对——A1 拆组,2026-09-03)
|
|
33
|
+
* P8 小批 B(2026-09-08):词表下沉配置,经 patRe("time_words") 取(见文件末尾配置层) */
|
|
34
|
+
export const timeWordsRe = () => patRe("time_words");
|
|
34
35
|
|
|
35
36
|
/** 历史日期/绝对日期(规则 2②:只要求事件证据锚,不要求 Get-Date——A1 拆组;含 ISO 格式,本机高频表述) */
|
|
36
|
-
export const
|
|
37
|
-
/\d{1,2}\s*月\s*\d{1,2}\s*日|\d{4}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日|20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/;
|
|
37
|
+
export const historicDateRe = () => patRe("historic_date");
|
|
38
38
|
|
|
39
39
|
/** 事件时间证据标注(规则 2②:过去事件时间必须绑定事件自身证据;"之前/当时"等模糊词不触发检测、不采信)
|
|
40
40
|
* A1 增补证据锚(2026-09-03):commit hash / 版本号 / 踩坑 N / 版本记录 vX —— 历史日期带锚即视为已标注来源,免 Get-Date */
|
|
41
|
-
export const
|
|
42
|
-
/日志\s*(?:ts|时间戳)|mtime|启动时间|进程\s*StartTime|文件\s*修改时间|来源[::]|ts\s*[:=]|Get-Date\s*输出|事件时间已核实|【时间未核实】|\b[0-9a-f]{7,40}\b|\bv\d+(?:\.\d+){1,2}\b|踩坑\s*\d+|版本(?:记录)?\s*v\d+(?:\.\d+){1,2}/i;
|
|
41
|
+
export const evidenceMarkRe = () => patRe("evidence_mark");
|
|
43
42
|
|
|
44
|
-
export const
|
|
45
|
-
/包在我身上|肯定能|绝对没问题|保证(?!不|无法)|一定可以|放心(?:,|,)?肯定|万无一失/;
|
|
43
|
+
export const promiseWordsRe = () => patRe("promise_words");
|
|
46
44
|
|
|
47
45
|
export const URL_RE = /https?:\/\/[^\s]+/i;
|
|
48
46
|
|
|
@@ -50,12 +48,11 @@ export const URL_RE = /https?:\/\/[^\s]+/i;
|
|
|
50
48
|
export const PRIVATE_NETWORK_RE =
|
|
51
49
|
/(?:127\.0\.0\.1|localhost|169\.254\.169\.254|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})/i;
|
|
52
50
|
|
|
53
|
-
export const
|
|
51
|
+
export const sourceMarkRe = () => patRe("source_mark");
|
|
54
52
|
|
|
55
53
|
export const CJK_RE = /[\u4e00-\u9fff]/;
|
|
56
54
|
|
|
57
|
-
export const
|
|
58
|
-
/DSH|dsh|插件|技能|规则|配置|迁移|手册|会话|装配|profile|bundle/i;
|
|
55
|
+
export const dshKeywordsRe = () => patRe("domain_words");
|
|
59
56
|
|
|
60
57
|
export const SELF_PROTECT_PATHS = [
|
|
61
58
|
"**/rule-engine.json",
|
|
@@ -63,21 +60,36 @@ export const SELF_PROTECT_PATHS = [
|
|
|
63
60
|
"**/rule-guard.json"
|
|
64
61
|
];
|
|
65
62
|
|
|
63
|
+
/** 路径规范化(纯字符串,不触盘):去包裹引号 → 统一分隔符 → 折叠重复分隔符 → 解析 . / .. → 小写。
|
|
64
|
+
* 用于本机受保护路径的「包含比较」:防 `\\`、`.`、`..` 等写法变体绕过守卫(2026-09-08 实测缺口)。 */
|
|
65
|
+
export function canonicalPath(p) {
|
|
66
|
+
if (typeof p !== "string") return "";
|
|
67
|
+
const s = p.replace(/^["']+|["']+$/g, "").replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const seg of s.split("/")) {
|
|
70
|
+
if (seg === ".") continue;
|
|
71
|
+
if (seg === "..") { if (out.length > 1) out.pop(); continue; }
|
|
72
|
+
out.push(seg);
|
|
73
|
+
}
|
|
74
|
+
return out.join("/").toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
/** 判断一条命令文本是否是「读取手册」类命令(manualPaths = 本机配置的手册路径列表;无配置 = 无对象,恒 false) */
|
|
67
78
|
export function isManualReadCommand(command, manualPaths = []) {
|
|
68
79
|
if (typeof command !== "string") return false;
|
|
69
80
|
if (!Array.isArray(manualPaths)) return false;
|
|
70
|
-
const
|
|
81
|
+
const n = canonicalPath(command);
|
|
82
|
+
const hit = manualPaths.some((pp) => n.includes(canonicalPath(String(pp))));
|
|
71
83
|
return hit && /get-content|cat|type|read|grep|findstr|str_replace_editor/i.test(command);
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
/** 本机追加文件路径匹配(A2-2
|
|
75
|
-
* manualPaths 为空 = 无本机配置 = 恒 false(守卫无对象,自然静默)。
|
|
86
|
+
/** 本机追加文件路径匹配(A2-2 算法):规范化 + 大小写不敏感 + 包含比较。
|
|
87
|
+
* manualPaths 为空 = 无本机配置 = 恒 false(守卫无对象,自然静默)。
|
|
88
|
+
* 2026-09-08:改用 canonicalPath——原实现只做斜杠替换,`\\` / `.` / `..` 写法变体可绕过守卫。 */
|
|
76
89
|
export function matchManualPath(p, manualPaths = []) {
|
|
77
90
|
if (typeof p !== "string" || !Array.isArray(manualPaths)) return false;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
return manualPaths.some((pp) => n.includes(norm(String(pp))));
|
|
91
|
+
const n = canonicalPath(p);
|
|
92
|
+
return manualPaths.some((pp) => n.includes(canonicalPath(String(pp))));
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
/** 判断一次工具调用是否算作「已读手册」(manualPaths = 本机配置的手册路径列表;无配置 = 无对象,恒 false) */
|
|
@@ -291,6 +303,12 @@ const ALLOW_SEG_RE =
|
|
|
291
303
|
// 0.5.12(F4):纯字符串/纯输出段(整段是引号包裹字符串或 $() 子表达式)——不是命令,放行。
|
|
292
304
|
// 保守边界:只匹配"整段即字符串"(^...$ 锚定),不让 node -e "..." / curl -o "..." 借道。
|
|
293
305
|
const PURE_STRING_SEG_RE = /^(?:"[^"\r\n]*"|'[^'\r\n]*'|`[^`\r\n]*`|\$\(\s*[^()\r\n]*\s*\))+$/;
|
|
306
|
+
// F5(2026-09-10 修复,第三方实测):`Remove-Item Env:NAME` 的**操作对象是进程环境变量,不是文件**,
|
|
307
|
+
// 此前被 MUTATING_CMD_RE 的 Remove-Item 命中判写(ERR-JCSE3O 的类判定来源)。
|
|
308
|
+
// 边界(保守):仅当整段目标都是 Env:/$env: 形式时豁免;`Remove-Item <路径>`、混合目标、
|
|
309
|
+
// 选项在目标之前(如 `Remove-Item -Force Env:X`)一律不豁免——宁可误拦不可放行真删除。
|
|
310
|
+
const ENV_SCOPE_REMOVE_SEG_RE =
|
|
311
|
+
/^\s*Remove-Item\s+(?:\$env:|Env:)[\w()]+\s*(?:-[\w]+\s+(?:"[^"]*"|'[^']*'|[^\s;|]+)\s*)*$/i;
|
|
294
312
|
// 剥离 try/catch/finally 控制块:块内语句与主语句同样接受只读判定(Try-* 不是 cmdlet)。
|
|
295
313
|
// 注意:不做全量 [{}] 替换——if/else 子表达式($(if(...){...}else{...}))会被切碎误判。
|
|
296
314
|
function stripControlBlocks(command) {
|
|
@@ -307,12 +325,38 @@ export function isMutationCommand(command) {
|
|
|
307
325
|
return typeof command === "string" && MUTATING_CMD_RE.test(command);
|
|
308
326
|
}
|
|
309
327
|
|
|
310
|
-
/** 只读命令链按分隔符折段(机制 B,2026-08-24):`; 换行 && || |`(管道前后是命令链;`2>&1`/`2>$null` 的 stderr 重定向不拆分)
|
|
328
|
+
/** 只读命令链按分隔符折段(机制 B,2026-08-24):`; 换行 && || |`(管道前后是命令链;`2>&1`/`2>$null` 的 stderr 重定向不拆分)
|
|
329
|
+
* F5(2026-09-10 修复,第三方实测):**引号感知**——此前裸拆 `|` 会把**引号内的 `|`** 当分隔符
|
|
330
|
+
* (正则 alternation `-Pattern 'a|b'`、格式串 `"{0} | x"`、`-notmatch 'x|y'`)→ 切出无法识别的残段
|
|
331
|
+
* → L354 保守判非只读 → 整条只读命令被规则 22 按 write 拦(ERR-4WOAKG / 9T886X / Z8M7YC / IKXLU8)。
|
|
332
|
+
* 现:`'…'` / `"…"` / `` `…` `` 内的分隔符不拆。转义语义按 PS:`'` 内不转义(`''` 表一个单引号);
|
|
333
|
+
* `"` 与 `` ` `` 内 `\` / `` ` `` 转义下一字符(兼容跨语言写法)。 */
|
|
311
334
|
function splitCommandSegments(command) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
335
|
+
const s = String(command);
|
|
336
|
+
const out = [];
|
|
337
|
+
let buf = "";
|
|
338
|
+
let quote = null;
|
|
339
|
+
for (let i = 0; i < s.length; i++) {
|
|
340
|
+
const c = s[i];
|
|
341
|
+
if (quote) {
|
|
342
|
+
buf += c;
|
|
343
|
+
if (quote !== "'" && (c === "\\" || c === "`")) { buf += s[i + 1] ?? ""; i++; continue; }
|
|
344
|
+
if (c === quote) quote = null;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (c === "'" || c === '"' || c === "`") { quote = c; buf += c; continue; }
|
|
348
|
+
if (c === "\r") continue;
|
|
349
|
+
if (c === "\n" || c === ";") { out.push(buf); buf = ""; continue; }
|
|
350
|
+
if (c === "&" && s[i + 1] === "&") { out.push(buf); buf = ""; i++; continue; }
|
|
351
|
+
if (c === "|") {
|
|
352
|
+
if (s[i + 1] === "|") { out.push(buf); buf = ""; i++; continue; }
|
|
353
|
+
if (i > 0 && /\d/.test(s[i - 1])) { buf += c; continue; } // 保留原 (?<!\d) 语义
|
|
354
|
+
out.push(buf); buf = ""; continue;
|
|
355
|
+
}
|
|
356
|
+
buf += c;
|
|
357
|
+
}
|
|
358
|
+
out.push(buf);
|
|
359
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
316
360
|
}
|
|
317
361
|
|
|
318
362
|
/** 判断命令文本是否只读(读文件/查询类,无写入/删除/提交副作用)。
|
|
@@ -333,6 +377,7 @@ export function isReadOnlyCommand(command) {
|
|
|
333
377
|
const s = String(seg).trim();
|
|
334
378
|
if (!s) return true; // 剥离残留的空段放行
|
|
335
379
|
if (s.startsWith("#")) return true;
|
|
380
|
+
if (ENV_SCOPE_REMOVE_SEG_RE.test(s)) return true; // F5:环境变量操作非文件删除(须在 MUTATING 之前)
|
|
336
381
|
if (MUTATING_CMD_RE.test(s)) return false;
|
|
337
382
|
if (MUTATING_METHOD_RE.test(s)) return false;
|
|
338
383
|
if (COM_CONTENT_ASSIGN_RE.test(s)) return false; // $doc.Text=... 是写文档
|
|
@@ -423,17 +468,20 @@ export function isAnalysisOp(toolName, args) {
|
|
|
423
468
|
|
|
424
469
|
// ═══════════ 0.5.10 已知环境坑特征表(建议5——错误码自动召回,K-01/K-05 依据)═══════════
|
|
425
470
|
// 工具/命令错误文本命中特征 → 审计 error-hint + 注入指向知识库的提示(低频:仅命中才提示;语义=指路不打扰)。
|
|
471
|
+
// hint 文案经 messages 层取(第 1 批 1b:去本机标识——原 hint 引用本机手册踩坑编号/代理端口/发布变量)
|
|
426
472
|
const KNOWN_PITFALLS = [
|
|
427
|
-
{ key: "SEC_E_NO_CREDENTIALS", re: /SEC_E_NO_CREDENTIALS/i,
|
|
428
|
-
{ key: "proxy-7890-refused", re: /ECONNREFUSED[^\n]*7890/i,
|
|
429
|
-
{ key: "sandbox-pipe-EPERM", re: /EPERM/i,
|
|
430
|
-
{ key: "ERR_MODULE_NOT_FOUND", re: /ERR_MODULE_NOT_FOUND/i,
|
|
473
|
+
{ key: "SEC_E_NO_CREDENTIALS", re: /SEC_E_NO_CREDENTIALS/i, hintKey: "pitfall.tls" },
|
|
474
|
+
{ key: "proxy-7890-refused", re: /ECONNREFUSED[^\n]*7890/i, hintKey: "pitfall.proxy" },
|
|
475
|
+
{ key: "sandbox-pipe-EPERM", re: /EPERM/i, hintKey: "pitfall.sandbox-pipe" },
|
|
476
|
+
{ key: "ERR_MODULE_NOT_FOUND", re: /ERR_MODULE_NOT_FOUND/i, hintKey: "pitfall.module-missing" }
|
|
431
477
|
];
|
|
432
478
|
|
|
433
479
|
/** 已知坑特征匹配(纯函数):错误文本命中特征表 → 返回条目;未命中 → null */
|
|
434
480
|
export function matchKnownPitfall(text) {
|
|
435
481
|
const t = String(text || "");
|
|
436
|
-
for (const p of KNOWN_PITFALLS)
|
|
482
|
+
for (const p of KNOWN_PITFALLS) {
|
|
483
|
+
if (p.re.test(t)) return { key: p.key, hint: getMessage(p.hintKey) };
|
|
484
|
+
}
|
|
437
485
|
return null;
|
|
438
486
|
}
|
|
439
487
|
|
|
@@ -458,17 +506,17 @@ export function isDangerousInlineNode(code) {
|
|
|
458
506
|
* 2026-09-06 M7 修复:opts.askApproved(本回合已获 ask 授权答复)→ 豁免——ask 答复后同一任务落盘
|
|
459
507
|
* 不再被保守误报 approval-gap(12A 授权链已由 ask 建立,M7 不重复提醒)。
|
|
460
508
|
*/
|
|
461
|
-
const
|
|
462
|
-
const
|
|
509
|
+
const planInstructionRe = () => patRe("plan_instruction");
|
|
510
|
+
const writeInstructionRe = () => patRe("write_instruction");
|
|
463
511
|
// 2026-09-06 0.6.x:建议+执行并存("按第三方的建议按顺序执行修改")=执行分点——执行词豁免(M7 误报修复)
|
|
464
|
-
const
|
|
512
|
+
const executeActionRe = () => patRe("execute_action");
|
|
465
513
|
export function needsApprovalReminder(userText, opts = {}) {
|
|
466
514
|
const t = String(userText || "");
|
|
467
515
|
if (!t) return false;
|
|
468
516
|
if (opts.askApproved) return false; // M7 修复:ask 授权答复豁免 approval-gap
|
|
469
|
-
if (!
|
|
470
|
-
if (
|
|
471
|
-
return !
|
|
517
|
+
if (!planInstructionRe().test(t)) return false;
|
|
518
|
+
if (executeActionRe().test(t)) return false; // 0.6.x:方案词+执行词并存=执行指令("按建议执行"含执行语)
|
|
519
|
+
return !writeInstructionRe().test(t);
|
|
472
520
|
}
|
|
473
521
|
|
|
474
522
|
let workspaceRoots = []; // 全局注册的工作区根(兜底层)
|
|
@@ -684,16 +732,42 @@ export function isSensitiveToolCall(toolName, args, sessionId) {
|
|
|
684
732
|
|
|
685
733
|
// ── 批次 5 文本健康检测词表(2026-08-24) ──────────────────────────────
|
|
686
734
|
/** 技术术语(规则 11③ 零基础表达:中文提问 + 术语 + 无解释 → 自证)。英文词带 \b,中文词组按字面。 */
|
|
687
|
-
export const
|
|
688
|
-
/\b(?:API|JSON|REST|WebSocket|OAuth|JWT|ORM|Schema|SSR|CSR|DI|Docker|Kubernetes|K8s|npm|pnpm|Node\.js|TypeScript|Git)\b|正则(?:表达|表达式)?|异步|回调|闭包|哈希|令牌|中间件|依赖注入|虚拟DOM|虚拟 DOM|数据库|SQL|序列化|反序列化|面向对象|函数式|类型推断|泛型/i;
|
|
735
|
+
export const techTermRe = () => patRe("tech_term");
|
|
689
736
|
|
|
690
737
|
/** 术语解释伴随词(出现任一 → 视为已解释,不触发密度自证) */
|
|
691
|
-
export const
|
|
692
|
-
/例如|比如|即\b|就是|简单说|换言之|类比|也就是说|通俗|直白|理解为|打个比方|举个例子/;
|
|
738
|
+
export const termExplanationRe = () => patRe("term_explanation");
|
|
693
739
|
|
|
694
740
|
/** 建议类表述(规则 16 重复推销:会话内同类建议 ≥2 → 自证) */
|
|
695
|
-
export const
|
|
696
|
-
|
|
741
|
+
export const suggestRe = () => patRe("suggest_words");
|
|
742
|
+
|
|
743
|
+
// A″ 批评检测形态(P8 小批 C):强形态(连续问号/叹号)+ 弱形态(反问/责问)
|
|
744
|
+
export const criticismShapeRe = () => patRe("criticism_shape");
|
|
745
|
+
export const criticismWeakRe = () => patRe("criticism_weak");
|
|
746
|
+
// A″ 反向检查(第三批):明确执行指令词
|
|
747
|
+
export const execDirectiveRe = () => patRe("exec_directive");
|
|
748
|
+
// 规则激活词(小批 B 第一小域):matcher.js 的情境匹配机词表
|
|
749
|
+
export const activateSkillRe = () => patRe("activate_skill");
|
|
750
|
+
export const activateApprovalRe = () => patRe("activate_approval");
|
|
751
|
+
export const activateNetworkRe = () => patRe("activate_network");
|
|
752
|
+
export const activateBackupRe = () => patRe("activate_backup");
|
|
753
|
+
export const activateTimeRe = () => patRe("activate_time");
|
|
754
|
+
export const activatePromiseRe = () => patRe("activate_promise");
|
|
755
|
+
export const activateSourceRe = () => patRe("activate_source");
|
|
756
|
+
export const deliveryClaimRe = () => patRe("delivery_claim");
|
|
757
|
+
export const internalRefRe = () => patRe("internal_ref");
|
|
758
|
+
export const verifyIntentRe = () => patRe("verify_intent");
|
|
759
|
+
export const emptyTalkRe = () => patRe("empty_talk");
|
|
760
|
+
export const deliveryNoVerifyRe = () => patRe("delivery_no_verify");
|
|
761
|
+
export const verifyEvidenceRe = () => patRe("verify_evidence");
|
|
762
|
+
export const mountMentionRe = () => patRe("mount_mention");
|
|
763
|
+
export const mountAuditOkRe = () => patRe("mount_audit_ok");
|
|
764
|
+
export const scopeOverreachRe = () => patRe("scope_overreach");
|
|
765
|
+
export const scopeBoundedRe = () => patRe("scope_bounded");
|
|
766
|
+
export const apologyOnlyRe = () => patRe("apology_only");
|
|
767
|
+
export const apologyWithCauseRe = () => patRe("apology_with_cause");
|
|
768
|
+
export const versionRecordMentionRe = () => patRe("version_record_mention");
|
|
769
|
+
export const versionRecordOnlyRe = () => patRe("version_record_only");
|
|
770
|
+
export const versionSyncOkRe = () => patRe("version_sync_ok");
|
|
697
771
|
|
|
698
772
|
/**
|
|
699
773
|
* v0.5.7 P0.5-6(用户拍板):否定/合规声明语境("不再/避免/停止/无 XX")——那不是重复推销。
|
|
@@ -702,7 +776,7 @@ export const SUGGEST_RE =
|
|
|
702
776
|
*/
|
|
703
777
|
export function isNegatingSuggestion(text) {
|
|
704
778
|
if (typeof text !== "string") return false;
|
|
705
|
-
return
|
|
779
|
+
return patRe("negating_suggestion").test(text);
|
|
706
780
|
}
|
|
707
781
|
|
|
708
782
|
/**
|
|
@@ -714,10 +788,11 @@ export function isNegatingSuggestion(text) {
|
|
|
714
788
|
* 修复背景:B3 规则 7 仅认引号+白名单标记词,无引号转述("用户之前说万无一失")误报;
|
|
715
789
|
* 规则 2 原本无引述豁免(引述用户原话"昨天"也报未核对)。
|
|
716
790
|
*/
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
791
|
+
const paraphraseBeforeRe = () => patRe("paraphrase_before");
|
|
792
|
+
const paraphraseMarkBeforeRe = () => patRe("paraphrase_mark_before");
|
|
793
|
+
const paraphraseMarkAfterRe = () => patRe("paraphrase_mark_after");
|
|
794
|
+
// 引号字符集(语言无关:ASCII + 中文弯引号)——不是行为词表,留在机制层
|
|
795
|
+
const QUOTE_CHARS_RE = /[“"「『‘'””」』’]/;
|
|
721
796
|
|
|
722
797
|
export function isQuoteOrParaphraseContext(text, wordRe) {
|
|
723
798
|
if (typeof text !== "string" || !(wordRe instanceof RegExp)) return false;
|
|
@@ -728,17 +803,273 @@ export function isQuoteOrParaphraseContext(text, wordRe) {
|
|
|
728
803
|
const after = text.slice(m.index + m[0].length);
|
|
729
804
|
// 引述包装 = 承诺词两侧 8 字符内都有引号(ASCII/中文弯引号均算;不区分左右——
|
|
730
805
|
// ASCII 单引号左右同形,分区判定会自相矛盾;双侧近邻引号即视为被引述包裹)。
|
|
731
|
-
const openNear =
|
|
732
|
-
const closeNear =
|
|
806
|
+
const openNear = QUOTE_CHARS_RE.test(before.slice(-8));
|
|
807
|
+
const closeNear = QUOTE_CHARS_RE.test(after.slice(0, 8));
|
|
733
808
|
if (openNear && closeNear) return true;
|
|
734
|
-
if (
|
|
735
|
-
if (
|
|
736
|
-
if (
|
|
809
|
+
if (paraphraseBeforeRe().test(before.slice(-14))) return true;
|
|
810
|
+
if (paraphraseMarkBeforeRe().test(before.slice(-12))) return true;
|
|
811
|
+
if (paraphraseMarkAfterRe().test(after.slice(0, 12))) return true;
|
|
737
812
|
}
|
|
738
813
|
return false;
|
|
739
814
|
}
|
|
740
815
|
|
|
741
816
|
/** v0.5.7 P0.5-5(用户拍板)+ B3 扩展:承诺词处于引述/改写/转述语境 → 是引述不是承诺 */
|
|
742
817
|
export function isPromiseQuoteContext(text) {
|
|
743
|
-
return isQuoteOrParaphraseContext(text,
|
|
818
|
+
return isQuoteOrParaphraseContext(text, promiseWordsRe());
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ═══════════ P8 小批 B(2026-09-08):检测正则配置层 ═══════════
|
|
822
|
+
// 双层模型(同 lexicon.js):
|
|
823
|
+
// ① 通用层 BUILTIN_PATTERNS —— 语言无关最小集,随 npm 包发布,不含中文;
|
|
824
|
+
// ② 个人层 rule-engine.json 的 patterns 键 —— undefined/null = 不干预(保持当前);
|
|
825
|
+
// 对象 = 按键完全替换(Override),未提供的键回退内置默认。
|
|
826
|
+
// 取词一律走 patRe(key)(带缓存);禁止在调用方缓存正则对象(配置注入后旧引用不更新)。
|
|
827
|
+
//
|
|
828
|
+
// 安全边界:本层只承载「文本健康检测」类正则(规则 2/5/7/11/16 的检测词表);
|
|
829
|
+
// 命令判定/路径守卫类正则(MUTATING_CMD_RE / READONLY_CMD_RE / CONFIG_FILE_RE 等)
|
|
830
|
+
// 一律留在机制层且不含中文——配置缺失不会削弱硬守卫。
|
|
831
|
+
|
|
832
|
+
/** 可配置检测正则键名(键 → 语义) */
|
|
833
|
+
export const PATTERN_KEYS = [
|
|
834
|
+
"time_words", // 规则 2① 当下时间词
|
|
835
|
+
"historic_date", // 规则 2② 历史/绝对日期
|
|
836
|
+
"evidence_mark", // 规则 2② 事件时间证据锚
|
|
837
|
+
"promise_words", // 规则 7 承诺/大话术词
|
|
838
|
+
"source_mark", // 规则 5 来源引用标记
|
|
839
|
+
"domain_words", // 规则 18 DSH 领域词
|
|
840
|
+
"plan_instruction", // M7 方案性指令词
|
|
841
|
+
"write_instruction", // M7 落盘性词(豁免提醒)
|
|
842
|
+
"execute_action", // M7 执行动作词(豁免提醒)
|
|
843
|
+
"tech_term", // 规则 11③ 技术术语
|
|
844
|
+
"term_explanation", // 规则 11③ 术语解释伴随词
|
|
845
|
+
"suggest_words", // 规则 16 建议类表述
|
|
846
|
+
"negating_suggestion", // 规则 16 否定/合规声明语境
|
|
847
|
+
"paraphrase_before", // 转述语境(第三人称 + 说/表示…)
|
|
848
|
+
"paraphrase_mark_before", // 引述标记(前)
|
|
849
|
+
"paraphrase_mark_after", // 引述标记(后)
|
|
850
|
+
"criticism_shape", // 规则 22② A″ 批评强形态(连续问号/叹号)
|
|
851
|
+
"criticism_weak", // 规则 22② A″ 批评弱形态(反问/责问)
|
|
852
|
+
"exec_directive", // A″ 反向检查:消息含明确执行指令词(弱信号仅留痕不提示)
|
|
853
|
+
"activate_skill", // 规则 12B 激活词
|
|
854
|
+
"activate_approval", // 规则 12A 激活词
|
|
855
|
+
"activate_network", // 规则 12C 激活词
|
|
856
|
+
"activate_backup", // 规则 13A 激活词
|
|
857
|
+
"activate_time", // 规则 2 激活词
|
|
858
|
+
"activate_promise", // 规则 7 激活词
|
|
859
|
+
"activate_source", // 规则 5 激活词
|
|
860
|
+
"delivery_claim", // 检测正则(小批 B 第一小域)
|
|
861
|
+
"internal_ref", // 检测正则(小批 B 第一小域)
|
|
862
|
+
"verify_intent", // 检测正则(小批 B 第一小域)
|
|
863
|
+
"empty_talk", // 检测正则(小批 B 第一小域)
|
|
864
|
+
"delivery_no_verify", // 检测正则(小批 B 第一小域)
|
|
865
|
+
"verify_evidence", // 检测正则(小批 B 第一小域)
|
|
866
|
+
"mount_mention", // 检测正则(小批 B 第一小域)
|
|
867
|
+
"mount_audit_ok", // 检测正则(小批 B 第一小域)
|
|
868
|
+
"scope_overreach", // 检测正则(小批 B 第一小域)
|
|
869
|
+
"scope_bounded", // 检测正则(小批 B 第一小域)
|
|
870
|
+
"apology_only", // 检测正则(小批 B 第一小域)
|
|
871
|
+
"apology_with_cause", // 检测正则(小批 B 第一小域)
|
|
872
|
+
"version_record_mention", // 检测正则(小批 B 第一小域)
|
|
873
|
+
"version_record_only", // 检测正则(小批 B 第一小域)
|
|
874
|
+
"version_sync_ok", // 检测正则(小批 B 第一小域)
|
|
875
|
+
];
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* 映射型配置键(键 → { 子键: source }):一张表多条正则的场景(如条款自证触发词)。
|
|
879
|
+
* 子键语义由消费方定义;与扁平正则键共用同一配置层与回退语义。
|
|
880
|
+
*/
|
|
881
|
+
export const PATTERN_MAP_KEYS = {
|
|
882
|
+
// 内置默认**空表**(判据 A,2026-09-09):键=规则号,属发布者私有的规则体系,
|
|
883
|
+
// 一律从 rule-engine.json 的 patterns.self_cert_hints 注入;空表时该机制静默不触发。
|
|
884
|
+
self_cert_hints: {}
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/** 数值型配置键(键 → 内置默认;与正则键共用同一配置层与校验/回退语义) */
|
|
888
|
+
export const PATTERN_NUM_KEYS = {
|
|
889
|
+
criticism_caps_ratio: 0.6 // 英文全大写比率阈值(≥ 该值且字母数 ≥6 视为"喊叫"强形态)
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
// 内置默认:语言无关最小集(无中文)
|
|
893
|
+
const BUILTIN_PATTERNS = {
|
|
894
|
+
time_words: "(?:today|yesterday|just now|a moment ago|\\d+\\s*minutes? ago)",
|
|
895
|
+
historic_date: "20\\d{2}[-/.]\\d{1,2}[-/.]\\d{1,2}",
|
|
896
|
+
evidence_mark: "mtime|ts\\s*[:=]|\\b[0-9a-f]{7,40}\\b|\\bv\\d+(?:\\.\\d+){1,2}\\b",
|
|
897
|
+
promise_words: "(?:i promise|guaranteed|absolutely (?:fine|no problem)|100% sure|no way it fails)",
|
|
898
|
+
source_mark: "via|source|reference|quoted from",
|
|
899
|
+
domain_words: "DSH|dsh|profile|bundle|plugin|skill|config|migrat|manual|session|assembly",
|
|
900
|
+
plan_instruction: "plan|proposal|adjust|supplement|suggest|evaluat|draft|refine|optimi[sz]e|improve|restructure|review",
|
|
901
|
+
write_instruction: "persist|write|publish|save to manual|confirm(?:ed)?",
|
|
902
|
+
execute_action: "execute|run|implement|start|apply|continue|proceed|operate",
|
|
903
|
+
tech_term:
|
|
904
|
+
"\\b(?:API|JSON|REST|WebSocket|OAuth|JWT|ORM|Schema|SSR|CSR|DI|Docker|Kubernetes|K8s|npm|pnpm|Node\\.js|TypeScript|Git)\\b|regex|async|callback|closure|hash|token|middleware|dependency injection|virtual DOM|database|SQL|serializ|deserializ|object-oriented|functional|type inference|generic",
|
|
905
|
+
term_explanation: "e\\.g\\.|for example|i\\.e\\.|in other words|analogy|plainly|understand (?:it )?as|for instance",
|
|
906
|
+
suggest_words: "i suggest|recommend|better option|optimi[sz]ation suggestion|how about",
|
|
907
|
+
negating_suggestion:
|
|
908
|
+
"(?:no longer|not repeat|avoid|stop|cancel|not yet|never again|already (?:echoed|responded)|no new (?:suggestion|repetition)|just waiting)",
|
|
909
|
+
paraphrase_before:
|
|
910
|
+
"(?:user|you|he|she|it|they|author|maintainer|client|customer|everyone|someone|netizen|colleague).{0,6}?(?:said|mentioned|stated|noted|claimed|emphasi[sz]ed|wrote|verbatim)",
|
|
911
|
+
paraphrase_mark_before: "quote|quoted|original text|paraphras|forward|according to|wrote|changed to|reword",
|
|
912
|
+
paraphrase_mark_after: "changed to|reword|paraphras|quote",
|
|
913
|
+
// A″ 强形态:连续问号/叹号(全角半角均计;标点非 CJK 汉字,故属语言无关形态)
|
|
914
|
+
criticism_shape: "(?:[??]{3,}|[!!]{4,})",
|
|
915
|
+
// A″ 弱形态:英文最小集(中文反问形态由本机 patterns 注入)
|
|
916
|
+
criticism_weak:
|
|
917
|
+
"(?:why (?:are you |do you )?(?:still|again|always)|you (?:again|always|never)|this is (?:wrong|not right|unacceptable)|what (?:are you|the hell))",
|
|
918
|
+
// A″ 反向检查(2026-09-09 第三批):消息含明确执行指令词 → 弱信号只留痕不注入提示
|
|
919
|
+
exec_directive:
|
|
920
|
+
"(?:write|update|execute|submit|persist|apply|implement|fix|modify|create|delete|publish|download|commit|push|run|refactor|rewrite)",
|
|
921
|
+
// 规则激活词(P8 小批 B 第一小域,2026-09-09):matcher.js 用
|
|
922
|
+
activate_skill: "(?:skill|plugin)",
|
|
923
|
+
activate_approval: "(?:execute|create|delete|overwrite|move|download|commit|config)",
|
|
924
|
+
activate_network: "(?:download|network|curl|proxy|blocked)",
|
|
925
|
+
activate_backup: "(?:delete|overwrite|migrate|backup)",
|
|
926
|
+
activate_time: "(?:time|today|yesterday|date)",
|
|
927
|
+
activate_promise: "(?:promise|guarantee|certainly)",
|
|
928
|
+
activate_source: "(?:quote|source|url|link)",
|
|
929
|
+
delivery_claim: "(?:done|completed|all (?:done|passed)|fixed|verified|finished)(?![^.]*(?:not yet|pending|incomplete|todo))",
|
|
930
|
+
internal_ref: "(?:manual|pitfall\\s*\\d+|clause\\s*\\d+|chapter\\s*\\d+|SKILL\\.md|AGENTS\\.md|source code|README|docs?\\s+\\w*(?:written|recorded|documented))",
|
|
931
|
+
verify_intent: "(?:verify|validate|cross-check|target|compare|confirm)",
|
|
932
|
+
empty_talk: "(?:got it|noted|i'?ll remember|sure, i'?ve noted)",
|
|
933
|
+
delivery_no_verify: "(?:done|delivered|completed)",
|
|
934
|
+
verify_evidence: "(?:runtime verif|mock|startup|actual test|test|verif)",
|
|
935
|
+
mount_mention: "(?:restart|assembly|mount)",
|
|
936
|
+
mount_audit_ok: "(?:audit passed|full audit|MOUNT CONSISTENT|DUPLICATES FOUND|audit-mount-consistency)",
|
|
937
|
+
scope_overreach: "(?:i (?:also |additionally )?(?:added|supplemented|included)|extra add)",
|
|
938
|
+
scope_bounded: "(?:only (?:the )?selected|not implemented|separately confirm)",
|
|
939
|
+
apology_only: "(?:sorry|my mistake|my fault|i was wrong)",
|
|
940
|
+
apology_with_cause: "(?:cause|reason|correction|prevent|avoid|mechanism|fix)",
|
|
941
|
+
version_record_mention: "(?:changelog|version record|v\\d+\\.\\d+)",
|
|
942
|
+
version_record_only: "(?:logged (?:to )?changelog|changelog updated)",
|
|
943
|
+
version_sync_ok: "(?:body|sync|no sync needed)"
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
let patternsOverride = null;
|
|
947
|
+
let patCache = new Map();
|
|
948
|
+
|
|
949
|
+
function compilePattern(source, flags) {
|
|
950
|
+
if (typeof source !== "string" || source.length === 0) return null;
|
|
951
|
+
try {
|
|
952
|
+
return new RegExp(source, flags);
|
|
953
|
+
} catch {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function resolvePatternSource(key) {
|
|
959
|
+
const ov = patternsOverride?.[key];
|
|
960
|
+
if (typeof ov === "string" && ov.length > 0) return ov;
|
|
961
|
+
return BUILTIN_PATTERNS[key] ?? "";
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** 数值型键的生效值(个人层优先 → 内置默认) */
|
|
965
|
+
/** 取当前生效的映射型配置(个人层优先 → 内置默认);无对象 = 空表 */
|
|
966
|
+
export function patMap(key) {
|
|
967
|
+
const ov = patternsOverride?.[key];
|
|
968
|
+
if (ov && typeof ov === "object" && !Array.isArray(ov)) return ov;
|
|
969
|
+
return PATTERN_MAP_KEYS[key] ?? {};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** 数值型键的生效值(个人层优先 → 内置默认) */
|
|
973
|
+
export function patNum(key) {
|
|
974
|
+
const ov = patternsOverride?.[key];
|
|
975
|
+
if (typeof ov === "number" && Number.isFinite(ov)) return ov;
|
|
976
|
+
return PATTERN_NUM_KEYS[key] ?? 0;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* 注入个人层检测正则(rule-engine.json 的 patterns 键)。
|
|
981
|
+
* 语义同 setLexicons:undefined/null = 不干预;对象 = 配置即真相;`{}` = 回退内置默认。
|
|
982
|
+
* @returns {{applied: string[], rejected: Array<{key: string, reason: string}>, noop?: boolean}}
|
|
983
|
+
*/
|
|
984
|
+
export function setPatterns(cfg) {
|
|
985
|
+
const applied = [];
|
|
986
|
+
const rejected = [];
|
|
987
|
+
if (cfg === undefined || cfg === null) return { applied, rejected, noop: true };
|
|
988
|
+
if (typeof cfg !== "object" || Array.isArray(cfg)) {
|
|
989
|
+
patternsOverride = null;
|
|
990
|
+
patCache = new Map();
|
|
991
|
+
return { applied, rejected };
|
|
992
|
+
}
|
|
993
|
+
const next = {};
|
|
994
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
995
|
+
// 映射型键(值 = 对象)
|
|
996
|
+
if (PATTERN_MAP_KEYS[k] !== undefined) {
|
|
997
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
998
|
+
rejected.push({ key: k, reason: "not-object" });
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const sub = {};
|
|
1002
|
+
for (const [sk, sv] of Object.entries(v)) {
|
|
1003
|
+
if (typeof sv !== "string" || !compilePattern(sv, "i")) {
|
|
1004
|
+
rejected.push({ key: `${k}.${sk}`, reason: "invalid-regex" });
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
sub[sk] = sv;
|
|
1008
|
+
}
|
|
1009
|
+
if (Object.keys(sub).length > 0) {
|
|
1010
|
+
next[k] = sub;
|
|
1011
|
+
applied.push(k);
|
|
1012
|
+
}
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
// 数值型键(阈值):0..1 的有限数;字符串数字也接受(JSON 手写容错)
|
|
1016
|
+
if (PATTERN_NUM_KEYS[k] !== undefined) {
|
|
1017
|
+
const num = typeof v === "number" ? v : Number(v);
|
|
1018
|
+
if (!Number.isFinite(num) || num < 0 || num > 1) {
|
|
1019
|
+
rejected.push({ key: k, reason: "invalid-number" });
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
next[k] = num;
|
|
1023
|
+
applied.push(k);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (!PATTERN_KEYS.includes(k)) {
|
|
1027
|
+
rejected.push({ key: k, reason: "unknown-key" });
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
1031
|
+
rejected.push({ key: k, reason: "empty-or-not-string" });
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (!compilePattern(v, "i")) {
|
|
1035
|
+
rejected.push({ key: k, reason: "invalid-regex" });
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
next[k] = v;
|
|
1039
|
+
applied.push(k);
|
|
1040
|
+
}
|
|
1041
|
+
patternsOverride = Object.keys(next).length > 0 ? next : null;
|
|
1042
|
+
patCache = new Map();
|
|
1043
|
+
return { applied, rejected };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** 显式回退内置默认(测试用;生产环境改配置后由插件重载生效) */
|
|
1047
|
+
export function resetPatterns() {
|
|
1048
|
+
patternsOverride = null;
|
|
1049
|
+
patCache = new Map();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** 当前生效的正则 source 快照(键 → source;数值键 → 数值;只读,供测试/诊断) */
|
|
1053
|
+
export function effectivePatterns() {
|
|
1054
|
+
const out = {};
|
|
1055
|
+
for (const k of PATTERN_KEYS) out[k] = resolvePatternSource(k);
|
|
1056
|
+
for (const k of Object.keys(PATTERN_NUM_KEYS)) out[k] = patNum(k);
|
|
1057
|
+
return out;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** 当前是否处于个人层覆盖状态 */
|
|
1061
|
+
export function hasPatternOverride() {
|
|
1062
|
+
return patternsOverride !== null;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** 取当前生效正则(唯一取词入口;调用方禁止缓存返回对象)。非法/空 source → 永不命中占位。 */
|
|
1066
|
+
export function patRe(key) {
|
|
1067
|
+
const source = resolvePatternSource(key);
|
|
1068
|
+
const ck = `${key}\u0000${source}`;
|
|
1069
|
+
let hit = patCache.get(ck);
|
|
1070
|
+
if (!hit) {
|
|
1071
|
+
hit = compilePattern(source, "i") || /(?!)/;
|
|
1072
|
+
patCache.set(ck, hit);
|
|
1073
|
+
}
|
|
1074
|
+
return hit;
|
|
744
1075
|
}
|