dsh-rule-engine 0.6.4 → 0.6.5
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 +461 -444
- package/lib/core/authorization.js +1 -1
- package/lib/core/intent.js +1 -1
- package/lib/core/llm-intent.js +13 -5
- package/lib/core/patterns.js +14 -4
- package/lib/core/state.js +4 -1
- package/lib/core/text-detect.js +20 -2
- package/lib/core/tool-catalog.js +5 -0
- package/lib/index.js +113 -14
- package/lib/messages.js +5 -0
- package/lib/service.js +43 -0
- package/package.json +4 -10
- package/scripts/audit-mount-consistency.mjs +0 -198
- package/scripts/build.sh +0 -13
- package/scripts/check-real.mjs +0 -24
- package/scripts/check-tool-coverage.mjs +0 -65
- package/scripts/dualtrack-check.mjs +0 -481
- package/scripts/dualtrack-whitelist.json +0 -7
- package/scripts/health-audit.mjs +0 -66
- package/scripts/lib/pnpm-exempt.mjs +0 -56
- package/scripts/local-residue-scan.mjs +0 -46
- package/scripts/plugins.json +0 -10
- package/scripts/probe-home.mjs +0 -3
- package/scripts/probe-intent.mjs +0 -14
- package/scripts/publish-aptitude-check.mjs +0 -105
- package/scripts/readme-version-check.mjs +0 -41
- package/scripts/release-plugin.mjs +0 -432
- package/scripts/rules-health.mjs +0 -55
- package/scripts/verify-all.mjs +0 -219
- package/scripts/verify-guard-live.mjs +0 -30
- package/scripts/verify-v474.mjs +0 -14
- package/upgrade-impact.json +0 -55
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
// dualtrack-check.mjs - 分层残留扫描闸(dualtrack-check)
|
|
2
|
-
//
|
|
3
|
-
// 依据:本机使用手册 SKILL.md:113「修改双轨制(2026-08-31 用户定稿)——
|
|
4
|
-
// 铁律=代码层零本机内容(发布门禁扫描红);机器校验(dualtrack-check)为验收兜底」。
|
|
5
|
-
// 本脚本是那句承诺的落地实现。
|
|
6
|
-
//
|
|
7
|
-
// 扫描范围(lib/**/*.js)——判据 A(2026-09-09 用户拍板,与方案 §一 一致):
|
|
8
|
-
// 闸只扫「**会随发布者/规则集/环境变化**」的内容,不扫通用中文文案。
|
|
9
|
-
// ① 本机标识:发布者私有词表(rule-engine.json 的 dualtrack.markers)命中的字符串
|
|
10
|
-
// ② 映射表键:对象字面量键名匹配 ^\d+[A-Z]?$ 或含 CJK(规则号索引=某人的规则体系)
|
|
11
|
-
// —— 通用中文文案(字符串字面量内的 CJK)**不计入**:第三方判据明示「中文≠个人化,
|
|
12
|
-
// 通用功能词/提示语是产品能力」。runtime 计数仍在 --report 里显示,供参考。
|
|
13
|
-
//
|
|
14
|
-
// 扫描判据 D(2026-09-10 分层架构 v3 · P0 追加):规则号字面量
|
|
15
|
-
// 依据:reports/2026-09-10-引擎分层架构-通用层零规则内容-v1.md §七。
|
|
16
|
-
// 模式:rule\d+[A-Za-z]?- / byId\.get\("\d+"\) / \[guardian:rule\d+\] / rule-hint\.\d+
|
|
17
|
-
// / ruleId\s*[=!]==?\s*"<N>"(2026-09-10 用户拍板纳入;§七 原列四条,此条据 §二 现状证据补入)
|
|
18
|
-
// 为何单列一判据:「机制以规则号为骨架」比「文案含编号」更根本,且与判据 A 不同维度
|
|
19
|
-
// (A 抓本机私有词/映射键;D 抓通用层住着规则体系),故各自独立棘轮。
|
|
20
|
-
// 口径:去注释后扫描(注释属开发溯源,非机制骨架);不设白名单——代码层零规则号。
|
|
21
|
-
//
|
|
22
|
-
// 棘轮(ratchet):基线记录各文件计数,只许降不许升。
|
|
23
|
-
// node scripts/dualtrack-check.mjs # 比对基线(CI/发布门禁用)
|
|
24
|
-
// node scripts/dualtrack-check.mjs --init # 首次生成基线
|
|
25
|
-
// node scripts/dualtrack-check.mjs --update # 手动更新基线(须在提交说明里写清改了什么)
|
|
26
|
-
// node scripts/dualtrack-check.mjs --report # 只打印各文件计数
|
|
27
|
-
// node scripts/dualtrack-check.mjs --root <dir> # 改扫描根(仅测试隔离用;非默认根会显著提示)
|
|
28
|
-
//
|
|
29
|
-
// 白名单(scripts/dualtrack-whitelist.json):
|
|
30
|
-
// files —— 整文件豁免(如 lib/lang/**,第 3 批语言包)
|
|
31
|
-
// strings —— 通用功能词精确豁免(第三方 §一:中文≠个人化,许可词/时间词是产品能力)
|
|
32
|
-
import fs from "node:fs";
|
|
33
|
-
import os from "node:os";
|
|
34
|
-
import path from "node:path";
|
|
35
|
-
import { fileURLToPath } from "node:url";
|
|
36
|
-
import { loadMarkers } from "../lib/core/dualtrack-markers.js";
|
|
37
|
-
|
|
38
|
-
// ── 命令行参数(提前解析:--root 必须在下面的路径常量之前生效)──
|
|
39
|
-
const ARGV = process.argv.slice(2);
|
|
40
|
-
const args = new Set(ARGV);
|
|
41
|
-
const rootIdx = ARGV.indexOf("--root");
|
|
42
|
-
const IS_CUSTOM_ROOT = rootIdx >= 0;
|
|
43
|
-
const ROOT = IS_CUSTOM_ROOT && ARGV[rootIdx + 1]
|
|
44
|
-
? path.resolve(ARGV[rootIdx + 1])
|
|
45
|
-
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
46
|
-
const LIB = path.join(ROOT, "lib");
|
|
47
|
-
const BASELINE_FILE = path.join(ROOT, "scripts", "dualtrack-baseline.json");
|
|
48
|
-
const WHITELIST_FILE = path.join(ROOT, "scripts", "dualtrack-whitelist.json");
|
|
49
|
-
const RESIDUE_FILE = path.join(ROOT, "scripts", "local-residue-markers.txt");
|
|
50
|
-
|
|
51
|
-
const CJK = /[\u4e00-\u9fff]/;
|
|
52
|
-
const MAP_KEY_RE = /^\d+[A-Z]?$/;
|
|
53
|
-
|
|
54
|
-
// ── 判据 D:规则号字面量(§七;口径=去注释后扫描,无白名单)──
|
|
55
|
-
const RULE_LITERAL_PATTERNS = [
|
|
56
|
-
["executor-name", /rule\d+[A-Za-z]?-/g, "内部执行器名 rule<N>[x]-"],
|
|
57
|
-
["byid-get", /byId\.get\(\s*["'`]\d+["'`]\s*\)/g, '按规则号取规则 byId.get("<N>")'],
|
|
58
|
-
["guardian-tag", /\[guardian:rule\d+\]/g, "对外文案 [guardian:rule<N>]"],
|
|
59
|
-
["rule-hint", /rule-hint\.\d+/g, "rule-hint.<N>"],
|
|
60
|
-
["ruleid-eq", /ruleId\s*[=!]==?\s*["'`]\d+[A-Za-z]?["'`]/g, '硬编码规则号比较 ruleId === "<N>"']
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
// ── 白名单 ──
|
|
64
|
-
/** 白名单两层(2026-09-09):包内通用白名单 + 本机 rule-engine.json 的 dualtrack.whitelist 合并。
|
|
65
|
-
* 包内那份随包发布(如 lib/lang/**);本机那份记录「我豁免我自己的某条」,不进包。 */
|
|
66
|
-
function loadWhitelist() {
|
|
67
|
-
const files = [];
|
|
68
|
-
const strings = new Set();
|
|
69
|
-
// ① 包内通用白名单
|
|
70
|
-
if (fs.existsSync(WHITELIST_FILE)) {
|
|
71
|
-
try {
|
|
72
|
-
const w = JSON.parse(fs.readFileSync(WHITELIST_FILE, "utf8"));
|
|
73
|
-
for (const f of w.files || []) files.push(f);
|
|
74
|
-
for (const s of w.strings || []) strings.add(s);
|
|
75
|
-
} catch (e) {
|
|
76
|
-
throw new Error(`白名单解析失败:${WHITELIST_FILE} — ${e.message}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// ② 本机白名单(rule-engine.json 的 dualtrack.whitelist)
|
|
80
|
-
try {
|
|
81
|
-
const p = path.join(process.env.DSH_HOME || path.join(os.homedir(), ".dsh"), "rule-engine.json");
|
|
82
|
-
if (fs.existsSync(p)) {
|
|
83
|
-
const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
84
|
-
const w = cfg?.dualtrack?.whitelist;
|
|
85
|
-
if (w && typeof w === "object") {
|
|
86
|
-
for (const f of w.files || []) files.push(f);
|
|
87
|
-
for (const s of w.strings || []) strings.add(s);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// 本机配置不可读 → 只用包内白名单(不阻断)
|
|
92
|
-
}
|
|
93
|
-
return { files, strings };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 整文件豁免匹配(支持 ** 与 * 通配,路径用 / 分隔,相对仓库根) */
|
|
97
|
-
function fileExempt(relPath, patterns) {
|
|
98
|
-
for (const p of patterns) {
|
|
99
|
-
const re = new RegExp("^" + p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\u0000").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*") + "$");
|
|
100
|
-
if (re.test(relPath)) return true;
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ── 去注释(判据 D 用):保留字符串/模板/正则原文,注释替换为空白(保持行结构) ──
|
|
106
|
-
function stripComments(src) {
|
|
107
|
-
let out = "";
|
|
108
|
-
let i = 0;
|
|
109
|
-
const n = src.length;
|
|
110
|
-
let prevSig = "";
|
|
111
|
-
const push = (s) => { out += s; };
|
|
112
|
-
while (i < n) {
|
|
113
|
-
const c = src[i];
|
|
114
|
-
const c2 = src[i + 1];
|
|
115
|
-
if (c === "/" && c2 === "/") {
|
|
116
|
-
while (i < n && src[i] !== "\n") { push(" "); i++; }
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (c === "/" && c2 === "*") {
|
|
120
|
-
push(" "); i += 2;
|
|
121
|
-
while (i < n && !(src[i] === "*" && src[i + 1] === "/")) {
|
|
122
|
-
push(src[i] === "\n" ? "\n" : " ");
|
|
123
|
-
i++;
|
|
124
|
-
}
|
|
125
|
-
if (i < n) { push(" "); i += 2; }
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
129
|
-
const q = c;
|
|
130
|
-
push(c); i++;
|
|
131
|
-
while (i < n) {
|
|
132
|
-
if (src[i] === "\\") { push(src[i] + (src[i + 1] || "")); i += 2; continue; }
|
|
133
|
-
push(src[i]);
|
|
134
|
-
if (src[i] === q) { i++; break; }
|
|
135
|
-
i++;
|
|
136
|
-
}
|
|
137
|
-
prevSig = "str";
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
// 正则字面量:仅在可能的正则位置整体复制(否则 / 是除号)
|
|
141
|
-
if (c === "/" && prevSig !== "ident" && prevSig !== ")" && prevSig !== "]" && prevSig !== "str") {
|
|
142
|
-
let j = i + 1;
|
|
143
|
-
let inClass = false;
|
|
144
|
-
let closed = false;
|
|
145
|
-
while (j < n) {
|
|
146
|
-
const ch = src[j];
|
|
147
|
-
if (ch === "\\") { j += 2; continue; }
|
|
148
|
-
if (ch === "\n") break;
|
|
149
|
-
if (ch === "[") inClass = true;
|
|
150
|
-
else if (ch === "]") inClass = false;
|
|
151
|
-
else if (ch === "/" && !inClass) { closed = true; break; }
|
|
152
|
-
j++;
|
|
153
|
-
}
|
|
154
|
-
if (closed) {
|
|
155
|
-
push(src.slice(i, j + 1));
|
|
156
|
-
i = j + 1;
|
|
157
|
-
while (i < n && /[a-z]/i.test(src[i])) { push(src[i]); i++; }
|
|
158
|
-
prevSig = "regex";
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
push(c);
|
|
163
|
-
if (/[A-Za-z0-9_$]/.test(c)) prevSig = "ident";
|
|
164
|
-
else if (c === ")") prevSig = ")";
|
|
165
|
-
else if (c === "]") prevSig = "]";
|
|
166
|
-
else if (!/\s/.test(c)) prevSig = "other";
|
|
167
|
-
i++;
|
|
168
|
-
}
|
|
169
|
-
return out;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ── 词法扫描:剔除注释,收集字符串字面量与对象键 ──
|
|
173
|
-
/**
|
|
174
|
-
* @returns {{strings: string[], mapKeys: string[]}}
|
|
175
|
-
* strings = 字符串字面量的**内容**(模板串按整段处理)
|
|
176
|
-
* mapKeys = 形如 "14": / '12E': / 14: 的对象键
|
|
177
|
-
*/
|
|
178
|
-
function tokenize(src) {
|
|
179
|
-
const strings = [];
|
|
180
|
-
const mapKeys = [];
|
|
181
|
-
let i = 0;
|
|
182
|
-
const n = src.length;
|
|
183
|
-
// 上一个有意义的 token 类型:用于判断 `/` 是正则还是除号(简化:只看是否可能在正则位置)
|
|
184
|
-
let prevSig = "";
|
|
185
|
-
while (i < n) {
|
|
186
|
-
const c = src[i];
|
|
187
|
-
// 行注释
|
|
188
|
-
if (c === "/" && src[i + 1] === "/") {
|
|
189
|
-
while (i < n && src[i] !== "\n") i++;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
// 块注释(含 JSDoc)
|
|
193
|
-
if (c === "/" && src[i + 1] === "*") {
|
|
194
|
-
i += 2;
|
|
195
|
-
while (i < n && !(src[i] === "*" && src[i + 1] === "/")) i++;
|
|
196
|
-
i += 2;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
// 字符串 / 模板串
|
|
200
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
201
|
-
const q = c;
|
|
202
|
-
const start = i;
|
|
203
|
-
i++;
|
|
204
|
-
let buf = "";
|
|
205
|
-
if (q === "`") {
|
|
206
|
-
// 模板串:整段作为一个字符串,但 `${...}` 内的表达式跳过(不计入内容,也不当键)
|
|
207
|
-
while (i < n) {
|
|
208
|
-
const ch = src[i];
|
|
209
|
-
if (ch === "\\") { buf += ch + (src[i + 1] || ""); i += 2; continue; }
|
|
210
|
-
if (ch === "$" && src[i + 1] === "{") {
|
|
211
|
-
let depth = 1;
|
|
212
|
-
i += 2;
|
|
213
|
-
while (i < n && depth > 0) {
|
|
214
|
-
const d = src[i];
|
|
215
|
-
if (d === "\\") { i += 2; continue; }
|
|
216
|
-
if (d === "{") depth++;
|
|
217
|
-
else if (d === "}") depth--;
|
|
218
|
-
else if (d === '"' || d === "'" || d === "`") {
|
|
219
|
-
const qq = d; i++;
|
|
220
|
-
while (i < n && src[i] !== qq) { if (src[i] === "\\") i++; i++; }
|
|
221
|
-
}
|
|
222
|
-
i++;
|
|
223
|
-
}
|
|
224
|
-
buf += "\u0000"; // 表达式占位(不含 CJK)
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (ch === "`") break;
|
|
228
|
-
buf += ch;
|
|
229
|
-
i++;
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
while (i < n && src[i] !== q) {
|
|
233
|
-
if (src[i] === "\\") { buf += src[i] + (src[i + 1] || ""); i += 2; continue; }
|
|
234
|
-
buf += src[i];
|
|
235
|
-
i++;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
i++; // 收尾引号
|
|
239
|
-
strings.push(buf);
|
|
240
|
-
// 对象键判定(2026-09-09 修正):必须「前面是 { 或 ,」且「后面(跳空白)是 :」
|
|
241
|
-
// —— 否则模板串分段与三元表达式会被误判成键(20 处误报的根因)。
|
|
242
|
-
let j = i;
|
|
243
|
-
while (j < n && /\s/.test(src[j])) j++;
|
|
244
|
-
if (src[j] === ":") {
|
|
245
|
-
let p = start - 1;
|
|
246
|
-
while (p >= 0 && /\s/.test(src[p])) p--;
|
|
247
|
-
if (src[p] === "{" || src[p] === ",") {
|
|
248
|
-
const plain = buf.replace(/\\(.)/g, "$1");
|
|
249
|
-
if (MAP_KEY_RE.test(plain) || CJK.test(plain)) mapKeys.push(plain);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
prevSig = "str";
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
// 正则字面量(跳过,不计入)
|
|
256
|
-
if (c === "/" && prevSig !== "ident" && prevSig !== ")" && prevSig !== "]") {
|
|
257
|
-
let j = i + 1;
|
|
258
|
-
let inClass = false;
|
|
259
|
-
let closed = false;
|
|
260
|
-
while (j < n) {
|
|
261
|
-
const ch = src[j];
|
|
262
|
-
if (ch === "\\") { j += 2; continue; }
|
|
263
|
-
if (ch === "\n") break;
|
|
264
|
-
if (ch === "[") inClass = true;
|
|
265
|
-
else if (ch === "]") inClass = false;
|
|
266
|
-
else if (ch === "/" && !inClass) { closed = true; break; }
|
|
267
|
-
j++;
|
|
268
|
-
}
|
|
269
|
-
if (closed) {
|
|
270
|
-
i = j + 1;
|
|
271
|
-
while (i < n && /[a-z]/i.test(src[i])) i++;
|
|
272
|
-
prevSig = "regex";
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
i++;
|
|
276
|
-
prevSig = "op";
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
if (/[A-Za-z_$]/.test(c)) {
|
|
280
|
-
let j = i;
|
|
281
|
-
while (j < n && /[A-Za-z0-9_$]/.test(src[j])) j++;
|
|
282
|
-
i = j;
|
|
283
|
-
prevSig = "ident";
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
if (c === ")") { prevSig = ")"; i++; continue; }
|
|
287
|
-
if (c === "]") { prevSig = "]"; i++; continue; }
|
|
288
|
-
if (!/\s/.test(c)) prevSig = "op";
|
|
289
|
-
i++;
|
|
290
|
-
}
|
|
291
|
-
return { strings, mapKeys };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ── 单文件扫描 ──
|
|
295
|
-
function scanFile(absPath, relPath, whitelist, residueMarks) {
|
|
296
|
-
const src = fs.readFileSync(absPath, "utf8");
|
|
297
|
-
const { strings, mapKeys } = tokenize(src);
|
|
298
|
-
// 判据 D:去注释后扫规则号字面量
|
|
299
|
-
const code = stripComments(src);
|
|
300
|
-
let ruleLiterals = 0;
|
|
301
|
-
const ruleLiteralHits = [];
|
|
302
|
-
for (const [id, re, desc] of RULE_LITERAL_PATTERNS) {
|
|
303
|
-
const m = code.match(re);
|
|
304
|
-
if (!m) continue;
|
|
305
|
-
ruleLiterals += m.length;
|
|
306
|
-
if (ruleLiteralHits.length < 3) ruleLiteralHits.push(`${id}×${m.length}(${desc})如 ${m[0]}`);
|
|
307
|
-
}
|
|
308
|
-
let runtime = 0;
|
|
309
|
-
const runtimeHits = [];
|
|
310
|
-
for (const s of strings) {
|
|
311
|
-
if (!CJK.test(s)) continue;
|
|
312
|
-
const plain = s.replace(/\\(.)/g, "$1").trim();
|
|
313
|
-
if (whitelist.strings.has(plain)) continue; // 通用功能词豁免
|
|
314
|
-
runtime++;
|
|
315
|
-
if (runtimeHits.length < 3) runtimeHits.push(plain.slice(0, 40));
|
|
316
|
-
}
|
|
317
|
-
const mapHits = mapKeys.filter((k) => MAP_KEY_RE.test(k) || CJK.test(k));
|
|
318
|
-
let local = 0;
|
|
319
|
-
const localHits = [];
|
|
320
|
-
for (const s of strings) {
|
|
321
|
-
for (const mark of residueMarks) {
|
|
322
|
-
if (mark && s.includes(mark)) {
|
|
323
|
-
local++;
|
|
324
|
-
if (localHits.length < 3) localHits.push(`${mark} ← ${s.slice(0, 40)}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return {
|
|
329
|
-
relPath,
|
|
330
|
-
runtime,
|
|
331
|
-
mapKeys: mapHits.length,
|
|
332
|
-
local,
|
|
333
|
-
ruleLiterals,
|
|
334
|
-
// 判据 A:total 只计「本机性」两类;通用中文文案(runtime)不计入闸
|
|
335
|
-
total: mapHits.length + local,
|
|
336
|
-
runtimeHits,
|
|
337
|
-
mapHits: mapHits.slice(0, 5),
|
|
338
|
-
localHits,
|
|
339
|
-
ruleLiteralHits
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function walk(dir, base = dir, out = []) {
|
|
344
|
-
for (const e of fs.readdirSync(dir)) {
|
|
345
|
-
const p = path.join(dir, e);
|
|
346
|
-
const st = fs.statSync(p);
|
|
347
|
-
if (st.isDirectory()) walk(p, base, out);
|
|
348
|
-
else if (/\.js$/.test(e)) out.push(path.relative(base, p).replace(/\\/g, "/"));
|
|
349
|
-
}
|
|
350
|
-
return out;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ── 主流程 ──
|
|
354
|
-
if (IS_CUSTOM_ROOT) console.log(`SCAN ROOT(非默认,仅测试隔离用):${ROOT}\n`);
|
|
355
|
-
const whitelist = loadWhitelist();
|
|
356
|
-
// 本机标识词表:与 B2 扫描器共用唯一加载器(本机配置优先 → 环境变量 → 包内示例)
|
|
357
|
-
const { markers: residueMarks, source: markersSource } = loadMarkers({ root: ROOT });
|
|
358
|
-
if (residueMarks.length === 0) {
|
|
359
|
-
console.error("REFUSED: 本机标识词表为空——请在 rule-engine.json 配置 dualtrack.markers,或设置 DUALTRACK_MARKERS 环境变量");
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const files = walk(LIB).filter((rel) => !fileExempt(rel, whitelist.files));
|
|
364
|
-
const results = files.map((rel) => scanFile(path.join(LIB, rel), rel, whitelist, residueMarks));
|
|
365
|
-
const counts = {};
|
|
366
|
-
const ruleCounts = {};
|
|
367
|
-
for (const r of results) {
|
|
368
|
-
counts[`lib/${r.relPath}`] = r.total;
|
|
369
|
-
ruleCounts[`lib/${r.relPath}`] = r.ruleLiterals;
|
|
370
|
-
}
|
|
371
|
-
const grandTotal = results.reduce((a, r) => a + r.total, 0);
|
|
372
|
-
const ruleGrandTotal = results.reduce((a, r) => a + r.ruleLiterals, 0);
|
|
373
|
-
|
|
374
|
-
if (args.has("--report")) {
|
|
375
|
-
for (const r of results.sort((a, b) => (b.total + b.ruleLiterals) - (a.total + a.ruleLiterals))) {
|
|
376
|
-
if (r.total === 0 && r.ruleLiterals === 0) continue;
|
|
377
|
-
console.log(` ${String(r.total).padStart(4)} lib/${r.relPath} [映射键 ${r.mapKeys} / 本机标识 ${r.local} / 通用中文文案 ${r.runtime}(不计入闸)] [规则号字面量 ${r.ruleLiterals}]`);
|
|
378
|
-
if (r.mapHits.length) console.log(` 命中键:${r.mapHits.join(" / ")}`);
|
|
379
|
-
if (r.ruleLiteralHits.length) console.log(` 规则号字面量:${r.ruleLiteralHits.join(";")}`);
|
|
380
|
-
}
|
|
381
|
-
console.log(`\nDUALTRACK REPORT:${results.length} 文件,判据 A(分层残留)合计 ${grandTotal},判据 D(规则号字面量)合计 ${ruleGrandTotal}`);
|
|
382
|
-
process.exit(0);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (args.has("--init") || args.has("--update")) {
|
|
386
|
-
const isUpdate = args.has("--update");
|
|
387
|
-
if (isUpdate && !fs.existsSync(BASELINE_FILE)) {
|
|
388
|
-
console.error("REFUSED: --update 需要已存在的基线;首次请用 --init");
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
if (!isUpdate && fs.existsSync(BASELINE_FILE)) {
|
|
392
|
-
console.error(
|
|
393
|
-
`REFUSED: 基线已存在(${BASELINE_FILE})——--init 仅用于首次生成。\n` +
|
|
394
|
-
" --init 会把当前计数覆盖为基线(棘轮失效);确需更新请用 --update(须在提交说明里写清改了什么)"
|
|
395
|
-
);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
const payload = {
|
|
399
|
-
generatedAt: new Date().toISOString(),
|
|
400
|
-
note: "dualtrack 棘轮基线:各文件计数,只许降不许升。files/total=判据 A(分层残留);ruleLiteralFiles/ruleLiteralsTotal=判据 D(规则号字面量)。--init 首次生成,--update 手动更新(须在提交说明里写清改了什么)。",
|
|
401
|
-
total: grandTotal,
|
|
402
|
-
files: counts,
|
|
403
|
-
ruleLiteralsTotal: ruleGrandTotal,
|
|
404
|
-
ruleLiteralFiles: ruleCounts
|
|
405
|
-
};
|
|
406
|
-
fs.writeFileSync(BASELINE_FILE, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
407
|
-
console.log(`${isUpdate ? "UPDATED" : "INITIALIZED"} ${BASELINE_FILE}`);
|
|
408
|
-
console.log(`基线:判据 A 合计 ${grandTotal}(${Object.keys(counts).length} 文件)/判据 D 合计 ${ruleGrandTotal}`);
|
|
409
|
-
process.exit(0);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// 默认:比对基线
|
|
413
|
-
if (!fs.existsSync(BASELINE_FILE)) {
|
|
414
|
-
console.error(`FAIL 基线不存在:${BASELINE_FILE}\n 首次请运行:node scripts/dualtrack-check.mjs --init`);
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
const baseline = JSON.parse(fs.readFileSync(BASELINE_FILE, "utf8"));
|
|
418
|
-
const base = baseline.files || {};
|
|
419
|
-
const regressions = [];
|
|
420
|
-
const improvements = [];
|
|
421
|
-
for (const [file, count] of Object.entries(counts)) {
|
|
422
|
-
const b = base[file];
|
|
423
|
-
if (b === undefined) {
|
|
424
|
-
if (count > 0) regressions.push(`${file}: 新增文件含残留 ${count} 处(基线无此文件)`);
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
if (count > b) {
|
|
428
|
-
const r = results.find((x) => `lib/${x.relPath}` === file);
|
|
429
|
-
const detail = r ? [...r.runtimeHits, ...r.localHits].slice(0, 2).join(";") : "";
|
|
430
|
-
regressions.push(`${file}: ${count} > 基线 ${b}${detail ? `(如:${detail})` : ""}`);
|
|
431
|
-
} else if (count < b) {
|
|
432
|
-
improvements.push(`${file}: ${count} < 基线 ${b}`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
for (const [file, b] of Object.entries(base)) {
|
|
436
|
-
if (counts[file] === undefined && b > 0) improvements.push(`${file}: 文件已删除(基线 ${b})`);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 判据 D 比对(独立棘轮;基线缺该维度 → fail-closed,提示 --update)
|
|
440
|
-
const ruleBase = baseline.ruleLiteralFiles;
|
|
441
|
-
const ruleRegressions = [];
|
|
442
|
-
const ruleImprovements = [];
|
|
443
|
-
if (!ruleBase || typeof ruleBase !== "object") {
|
|
444
|
-
ruleRegressions.push("基线缺「规则号字面量」维度(ruleLiteralFiles)——判据 D 于 2026-09-10 新增,请运行 --update 记录基线");
|
|
445
|
-
} else {
|
|
446
|
-
for (const [file, count] of Object.entries(ruleCounts)) {
|
|
447
|
-
const b = ruleBase[file];
|
|
448
|
-
if (b === undefined) {
|
|
449
|
-
if (count > 0) ruleRegressions.push(`${file}: 新增文件含规则号字面量 ${count} 处(基线无此文件)`);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
if (count > b) {
|
|
453
|
-
const r = results.find((x) => `lib/${x.relPath}` === file);
|
|
454
|
-
const detail = r ? r.ruleLiteralHits.slice(0, 2).join(";") : "";
|
|
455
|
-
ruleRegressions.push(`${file}: ${count} > 基线 ${b}${detail ? `(如:${detail})` : ""}`);
|
|
456
|
-
} else if (count < b) {
|
|
457
|
-
ruleImprovements.push(`${file}: ${count} < 基线 ${b}`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
for (const [file, b] of Object.entries(ruleBase)) {
|
|
461
|
-
if (ruleCounts[file] === undefined && b > 0) ruleImprovements.push(`${file}: 文件已删除(基线 ${b})`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (regressions.length > 0 || ruleRegressions.length > 0) {
|
|
466
|
-
if (regressions.length > 0) {
|
|
467
|
-
console.error(`DUALTRACK FAIL(判据 A 分层残留):${regressions.length} 项超出基线(只许降不许升)`);
|
|
468
|
-
for (const r of regressions) console.error(` ✗ ${r}`);
|
|
469
|
-
}
|
|
470
|
-
if (ruleRegressions.length > 0) {
|
|
471
|
-
console.error(`DUALTRACK FAIL(判据 D 规则号字面量):${ruleRegressions.length} 项超出基线(只许降不许升)`);
|
|
472
|
-
for (const r of ruleRegressions) console.error(` ✗ ${r}`);
|
|
473
|
-
}
|
|
474
|
-
console.error("\n 处置:分层残留 → 迁个人层/配置层,或(确属通用功能词)登记 scripts/dualtrack-whitelist.json");
|
|
475
|
-
console.error(" 规则号字面量 → 改为通用 kind 名(架构 v3 §三 L1);判据 D 不设白名单。");
|
|
476
|
-
process.exit(1);
|
|
477
|
-
}
|
|
478
|
-
const downCount = improvements.length + ruleImprovements.length;
|
|
479
|
-
console.log(`DUALTRACK OK:判据 A 合计 ${grandTotal}(基线 ${baseline.total ?? "?"})/判据 D 合计 ${ruleGrandTotal}(基线 ${baseline.ruleLiteralsTotal ?? "?"})${downCount ? `,${downCount} 个文件下降可 --update` : ""}`);
|
|
480
|
-
for (const im of [...improvements, ...ruleImprovements].slice(0, 5)) console.log(` ↓ ${im}`);
|
|
481
|
-
process.exit(0);
|
package/scripts/health-audit.mjs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// health-audit.mjs - 健康审计(找茬,不是证明;2026-08-26)。
|
|
2
|
-
// 输出"问题清单"(近 24h),无问题也要列出"查了什么":
|
|
3
|
-
// ① 失败/降级类统计:intent-llm(成功/失败降级)、judge-pass/false/unavailable、
|
|
4
|
-
// verify-gap、inject-skip、source-skip——失败可见化("LLM 意图 31 次全降级"从此自浮现);
|
|
5
|
-
// ② 接口接线交叉:关键导出符号在 lib 下引用计数 ≤1(仅定义,无引用)→ 疑似未接线;
|
|
6
|
-
// ③ 与 verify-all 的真实判例口径一致(供人工核对)。
|
|
7
|
-
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
|
|
11
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
-
const auditPath = join(process.env.DSH_HOME || join(process.env.USERPROFILE || "", ".dsh"), "rule-engine.log.jsonl");
|
|
13
|
-
const now = Date.now();
|
|
14
|
-
const cuts = { intentFail: 0, intentOk: 0, judgePass: 0, judgeFalse: 0, judgeUnavail: 0, verifyGap: 0, injectSkip: 0, sourceSkip: 0, denyTotal: 0, labelIncorrect: 0, labelCorrect: 0, errorHint: 0 };
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
for (const line of readFileSync(auditPath, "utf8").split("\n")) {
|
|
18
|
-
const m = line.match(/"ts":"([^"]+)"/);
|
|
19
|
-
if (!m || now - new Date(m[1]).getTime() > 24 * 3600 * 1000) continue;
|
|
20
|
-
if (line.includes('"kind":"intent-llm"')) (line.includes("LLM 失败降级") ? cuts.intentFail++ : cuts.intentOk++);
|
|
21
|
-
if (line.includes('"kind":"judge-pass"')) cuts.judgePass++;
|
|
22
|
-
if (line.includes('"kind":"judge-false"')) cuts.judgeFalse++;
|
|
23
|
-
if (line.includes('"kind":"judge-unavailable"')) cuts.judgeUnavail++;
|
|
24
|
-
if (line.includes('"kind":"verify-gap"')) cuts.verifyGap++;
|
|
25
|
-
if (line.includes('"kind":"inject-skip"')) cuts.injectSkip++;
|
|
26
|
-
if (line.includes('"kind":"source-skip"')) cuts.sourceSkip++;
|
|
27
|
-
if (line.includes('"kind":"deny"')) cuts.denyTotal++;
|
|
28
|
-
if (line.includes('"kind":"task-label"') && line.includes("incorrect")) cuts.labelIncorrect++;
|
|
29
|
-
if (line.includes('"kind":"task-label"') && line.includes("correct")) cuts.labelCorrect++;
|
|
30
|
-
if (line.includes('"kind":"error-hint"')) cuts.errorHint++;
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// 无日志文件:一切 0
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ② 接线交叉:导出符号引用计数(>1 = 定义+引用;=1 = 疑似仅定义未接线)
|
|
37
|
-
const EXPORTS = [
|
|
38
|
-
"setSessionWorkspaceRoot", "setWorkspaceRoots", "isVerificationCommand",
|
|
39
|
-
"isNegatingSuggestion", "isPromiseQuoteContext", "judgeViolation",
|
|
40
|
-
"shouldDetectTurn", "shouldDeliver", "isReadOnlyCommand"
|
|
41
|
-
];
|
|
42
|
-
let libText = "";
|
|
43
|
-
(function walk(d) {
|
|
44
|
-
for (const f of readdirSync(d)) {
|
|
45
|
-
const p = join(d, f);
|
|
46
|
-
if (statSync(p).isDirectory()) walk(p);
|
|
47
|
-
else if (/\.js$/.test(f)) libText += readFileSync(p, "utf8") + "\n";
|
|
48
|
-
}
|
|
49
|
-
})(join(root, "lib"));
|
|
50
|
-
const orphans = EXPORTS.filter((n) => ((libText.match(new RegExp("\\b" + n + "\\b", "g")) || []).length) <= 1);
|
|
51
|
-
|
|
52
|
-
console.log("== 健康审计(近 24h)==");
|
|
53
|
-
console.log(`intent-llm 失败降级 ${cuts.intentFail} 条 / 成功 ${cuts.intentOk} 条`);
|
|
54
|
-
console.log(`judge-pass ${cuts.judgePass} 条 / judge-false ${cuts.judgeFalse} 条 / judge-unavailable ${cuts.judgeUnavail} 条`);
|
|
55
|
-
console.log(`verify-gap ${cuts.verifyGap} 条 / inject-skip ${cuts.injectSkip} 条 / source-skip ${cuts.sourceSkip} 条`);
|
|
56
|
-
console.log(`误判打标(建议4):deny 总数 ${cuts.denyTotal} 条 → label incorrect ${cuts.labelIncorrect} 条 / correct ${cuts.labelCorrect} 条${cuts.denyTotal > 0 && cuts.labelIncorrect > 0 ? `(incorrect 占比 ${(cuts.labelIncorrect / cuts.denyTotal * 100).toFixed(1)}%)` : ""}——词表迭代量化依据`);
|
|
57
|
-
console.log(`已知坑召回(建议5):error-hint ${cuts.errorHint} 条`);
|
|
58
|
-
console.log(`接线交叉:${EXPORTS.length} 个关键导出 → 疑似未接线 ${orphans.length ? orphans.join("、") : "0 个"}`);
|
|
59
|
-
if (cuts.judgePass + cuts.judgeFalse === 0) {
|
|
60
|
-
console.log("⚠️ 问题:近 24h 无真实判例——裁决器无运行证据(需实弹)");
|
|
61
|
-
}
|
|
62
|
-
if (cuts.intentFail > 0) {
|
|
63
|
-
console.log(`⚠️ 问题:intent-llm 失败降级 ${cuts.intentFail} 条——LLM 意图判定不可用(需查模型路由/密钥)`);
|
|
64
|
-
}
|
|
65
|
-
console.log(orphans.length ? `❌ 问题:疑似未接线:${orphans.join("、")}` : `✅ 接线:全部导出有引用`);
|
|
66
|
-
console.log("(已查:失败统计 8 类 + 接线交叉 9 个导出;无问题也列出如上)");
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// pnpm-exempt.mjs — pnpm-workspace minimumReleaseAgeExclude 豁免判定(verify-all ⑬ 与 release-plugin 预插共享单源)
|
|
2
|
-
// 约定(exempt 语义):行含包名(`pkg@`)且版本段任一匹配——yaml 支持 `- pkg@1.5.3 || 1.5.4` 形态(段可带/不带包名前缀)
|
|
3
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
export function exemptPkg(wsYaml, pkg, ver) {
|
|
6
|
-
if (typeof wsYaml !== "string" || wsYaml.length === 0) return false;
|
|
7
|
-
return wsYaml.split(/\r?\n/).some((l) => l.includes(`${pkg}@`) && l.split(/\s*\|\|\s*/).some((tok) => {
|
|
8
|
-
const t = tok.trim().replace(/^-\s*/, "");
|
|
9
|
-
return t === ver || t === `${pkg}@${ver}`;
|
|
10
|
-
}));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** 追加豁免行(已存在=幂等跳过;返回 {yaml, added}) */
|
|
14
|
-
export function addExemptLine(wsYaml, pkg, ver) {
|
|
15
|
-
if (exemptPkg(wsYaml, pkg, ver)) return { yaml: wsYaml, added: false };
|
|
16
|
-
const sep = wsYaml.endsWith("\n") ? "" : "\n";
|
|
17
|
-
return { yaml: `${wsYaml}${sep} - ${pkg}@${ver}\n`, added: true };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 读 yaml(fail-closed:读取失败抛错,调用方中止发布) */
|
|
21
|
-
export function readYamlOrThrow(path) {
|
|
22
|
-
try {
|
|
23
|
-
const text = readFileSync(path, "utf8");
|
|
24
|
-
if (!text.includes("minimumReleaseAgeExclude")) throw new Error(`yaml 缺 minimumReleaseAgeExclude 块:${path}`);
|
|
25
|
-
return text;
|
|
26
|
-
} catch (e) {
|
|
27
|
-
if (e.code === "ENOENT") throw new Error(`yaml 不存在(fail-closed):${path}`);
|
|
28
|
-
throw e;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 预插核心(release-plugin bump 后 publish 前调用;单测可注入 dryRun/write/log):
|
|
34
|
-
* 已存在=幂等跳过;yaml 缺失/坏=抛错(fail-closed);dryRun=只打印不写。
|
|
35
|
-
*/
|
|
36
|
-
export function ensureExempt(yamlPath, pkgName, ver, opts = {}) {
|
|
37
|
-
const { dryRun = false, write = (p, c) => writeFileSync(p, c, "utf8"), log = console.log } = opts;
|
|
38
|
-
let yaml;
|
|
39
|
-
try {
|
|
40
|
-
yaml = readYamlOrThrow(yamlPath);
|
|
41
|
-
} catch (e) {
|
|
42
|
-
throw new Error(`豁免预插失败(fail-closed 中止):${e.message}`);
|
|
43
|
-
}
|
|
44
|
-
const { yaml: nextYaml, added } = addExemptLine(yaml, pkgName, ver);
|
|
45
|
-
if (dryRun) {
|
|
46
|
-
log(`[DRY-RUN] 豁免预插:${added ? `将追加 ${pkgName}@${ver}` : `已存在(幂等跳过)`}`);
|
|
47
|
-
return { added, dryRun: true };
|
|
48
|
-
}
|
|
49
|
-
if (added) {
|
|
50
|
-
write(yamlPath, nextYaml);
|
|
51
|
-
log(`豁免预插:+ ${pkgName}@${ver}`);
|
|
52
|
-
} else {
|
|
53
|
-
log(`豁免预插:${pkgName}@${ver} 已存在(幂等跳过)`);
|
|
54
|
-
}
|
|
55
|
-
return { added };
|
|
56
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// local-residue-scan.mjs — 发布物本机痕迹扫描(仅扫 lib/;无 --pack 模式)
|
|
3
|
-
// 用法:node scripts/local-residue-scan.mjs (在包根目录运行;exit 0 = 干净,exit 1 = 有命中)
|
|
4
|
-
|
|
5
|
-
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
6
|
-
import { join, extname } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { dirname } from "node:path";
|
|
9
|
-
|
|
10
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
|
|
12
|
-
// 词表唯一源(2026-09-09 起配置化):本机 rule-engine.json 的 dualtrack.markers 优先,
|
|
13
|
-
// 其次 DUALTRACK_MARKERS 环境变量,最后回退包内示例文件——见 lib/core/dualtrack-markers.js。
|
|
14
|
-
// 理由:词表是**发布者私有数据**,不应随公开仓库/包分发。
|
|
15
|
-
import { loadMarkers } from "../lib/core/dualtrack-markers.js";
|
|
16
|
-
const { markers: MARKERS, source: MARKERS_SOURCE } = loadMarkers({ root });
|
|
17
|
-
if (MARKERS.length === 0) {
|
|
18
|
-
console.error("REFUSED: 本机标识词表为空——请在 rule-engine.json 配置 dualtrack.markers,或设置 DUALTRACK_MARKERS 环境变量");
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
// 注意:不扫 package.json(作者署名/仓库地址为合法项,A6 口径见其节);tool-catalog.js 的
|
|
22
|
-
// dev_/esr_/engram_ 前缀规则按阶段 A「关键裁决 3」保留,静态枚举列 0.6.x 跟进。
|
|
23
|
-
|
|
24
|
-
const TEXT_EXT = new Set([".js", ".mjs", ".cjs", ".json", ".md", ".yml", ".yaml"]);
|
|
25
|
-
function* walk(dir) {
|
|
26
|
-
for (const e of readdirSync(dir)) {
|
|
27
|
-
const p = join(dir, e);
|
|
28
|
-
const s = statSync(p);
|
|
29
|
-
if (s.isDirectory()) { if (e !== "node_modules" && e !== ".git") yield* walk(p); }
|
|
30
|
-
else if (TEXT_EXT.has(extname(e))) yield p;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let hits = 0;
|
|
35
|
-
for (const file of walk(join(root, "lib"))) {
|
|
36
|
-
const text = readFileSync(file, "utf8");
|
|
37
|
-
for (const m of MARKERS) {
|
|
38
|
-
const lines = text.split("\n");
|
|
39
|
-
lines.forEach((line, i) => {
|
|
40
|
-
if (line.includes(m)) { console.log(`HIT ${file}:${i + 1} [${m}] ${line.trim()}`); hits++; }
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (hits) { console.error(`\nRESIDUE SCAN FAILED(${hits} 处本机痕迹)`); process.exit(1); }
|
|
46
|
-
console.log("RESIDUE SCAN OK(发布面零本机痕迹)");
|
package/scripts/plugins.json
DELETED
package/scripts/probe-home.mjs
DELETED