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.
@@ -0,0 +1,335 @@
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
+ // 棘轮(ratchet):基线记录各文件计数,只许降不许升。
15
+ // node scripts/dualtrack-check.mjs # 比对基线(CI/发布门禁用)
16
+ // node scripts/dualtrack-check.mjs --init # 首次生成基线
17
+ // node scripts/dualtrack-check.mjs --update # 手动更新基线(须在提交说明里写清改了什么)
18
+ // node scripts/dualtrack-check.mjs --report # 只打印各文件计数
19
+ //
20
+ // 白名单(scripts/dualtrack-whitelist.json):
21
+ // files —— 整文件豁免(如 lib/lang/**,第 3 批语言包)
22
+ // strings —— 通用功能词精确豁免(第三方 §一:中文≠个人化,许可词/时间词是产品能力)
23
+ import fs from "node:fs";
24
+ import os from "node:os";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import { loadMarkers } from "../lib/core/dualtrack-markers.js";
28
+
29
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
30
+ const LIB = path.join(ROOT, "lib");
31
+ const BASELINE_FILE = path.join(ROOT, "scripts", "dualtrack-baseline.json");
32
+ const WHITELIST_FILE = path.join(ROOT, "scripts", "dualtrack-whitelist.json");
33
+ const RESIDUE_FILE = path.join(ROOT, "scripts", "local-residue-markers.txt");
34
+
35
+ const CJK = /[\u4e00-\u9fff]/;
36
+ const MAP_KEY_RE = /^\d+[A-Z]?$/;
37
+
38
+ // ── 白名单 ──
39
+ /** 白名单两层(2026-09-09):包内通用白名单 + 本机 rule-engine.json 的 dualtrack.whitelist 合并。
40
+ * 包内那份随包发布(如 lib/lang/**);本机那份记录「我豁免我自己的某条」,不进包。 */
41
+ function loadWhitelist() {
42
+ const files = [];
43
+ const strings = new Set();
44
+ // ① 包内通用白名单
45
+ if (fs.existsSync(WHITELIST_FILE)) {
46
+ try {
47
+ const w = JSON.parse(fs.readFileSync(WHITELIST_FILE, "utf8"));
48
+ for (const f of w.files || []) files.push(f);
49
+ for (const s of w.strings || []) strings.add(s);
50
+ } catch (e) {
51
+ throw new Error(`白名单解析失败:${WHITELIST_FILE} — ${e.message}`);
52
+ }
53
+ }
54
+ // ② 本机白名单(rule-engine.json 的 dualtrack.whitelist)
55
+ try {
56
+ const p = path.join(process.env.DSH_HOME || path.join(os.homedir(), ".dsh"), "rule-engine.json");
57
+ if (fs.existsSync(p)) {
58
+ const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
59
+ const w = cfg?.dualtrack?.whitelist;
60
+ if (w && typeof w === "object") {
61
+ for (const f of w.files || []) files.push(f);
62
+ for (const s of w.strings || []) strings.add(s);
63
+ }
64
+ }
65
+ } catch {
66
+ // 本机配置不可读 → 只用包内白名单(不阻断)
67
+ }
68
+ return { files, strings };
69
+ }
70
+
71
+ /** 整文件豁免匹配(支持 ** 与 * 通配,路径用 / 分隔,相对仓库根) */
72
+ function fileExempt(relPath, patterns) {
73
+ for (const p of patterns) {
74
+ const re = new RegExp("^" + p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\u0000").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*") + "$");
75
+ if (re.test(relPath)) return true;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ // ── 词法扫描:剔除注释,收集字符串字面量与对象键 ──
81
+ /**
82
+ * @returns {{strings: string[], mapKeys: string[]}}
83
+ * strings = 字符串字面量的**内容**(模板串按整段处理)
84
+ * mapKeys = 形如 "14": / '12E': / 14: 的对象键
85
+ */
86
+ function tokenize(src) {
87
+ const strings = [];
88
+ const mapKeys = [];
89
+ let i = 0;
90
+ const n = src.length;
91
+ // 上一个有意义的 token 类型:用于判断 `/` 是正则还是除号(简化:只看是否可能在正则位置)
92
+ let prevSig = "";
93
+ while (i < n) {
94
+ const c = src[i];
95
+ // 行注释
96
+ if (c === "/" && src[i + 1] === "/") {
97
+ while (i < n && src[i] !== "\n") i++;
98
+ continue;
99
+ }
100
+ // 块注释(含 JSDoc)
101
+ if (c === "/" && src[i + 1] === "*") {
102
+ i += 2;
103
+ while (i < n && !(src[i] === "*" && src[i + 1] === "/")) i++;
104
+ i += 2;
105
+ continue;
106
+ }
107
+ // 字符串 / 模板串
108
+ if (c === '"' || c === "'" || c === "`") {
109
+ const q = c;
110
+ const start = i;
111
+ i++;
112
+ let buf = "";
113
+ if (q === "`") {
114
+ // 模板串:整段作为一个字符串,但 `${...}` 内的表达式跳过(不计入内容,也不当键)
115
+ while (i < n) {
116
+ const ch = src[i];
117
+ if (ch === "\\") { buf += ch + (src[i + 1] || ""); i += 2; continue; }
118
+ if (ch === "$" && src[i + 1] === "{") {
119
+ let depth = 1;
120
+ i += 2;
121
+ while (i < n && depth > 0) {
122
+ const d = src[i];
123
+ if (d === "\\") { i += 2; continue; }
124
+ if (d === "{") depth++;
125
+ else if (d === "}") depth--;
126
+ else if (d === '"' || d === "'" || d === "`") {
127
+ const qq = d; i++;
128
+ while (i < n && src[i] !== qq) { if (src[i] === "\\") i++; i++; }
129
+ }
130
+ i++;
131
+ }
132
+ buf += "\u0000"; // 表达式占位(不含 CJK)
133
+ continue;
134
+ }
135
+ if (ch === "`") break;
136
+ buf += ch;
137
+ i++;
138
+ }
139
+ } else {
140
+ while (i < n && src[i] !== q) {
141
+ if (src[i] === "\\") { buf += src[i] + (src[i + 1] || ""); i += 2; continue; }
142
+ buf += src[i];
143
+ i++;
144
+ }
145
+ }
146
+ i++; // 收尾引号
147
+ strings.push(buf);
148
+ // 对象键判定(2026-09-09 修正):必须「前面是 { 或 ,」且「后面(跳空白)是 :」
149
+ // —— 否则模板串分段与三元表达式会被误判成键(20 处误报的根因)。
150
+ let j = i;
151
+ while (j < n && /\s/.test(src[j])) j++;
152
+ if (src[j] === ":") {
153
+ let p = start - 1;
154
+ while (p >= 0 && /\s/.test(src[p])) p--;
155
+ if (src[p] === "{" || src[p] === ",") {
156
+ const plain = buf.replace(/\\(.)/g, "$1");
157
+ if (MAP_KEY_RE.test(plain) || CJK.test(plain)) mapKeys.push(plain);
158
+ }
159
+ }
160
+ prevSig = "str";
161
+ continue;
162
+ }
163
+ // 正则字面量(跳过,不计入)
164
+ if (c === "/" && prevSig !== "ident" && prevSig !== ")" && prevSig !== "]") {
165
+ let j = i + 1;
166
+ let inClass = false;
167
+ let closed = false;
168
+ while (j < n) {
169
+ const ch = src[j];
170
+ if (ch === "\\") { j += 2; continue; }
171
+ if (ch === "\n") break;
172
+ if (ch === "[") inClass = true;
173
+ else if (ch === "]") inClass = false;
174
+ else if (ch === "/" && !inClass) { closed = true; break; }
175
+ j++;
176
+ }
177
+ if (closed) {
178
+ i = j + 1;
179
+ while (i < n && /[a-z]/i.test(src[i])) i++;
180
+ prevSig = "regex";
181
+ continue;
182
+ }
183
+ i++;
184
+ prevSig = "op";
185
+ continue;
186
+ }
187
+ if (/[A-Za-z_$]/.test(c)) {
188
+ let j = i;
189
+ while (j < n && /[A-Za-z0-9_$]/.test(src[j])) j++;
190
+ i = j;
191
+ prevSig = "ident";
192
+ continue;
193
+ }
194
+ if (c === ")") { prevSig = ")"; i++; continue; }
195
+ if (c === "]") { prevSig = "]"; i++; continue; }
196
+ if (!/\s/.test(c)) prevSig = "op";
197
+ i++;
198
+ }
199
+ return { strings, mapKeys };
200
+ }
201
+
202
+ // ── 单文件扫描 ──
203
+ function scanFile(absPath, relPath, whitelist, residueMarks) {
204
+ const src = fs.readFileSync(absPath, "utf8");
205
+ const { strings, mapKeys } = tokenize(src);
206
+ let runtime = 0;
207
+ const runtimeHits = [];
208
+ for (const s of strings) {
209
+ if (!CJK.test(s)) continue;
210
+ const plain = s.replace(/\\(.)/g, "$1").trim();
211
+ if (whitelist.strings.has(plain)) continue; // 通用功能词豁免
212
+ runtime++;
213
+ if (runtimeHits.length < 3) runtimeHits.push(plain.slice(0, 40));
214
+ }
215
+ const mapHits = mapKeys.filter((k) => MAP_KEY_RE.test(k) || CJK.test(k));
216
+ let local = 0;
217
+ const localHits = [];
218
+ for (const s of strings) {
219
+ for (const mark of residueMarks) {
220
+ if (mark && s.includes(mark)) {
221
+ local++;
222
+ if (localHits.length < 3) localHits.push(`${mark} ← ${s.slice(0, 40)}`);
223
+ }
224
+ }
225
+ }
226
+ return {
227
+ relPath,
228
+ runtime,
229
+ mapKeys: mapHits.length,
230
+ local,
231
+ // 判据 A:total 只计「本机性」两类;通用中文文案(runtime)不计入闸
232
+ total: mapHits.length + local,
233
+ runtimeHits,
234
+ mapHits: mapHits.slice(0, 5),
235
+ localHits
236
+ };
237
+ }
238
+
239
+ function walk(dir, base = dir, out = []) {
240
+ for (const e of fs.readdirSync(dir)) {
241
+ const p = path.join(dir, e);
242
+ const st = fs.statSync(p);
243
+ if (st.isDirectory()) walk(p, base, out);
244
+ else if (/\.js$/.test(e)) out.push(path.relative(base, p).replace(/\\/g, "/"));
245
+ }
246
+ return out;
247
+ }
248
+
249
+ // ── 主流程 ──
250
+ const args = new Set(process.argv.slice(2));
251
+ const whitelist = loadWhitelist();
252
+ // 本机标识词表:与 B2 扫描器共用唯一加载器(本机配置优先 → 环境变量 → 包内示例)
253
+ const { markers: residueMarks, source: markersSource } = loadMarkers({ root: ROOT });
254
+ if (residueMarks.length === 0) {
255
+ console.error("REFUSED: 本机标识词表为空——请在 rule-engine.json 配置 dualtrack.markers,或设置 DUALTRACK_MARKERS 环境变量");
256
+ process.exit(1);
257
+ }
258
+
259
+ const files = walk(LIB).filter((rel) => !fileExempt(rel, whitelist.files));
260
+ const results = files.map((rel) => scanFile(path.join(LIB, rel), rel, whitelist, residueMarks));
261
+ const counts = {};
262
+ for (const r of results) counts[`lib/${r.relPath}`] = r.total;
263
+ const grandTotal = results.reduce((a, r) => a + r.total, 0);
264
+
265
+ if (args.has("--report")) {
266
+ for (const r of results.sort((a, b) => b.total - a.total)) {
267
+ if (r.total === 0) continue;
268
+ console.log(` ${String(r.total).padStart(4)} lib/${r.relPath} [映射键 ${r.mapKeys} / 本机标识 ${r.local} / 通用中文文案 ${r.runtime}(不计入闸)]`);
269
+ if (r.mapHits.length) console.log(` 命中键:${r.mapHits.join(" / ")}`);
270
+ }
271
+ console.log(`\nDUALTRACK REPORT:${results.length} 文件,合计 ${grandTotal}`);
272
+ process.exit(0);
273
+ }
274
+
275
+ if (args.has("--init") || args.has("--update")) {
276
+ const isUpdate = args.has("--update");
277
+ if (isUpdate && !fs.existsSync(BASELINE_FILE)) {
278
+ console.error("REFUSED: --update 需要已存在的基线;首次请用 --init");
279
+ process.exit(1);
280
+ }
281
+ if (!isUpdate && fs.existsSync(BASELINE_FILE)) {
282
+ console.error(
283
+ `REFUSED: 基线已存在(${BASELINE_FILE})——--init 仅用于首次生成。\n` +
284
+ " --init 会把当前计数覆盖为基线(棘轮失效);确需更新请用 --update(须在提交说明里写清改了什么)"
285
+ );
286
+ process.exit(1);
287
+ }
288
+ const payload = {
289
+ generatedAt: new Date().toISOString(),
290
+ note: "dualtrack 棘轮基线:各文件「分层残留」计数,只许降不许升。--init 首次生成,--update 手动更新(须在提交说明里写清改了什么)。",
291
+ total: grandTotal,
292
+ files: counts
293
+ };
294
+ fs.writeFileSync(BASELINE_FILE, JSON.stringify(payload, null, 2) + "\n", "utf8");
295
+ console.log(`${isUpdate ? "UPDATED" : "INITIALIZED"} ${BASELINE_FILE}`);
296
+ console.log(`基线合计:${grandTotal}(${Object.keys(counts).length} 个文件)`);
297
+ process.exit(0);
298
+ }
299
+
300
+ // 默认:比对基线
301
+ if (!fs.existsSync(BASELINE_FILE)) {
302
+ console.error(`FAIL 基线不存在:${BASELINE_FILE}\n 首次请运行:node scripts/dualtrack-check.mjs --init`);
303
+ process.exit(1);
304
+ }
305
+ const baseline = JSON.parse(fs.readFileSync(BASELINE_FILE, "utf8"));
306
+ const base = baseline.files || {};
307
+ const regressions = [];
308
+ const improvements = [];
309
+ for (const [file, count] of Object.entries(counts)) {
310
+ const b = base[file];
311
+ if (b === undefined) {
312
+ if (count > 0) regressions.push(`${file}: 新增文件含残留 ${count} 处(基线无此文件)`);
313
+ continue;
314
+ }
315
+ if (count > b) {
316
+ const r = results.find((x) => `lib/${x.relPath}` === file);
317
+ const detail = r ? [...r.runtimeHits, ...r.localHits].slice(0, 2).join(";") : "";
318
+ regressions.push(`${file}: ${count} > 基线 ${b}${detail ? `(如:${detail})` : ""}`);
319
+ } else if (count < b) {
320
+ improvements.push(`${file}: ${count} < 基线 ${b}`);
321
+ }
322
+ }
323
+ for (const [file, b] of Object.entries(base)) {
324
+ if (counts[file] === undefined && b > 0) improvements.push(`${file}: 文件已删除(基线 ${b})`);
325
+ }
326
+
327
+ if (regressions.length > 0) {
328
+ console.error(`DUALTRACK FAIL:${regressions.length} 项超出基线(只许降不许升)`);
329
+ for (const r of regressions) console.error(` ✗ ${r}`);
330
+ console.error("\n 处置:把新增内容迁个人层/配置层,或(确属通用功能词)登记 scripts/dualtrack-whitelist.json");
331
+ process.exit(1);
332
+ }
333
+ console.log(`DUALTRACK OK:合计 ${grandTotal}(基线 ${baseline.total ?? "?"})${improvements.length ? `,${improvements.length} 个文件下降可 --update` : ""}`);
334
+ for (const im of improvements.slice(0, 5)) console.log(` ↓ ${im}`);
335
+ process.exit(0);
@@ -0,0 +1,7 @@
1
+ {
2
+ "note": "dualtrack-check 白名单。登记即豁免——但登记是一次「分类决策」,请写清理由。",
3
+ "files": [
4
+ "lib/lang/**"
5
+ ],
6
+ "strings": []
7
+ }
@@ -2,16 +2,22 @@
2
2
  // local-residue-scan.mjs — 发布物本机痕迹扫描(仅扫 lib/;无 --pack 模式)
3
3
  // 用法:node scripts/local-residue-scan.mjs (在包根目录运行;exit 0 = 干净,exit 1 = 有命中)
4
4
 
5
- import { readFileSync, readdirSync, statSync } from "node:fs";
5
+ import { readdirSync, statSync, readFileSync } from "node:fs";
6
6
  import { join, extname } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { dirname } from "node:path";
9
9
 
10
10
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
11
11
 
12
- // 词表唯一源(v1.2):scripts/local-residue-markers.txt,与阶段 A A6 验收共用同一文件。
13
- const MARKERS = readFileSync(join(root, "scripts", "local-residue-markers.txt"), "utf8")
14
- .split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("#"));
12
+ // 词表唯一源(2026-09-09 起配置化):本机 rule-engine.jsondualtrack.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
+ }
15
21
  // 注意:不扫 package.json(作者署名/仓库地址为合法项,A6 口径见其节);tool-catalog.js 的
