dsh-rule-engine 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -28,7 +28,8 @@ import {
28
28
  PRIVATE_NETWORK_RE,
29
29
  setWorkspaceRoot,
30
30
  setWorkspaceRoots,
31
- setSessionWorkspaceRoot
31
+ setSessionWorkspaceRoot,
32
+ setPatterns
32
33
  } from "./core/patterns.js";
33
34
  import {
34
35
  askQuestionCoreText,
@@ -62,7 +63,11 @@ import { toolClass } from "./core/tool-catalog.js";
62
63
  import { parseWhitelist, mergeWhitelist, serializeWhitelist } from "./core/whitelist.js";
63
64
  import { state } from "./core/runtime.js";
64
65
  import { APPROVE_TYPES, normalizePath, setTypeHints } from "./core/authorization.js";
65
- import { DELIVERY_RE, detectViolations, extractAssistantText } from "./core/text-detect.js";
66
+ import { setLexicons } from "./core/lexicon.js";
67
+ import { getMessage } from "./messages.js";
68
+ import { getBlindSpots, getFreedomSurface, setGuardFreedom } from "./core/guard-core.js";
69
+ import { detectLang, getLang, getLangSource, pickLang } from "./core/lang.js";
70
+ import { DELIVERY_RE, detectViolations, extractAssistantText, hasExplicitExecWord, setCriticismPersonal } from "./core/text-detect.js";
66
71
  import { buildTurnCard } from "./core/turn-card.js";
67
72
  import { shouldDetectTurn, shouldDeliver } from "./core/semantic.js";
68
73
  import { judgeViolation, judgeViolationsBatch } from "./core/judge.js";
@@ -102,8 +107,45 @@ export const name = "dsh-rule-engine";
102
107
  export const inject = ["tools", "commands", "agents", "workspaceRegistry", "skills", "llm"];
103
108
 
104
109
  const pluginConfig = loadPluginConfig();
110
+ // §1.3 行为闸判定(纯函数,2026-09-08 第二批 A″):批评疑似回合冻结写类工具——返回拒绝原因或 null
111
+ export function criticismFreezeDecision(session, toolName, args) {
112
+ if (!session?.turn?.criticismFrozen) return null;
113
+ if (toolClass(toolName, args || {}) !== "mutating") return null;
114
+ return "本回合命中批评/指错疑似信号——写类工具已冻结(A″ 行为闸)。请按四段模板回应:① 停止;② 归因四问;③ 三件套(事实/原因/方案);④ 等指令。只读操作不受限。";
115
+ }
105
116
  // C4:启动时合并配置层 typeHints(本机扩展授权类型;默认空=仅内置 8+archive 类)
106
117
  setTypeHints(pluginConfig.typeHints);
118
+ // P8 小批 A(2026-09-08):行为词表配置层注入(rule-engine.json 的 lexicons 键)——
119
+ // null/缺省 = 用内置通用最小集;非空 = 按键完全替换(Override 模式,同 setTypeHints)。
120
+ // 非法键/非法正则被拒绝并记审计(不静默半套生效)。
121
+ {
122
+ const lexRes = setLexicons(pluginConfig.lexicons);
123
+ if (lexRes.rejected.length > 0) {
124
+ audit({
125
+ kind: "lexicon-config-rejected",
126
+ rule: "__lexicon-config",
127
+ name: "词表配置注入:部分键被拒绝",
128
+ event: "startup",
129
+ reason: `lexicons 配置被拒绝的键:${lexRes.rejected.map((r) => `${r.key}(${r.reason})`).join(", ")};生效键:${lexRes.applied.join(", ") || "无"}`,
130
+ session: "global"
131
+ });
132
+ }
133
+ // P8 小批 B(2026-09-08):检测正则配置层注入(rule-engine.json 的 patterns 键)——同 Override 语义。
134
+ // 仅覆盖「文本健康检测」类正则(规则 2/5/7/11/16 与 M7 提醒);命令/路径守卫类正则恒在机制层。
135
+ const patRes = setPatterns(pluginConfig.patterns);
136
+ if (patRes.rejected.length > 0) {
137
+ audit({
138
+ kind: "pattern-config-rejected",
139
+ rule: "__pattern-config",
140
+ name: "检测正则配置注入:部分键被拒绝",
141
+ event: "startup",
142
+ reason: `patterns 配置被拒绝的键:${patRes.rejected.map((r) => `${r.key}(${r.reason})`).join(", ")};生效键:${patRes.applied.join(", ") || "无"}`,
143
+ session: "global"
144
+ });
145
+ }
146
+ }
147
+ // §1.3(2026-09-08 第二批):本机辱骂词表注入(通用发布面恒空——语言无关形态检测恒在)
148
+ setCriticismPersonal(pluginConfig.criticismPersonal);
107
149
  state.enabled = pluginConfig.enabled;
108
150
  applyTaskContractConfig(state, pluginConfig);
109
151
  // T3 通用化(2026-08-31):声明式绑定覆盖表注入(rule-engine.json 可选键 handlerOverrides;缺省 {})
@@ -151,6 +193,11 @@ if (!state.localIntegrations?.entryScript) {
151
193
  }
152
194
  // reloadRules 内部已统一刷新理解产物(P0-3),此处不再重复写
153
195
  reloadRules(state);
196
+ // 第三批第 1 波 §4:首启语言探测——读规则集文本(AGENTS.md 解析结果),含 CJK → zh-CN。
197
+ // 仅内存缓存、每次启动重探、不落盘(无配置文件副作用)。影响:报错/提示/自由面/盲区展示语言。
198
+ detectLang(state.configs.map((c) => `${c.ruleId} ${c.title}`).join(" "));
199
+ // 第 1 批 1b:本机自由面/盲区注入(rule-engine.json 的 guardFreedom 键;缺省=通用骨架)
200
+ setGuardFreedom(pluginConfig.guardFreedom);
154
201
 
155
202
  // ── 工具函数 ────────────────────────────────────────────────────────────────
156
203
 
@@ -486,12 +533,12 @@ export function handleSessionEvent(ctx, session, event) {
486
533
  rule: "__engram-gap",
487
534
  name: "双通道记忆缺失",
488
535
  event: "turn/end",
489
- reason: "本回合经本机配置的统一入口落盘了手册/AGENTS,但未同轮调用 engram_store;按规则 19/77/M8 应在同一回合完成记忆沉淀",
536
+ reason: getMessage("memory.sediment-missing"),
490
537
  session: sid
491
538
  });
