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/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) */
|
|
@@ -423,17 +435,20 @@ export function isAnalysisOp(toolName, args) {
|
|
|
423
435
|
|
|
424
436
|
// ═══════════ 0.5.10 已知环境坑特征表(建议5——错误码自动召回,K-01/K-05 依据)═══════════
|
|
425
437
|
// 工具/命令错误文本命中特征 → 审计 error-hint + 注入指向知识库的提示(低频:仅命中才提示;语义=指路不打扰)。
|
|
438
|
+
// hint 文案经 messages 层取(第 1 批 1b:去本机标识——原 hint 引用本机手册踩坑编号/代理端口/发布变量)
|
|
426
439
|
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,
|
|
440
|
+
{ key: "SEC_E_NO_CREDENTIALS", re: /SEC_E_NO_CREDENTIALS/i, hintKey: "pitfall.tls" },
|
|
441
|
+
{ key: "proxy-7890-refused", re: /ECONNREFUSED[^\n]*7890/i, hintKey: "pitfall.proxy" },
|
|
442
|
+
{ key: "sandbox-pipe-EPERM", re: /EPERM/i, hintKey: "pitfall.sandbox-pipe" },
|
|
443
|
+
{ key: "ERR_MODULE_NOT_FOUND", re: /ERR_MODULE_NOT_FOUND/i, hintKey: "pitfall.module-missing" }
|
|
431
444
|
];
|
|
432
445
|
|
|
433
446
|
/** 已知坑特征匹配(纯函数):错误文本命中特征表 → 返回条目;未命中 → null */
|
|
434
447
|
export function matchKnownPitfall(text) {
|
|
435
448
|
const t = String(text || "");
|
|
436
|
-
for (const p of KNOWN_PITFALLS)
|
|
449
|
+
for (const p of KNOWN_PITFALLS) {
|
|
450
|
+
if (p.re.test(t)) return { key: p.key, hint: getMessage(p.hintKey) };
|
|
451
|
+
}
|
|
437
452
|
return null;
|
|
438
453
|
}
|
|
439
454
|
|
|
@@ -458,17 +473,17 @@ export function isDangerousInlineNode(code) {
|
|
|
458
473
|
* 2026-09-06 M7 修复:opts.askApproved(本回合已获 ask 授权答复)→ 豁免——ask 答复后同一任务落盘
|
|
459
474
|
* 不再被保守误报 approval-gap(12A 授权链已由 ask 建立,M7 不重复提醒)。
|
|
460
475
|
*/
|
|
461
|
-
const
|
|
462
|
-
const
|
|
476
|
+
const planInstructionRe = () => patRe("plan_instruction");
|
|
477
|
+
const writeInstructionRe = () => patRe("write_instruction");
|
|
463
478
|
// 2026-09-06 0.6.x:建议+执行并存("按第三方的建议按顺序执行修改")=执行分点——执行词豁免(M7 误报修复)
|
|
464
|
-
const
|
|
479
|
+
const executeActionRe = () => patRe("execute_action");
|
|
465
480
|
export function needsApprovalReminder(userText, opts = {}) {
|
|
466
481
|
const t = String(userText || "");
|
|
467
482
|
if (!t) return false;
|
|
468
483
|
if (opts.askApproved) return false; // M7 修复:ask 授权答复豁免 approval-gap
|
|
469
|
-
if (!
|
|
470
|
-
if (
|
|
471
|
-
return !
|
|
484
|
+
if (!planInstructionRe().test(t)) return false;
|
|
485
|
+
if (executeActionRe().test(t)) return false; // 0.6.x:方案词+执行词并存=执行指令("按建议执行"含执行语)
|
|
486
|
+
return !writeInstructionRe().test(t);
|
|
472
487
|
}
|
|
473
488
|
|
|
474
489
|
let workspaceRoots = []; // 全局注册的工作区根(兜底层)
|
|
@@ -684,16 +699,42 @@ export function isSensitiveToolCall(toolName, args, sessionId) {
|
|
|
684
699
|
|
|
685
700
|
// ── 批次 5 文本健康检测词表(2026-08-24) ──────────────────────────────
|
|
686
701
|
/** 技术术语(规则 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;
|
|
702
|
+
export const techTermRe = () => patRe("tech_term");
|
|
689
703
|
|
|
690
704
|
/** 术语解释伴随词(出现任一 → 视为已解释,不触发密度自证) */
|
|
691
|
-
export const
|
|
692
|
-
/例如|比如|即\b|就是|简单说|换言之|类比|也就是说|通俗|直白|理解为|打个比方|举个例子/;
|
|
705
|
+
export const termExplanationRe = () => patRe("term_explanation");
|
|
693
706
|
|
|
694
707
|
/** 建议类表述(规则 16 重复推销:会话内同类建议 ≥2 → 自证) */
|
|
695
|
-
export const
|
|
696
|
-
|
|
708
|
+
export const suggestRe = () => patRe("suggest_words");
|
|
709
|
+
|
|
710
|
+
// A″ 批评检测形态(P8 小批 C):强形态(连续问号/叹号)+ 弱形态(反问/责问)
|
|
711
|
+
export const criticismShapeRe = () => patRe("criticism_shape");
|
|
712
|
+
export const criticismWeakRe = () => patRe("criticism_weak");
|
|
713
|
+
// A″ 反向检查(第三批):明确执行指令词
|
|
714
|
+
export const execDirectiveRe = () => patRe("exec_directive");
|
|
715
|
+
// 规则激活词(小批 B 第一小域):matcher.js 的情境匹配机词表
|
|
716
|
+
export const activateSkillRe = () => patRe("activate_skill");
|
|
717
|
+
export const activateApprovalRe = () => patRe("activate_approval");
|
|
718
|
+
export const activateNetworkRe = () => patRe("activate_network");
|
|
719
|
+
export const activateBackupRe = () => patRe("activate_backup");
|
|
720
|
+
export const activateTimeRe = () => patRe("activate_time");
|
|
721
|
+
export const activatePromiseRe = () => patRe("activate_promise");
|
|
722
|
+
export const activateSourceRe = () => patRe("activate_source");
|
|
723
|
+
export const deliveryClaimRe = () => patRe("delivery_claim");
|
|
724
|
+
export const internalRefRe = () => patRe("internal_ref");
|
|
725
|
+
export const verifyIntentRe = () => patRe("verify_intent");
|
|
726
|
+
export const emptyTalkRe = () => patRe("empty_talk");
|
|
727
|
+
export const deliveryNoVerifyRe = () => patRe("delivery_no_verify");
|
|
728
|
+
export const verifyEvidenceRe = () => patRe("verify_evidence");
|
|
729
|
+
export const mountMentionRe = () => patRe("mount_mention");
|
|
730
|
+
export const mountAuditOkRe = () => patRe("mount_audit_ok");
|
|
731
|
+
export const scopeOverreachRe = () => patRe("scope_overreach");
|
|
732
|
+
export const scopeBoundedRe = () => patRe("scope_bounded");
|
|
733
|
+
export const apologyOnlyRe = () => patRe("apology_only");
|
|
734
|
+
export const apologyWithCauseRe = () => patRe("apology_with_cause");
|
|
735
|
+
export const versionRecordMentionRe = () => patRe("version_record_mention");
|
|
736
|
+
export const versionRecordOnlyRe = () => patRe("version_record_only");
|
|
737
|
+
export const versionSyncOkRe = () => patRe("version_sync_ok");
|
|
697
738
|
|
|
698
739
|
/**
|
|
699
740
|
* v0.5.7 P0.5-6(用户拍板):否定/合规声明语境("不再/避免/停止/无 XX")——那不是重复推销。
|
|
@@ -702,7 +743,7 @@ export const SUGGEST_RE =
|
|
|
702
743
|
*/
|
|
703
744
|
export function isNegatingSuggestion(text) {
|
|
704
745
|
if (typeof text !== "string") return false;
|
|
705
|
-
return
|
|
746
|
+
return patRe("negating_suggestion").test(text);
|
|
706
747
|
}
|
|
707
748
|
|
|
708
749
|
/**
|
|
@@ -714,10 +755,11 @@ export function isNegatingSuggestion(text) {
|
|
|
714
755
|
* 修复背景:B3 规则 7 仅认引号+白名单标记词,无引号转述("用户之前说万无一失")误报;
|
|
715
756
|
* 规则 2 原本无引述豁免(引述用户原话"昨天"也报未核对)。
|
|
716
757
|
*/
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
758
|
+
const paraphraseBeforeRe = () => patRe("paraphrase_before");
|
|
759
|
+
const paraphraseMarkBeforeRe = () => patRe("paraphrase_mark_before");
|
|
760
|
+
const paraphraseMarkAfterRe = () => patRe("paraphrase_mark_after");
|
|
761
|
+
// 引号字符集(语言无关:ASCII + 中文弯引号)——不是行为词表,留在机制层
|
|
762
|
+
const QUOTE_CHARS_RE = /[“"「『‘'””」』’]/;
|
|
721
763
|
|
|
722
764
|
export function isQuoteOrParaphraseContext(text, wordRe) {
|
|
723
765
|
if (typeof text !== "string" || !(wordRe instanceof RegExp)) return false;
|
|
@@ -728,17 +770,273 @@ export function isQuoteOrParaphraseContext(text, wordRe) {
|
|
|
728
770
|
const after = text.slice(m.index + m[0].length);
|
|
729
771
|
// 引述包装 = 承诺词两侧 8 字符内都有引号(ASCII/中文弯引号均算;不区分左右——
|
|
730
772
|
// ASCII 单引号左右同形,分区判定会自相矛盾;双侧近邻引号即视为被引述包裹)。
|
|
731
|
-
const openNear =
|
|
732
|
-
const closeNear =
|
|
773
|
+
const openNear = QUOTE_CHARS_RE.test(before.slice(-8));
|
|
774
|
+
const closeNear = QUOTE_CHARS_RE.test(after.slice(0, 8));
|
|
733
775
|
if (openNear && closeNear) return true;
|
|
734
|
-
if (
|
|
735
|
-
if (
|
|
736
|
-
if (
|
|
776
|
+
if (paraphraseBeforeRe().test(before.slice(-14))) return true;
|
|
777
|
+
if (paraphraseMarkBeforeRe().test(before.slice(-12))) return true;
|
|
778
|
+
if (paraphraseMarkAfterRe().test(after.slice(0, 12))) return true;
|
|
737
779
|
}
|
|
738
780
|
return false;
|
|
739
781
|
}
|
|
740
782
|
|
|
741
783
|
/** v0.5.7 P0.5-5(用户拍板)+ B3 扩展:承诺词处于引述/改写/转述语境 → 是引述不是承诺 */
|
|
742
784
|
export function isPromiseQuoteContext(text) {
|
|
743
|
-
return isQuoteOrParaphraseContext(text,
|
|
785
|
+
return isQuoteOrParaphraseContext(text, promiseWordsRe());
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ═══════════ P8 小批 B(2026-09-08):检测正则配置层 ═══════════
|
|
789
|
+
// 双层模型(同 lexicon.js):
|
|
790
|
+
// ① 通用层 BUILTIN_PATTERNS —— 语言无关最小集,随 npm 包发布,不含中文;
|
|
791
|
+
// ② 个人层 rule-engine.json 的 patterns 键 —— undefined/null = 不干预(保持当前);
|
|
792
|
+
// 对象 = 按键完全替换(Override),未提供的键回退内置默认。
|
|
793
|
+
// 取词一律走 patRe(key)(带缓存);禁止在调用方缓存正则对象(配置注入后旧引用不更新)。
|
|
794
|
+
//
|
|
795
|
+
// 安全边界:本层只承载「文本健康检测」类正则(规则 2/5/7/11/16 的检测词表);
|
|
796
|
+
// 命令判定/路径守卫类正则(MUTATING_CMD_RE / READONLY_CMD_RE / CONFIG_FILE_RE 等)
|
|
797
|
+
// 一律留在机制层且不含中文——配置缺失不会削弱硬守卫。
|
|
798
|
+
|
|
799
|
+
/** 可配置检测正则键名(键 → 语义) */
|
|
800
|
+
export const PATTERN_KEYS = [
|
|
801
|
+
"time_words", // 规则 2① 当下时间词
|
|
802
|
+
"historic_date", // 规则 2② 历史/绝对日期
|
|
803
|
+
"evidence_mark", // 规则 2② 事件时间证据锚
|
|
804
|
+
"promise_words", // 规则 7 承诺/大话术词
|
|
805
|
+
"source_mark", // 规则 5 来源引用标记
|
|
806
|
+
"domain_words", // 规则 18 DSH 领域词
|
|
807
|
+
"plan_instruction", // M7 方案性指令词
|
|
808
|
+
"write_instruction", // M7 落盘性词(豁免提醒)
|
|
809
|
+
"execute_action", // M7 执行动作词(豁免提醒)
|
|
810
|
+
"tech_term", // 规则 11③ 技术术语
|
|
811
|
+
"term_explanation", // 规则 11③ 术语解释伴随词
|
|
812
|
+
"suggest_words", // 规则 16 建议类表述
|
|
813
|
+
"negating_suggestion", // 规则 16 否定/合规声明语境
|
|
814
|
+
"paraphrase_before", // 转述语境(第三人称 + 说/表示…)
|
|
815
|
+
"paraphrase_mark_before", // 引述标记(前)
|
|
816
|
+
"paraphrase_mark_after", // 引述标记(后)
|
|
817
|
+
"criticism_shape", // 规则 22② A″ 批评强形态(连续问号/叹号)
|
|
818
|
+
"criticism_weak", // 规则 22② A″ 批评弱形态(反问/责问)
|
|
819
|
+
"exec_directive", // A″ 反向检查:消息含明确执行指令词(弱信号仅留痕不提示)
|
|
820
|
+
"activate_skill", // 规则 12B 激活词
|
|
821
|
+
"activate_approval", // 规则 12A 激活词
|
|
822
|
+
"activate_network", // 规则 12C 激活词
|
|
823
|
+
"activate_backup", // 规则 13A 激活词
|
|
824
|
+
"activate_time", // 规则 2 激活词
|
|
825
|
+
"activate_promise", // 规则 7 激活词
|
|
826
|
+
"activate_source", // 规则 5 激活词
|
|
827
|
+
"delivery_claim", // 检测正则(小批 B 第一小域)
|
|
828
|
+
"internal_ref", // 检测正则(小批 B 第一小域)
|
|
829
|
+
"verify_intent", // 检测正则(小批 B 第一小域)
|
|
830
|
+
"empty_talk", // 检测正则(小批 B 第一小域)
|
|
831
|
+
"delivery_no_verify", // 检测正则(小批 B 第一小域)
|
|
832
|
+
"verify_evidence", // 检测正则(小批 B 第一小域)
|
|
833
|
+
"mount_mention", // 检测正则(小批 B 第一小域)
|
|
834
|
+
"mount_audit_ok", // 检测正则(小批 B 第一小域)
|
|
835
|
+
"scope_overreach", // 检测正则(小批 B 第一小域)
|
|
836
|
+
"scope_bounded", // 检测正则(小批 B 第一小域)
|
|
837
|
+
"apology_only", // 检测正则(小批 B 第一小域)
|
|
838
|
+
"apology_with_cause", // 检测正则(小批 B 第一小域)
|
|
839
|
+
"version_record_mention", // 检测正则(小批 B 第一小域)
|
|
840
|
+
"version_record_only", // 检测正则(小批 B 第一小域)
|
|
841
|
+
"version_sync_ok", // 检测正则(小批 B 第一小域)
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* 映射型配置键(键 → { 子键: source }):一张表多条正则的场景(如条款自证触发词)。
|
|
846
|
+
* 子键语义由消费方定义;与扁平正则键共用同一配置层与回退语义。
|
|
847
|
+
*/
|
|
848
|
+
export const PATTERN_MAP_KEYS = {
|
|
849
|
+
// 内置默认**空表**(判据 A,2026-09-09):键=规则号,属发布者私有的规则体系,
|
|
850
|
+
// 一律从 rule-engine.json 的 patterns.self_cert_hints 注入;空表时该机制静默不触发。
|
|
851
|
+
self_cert_hints: {}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
/** 数值型配置键(键 → 内置默认;与正则键共用同一配置层与校验/回退语义) */
|
|
855
|
+
export const PATTERN_NUM_KEYS = {
|
|
856
|
+
criticism_caps_ratio: 0.6 // 英文全大写比率阈值(≥ 该值且字母数 ≥6 视为"喊叫"强形态)
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// 内置默认:语言无关最小集(无中文)
|
|
860
|
+
const BUILTIN_PATTERNS = {
|
|
861
|
+
time_words: "(?:today|yesterday|just now|a moment ago|\\d+\\s*minutes? ago)",
|
|
862
|
+
historic_date: "20\\d{2}[-/.]\\d{1,2}[-/.]\\d{1,2}",
|
|
863
|
+
evidence_mark: "mtime|ts\\s*[:=]|\\b[0-9a-f]{7,40}\\b|\\bv\\d+(?:\\.\\d+){1,2}\\b",
|
|
864
|
+
promise_words: "(?:i promise|guaranteed|absolutely (?:fine|no problem)|100% sure|no way it fails)",
|
|
865
|
+
source_mark: "via|source|reference|quoted from",
|
|
866
|
+
domain_words: "DSH|dsh|profile|bundle|plugin|skill|config|migrat|manual|session|assembly",
|
|
867
|
+
plan_instruction: "plan|proposal|adjust|supplement|suggest|evaluat|draft|refine|optimi[sz]e|improve|restructure|review",
|
|
868
|
+
write_instruction: "persist|write|publish|save to manual|confirm(?:ed)?",
|
|
869
|
+
execute_action: "execute|run|implement|start|apply|continue|proceed|operate",
|
|
870
|
+
tech_term:
|
|
871
|
+
"\\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",
|
|
872
|
+
term_explanation: "e\\.g\\.|for example|i\\.e\\.|in other words|analogy|plainly|understand (?:it )?as|for instance",
|
|
873
|
+
suggest_words: "i suggest|recommend|better option|optimi[sz]ation suggestion|how about",
|
|
874
|
+
negating_suggestion:
|
|
875
|
+
"(?:no longer|not repeat|avoid|stop|cancel|not yet|never again|already (?:echoed|responded)|no new (?:suggestion|repetition)|just waiting)",
|
|
876
|
+
paraphrase_before:
|
|
877
|
+
"(?: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)",
|
|
878
|
+
paraphrase_mark_before: "quote|quoted|original text|paraphras|forward|according to|wrote|changed to|reword",
|
|
879
|
+
paraphrase_mark_after: "changed to|reword|paraphras|quote",
|
|
880
|
+
// A″ 强形态:连续问号/叹号(全角半角均计;标点非 CJK 汉字,故属语言无关形态)
|
|
881
|
+
criticism_shape: "(?:[??]{3,}|[!!]{4,})",
|
|
882
|
+
// A″ 弱形态:英文最小集(中文反问形态由本机 patterns 注入)
|
|
883
|
+
criticism_weak:
|
|
884
|
+
"(?:why (?:are you |do you )?(?:still|again|always)|you (?:again|always|never)|this is (?:wrong|not right|unacceptable)|what (?:are you|the hell))",
|
|
885
|
+
// A″ 反向检查(2026-09-09 第三批):消息含明确执行指令词 → 弱信号只留痕不注入提示
|
|
886
|
+
exec_directive:
|
|
887
|
+
"(?:write|update|execute|submit|persist|apply|implement|fix|modify|create|delete|publish|download|commit|push|run|refactor|rewrite)",
|
|
888
|
+
// 规则激活词(P8 小批 B 第一小域,2026-09-09):matcher.js 用
|
|
889
|
+
activate_skill: "(?:skill|plugin)",
|
|
890
|
+
activate_approval: "(?:execute|create|delete|overwrite|move|download|commit|config)",
|
|
891
|
+
activate_network: "(?:download|network|curl|proxy|blocked)",
|
|
892
|
+
activate_backup: "(?:delete|overwrite|migrate|backup)",
|
|
893
|
+
activate_time: "(?:time|today|yesterday|date)",
|
|
894
|
+
activate_promise: "(?:promise|guarantee|certainly)",
|
|
895
|
+
activate_source: "(?:quote|source|url|link)",
|
|
896
|
+
delivery_claim: "(?:done|completed|all (?:done|passed)|fixed|verified|finished)(?![^.]*(?:not yet|pending|incomplete|todo))",
|
|
897
|
+
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))",
|
|
898
|
+
verify_intent: "(?:verify|validate|cross-check|target|compare|confirm)",
|
|
899
|
+
empty_talk: "(?:got it|noted|i'?ll remember|sure, i'?ve noted)",
|
|
900
|
+
delivery_no_verify: "(?:done|delivered|completed)",
|
|
901
|
+
verify_evidence: "(?:runtime verif|mock|startup|actual test|test|verif)",
|
|
902
|
+
mount_mention: "(?:restart|assembly|mount)",
|
|
903
|
+
mount_audit_ok: "(?:audit passed|full audit|MOUNT CONSISTENT|DUPLICATES FOUND|audit-mount-consistency)",
|
|
904
|
+
scope_overreach: "(?:i (?:also |additionally )?(?:added|supplemented|included)|extra add)",
|
|
905
|
+
scope_bounded: "(?:only (?:the )?selected|not implemented|separately confirm)",
|
|
906
|
+
apology_only: "(?:sorry|my mistake|my fault|i was wrong)",
|
|
907
|
+
apology_with_cause: "(?:cause|reason|correction|prevent|avoid|mechanism|fix)",
|
|
908
|
+
version_record_mention: "(?:changelog|version record|v\\d+\\.\\d+)",
|
|
909
|
+
version_record_only: "(?:logged (?:to )?changelog|changelog updated)",
|
|
910
|
+
version_sync_ok: "(?:body|sync|no sync needed)"
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
let patternsOverride = null;
|
|
914
|
+
let patCache = new Map();
|
|
915
|
+
|
|
916
|
+
function compilePattern(source, flags) {
|
|
917
|
+
if (typeof source !== "string" || source.length === 0) return null;
|
|
918
|
+
try {
|
|
919
|
+
return new RegExp(source, flags);
|
|
920
|
+
} catch {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function resolvePatternSource(key) {
|
|
926
|
+
const ov = patternsOverride?.[key];
|
|
927
|
+
if (typeof ov === "string" && ov.length > 0) return ov;
|
|
928
|
+
return BUILTIN_PATTERNS[key] ?? "";
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** 数值型键的生效值(个人层优先 → 内置默认) */
|
|
932
|
+
/** 取当前生效的映射型配置(个人层优先 → 内置默认);无对象 = 空表 */
|
|
933
|
+
export function patMap(key) {
|
|
934
|
+
const ov = patternsOverride?.[key];
|
|
935
|
+
if (ov && typeof ov === "object" && !Array.isArray(ov)) return ov;
|
|
936
|
+
return PATTERN_MAP_KEYS[key] ?? {};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** 数值型键的生效值(个人层优先 → 内置默认) */
|
|
940
|
+
export function patNum(key) {
|
|
941
|
+
const ov = patternsOverride?.[key];
|
|
942
|
+
if (typeof ov === "number" && Number.isFinite(ov)) return ov;
|
|
943
|
+
return PATTERN_NUM_KEYS[key] ?? 0;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* 注入个人层检测正则(rule-engine.json 的 patterns 键)。
|
|
948
|
+
* 语义同 setLexicons:undefined/null = 不干预;对象 = 配置即真相;`{}` = 回退内置默认。
|
|
949
|
+
* @returns {{applied: string[], rejected: Array<{key: string, reason: string}>, noop?: boolean}}
|
|
950
|
+
*/
|
|
951
|
+
export function setPatterns(cfg) {
|
|
952
|
+
const applied = [];
|
|
953
|
+
const rejected = [];
|
|
954
|
+
if (cfg === undefined || cfg === null) return { applied, rejected, noop: true };
|
|
955
|
+
if (typeof cfg !== "object" || Array.isArray(cfg)) {
|
|
956
|
+
patternsOverride = null;
|
|
957
|
+
patCache = new Map();
|
|
958
|
+
return { applied, rejected };
|
|
959
|
+
}
|
|
960
|
+
const next = {};
|
|
961
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
962
|
+
// 映射型键(值 = 对象)
|
|
963
|
+
if (PATTERN_MAP_KEYS[k] !== undefined) {
|
|
964
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
965
|
+
rejected.push({ key: k, reason: "not-object" });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const sub = {};
|
|
969
|
+
for (const [sk, sv] of Object.entries(v)) {
|
|
970
|
+
if (typeof sv !== "string" || !compilePattern(sv, "i")) {
|
|
971
|
+
rejected.push({ key: `${k}.${sk}`, reason: "invalid-regex" });
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
sub[sk] = sv;
|
|
975
|
+
}
|
|
976
|
+
if (Object.keys(sub).length > 0) {
|
|
977
|
+
next[k] = sub;
|
|
978
|
+
applied.push(k);
|
|
979
|
+
}
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
// 数值型键(阈值):0..1 的有限数;字符串数字也接受(JSON 手写容错)
|
|
983
|
+
if (PATTERN_NUM_KEYS[k] !== undefined) {
|
|
984
|
+
const num = typeof v === "number" ? v : Number(v);
|
|
985
|
+
if (!Number.isFinite(num) || num < 0 || num > 1) {
|
|
986
|
+
rejected.push({ key: k, reason: "invalid-number" });
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
next[k] = num;
|
|
990
|
+
applied.push(k);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (!PATTERN_KEYS.includes(k)) {
|
|
994
|
+
rejected.push({ key: k, reason: "unknown-key" });
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
998
|
+
rejected.push({ key: k, reason: "empty-or-not-string" });
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (!compilePattern(v, "i")) {
|
|
1002
|
+
rejected.push({ key: k, reason: "invalid-regex" });
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
next[k] = v;
|
|
1006
|
+
applied.push(k);
|
|
1007
|
+
}
|
|
1008
|
+
patternsOverride = Object.keys(next).length > 0 ? next : null;
|
|
1009
|
+
patCache = new Map();
|
|
1010
|
+
return { applied, rejected };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/** 显式回退内置默认(测试用;生产环境改配置后由插件重载生效) */
|
|
1014
|
+
export function resetPatterns() {
|
|
1015
|
+
patternsOverride = null;
|
|
1016
|
+
patCache = new Map();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** 当前生效的正则 source 快照(键 → source;数值键 → 数值;只读,供测试/诊断) */
|
|
1020
|
+
export function effectivePatterns() {
|
|
1021
|
+
const out = {};
|
|
1022
|
+
for (const k of PATTERN_KEYS) out[k] = resolvePatternSource(k);
|
|
1023
|
+
for (const k of Object.keys(PATTERN_NUM_KEYS)) out[k] = patNum(k);
|
|
1024
|
+
return out;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** 当前是否处于个人层覆盖状态 */
|
|
1028
|
+
export function hasPatternOverride() {
|
|
1029
|
+
return patternsOverride !== null;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** 取当前生效正则(唯一取词入口;调用方禁止缓存返回对象)。非法/空 source → 永不命中占位。 */
|
|
1033
|
+
export function patRe(key) {
|
|
1034
|
+
const source = resolvePatternSource(key);
|
|
1035
|
+
const ck = `${key}\u0000${source}`;
|
|
1036
|
+
let hit = patCache.get(ck);
|
|
1037
|
+
if (!hit) {
|
|
1038
|
+
hit = compilePattern(source, "i") || /(?!)/;
|
|
1039
|
+
patCache.set(ck, hit);
|
|
1040
|
+
}
|
|
1041
|
+
return hit;
|
|
744
1042
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// quality-ledger.js - 质量账本(机制进发布面,数据留本机,签名不外泄)
|
|
2
|
+
//
|
|
3
|
+
// 设计原则(第三批方案 §一):
|
|
4
|
+
// · 机制层(本文件,随包发布):taskSignature / recordQuality / qualityTrend —— 每个安装者可用;
|
|
5
|
+
// · 数据层(~/.dsh/quality-ledger.jsonl,**永不发布**):每单一条记录;
|
|
6
|
+
// · 配置层(rule-engine.json 的 qualityLedger 键,默认 enabled:false):不开不产生任何文件;
|
|
7
|
+
// · 隐私:落盘的是 sha256 归一化指纹(12 位),**单向不可逆**——看到 jsonl 也只知道
|
|
8
|
+
// "有个指纹的任务做过",不知道任务内容。
|
|
9
|
+
//
|
|
10
|
+
// 不做的事:不拦截、不评分、不上传(纯旁路统计)。
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
/** 默认配置(可被 rule-engine.json 的 qualityLedger 覆盖) */
|
|
17
|
+
export const DEFAULT_LEDGER_CONFIG = {
|
|
18
|
+
enabled: false, // 默认关——不开不产生任何文件
|
|
19
|
+
window: 5, // 趋势窗口(最近 N 单 vs 之前 N 单)
|
|
20
|
+
z: 1.96 // 预留:二期 Wilson 区间用
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** 账本文件路径(本机数据,永不发布) */
|
|
24
|
+
export function ledgerPath(home) {
|
|
25
|
+
const base = home || process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
26
|
+
return path.join(base, "quality-ledger.jsonl");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 任务签名:对 purpose + 验收断言做**归一化**后取 sha256 前 12 位。
|
|
31
|
+
* 归一化规则(公开可审计):去引号 → 绝对路径替换为 <path> → 数字替换为 <n> → 折叠空白 → 小写。
|
|
32
|
+
* 目的:同一类任务(措辞/数字/路径不同)得到同一签名,从而能统计"同类任务返工率"。
|
|
33
|
+
*/
|
|
34
|
+
export function taskSignature(purpose, assertions = []) {
|
|
35
|
+
const parts = [String(purpose || ""), ...(Array.isArray(assertions) ? assertions : [assertions])];
|
|
36
|
+
const norm = parts
|
|
37
|
+
.join("\n")
|
|
38
|
+
.replace(/["'`「」『』]/g, "")
|
|
39
|
+
.replace(/[A-Za-z]:[\\/][^\s,;)]*/g, "<path>")
|
|
40
|
+
.replace(/\d+(?:\.\d+)?/g, "<n>")
|
|
41
|
+
.replace(/\s+/g, " ")
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
return createHash("sha256").update(norm, "utf8").digest("hex").slice(0, 12);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 记录一单的质量指标(旁路统计;失败静默——账本不能影响主流程)。
|
|
49
|
+
* @param {object} entry - { purpose, assertions, rework, interventions, frictions, tokens, ts }
|
|
50
|
+
* @param {object} opts - { config, home, now }
|
|
51
|
+
* @returns {{ok: boolean, skipped?: string, path?: string}}
|
|
52
|
+
*/
|
|
53
|
+
export function recordQuality(entry = {}, opts = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const cfg = { ...DEFAULT_LEDGER_CONFIG, ...(opts.config || {}) };
|
|
56
|
+
if (!cfg.enabled) return { ok: false, skipped: "disabled" }; // 默认关:不落盘
|
|
57
|
+
const purpose = entry.purpose ?? entry.task ?? "";
|
|
58
|
+
if (!purpose) return { ok: false, skipped: "no-purpose" };
|
|
59
|
+
const rec = {
|
|
60
|
+
sig: taskSignature(purpose, entry.assertions || []),
|
|
61
|
+
ts: typeof entry.ts === "number" ? entry.ts : (opts.now ?? Date.now()),
|
|
62
|
+
rework: Number(entry.rework) || 0,
|
|
63
|
+
interventions: Number(entry.interventions) || 0,
|
|
64
|
+
frictions: Number(entry.frictions) || 0,
|
|
65
|
+
tokens: Number(entry.tokens) || 0
|
|
66
|
+
};
|
|
67
|
+
const file = ledgerPath(opts.home);
|
|
68
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
+
fs.appendFileSync(file, JSON.stringify(rec) + "\n", "utf8");
|
|
70
|
+
return { ok: true, path: file };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return { ok: false, skipped: `error:${error instanceof Error ? error.message : String(error)}` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 读取账本(坏行跳过;返回按时间升序的记录数组) */
|
|
77
|
+
export function loadLedger(opts = {}) {
|
|
78
|
+
const file = ledgerPath(opts.home);
|
|
79
|
+
if (!fs.existsSync(file)) return [];
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
82
|
+
const s = line.trim();
|
|
83
|
+
if (!s) continue;
|
|
84
|
+
try {
|
|
85
|
+
const rec = JSON.parse(s);
|
|
86
|
+
if (rec && typeof rec.sig === "string") out.push(rec);
|
|
87
|
+
} catch {
|
|
88
|
+
// 坏行跳过(账本是旁路数据,不能因一行损坏而失效)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 指标方向:后窗 vs 前窗的均值比较(值越小越好) */
|
|
95
|
+
function direction(before, after) {
|
|
96
|
+
if (before === after) return "持平";
|
|
97
|
+
return after < before ? "改善" : "恶化";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 质量趋势:按签名取最近 window 单 vs 之前 window 单,输出各指标方向 + 明细。
|
|
102
|
+
* @returns {{sig, count, window, recent, previous, metrics, summary}}
|
|
103
|
+
*/
|
|
104
|
+
export function qualityTrend(signature, opts = {}) {
|
|
105
|
+
const cfg = { ...DEFAULT_LEDGER_CONFIG, ...(opts.config || {}) };
|
|
106
|
+
const w = Math.max(1, Number(opts.window ?? cfg.window) || 5);
|
|
107
|
+
const rows = loadLedger(opts).filter((r) => r.sig === signature);
|
|
108
|
+
const recent = rows.slice(-w);
|
|
109
|
+
const previous = rows.slice(-2 * w, -w);
|
|
110
|
+
const avg = (list, key) => (list.length === 0 ? 0 : list.reduce((a, r) => a + (Number(r[key]) || 0), 0) / list.length);
|
|
111
|
+
const metrics = {};
|
|
112
|
+
for (const key of ["rework", "interventions", "frictions", "tokens"]) {
|
|
113
|
+
const b = avg(previous, key);
|
|
114
|
+
const a = avg(recent, key);
|
|
115
|
+
metrics[key] = { before: Number(b.toFixed(2)), after: Number(a.toFixed(2)), direction: direction(b, a) };
|
|
116
|
+
}
|
|
117
|
+
const worst = Object.entries(metrics).filter(([, m]) => m.direction === "恶化").map(([k]) => k);
|
|
118
|
+
const summary = rows.length < 2
|
|
119
|
+
? `样本不足(${rows.length} 单)——至少 2 单才能比较`
|
|
120
|
+
: worst.length === 0
|
|
121
|
+
? `方向:持平或改善(近 ${recent.length} 单 vs 前 ${previous.length} 单)`
|
|
122
|
+
: `方向:${worst.join("、")} 恶化(近 ${recent.length} 单 vs 前 ${previous.length} 单)`;
|
|
123
|
+
return { sig: signature, count: rows.length, window: w, recent, previous, metrics, summary };
|
|
124
|
+
}
|