16
22
  // dev_/esr_/engram_ 前缀规则按阶段 A「关键裁决 3」保留,静态枚举列 0.6.x 跟进。
17
23
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "profile": "web",
3
+ "plugins": [
4
+ {
5
+ "name": "dsh-rule-engine",
6
+ "dir": ".",
7
+ "repo": "jilian-dsh/dsh-rule-engine"
8
+ }
9
+ ]
10
+ }
@@ -163,6 +163,10 @@ if (dryRun) {
163
163
  const replaced = t.replaceAll(oldBadgeVer, nextUrl).replaceAll(oldBadgeUrl, nextUrl);
164
164
  console.log(`[DRY-RUN] README 徽章将变为: ${replaced.match(/version-[^\s]+/)?.[0] || "(未命中,检查徽章形态)"}`);
165
165
  }
166
+ // §1.2(2026-09-08)dry-run 可观测项(单测锚点)
167
+ console.log(`[DRY-RUN] plugins.json 条目: ${entry ? `${entry.name} (dir=${entry.dir})` : "(未命中——将回退目录路径参数)"}`);
168
+ console.log(`[DRY-RUN] dist-tags 轮询: ${Math.max(30, Number(process.env.RELEASE_DIST_TAG_POLL_SEC) || 180)}s(RELEASE_DIST_TAG_POLL_SEC 可配)`);
169
+ console.log(`[DRY-RUN] README 四点同步: 徽章 / 正文当前版本 / 历史表新行 / 固定源占位注释`);
166
170
  process.exit(0);