492
539
  maybeInject(ctx, sid, {
493
540
  ruleId: "__engram-gap",
494
- reason: "规则 19/M8:手册/AGENTS 落盘后应在同一回合补 engram_store,否则记忆机制断链(已记审计)"
541
+ reason: getMessage("memory.sediment-missing")
495
542
  });
496
543
  }
497
544
  // F1(2026-08-28 阶段三):规则 2 时序竞态修复——assistant/message 检测到时间词违规时
@@ -1075,7 +1122,7 @@ export function handleSessionEvent(ctx, session, event) {
1075
1122
  // 交付声明机器闸门(机制批 M3,规则 23④):完成类声明 → 核对同会话 30 分钟内 verify-pass 记录,缺失注入纠正
1076
1123
  // F2(2026-08-28 阶段三):词面收紧——"完成"裸词太宽("完成社区检索/尚未完成/正在完成"均误触),
1077
1124
  // 改为强完成声明模式且排除否定/进行态;仍由 LLM 裁决层兜底(deliverSuspects)。
1078
- if (DELIVERY_RE.test(text)) {
1125
+ if (DELIVERY_RE().test(text)) {
1079
1126
  const windowStart = Date.now() - 30 * 60 * 1000;
1080
1127
  const hasPass = (state.verifyPass || []).some((v) => v.sessionId === sid && v.at > windowStart);
1081
1128
  if (!hasPass) {
@@ -1130,6 +1177,31 @@ export function handleSessionEvent(ctx, session, event) {
1130
1177
  }
1131
1178
  // F1(2026-08-28 阶段三):规则 2 违规不在此时投递——标记 pendingRule2,turn/end 复核(Get-Date 定案)①
1132
1179
  let violations = detectViolations({ configs: state.configs, session: s, text, reasoningText: s.turn.reasoningText, mountRevision: state.mountRevision, rule5Window: pluginConfig.rule5SourceWindow ?? 3 });
1180
+ // §1.3 行为闸标记(2026-09-08 第二批 A″):批评/指错疑似 → 本回合冻结写类工具(turn 级,下回合自动解除)
1181
+ const critHit = violations.find((v) => v.mode === "criticism" && v.suspect);
1182
+ if (critHit) {
1183
+ s.turn.criticismSuspect = critHit.suspect;
1184
+ if (critHit.suspect === "strong") {
1185
+ s.turn.criticismFrozen = true;
1186
+ } else {
1187
+ // 弱信号(2026-09-09 第三批后果分级):不冻结;含执行指令词则仅留痕不提示
1188
+ const execDirective = hasExplicitExecWord(s.turn.userText || "");
1189
+ if (!execDirective) {
1190
+ maybeInject(ctx, sid, {
1191
+ ruleId: "22",
1192
+ reason: "用户消息疑似批评/质问形态(规则 22②,待确认):若确属批评——批评不构成任何授权,请先按归因四问产出三件套(事实/原因/方案)再等指令;若只是普通提问可忽略本条"
1193
+ });
1194
+ }
1195
+ }
1196
+ audit({
1197
+ kind: "criticism-suspect",
1198
+ rule: "22",
1199
+ name: "批评疑似信号(A″)",
1200
+ event: "assistant/message",
1201
+ reason: critHit.suspect === "strong" ? "信号=strong——本回合写类工具冻结(A″ 强信号闸)" : "信号=weak——不冻结(后果分级:留痕+提示)",
1202
+ session: sid
1203
+ });
1204
+ }
1133
1205
  const rule2s = violations.filter((v) => v.ruleId === "2");
1134
1206
  for (const v of rule2s) {
1135
1207
  if (!s.turn.pendingRule2) s.turn.pendingRule2 = v.reason;
@@ -1197,7 +1269,8 @@ const COMMAND_SPECS = [
1197
1269
  { name: "contract", args: "", desc: "查看当前任务契约" },
1198
1270
  { name: "contract categories", args: "...", desc: "设定契约类别白名单(0.5.12)" },
1199
1271
  { name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" },
1200
- { name: "approve", args: "<type> <路径> [min]", desc: "物理确认:授予指定类型+路径的临时授权(仅用户输入;默认 10 分钟)" }
1272
+ { name: "approve", args: "<type> <路径> [min]", desc: "物理确认:授予指定类型+路径的临时授权(仅用户输入;默认 10 分钟)" },
1273
+ { name: "freedom", args: "", desc: "查看守卫自由面(不拦什么)与盲区(看不见/判不准什么)(第三批 P1-2)" }
1201
1274
  ];
1202
1275
 
1203
1276
  const USAGE = [
@@ -1219,6 +1292,8 @@ function parseCommand(rawInput) {
1219
1292
  if (!text || /^(status|state)$/i.test(text)) return { kind: "status" };
1220
1293
  if (/^(rules|list|ls)$/i.test(text)) return { kind: "rules" };
1221
1294
  if (/^(active)$/i.test(text)) return { kind: "active" };
1295
+ // 第三批 P1-2:自由面/盲区查询(parseCommand 是白名单解析器——新子命令必须在此登记,否则落 invalid)
1296
+ if (/^(freedom|blindspots|free)$/i.test(text)) return { kind: "freedom" };
1222
1297
  if (/^(help|\?)$/i.test(text)) return { kind: "help" };
1223
1298
  let m = text.match(/^unlock\s*(\d+)?$/i);
1224
1299
  if (m) return { kind: "unlock", minutes: m[1] ? Number(m[1]) : 10 };
@@ -1294,6 +1369,7 @@ async function executeGuard(ctx, invocation) {
1294
1369
  ` 解锁剩余:${fmtRemain(remainMs(state.unlockUntil))}(配置写保护豁免)`,
1295
1370
  ` 放行剩余:${fmtRemain(remainMs(state.bypassUntil))}(全部守卫暂停)`,
1296
1371
  ` 授权存储:内存态(重启失效)`,
1372
+ ` 展示语言:${getLang()}(来源 ${getLangSource()};启动时按 AGENTS.md 语言探测,不落盘)`,
1297
1373
  ` 审计日志:${auditFilePath()}`,
1298
1374
  ` LLM 意图判定:${state.llmIntentCfg?.enabled ? `开启(今日 ${[...(state.llmIntentBudget?.values() || [])].reduce((a, b) => a + b, 0)} 次/${pluginConfig.llmIntent?.dailyLimitPerSession ?? 50},缓存命中 ${state.llmIntentHits || 0},缓存 ${state.llmIntentCache?.size || 0} 条${state.llmIntentLast ? `;最近:${state.llmIntentLast.text} → ${state.llmIntentLast.source}${state.llmIntentLast.verdict ? `(conf=${state.llmIntentLast.verdict.confidence})` : "【降级词表】"}` : ""})` : "关闭"}`,
1299
1375
  ` 最近激活:${state.lastActive.length ? state.lastActive.map((a) => a.ruleId).join("、") : "无"}`,
@@ -1301,6 +1377,14 @@ async function executeGuard(ctx, invocation) {
1301
1377
  ];
1302
1378
  return { kind: "success", text: parts.join("\n") };
1303
1379
  }
1380
+ case "freedom": {
1381
+ const parts = [pickLang("守卫自由面(无需授权即可执行):", "Guard freedom surface (no authorization required):"), ""];
1382
+ for (const s of getFreedomSurface()) parts.push(` ✓ ${s}`);
1383
+ parts.push("", pickLang("守卫盲区(本引擎看不见/判不准——它不是安全边界):", "Guard blind spots (invisible/imprecise — this engine is NOT a security boundary):"), "");
1384
+ for (const s of getBlindSpots()) parts.push(` ⚠ ${s}`);
1385
+ parts.push("", pickLang("提示:被拦时先看拒绝文案里的「放行:…」段——那是本回合的具体放行条件。", "Tip: when blocked, read the \"放行 / how to unblock\" segment in the rejection text — it states this turn's exact unblock condition."));
1386
+ return { kind: "success", text: parts.join("\n") };
1387
+ }
1304
1388
  case "rules": {
1305
1389
  if (state.configs.length === 0) return { kind: "success", text: "当前没有可执行规则(AGENTS.md 为空或缺失)。" };
1306
1390
  const parts = [`共 ${state.configs.length} 条规则:`, ""];
@@ -1883,6 +1967,35 @@ export function apply(ctx) {
1883
1967
  }
1884
1968
  });
1885
1969
 
1970
+ // §1.3 行为闸(2026-09-08 第二批 A″):批评/指错疑似回合 → 冻结全部写类工具(零调用);
1971
+ // 只读/分析类不受限;标记为 turn 级,下一回合自动解除(用户可继续只读工作)。
1972
+ ctx.on("tools/pre-execute", async (exec, next) => {
1973
+ try {
1974
+ const sid = sessionIdOfExec(exec);
1975
+ const s = getSessionState(state, sid);
1976
+ if (!s?.turn?.criticismFrozen) return next();
1977
+ const freeze = criticismFreezeDecision(s, exec?.name, exec?.arguments);
1978
+ if (!freeze) return next();
1979
+ audit({
1980
+ kind: "criticism-freeze",
1981
+ rule: "22",
1982
+ name: "批评回合冻结写类工具",
1983
+ event: "tools/pre-execute",
1984
+ tool: exec?.name,
1985
+ args: summarizeArgs(exec?.arguments),
1986
+ reason: `疑似信号=${s.turn.criticismSuspect}(A″ 行为闸:批评不构成授权)`,
1987
+ session: sid
1988
+ });
1989
+ return {
1990
+ kind: "deny",
1991
+ reason: "[guardian:rule22] 【硬拦截】本回合命中批评/指错疑似信号——写类工具已冻结(A″ 行为闸)。请按四段模板回应:① 停止;② 归因四问;③ 三件套(事实/原因/方案);④ 等指令。只读操作不受限。"
1992
+ };
1993
+ } catch (error) {
1994
+ ctx.logger?.warn?.("[dsh-rule-engine] criticism-freeze error", error);
1995
+ return next();
1996
+ }
1997
+ });
1998
+
1886
1999
  // 2. session/event 监听:文本纠察 + 时序状态
1887
2000
  ctx.on("session/event", (session, event) => {
1888
2001
  try {
@@ -0,0 +1,134 @@
1
+ // messages.js - 用户可见文案的集中层(第三批清淤 · 第 1 批 1a)
2
+ //
3
+ // 为什么有这个文件(分层判据,方案 §一):
4
+ // 「面向用户的文案」随语言变化 → 属个人层。本文件提供**取词机制**(通用层),
5
+ // 内置默认用**英文**(发布面零中文的必经步骤),本机中文由 rule-engine.json 的
6
+ // messages 键覆盖。个人层加载后功能完全可用;通用安装者拿到的是可用的英文默认。
7
+ //
8
+ // Override 语义(同 lexicons / patterns):
9
+ // 键缺省 → 用内置默认;键存在 → 按键完全替换;{} → 回退内置;删键 → 回退。
10
+ //
11
+ // 用法:
12
+ // import { getMessage } from "./messages.js";
13
+ // getMessage("guard.unlock.done", { minutes: 10 });
14
+ // → "Unlocked config write protection for 10 minutes."(或本机覆盖文案)
15
+ //
16
+ // 缺失 key 返回 key 本身(可见失败,不静默吞掉)。
17
+ let overrides = null;
18
+
19
+ /** 内置默认文案(英文;通用层)。key 采用 `<域>.<对象>.<动作>` 命名。 */
20
+ export const DEFAULT_MESSAGES = {
21
+ // ── /guard 通用 ──
22
+ "guard.usage.header": "Usage:",
23
+ "guard.usage.footer": "Notes:",
24
+ "guard.invalid": "Unknown subcommand. Run /guard help for the list.",
25
+ "guard.rule-hint-fallback": "See /guard rules for this rule's details",
26
+ // ── /guard freedom ──
27
+ "guard.freedom.title": "Guard freedom surface (no authorization required):",
28
+ "guard.freedom.blind.title": "Guard blind spots (invisible/imprecise — this engine is NOT a security boundary):",
29
+ "guard.freedom.tip": "Tip: when blocked, read the \"how to unblock\" segment in the rejection text — it states this turn's exact condition.",
30
+ // ── /guard status ──
31
+ "guard.status.lang": "Display language: {lang} (source {source}; detected from AGENTS.md at startup, not persisted)",
32
+ // ── 逃生门 ──
33
+ "guard.unlock.done": "Config write protection unlocked for {minutes} minutes. You may now let the assistant edit rule-engine.json / rule-understanding.json / AGENTS.md; run /guard lock afterwards or wait for auto-restore.",
34
+ "guard.bypass.done": "All guards suspended for {minutes} minutes (audited).",
35
+ // ── 已知环境坑提示(原 patterns.js KNOWN_PITFALLS 的 hint,第 1 批 1b 迁出)──
36
+ "pitfall.tls": "TLS credential failure (common in sandbox / restricted sessions) — consult your manual's TLS entry; setting NODE_USE_ENV_PROXY may help Node fetch.",
37
+ "pitfall.proxy": "Proxy connection refused — make sure your proxy is running; git/gh often work direct while the npm registry may not (your release script may support DSH_RELEASE_PROXY).",
38
+ "pitfall.sandbox-pipe": "Child-process pipes are restricted in a confined sandbox — run with full filesystem access.",
39
+ "pitfall.module-missing": "Module missing — check the dependency closure / empty package shells.",
40
+ // ── 装配审计提示(原 guard-core/text-detect 写死本机脚本名,1b 去本机化)──
41
+ "pitfall.mount-audit": "Run your mount-consistency audit script (the one configured for this machine) and pass it before continuing.",
42
+ // ── 批评检测引用(原 text-detect 引用本机手册章节号,1b 去本机化)──
43
+ "criticism.strong": "User message matched a strong criticism/rebuke signal (rule 22②, pending adjudication): criticism is NOT authorization — write-class tools are frozen this turn. Respond with the four-part template: 1) stop; 2) attribution questions; 3) facts/cause/plan; 4) wait for instructions.",
44
+ "criticism.weak": "User message looks like a criticism/challenge (rule 22②, pending adjudication): if it really is criticism — it is not authorization. Produce facts/cause/plan via the attribution questions, then wait. Ignore this note if it was just an ordinary question.",
45
+ // ── M8 记忆沉淀提示(原 index.js 引用本机规则号,1b 去本机化)──
46
+ "memory.sediment-missing": "This turn persisted manual/AGENTS changes through the configured entry script but did not call engram_store in the same turn; the memory-sediment chain should complete within one turn.",
47
+ // ── 规则放行提示(原 guard-core RULE_HINTS,1c 迁出)──
48
+ "rule-hint.1": "Analyze the root cause first, then continue once the problem is confirmed",
49
+ "rule-hint.9": "Use a script file, or an explicit UTF-8 (no BOM) flow",
50
+ "rule-hint.12A": "Ask the user for a matching authorization first (ask_user_question)",
51
+ "rule-hint.13A": "Back up the target path first (copy to .bak / .backups/trash-)",
52
+ "rule-hint.18": "Read the configured manual first (path from localIntegrations; unconfigured = this rule has no object)",
53
+ "rule-hint.21": "Confirm per the rule's grading before persisting",
54
+ "rule-hint.22": "Answer/present a plan first, or add an explicit execution clause (tool class + path range); authorized changes must fall inside this turn's clause or ask answer",
55
+ "rule-hint.24": "Confirm the plugin is a dsh.bundle, or switch to the correct mount method",
56
+ "rule-hint.27": "Run your mount-consistency audit script (the one configured for this machine) before continuing",
57
+ // ── 条款自证提示(原 text-detect SELF_CERT_REASONS,1c 迁出)──
58
+ "self-cert.14": "Report completely in one go (rule 14): separate facts from inferences",
59
+ "self-cert.22": "Detected \"noted it\"-style filler; the correct action is to persist and report",
60
+ "self-cert.23": "Delivery/completion claim without runtime verification evidence",
61
+ "self-cert.16": "Use the bound-check format for suggestions (or give an effort-level suggestion with reasons) to avoid interrupting repeatedly",
62
+ "self-cert.12E": "Confirm the UI really exists and state capability boundaries first (rule 12E)",
63
+ "self-cert.26": "Confirm a formal Release Asset (release.assets) with a tgz/zip attached (rule 26)",
64
+ "self-cert.10": "Self-certify per rule 10: state that context is not inherited by default, restore by priority, cite sources",
65
+ "self-cert.12C": "Self-certify per rule 12C: announce proxy needs, stop on failure, verify downloads, test connectivity first",
66
+ "self-cert.13B": "Self-certify per rule 13B: user fully exits DSH before session replacement; run all three verification layers; check mtime",
67
+ "self-cert.15": "Self-certify per rule 15: do not judge recency by mtime/filename; keep and ask when version/purpose is unclear",
68
+ "self-cert.19": "Self-certify per rule 19: sediment per items ①-⑧ and report the synced body locations",
69
+ "self-cert.28": "Self-certify per rule 28: classify new files per the workspace index; check README or use _inbox when unsure",
70
+ "self-cert.29": "Self-certify per rule 29: verify the dependency closure before restarting (non-empty key packages / intact top-level junctions / no dangling)",
71
+ "self-cert.30": "Self-certify per rule 30: prove no dependency breakage + authorization before acting; attach verification evidence afterwards",
72
+ "self-cert.31": "Self-certify per rule 31: state verification target → what hit the wall and why → source of the conclusion; do not keep guessing"
73
+ };
74
+
75
+ /** 当前生效文案快照(诊断用;键 → 文本) */
76
+ export function effectiveMessages() {
77
+ const out = {};
78
+ for (const k of Object.keys(DEFAULT_MESSAGES)) out[k] = overrides?.[k] ?? DEFAULT_MESSAGES[k];
79
+ return out;
80
+ }
81
+
82
+ /**
83
+ * 注入个人层文案(rule-engine.json 的 messages 键)。
84
+ * 语义同 setLexicons:undefined/null = 不干预;对象 = 配置即真相;{} = 回退内置默认。
85
+ * @returns {{applied: string[], rejected: Array<{key: string, reason: string}>, noop?: boolean}}
86
+ */
87
+ export function setMessages(cfg) {
88
+ const applied = [];
89
+ const rejected = [];
90
+ if (cfg === undefined || cfg === null) return { applied, rejected, noop: true };
91
+ if (typeof cfg !== "object" || Array.isArray(cfg)) {
92
+ overrides = null;
93
+ return { applied, rejected };
94
+ }
95
+ const next = {};
96
+ for (const [k, v] of Object.entries(cfg)) {
97
+ if (typeof v !== "string" || v.length === 0) {
98
+ rejected.push({ key: k, reason: "empty-or-not-string" });
99
+ continue;
100
+ }
101
+ next[k] = v;
102
+ applied.push(k);
103
+ }
104
+ overrides = Object.keys(next).length > 0 ? next : null;
105
+ return { applied, rejected };
106
+ }
107
+
108
+ /** 显式回退内置默认(测试用) */
109
+ export function resetMessages() {
110
+ overrides = null;
111
+ }
112
+
113
+ /**
114
+ * 取一条文案并做参数插值。
115
+ * @param {string} key 文案键
116
+ * @param {Record<string, unknown>} params `{name}` 占位符的取值
117
+ * @returns {string} 文案;键不存在时返回 key 本身(可见失败)
118
+ */
119
+ export function getMessage(key, params = {}) {
120
+ const raw = overrides?.[key] ?? DEFAULT_MESSAGES[key];
121
+ if (typeof raw !== "string") return String(key);
122
+ if (!params || typeof params !== "object") return raw;
123
+ return raw.replace(/\{(\w+)\}/g, (m, name) => (name in params ? String(params[name]) : m));
124
+ }
125
+
126
+ /** 该键是否有文案(内置或覆盖) */
127
+ export function hasMessage(key) {
128
+ return typeof (overrides?.[key] ?? DEFAULT_MESSAGES[key]) === "string";
129
+ }
130
+
131
+ /** 是否处于个人层覆盖状态 */
132
+ export function hasMessageOverride() {
133
+ return overrides !== null;
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rule-engine",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -13,6 +13,7 @@
13
13
  "lib",
14
14
  "scripts",
15
15
  "!scripts/local-residue-markers.txt",
16
+ "!scripts/dualtrack-baseline.json",
16
17
  "upgrade-impact.json",
17
18
  "cordis.patch.yml",
18
19
  "README.md",
@@ -46,7 +47,7 @@
46
47
  "check": "node --check lib/index.js",
47
48
  "verify": "node scripts/verify-all.mjs",
48
49
  "audit:mount": "node scripts/audit-mount-consistency.mjs --profile web",
49
- "check:meta": "node scripts/readme-version-check.mjs && node scripts/local-residue-scan.mjs"
50
+ "check:meta": "node scripts/readme-version-check.mjs && node scripts/local-residue-scan.mjs && node scripts/dualtrack-check.mjs"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "@deepseek-ai/dsh-home-paths": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0 || >=0.1.2-rc.0 <0.2.0",
@@ -38,7 +38,9 @@ function parseNames() {
38
38
  if (!cols[2] || !cols[2].startsWith("@deepseek-ai")) continue;
39
39
  const add = (s) => {
40
40
  if (!s || s === "-") return;
41
- for (const x of s.split(",")) {
41
+ // 分隔符:英文文档用逗号「,」,中文文档用顿号「、」(0.6.3 修复:曾只按逗号分割,
42
+ // 致中文素材的多工具名整串被丢弃——59 个工具名只剩 11 个,且仍打印 COVERAGE-OK 的静默弱化)
43
+ for (const x of s.split(/[,、,]/)) {
42
44
  const n = x.trim();
43
45
  if (/^[A-Za-z_][A-Za-z0-9_:.-]*$/.test(n)) names.add(n);
44
46
  }