167
171
  }
168
172
  writeFileSync(pkgPath, readFileSync(pkgPath, "utf8").replace(`"version": "${oldVer}"`, `"version": "${nextVer}"`));
@@ -182,7 +186,19 @@ for (const readme of ["README.md", "README.en.md"]) {
182
186
  if (replaced !== text) writeFileSync(rp, replaced);
183
187
  }
184
188
  }
185
- console.log("版本已 bump(package.json + README 徽章)");
189
+ // §1.2-②(2026-09-08 第二批):README 正文"当前版本"行同步 + 固定源占位注释(发布后回填锚点,可 grep)
190
+ for (const readme of ["README.md", "README.en.md"]) {
191
+ const rp = join(dir, readme);
192
+ if (!existsSync(rp)) continue;
193
+ let rtxt = readFileSync(rp, "utf8");
194
+ const before = rtxt;
195
+ rtxt = rtxt.replace(/(> 当前版本 \*\*)\d+\.\d+\.\d+(\*\*)/, `$1${nextVer}$2`);
196
+ if (readme === "README.md" && rtxt.includes("## 发行固定源") && !rtxt.includes("<!-- fixed-source:")) {
197
+ rtxt = rtxt.replace("## 发行固定源", "## 发行固定源\n\n<!-- fixed-source: 待发布回填 -->");
198
+ }
199
+ if (rtxt !== before) writeFileSync(rp, rtxt);
200
+ }
201
+ console.log("版本已 bump(package.json + README 徽章 + 正文当前版本 + 固定源占位)");
186
202
 
187
203
  // B1(2026-09-03):README 版本表行自动插入(阶段 C 人脑核对已失效的自动化;幂等:已存在 nextVer 行则跳过)
188
204
  // 位置:版本表(| 版本 | 日期 | 要点 |)表头后第一数据行之前(最新在上);内容用调用方 notes 参数或 git log 最近提交信息
@@ -273,16 +289,18 @@ run(`cd /d "${dir}" && ${proxyPrefix()}npm publish ${tgz}`);
273
289
  // B1(2026-09-03):publish 后校验 registry dist-tags(0.5.16 事故:publish 自报成功但 latest 未切;
274
290
  // 脚本此前只跑 publish 不校验发布态——三通道验证铁律:npm 通道以 dist-tags 为准,self-report 不算数)
275
291
  // B1 v2(2026-09-03 同日晚):registry 传播以分钟计,publish 后立即校验曾误报中止(0.5.17 实弹)→
276
- // 轮询重试 60s(6×10s);仍不符才中止,并按"版本是否已落盘"给出人工修正路径
277
- console.log("\n=== npm 发布态校验(dist-tags,轮询最多 60s)===");
292
+ // §1.2-③(2026-09-08 第二批):轮询 60s→180s(本机首次传播实测 >60s);RELEASE_DIST_TAG_POLL_SEC 可配(下限 30)
293
+ const pollSec = Math.max(30, Number(process.env.RELEASE_DIST_TAG_POLL_SEC) || 180);
294
+ const attempts = Math.ceil(pollSec / 10);
295
+ console.log(`\n=== npm 发布态校验(dist-tags,轮询最多 ${pollSec}s / ${attempts} 次)===`);
278
296
  function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
279
297
  const queryTags = () => quiet(`cd /d "${dir}" && ${proxyPrefix()}npm view ${name} dist-tags.latest`);
280
298
  const queryVer = () => quiet(`cd /d "${dir}" && ${proxyPrefix()}npm view ${name}@${nextVer} version`);
281
299
  let latestActual = "";
282
- for (let i = 0; i < 6; i++) {
300
+ for (let i = 0; i < attempts; i++) {
283
301
  latestActual = queryTags();
284
302
  if (latestActual === nextVer) break;
285
- if (i < 5) { console.log(` [B1] 等待 registry 传播(${i + 1}/5,latest=${latestActual || "(空)"})…`); sleepMs(10000); }
303
+ if (i < attempts - 1) { console.log(` [B1] 等待 registry 传播(${i + 1}/${attempts - 1},latest=${latestActual || "(空)"})…`); sleepMs(10000); }
286
304
  }
287
305
  if (latestActual !== nextVer) {
288
306
  const verExists = queryVer() === nextVer;
@@ -131,6 +131,10 @@ step("发布门禁 B1(README 版本四性一致性:package.json/徽章/正
131
131
  // ── ⑩ 发布门禁 B2(阶段 B,2026-09-04:lib/ 本机痕迹扫描,词表唯一源)──
132
132
  step("发布门禁 B2(lib/ 本机痕迹扫描——词表唯一源,命中即红)", process.execPath, [join(root, "scripts", "local-residue-scan.mjs")]);
133
133
 
134
+ // ── ⑩′ 发布门禁 B3:dualtrack-check(2026-09-09 第三批第 1 波:分层残留棘轮闸——
135
+ // 手册 SKILL.md:113「机器校验(dualtrack-check)为验收兜底」的落地;只许降不许升)──
136
+ step("发布门禁 B3(dualtrack 分层残留棘轮——超出基线即红)", process.execPath, [join(root, "scripts", "dualtrack-check.mjs")]);
137
+
134
138
  // ── ⑪ 存在性扫描(泄露预防,2026-09-05:真实路径判据——REAL_PATHS_SCAN 指向扫描器,0 命中才算过;
135
139
  // 未设置=WARN(本机增强门禁,通用环境无此工具)──
136
140
  {
@@ -1,14 +1,14 @@
1
1
  // verify-v474.mjs - v4.74 记录项引擎在位抽查(分点切分/规则2双检/词表跑)
2
2
  import { parseUserIntents } from "../lib/core/intent.js";
3
- import { ACTION_WORDS_RE } from "../lib/core/lexicon.js";
4
- import { EVIDENCE_MARK_RE, TIME_WORDS } from "../lib/core/patterns.js";
3
+ import { actionWordsRe } from "../lib/core/lexicon.js";
4
+ import { evidenceMarkRe, timeWordsRe } from "../lib/core/patterns.js";
5
5
 
6
6
  // ① 分点切分(标点后编号——用户"1、问2、问3、跑"同行)
7
7
  const mixed = parseUserIntents("1、为什么预算会满?2、检查时间规则?3、跑");
8
8
  console.log("分点(同行标点):", mixed.hasExecute && mixed.clauses[2].type === "execute" ? "✅" : "❌");
9
- // ② 词表补跑
10
- console.log("词表含跑:", ACTION_WORDS_RE.test("跑") ? "✅" : "❌");
11
- // ③ 规则2双检:EVIDENCE_MARK_RE 存在、TIME_WORDS 具体词
12
- console.log("规则2证据词表:", EVIDENCE_MARK_RE.test("日志 ts=2026-08-27T15:21:05Z") ? "✅" : "❌");
13
- console.log("规则2具体词(昨天):", TIME_WORDS.test("昨天") ? "✅" : "❌");
14
- console.log("规则2模糊词(之前):", TIME_WORDS.test("之前") ? "❌(应不命中)" : "✅(不命中)");
9
+ // ② 词表补跑(P8 小批 A 起词表经函数取;本机需 lexicons 配置注入中文词表)
10
+ console.log("词表含跑:", actionWordsRe().test("跑") ? "✅" : "❌");
11
+ // ③ 规则2双检:证据锚、具体时间词(P8 小批 B 起经访问器取;本机需 patterns 配置注入中文表)
12
+ console.log("规则2证据词表:", evidenceMarkRe().test("日志 ts=2026-08-27T15:21:05Z") ? "✅" : "❌");
13
+ console.log("规则2具体词(昨天):", timeWordsRe().test("昨天") ? "✅" : "❌");
14
+ console.log("规则2模糊词(之前):", timeWordsRe().test("之前") ? "❌(应不命中)" : "✅(不命中)");